From 14956186dacc4493d6d74381002ffff22daedb01 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 08:20:51 +0900 Subject: [PATCH 01/62] feat(persistence): retention deletion legal-hold SQL contracts (0007) (#45) * feat(persistence): retention deletion legal-hold SQL contracts (0007) Restack onto main after restore-integrity and concurrent-write merges. Migration 0007 plus fail-closed retention/hold/tombstone SQL, catalog validation, and live-session method surface. * fix(persistence): restack retention 0007 live proof onto hang-safe concurrent tests Restore main live concurrent timeouts/NOWAIT join helpers and keep 0007 retention legal-hold live exercises under clippy pedantic limits. * fix(persistence): split retention live proofs under clippy line limits * ci: re-trigger exact-head checks for PR #45 after thrash cancel * fix(persistence): close retention and restore coverage gaps for 0007 Cover assert_restore_integrity, non-active supersede policies, SHA-256 hex arms, and compact concurrent revise SQL so exact-head line/branch gates reach 100%. * fix(persistence): legal-hold release path, indexes, and hold/deletion serialization Add system_to windows for retention policies and legal holds, controlled release_legal_hold succession, advisory locks against hold/deletion TOCTOU, lookup indexes for trigger paths, and table-guarded 0007 rollback drops. * chore(ci): repair PR 45 review and exact-head failures * chore(ci): stage PR 45 regression proof * test(persistence): expose tenant binding RED contract * chore(ci): stage PR 45 implementation repair * chore(ci): retry PR 45 repair with executable RED proof * fix(ci): bind audit events with their tenant parameter * chore(ci): repair PR 45 audit tenant binding variable * fix(ci): make PR 45 repair parameter-aware * fix(ci): harden and consolidate PR 45 repair * fix(ci): avoid clone-on-copy in PR 45 regressions * test(persistence): specify lifecycle lock contract * fix(ci): use valid PostgreSQL advisory-lock signature * fix(persistence): stage valid lifecycle advisory locks * test(persistence): lock legal-hold races with a supported signature * chore(ci): verify lifecycle lock repair in PR 45 * chore(ci): remove superseded PR 45 repair workflow * chore(ci): remove narrow PR 45 repair workflow * fix(ci): leave lifecycle lock repair to symmetric lock script * test(persistence): use symmetric lifecycle lock regression * ci(persistence): print exact uncovered repair lines * ci(persistence): cancel superseded repair heads * test(persistence): pin lifecycle migration statement ordering * fix(persistence): drop lifecycle triggers before routines * fix(ci): exclude live SQLx adapter from repair coverage * test(persistence): require lifecycle vocabulary and tombstone guards * fix(persistence): enforce lifecycle schema and request integrity * fix(test): mark PostgreSQL in migration contract docs * fix(persistence): close PR #45 lifecycle migration and tenant binds Move 0007 routine privileges after function definitions so live apply succeeds, add deletion/tombstone vocabulary CHECKs and tombstone/request guard, serialize hold/deletion with single-bigint advisory locks, bind session tenant GUC on tenant-scoped live inserts, fix clippy doc markup, and remove thrashing PR-45 repair workflows. * test(persistence): cover retention windows and artifact assert ref path Exercise status/window inconsistency and inverted system bounds for policies and holds, and assert SQL rendering for protected object refs, so production line/branch coverage remains complete for PR #45. * test(persistence): cover active hold with closed system_to branch blocks_deletion short-circuits on open system_to for active holds; exercise the closed-window path so branch coverage stays complete under 0007. * test(persistence): cover retention lifecycle window failures * style(persistence): rustfmt retention lifecycle window contract tests CI cargo fmt --check failed on AvailableTime parse and held_document_id expect layout. * test(persistence): assert active-analysis select excludes tombstones Wrap select_active_analysis_document_sql in an EXISTS check so the live PostgreSQL proof fails closed when a tombstoned document remains visible, rather than only proving the SELECT is executable. * ci: re-trigger exact-head checks for PR #45 after OpenCode thrash Dismiss stale REQUEST_CHANGES on prior head; ruleset requires zero approvals and dismisses reviews on push. Exact-head evidence was green. * fix(persistence): bind deletion requests and kind-align analysis exclusion Keep cache_export_removal tombstones in active analysis, require deletion_request tenant/class/purpose to match the cited retention policy, and map the legal-hold trigger to LegalHoldBlocksDeletion. Co-authored-by: Seongho Bae --------- Co-authored-by: Cursor Agent Co-authored-by: Seongho Bae --- CHANGELOG.md | 1 + DOCUMENTATION.md | 1 + .../persistence_postgres/src/artifact_sql.rs | 4 + .../persistence_postgres/src/document_sql.rs | 25 +- crates/persistence_postgres/src/error.rs | 28 + crates/persistence_postgres/src/lib.rs | 32 +- .../src/live_repository.rs | 303 ++++- crates/persistence_postgres/src/migration.rs | 130 ++- .../persistence_postgres/src/retention_sql.rs | 1024 +++++++++++++++++ crates/persistence_postgres/src/sqlx_live.rs | 4 + .../tests/live_postgres.rs | 374 ++++-- .../tests/migration_ordering_contract.rs | 91 ++ .../retention_deletion_legal_hold_contract.rs | 159 +++ .../retention_lifecycle_window_contract.rs | 112 ++ .../tests/retention_schema_guard_contract.rs | 109 ++ .../tests/tenant_session_binding_contract.rs | 39 + docs/ERD.md | 47 +- docs/OPERABILITY.md | 2 + docs/PRIVACY_DATA_GOVERNANCE.md | 6 +- docs/THREAT_MODEL.md | 2 +- docs/adr/0009-purpose-bound-pii-governance.md | 2 +- docs/adr/README.md | 4 +- .../research/retention-deletion-legal-hold.md | 37 + docs/research/standards-and-literature.md | 8 + ...007_retention_deletion_legal_hold.down.sql | 65 ++ .../0007_retention_deletion_legal_hold.up.sql | 695 +++++++++++ 26 files changed, 3201 insertions(+), 103 deletions(-) create mode 100644 crates/persistence_postgres/src/retention_sql.rs create mode 100644 crates/persistence_postgres/tests/migration_ordering_contract.rs create mode 100644 crates/persistence_postgres/tests/retention_deletion_legal_hold_contract.rs create mode 100644 crates/persistence_postgres/tests/retention_lifecycle_window_contract.rs create mode 100644 crates/persistence_postgres/tests/retention_schema_guard_contract.rs create mode 100644 crates/persistence_postgres/tests/tenant_session_binding_contract.rs create mode 100644 docs/research/retention-deletion-legal-hold.md create mode 100644 migrations/0007_retention_deletion_legal_hold.down.sql create mode 100644 migrations/0007_retention_deletion_legal_hold.up.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index c1cc6e87..f3764d25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang ### Added +- `persistence_postgres` retention/deletion/legal-hold (migration `0007`): policy rows, legal holds that block completed deletion, evidence tombstones without raw-source restore, analysis exclusion only for `logical_revocation`/`identity_tombstone` (not `cache_export_removal`), and deletion requests bound to the cited retention policy's tenant/class/purpose. - `persistence_postgres` backup/restore integrity: restored snapshots stay unusable until tenant, canonical `SHA-256`, knowledge-cutoff eligibility, temporal window order, and append-only triggers revalidate; SQL probes raise `restore integrity failed` (ADR 0013). - `persistence_postgres` concurrent document-write stress: atomic revise `DO` block that requires exactly one open `system_to` close, SQLSTATE mapping onto `ConcurrentWriteConflict` / `DuplicateDocumentRecord`, and live multi-session insert/revise/append-only proofs. No new migration number. - `tepp_api` naruon HTTP interchange: versioned `https` POST contracts for analysis-run create and modular export authorization that refuse table-access URLs, review/Copilot credential headers, reserved standard-header redefinition, principal-only export idempotency keys, and lexical inference claims (ADR 0011). diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 230c5abe..f60654ab 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -33,6 +33,7 @@ TEPP's approved PRD v0.4 and implementation plan are the primary product baselin | Hourly NIM product-development operations | [`docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md`](docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md) | | Actions workflow fleet audit | [`docs/operations/ACTIONS_WORKFLOW_FLEET.md`](docs/operations/ACTIONS_WORKFLOW_FLEET.md) | | Actions fleet research doctoring | [`docs/research/actions-workflow-fleet.md`](docs/research/actions-workflow-fleet.md) | +| Retention/deletion/legal-hold doctoring | [`docs/research/retention-deletion-legal-hold.md`](docs/research/retention-deletion-legal-hold.md) | | Hourly NIM OpenCode doctoring | [`docs/doctoring/hourly-nim-opencode-development.md`](docs/doctoring/hourly-nim-opencode-development.md) | | Change history | [`CHANGELOG.md`](CHANGELOG.md) | diff --git a/crates/persistence_postgres/src/artifact_sql.rs b/crates/persistence_postgres/src/artifact_sql.rs index 91e1ed74..d654d183 100644 --- a/crates/persistence_postgres/src/artifact_sql.rs +++ b/crates/persistence_postgres/src/artifact_sql.rs @@ -222,6 +222,10 @@ mod tests { with_ref.source_size_bytes = 0; let referenced = insert_source_artifact_sql(&with_ref).expect("ref"); assert!(referenced.contains("s3://tepp/object")); + let referenced_assertion = + assert_source_artifact_matches_sql(&with_ref).expect("referenced assertion"); + assert!(referenced_assertion.contains("s3://tepp/object")); + assert!(referenced_assertion.contains("IS NOT DISTINCT FROM 's3://tepp/object'")); assert_eq!( insert_source_artifact_sql(&SourceArtifactRecord { diff --git a/crates/persistence_postgres/src/document_sql.rs b/crates/persistence_postgres/src/document_sql.rs index ac7a6e68..8546999e 100644 --- a/crates/persistence_postgres/src/document_sql.rs +++ b/crates/persistence_postgres/src/document_sql.rs @@ -68,21 +68,16 @@ pub fn revise_document_sqls(record: &DocumentRecord) -> Result<[String; 2], Pers pub fn revise_document_atomic_sql(record: &DocumentRecord) -> Result { let insert = insert_document_sql(record)?; Ok(format!( - "DO $tepp$ \ - DECLARE closed_count integer; \ - BEGIN \ - PERFORM 1 FROM document_record \ - WHERE document_record_id = '{document_id}'::uuid AND system_to IS NULL \ - FOR UPDATE NOWAIT; \ - UPDATE document_record SET system_to = '{system_from}'::timestamptz \ - WHERE document_record_id = '{document_id}'::uuid AND system_to IS NULL; \ - GET DIAGNOSTICS closed_count = ROW_COUNT; \ - IF closed_count <> 1 THEN \ - RAISE EXCEPTION 'concurrent document revision conflict' \ - USING ERRCODE = 'serialization_failure'; \ - END IF; \ - {insert}; \ - END $tepp$", + "DO $tepp$ DECLARE closed_count integer; BEGIN \ + PERFORM 1 FROM document_record WHERE document_record_id = '{document_id}'::uuid \ + AND system_to IS NULL FOR UPDATE NOWAIT; \ + UPDATE document_record SET system_to = '{system_from}'::timestamptz \ + WHERE document_record_id = '{document_id}'::uuid AND system_to IS NULL; \ + GET DIAGNOSTICS closed_count = ROW_COUNT; \ + IF closed_count <> 1 THEN \ + RAISE EXCEPTION 'concurrent document revision conflict' \ + USING ERRCODE = 'serialization_failure'; END IF; \ + {insert}; END $tepp$", system_from = record.system_from.to_rfc3339(), document_id = record.document_record_id, )) diff --git a/crates/persistence_postgres/src/error.rs b/crates/persistence_postgres/src/error.rs index 2151eb3e..bf5f7cd7 100644 --- a/crates/persistence_postgres/src/error.rs +++ b/crates/persistence_postgres/src/error.rs @@ -42,6 +42,12 @@ pub enum PersistenceError { ConcurrentWriteConflict, /// A restored snapshot failed integrity revalidation and is not usable. RestoreIntegrityFailed, + /// A retention, hold, deletion, or tombstone record failed closed validation. + InvalidRetentionLifecycle, + /// An active legal hold blocked completed deletion. + LegalHoldBlocksDeletion, + /// Tombstoned evidence cannot be restored, and raw-source deletion cannot keep reproduction available. + UngovernedEvidenceRestore, } impl fmt::Display for PersistenceError { @@ -65,6 +71,9 @@ impl fmt::Display for PersistenceError { Self::InvalidAuditEvent => "invalid audit event", Self::ConcurrentWriteConflict => "concurrent write conflict", Self::RestoreIntegrityFailed => "restore integrity failed", + Self::InvalidRetentionLifecycle => "invalid retention lifecycle", + Self::LegalHoldBlocksDeletion => "legal hold blocks deletion", + Self::UngovernedEvidenceRestore => "ungoverned evidence restore", }; formatter.write_str(message) } @@ -96,6 +105,8 @@ pub enum MigrationContractError { MissingAppendOnlyTrigger, /// Temporal interval ordering checks were incomplete when declared. MissingTemporalIntervalConstraint, + /// Retention/legal-hold migration declarations were incomplete. + MissingRetentionLegalHold, } impl fmt::Display for MigrationContractError { @@ -111,6 +122,7 @@ impl fmt::Display for MigrationContractError { Self::MissingTenantSessionGuc => "missing tenant session guc", Self::MissingAppendOnlyTrigger => "missing append-only immutability trigger", Self::MissingTemporalIntervalConstraint => "missing temporal interval constraint", + Self::MissingRetentionLegalHold => "missing retention legal hold", }; formatter.write_str(message) } @@ -197,6 +209,18 @@ mod tests { PersistenceError::RestoreIntegrityFailed.to_string(), "restore integrity failed" ); + assert_eq!( + PersistenceError::InvalidRetentionLifecycle.to_string(), + "invalid retention lifecycle" + ); + assert_eq!( + PersistenceError::LegalHoldBlocksDeletion.to_string(), + "legal hold blocks deletion" + ); + assert_eq!( + PersistenceError::UngovernedEvidenceRestore.to_string(), + "ungoverned evidence restore" + ); assert_eq!( MigrationContractError::SingleWordObjectName.to_string(), "single-word database object name" @@ -237,5 +261,9 @@ mod tests { MigrationContractError::MissingTemporalIntervalConstraint.to_string(), "missing temporal interval constraint" ); + assert_eq!( + MigrationContractError::MissingRetentionLegalHold.to_string(), + "missing retention legal hold" + ); } } diff --git a/crates/persistence_postgres/src/lib.rs b/crates/persistence_postgres/src/lib.rs index ceb97952..1b008eb6 100644 --- a/crates/persistence_postgres/src/lib.rs +++ b/crates/persistence_postgres/src/lib.rs @@ -19,7 +19,10 @@ //! exactly one open row to close, and live `SQLx` maps racing SQLSTATEs onto //! typed conflict errors. Restore integrity probes refuse to mark analytical //! state usable until tenant, digest, cutoff, temporal windows, and append-only -//! triggers revalidate. +//! triggers revalidate. Retention, deletion, and legal-hold SQL (migration +//! `0007`) records policy-driven lifecycle without restoring tombstoned +//! evidence or completing a deletion under an active hold. Analysis exclusion +//! is kind-aligned, and deletion requests bind to the cited policy. mod artifact_sql; mod concurrent_write; @@ -38,6 +41,7 @@ mod model_run_sql; mod naming; mod relation_sql; mod restore_integrity; +mod retention_sql; mod sql_session; mod sqlx_gate; #[cfg(feature = "live-sqlx")] @@ -164,6 +168,32 @@ pub use restore_integrity::backup_scope_tables; pub use restore_integrity::mark_restored_state_usable; /// SQL probes that fail closed on unusable restored rows. pub use restore_integrity::restore_integrity_probe_sqls; +/// Auditable deletion request row. +pub use retention_sql::DeletionRequestRecord; +/// Append-only evidence tombstone row. +pub use retention_sql::EvidenceTombstoneRecord; +/// Legal or contractual hold row. +pub use retention_sql::LegalHoldRecord; +/// Tenant-scoped retention policy row. +pub use retention_sql::RetentionPolicyRecord; +/// Map a lifecycle SQL failure message onto a typed persistence error. +pub use retention_sql::classify_lifecycle_sql_failure; +/// Render insert SQL for a completed deletion after hold evaluation. +pub use retention_sql::insert_completed_deletion_request_sql; +/// Render insert SQL for a validated deletion request. +pub use retention_sql::insert_deletion_request_sql; +/// Render insert SQL for a validated evidence tombstone. +pub use retention_sql::insert_evidence_tombstone_sql; +/// Render insert SQL for a validated legal hold. +pub use retention_sql::insert_legal_hold_sql; +/// Render insert SQL for a validated retention policy. +pub use retention_sql::insert_retention_policy_sql; +/// Render SQL that releases one active legal hold. +pub use retention_sql::release_legal_hold_sql; +/// Render active-analysis selection that excludes revoked or tombstoned documents. +pub use retention_sql::select_active_analysis_document_sql; +/// Render supersede SQL for a successive retention policy. +pub use retention_sql::supersede_retention_policy_sql; /// Recording SQL transport for offline contract tests. pub use sql_session::RecordingSqlSession; /// Live SQL transport contract. diff --git a/crates/persistence_postgres/src/live_repository.rs b/crates/persistence_postgres/src/live_repository.rs index 358493b4..82d7581a 100644 --- a/crates/persistence_postgres/src/live_repository.rs +++ b/crates/persistence_postgres/src/live_repository.rs @@ -29,7 +29,14 @@ use crate::model_run_sql::{ }; use crate::relation_sql::{EventRelationRecord, insert_event_relation_sql}; use crate::restore_integrity::restore_integrity_probe_sqls; +use crate::retention_sql::{ + DeletionRequestRecord, EvidenceTombstoneRecord, LegalHoldRecord, RetentionPolicyRecord, + insert_completed_deletion_request_sql, insert_deletion_request_sql, + insert_evidence_tombstone_sql, insert_legal_hold_sql, insert_retention_policy_sql, + select_active_analysis_document_sql, +}; use crate::sql_session::{SqlSession, apply_sql_batch}; +use crate::tenant_session::set_session_tenant_sql; use crate::{MigrationContractError, PersistenceError}; use temporal_core::{EventTime, SystemTime}; use uuid::Uuid; @@ -93,12 +100,23 @@ impl LiveDocumentRepository { Ok(()) } + /// Bind the session tenant GUC before tenant-scoped persistence operations. + fn bind_session_tenant(&mut self, tenant_record_id: Uuid) -> Result<(), PersistenceError> { + self.session + .execute(&set_session_tenant_sql(tenant_record_id)) + } + /// Insert the first system-time version of a document identity. /// + /// The session tenant GUC is bound first so + /// `reject_tombstoned_evidence_restore` can fail closed instead of aborting + /// a legitimate first insert. + /// /// # Errors /// /// Returns digest or transport failures. pub fn insert(&mut self, record: &DocumentRecord) -> Result<(), PersistenceError> { + self.bind_session_tenant(record.tenant_record_id)?; let sql = insert_document_sql(record)?; self.session.execute(&sql) } @@ -109,6 +127,7 @@ impl LiveDocumentRepository { /// /// Returns digest, concurrent-write, or transport failures. pub fn revise(&mut self, record: &DocumentRecord) -> Result<(), PersistenceError> { + self.bind_session_tenant(record.tenant_record_id)?; let sql = revise_document_atomic_sql(record)?; self.session.execute(&sql) } @@ -155,6 +174,7 @@ impl LiveDocumentRepository { /// /// Returns action-code validation or transport failures. pub fn append_audit(&mut self, event: &AuditEvent) -> Result<(), PersistenceError> { + self.bind_session_tenant(event.tenant_record_id)?; let sql = append_audit_sql(event)?; self.session.execute(&sql) } @@ -168,6 +188,7 @@ impl LiveDocumentRepository { &mut self, record: &ReproducibilityManifestRecord, ) -> Result<(), PersistenceError> { + self.bind_session_tenant(record.tenant_record_id)?; let sql = insert_reproducibility_manifest_sql(record)?; self.session.execute(&sql) } @@ -213,6 +234,7 @@ impl LiveDocumentRepository { &mut self, record: &CorpusSplitManifestRecord, ) -> Result<(), PersistenceError> { + self.bind_session_tenant(record.tenant_record_id)?; let sql = insert_corpus_split_manifest_sql(record)?; self.session.execute(&sql) } @@ -223,6 +245,7 @@ impl LiveDocumentRepository { /// /// Returns digest/label validation or transport failures. pub fn insert_model_run(&mut self, record: &ModelRunRecord) -> Result<(), PersistenceError> { + self.bind_session_tenant(record.tenant_record_id)?; let sql = insert_model_run_sql(record)?; self.session.execute(&sql) } @@ -236,6 +259,7 @@ impl LiveDocumentRepository { &mut self, record: &ModelArtifactRecord, ) -> Result<(), PersistenceError> { + self.bind_session_tenant(record.tenant_record_id)?; let sql = insert_model_artifact_sql(record)?; self.session.execute(&sql) } @@ -249,6 +273,7 @@ impl LiveDocumentRepository { &mut self, record: &MembershipAssignmentRecord, ) -> Result<(), PersistenceError> { + self.bind_session_tenant(record.tenant_record_id)?; let sql = insert_membership_assignment_sql(record)?; self.session.execute(&sql) } @@ -275,6 +300,7 @@ impl LiveDocumentRepository { &mut self, record: &EventRelationRecord, ) -> Result<(), PersistenceError> { + self.bind_session_tenant(record.tenant_record_id)?; let sql = insert_event_relation_sql(record)?; self.session.execute(&sql) } @@ -289,6 +315,7 @@ impl LiveDocumentRepository { &mut self, record: &EventMentionRecord, ) -> Result<(), PersistenceError> { + self.bind_session_tenant(record.tenant_record_id)?; let sql = insert_event_mention_sql(record)?; self.session.execute(&sql) } @@ -302,6 +329,7 @@ impl LiveDocumentRepository { &mut self, record: &EventInstanceRecord, ) -> Result<(), PersistenceError> { + self.bind_session_tenant(record.tenant_record_id)?; let sql = insert_event_instance_sql(record)?; self.session.execute(&sql) } @@ -333,6 +361,7 @@ impl LiveDocumentRepository { &mut self, record: &SourceArtifactRecord, ) -> Result<(), PersistenceError> { + self.bind_session_tenant(record.tenant_record_id)?; let sql = insert_source_artifact_sql(record)?; self.session.execute(&sql)?; let assertion = assert_source_artifact_matches_sql(record)?; @@ -374,6 +403,93 @@ impl LiveDocumentRepository { let sql = select_model_artifacts_by_run_sql(model_run_id); self.session.execute(&sql) } + + /// Insert a tenant-scoped retention policy. + /// + /// # Errors + /// + /// Returns lifecycle validation or transport failures. + pub fn insert_retention_policy( + &mut self, + record: &RetentionPolicyRecord, + ) -> Result<(), PersistenceError> { + self.bind_session_tenant(record.tenant_record_id)?; + let sql = insert_retention_policy_sql(record)?; + self.session.execute(&sql) + } + + /// Insert a legal or contractual hold. + /// + /// # Errors + /// + /// Returns lifecycle validation or transport failures. + pub fn insert_legal_hold(&mut self, record: &LegalHoldRecord) -> Result<(), PersistenceError> { + self.bind_session_tenant(record.tenant_record_id)?; + let sql = insert_legal_hold_sql(record)?; + self.session.execute(&sql) + } + + /// Insert a deletion request that is not evaluated against holds. + /// + /// # Errors + /// + /// Returns lifecycle validation, a cited-policy mismatch, or transport + /// failures. + pub fn insert_deletion_request( + &mut self, + record: &DeletionRequestRecord, + policy: &RetentionPolicyRecord, + ) -> Result<(), PersistenceError> { + record.bind_cited_policy(policy)?; + self.bind_session_tenant(record.tenant_record_id)?; + let sql = insert_deletion_request_sql(record)?; + self.session.execute(&sql) + } + + /// Complete a deletion only when no supplied hold covers the target. + /// + /// # Errors + /// + /// Returns lifecycle validation, a cited-policy mismatch, + /// [`PersistenceError::LegalHoldBlocksDeletion`], or transport failures. + pub fn insert_completed_deletion_request( + &mut self, + record: &DeletionRequestRecord, + policy: &RetentionPolicyRecord, + holds: &[LegalHoldRecord], + ) -> Result<(), PersistenceError> { + record.bind_cited_policy(policy)?; + self.bind_session_tenant(record.tenant_record_id)?; + let sql = insert_completed_deletion_request_sql(record, holds)?; + self.session.execute(&sql) + } + + /// Insert an evidence tombstone without raw source text. + /// + /// # Errors + /// + /// Returns lifecycle validation, ungoverned-restore, or transport failures. + pub fn insert_evidence_tombstone( + &mut self, + record: &EvidenceTombstoneRecord, + ) -> Result<(), PersistenceError> { + self.bind_session_tenant(record.tenant_record_id)?; + let sql = insert_evidence_tombstone_sql(record)?; + self.session.execute(&sql) + } + + /// Look up a document only when it remains eligible for active analysis. + /// + /// # Errors + /// + /// Returns transport failures. + pub fn submit_active_analysis_document( + &mut self, + document_record_id: Uuid, + ) -> Result<(), PersistenceError> { + let sql = select_active_analysis_document_sql(document_record_id); + self.session.execute(&sql) + } } /// Migration application failures distinguishing contract vs transport errors. @@ -640,6 +756,176 @@ mod tests { ); } + fn sample_policy() -> crate::RetentionPolicyRecord { + crate::RetentionPolicyRecord { + retention_policy_id: uuid::Uuid::nil(), + tenant_record_id: uuid::Uuid::nil(), + data_class_code: "raw_source".into(), + processing_purpose_code: "psychometric_analysis".into(), + retention_period_days: 365, + policy_status_code: "active".into(), + authority_citation: "adr-0009".into(), + system_time: SystemTime::parse_rfc3339("2026-01-01T00:00:00Z").expect("s"), + system_to: None, + available_time: AvailableTime::parse_rfc3339("2026-01-01T00:00:00Z").expect("a"), + } + } + + fn sample_hold() -> crate::LegalHoldRecord { + crate::LegalHoldRecord { + legal_hold_id: uuid::Uuid::from_u128(2), + tenant_record_id: uuid::Uuid::nil(), + hold_scope_code: "document".into(), + held_document_id: Some(uuid::Uuid::nil()), + hold_authority_code: "contract".into(), + hold_status_code: "active".into(), + authority_citation: "hold-authority".into(), + system_time: SystemTime::parse_rfc3339("2026-01-01T00:00:00Z").expect("s"), + system_to: None, + available_time: AvailableTime::parse_rfc3339("2026-01-01T00:00:00Z").expect("a"), + } + } + + fn sample_deletion() -> crate::DeletionRequestRecord { + crate::DeletionRequestRecord { + deletion_request_id: uuid::Uuid::from_u128(4), + tenant_record_id: uuid::Uuid::nil(), + retention_policy_id: uuid::Uuid::nil(), + target_document_id: uuid::Uuid::nil(), + target_data_class_code: "raw_source".into(), + processing_purpose_code: "psychometric_analysis".into(), + deletion_kind_code: "identity_tombstone".into(), + request_status_code: "blocked_by_hold".into(), + legal_hold_id: Some(uuid::Uuid::from_u128(2)), + system_time: SystemTime::parse_rfc3339("2026-01-01T00:00:00Z").expect("s"), + available_time: AvailableTime::parse_rfc3339("2026-01-01T00:00:00Z").expect("a"), + } + } + + fn sample_tombstone() -> crate::EvidenceTombstoneRecord { + crate::EvidenceTombstoneRecord { + evidence_tombstone_id: uuid::Uuid::from_u128(5), + tenant_record_id: uuid::Uuid::nil(), + tombstoned_document_id: uuid::Uuid::from_u128(9), + deletion_request_id: uuid::Uuid::from_u128(4), + evidence_digest: "ab".repeat(32), + target_data_class_code: "raw_source".into(), + deletion_kind_code: "identity_tombstone".into(), + reproduction_status_code: "unavailable".into(), + system_time: SystemTime::parse_rfc3339("2026-01-01T00:00:00Z").expect("s"), + available_time: AvailableTime::parse_rfc3339("2026-01-01T00:00:00Z").expect("a"), + } + } + + fn exercise_retention_legal_hold(repo: &mut LiveDocumentRepository) { + repo.insert_retention_policy(&sample_policy()) + .expect("policy insert"); + repo.insert_legal_hold(&sample_hold()).expect("hold insert"); + repo.insert_deletion_request(&sample_deletion(), &sample_policy()) + .expect("blocked request"); + let mut completed = sample_deletion(); + completed.request_status_code = "completed".into(); + completed.legal_hold_id = None; + assert_eq!( + repo.insert_completed_deletion_request(&completed, &sample_policy(), &[sample_hold()]), + Err(PersistenceError::LegalHoldBlocksDeletion) + ); + repo.insert_completed_deletion_request(&completed, &sample_policy(), &[]) + .expect("unheld completion"); + repo.insert_evidence_tombstone(&sample_tombstone()) + .expect("tombstone"); + repo.submit_active_analysis_document(uuid::Uuid::nil()) + .expect("active analysis"); + assert!( + repo.session() + .executed() + .iter() + .any(|sql| sql.contains("INSERT INTO retention_policy")) + ); + assert!( + repo.session() + .executed() + .iter() + .any(|sql| sql.contains("INSERT INTO legal_hold")) + ); + assert!( + repo.session() + .executed() + .iter() + .any(|sql| sql.contains("INSERT INTO evidence_tombstone")) + ); + let mut invalid = sample_policy(); + invalid.retention_period_days = 0; + assert_eq!( + repo.insert_retention_policy(&invalid), + Err(PersistenceError::InvalidRetentionLifecycle) + ); + let mut bad_hold = sample_hold(); + bad_hold.held_document_id = None; + assert_eq!( + repo.insert_legal_hold(&bad_hold), + Err(PersistenceError::InvalidRetentionLifecycle) + ); + let mut bad_request = sample_deletion(); + bad_request.deletion_kind_code = "hard_delete".into(); + assert_eq!( + repo.insert_deletion_request(&bad_request, &sample_policy()), + Err(PersistenceError::InvalidRetentionLifecycle) + ); + let mut mismatched_purpose = sample_deletion(); + mismatched_purpose.processing_purpose_code = "export_fulfillment".into(); + assert_eq!( + repo.insert_deletion_request(&mismatched_purpose, &sample_policy()), + Err(PersistenceError::InvalidRetentionLifecycle) + ); + let mut restore = sample_tombstone(); + restore.reproduction_status_code = "unaffected".into(); + assert_eq!( + repo.insert_evidence_tombstone(&restore), + Err(PersistenceError::UngovernedEvidenceRestore) + ); + } + + #[test] + fn insert_and_revise_bind_tenant_session_before_document_sql() { + let mut repo = LiveDocumentRepository::new(RecordingSqlSession::new()); + let record = sample_record(); + repo.insert(&record).expect("insert"); + let executed = repo.session().executed(); + assert!( + executed.len() >= 2, + "tenant GUC must precede document insert so 0007 restore trigger can run" + ); + let tenant_bind = &executed[0]; + assert!( + tenant_bind.contains("tepp.current_tenant_record_id"), + "insert must set the tenant GUC before SQL reaches document_record" + ); + assert!( + tenant_bind.contains(&record.tenant_record_id.to_string()), + "tenant GUC bind must include the document tenant identity" + ); + assert!( + executed[1].contains("INSERT INTO document_record"), + "document insert follows tenant bind" + ); + + let mut revised = record.clone(); + revised.revision_number = 2; + revised.system_from = + SystemTime::parse_rfc3339("2026-02-01T00:00:00Z").expect("later system"); + repo.revise(&revised).expect("revise"); + let executed = repo.session().executed(); + let revise_bind = executed + .iter() + .rposition(|sql| sql.contains("tepp.current_tenant_record_id")) + .expect("revise must bind tenant session"); + assert!( + executed[revise_bind + 1].contains("DO $tepp$"), + "atomic revise must run after tenant bind" + ); + } + #[test] fn live_repository_applies_migrations_and_document_sql() { let mut repo = LiveDocumentRepository::new(RecordingSqlSession::new()); @@ -647,14 +933,6 @@ mod tests { let applied = repo.apply_migrations(&catalog).expect("migrate"); assert!(applied >= 1); assert!(!repo.session().executed().is_empty()); - repo.assert_restore_integrity() - .expect("restore integrity probes"); - assert!( - repo.session() - .executed() - .iter() - .any(|sql| sql.contains("restore integrity failed")) - ); repo.insert(&sample_record()).expect("insert"); let mut revised = sample_record(); @@ -711,6 +989,15 @@ mod tests { exercise_event_mention(&mut repo); exercise_event_instance(&mut repo); exercise_source_artifact(&mut repo); + exercise_retention_legal_hold(&mut repo); + repo.assert_restore_integrity() + .expect("restore integrity probes render through the session"); + assert!( + repo.session() + .executed() + .iter() + .any(|sql| sql.contains("restore integrity failed") || sql.contains("tepp_restore")) + ); let audit = AuditEvent { audit_event_id: uuid::Uuid::nil(), diff --git a/crates/persistence_postgres/src/migration.rs b/crates/persistence_postgres/src/migration.rs index 77763e2a..60ef4161 100644 --- a/crates/persistence_postgres/src/migration.rs +++ b/crates/persistence_postgres/src/migration.rs @@ -24,6 +24,10 @@ const MEMBERSHIP_UP: &str = include_str!("../../../migrations/0006_typed_membership_assignment.up.sql"); const MEMBERSHIP_DOWN: &str = include_str!("../../../migrations/0006_typed_membership_assignment.down.sql"); +const RETENTION_UP: &str = + include_str!("../../../migrations/0007_retention_deletion_legal_hold.up.sql"); +const RETENTION_DOWN: &str = + include_str!("../../../migrations/0007_retention_deletion_legal_hold.down.sql"); /// Forward and rollback SQL for one migration unit. #[derive(Clone, Debug, Eq, PartialEq)] @@ -41,10 +45,10 @@ impl MigrationCatalog { /// sources are unexpectedly empty. pub fn from_embedded() -> Result { let up_sql = format!( - "{FOUNDATION_UP}\n{RLS_UP}\n{MODEL_RUN_UP}\n{APPEND_ONLY_UP}\n{TEMPORAL_ORDER_UP}\n{MEMBERSHIP_UP}" + "{FOUNDATION_UP}\n{RLS_UP}\n{MODEL_RUN_UP}\n{APPEND_ONLY_UP}\n{TEMPORAL_ORDER_UP}\n{MEMBERSHIP_UP}\n{RETENTION_UP}" ); let down_sql = format!( - "{MEMBERSHIP_DOWN}\n{TEMPORAL_ORDER_DOWN}\n{APPEND_ONLY_DOWN}\n{MODEL_RUN_DOWN}\n{RLS_DOWN}\n{FOUNDATION_DOWN}" + "{RETENTION_DOWN}\n{MEMBERSHIP_DOWN}\n{TEMPORAL_ORDER_DOWN}\n{APPEND_ONLY_DOWN}\n{MODEL_RUN_DOWN}\n{RLS_DOWN}\n{FOUNDATION_DOWN}" ); Self::from_sources(&up_sql, &down_sql) } @@ -119,6 +123,9 @@ pub fn validate_migration_catalog( if declares_temporal_interval_ordering(catalog.up_sql()) { validate_temporal_interval_ordering(catalog.up_sql())?; } + if declares_retention_legal_hold(catalog.up_sql()) { + validate_retention_legal_hold(catalog.up_sql())?; + } Ok(()) } @@ -185,6 +192,47 @@ fn validate_temporal_interval_ordering(up_sql: &str) -> Result<(), MigrationCont Ok(()) } +fn declares_retention_legal_hold(up_sql: &str) -> bool { + let lower = up_sql.to_ascii_lowercase(); + lower.contains("retention_policy") + || lower.contains("legal_hold") + || lower.contains("evidence_tombstone") +} + +fn validate_retention_legal_hold(up_sql: &str) -> Result<(), MigrationContractError> { + let lower = up_sql.to_ascii_lowercase(); + let required_tables = [ + "retention_policy", + "legal_hold", + "deletion_request", + "evidence_tombstone", + ]; + for table in required_tables { + if !lower.contains(&format!("create table {table}")) { + return Err(MigrationContractError::MissingRetentionLegalHold); + } + } + if !lower.contains("create or replace function reject_held_evidence_deletion") { + return Err(MigrationContractError::MissingRetentionLegalHold); + } + if !lower.contains("create or replace function reject_tombstoned_evidence_restore") { + return Err(MigrationContractError::MissingRetentionLegalHold); + } + if !lower.contains("create trigger deletion_request_reject_held_deletion") { + return Err(MigrationContractError::MissingRetentionLegalHold); + } + if !lower.contains("create trigger document_record_reject_tombstone_restore") { + return Err(MigrationContractError::MissingRetentionLegalHold); + } + if !lower.contains("constraint retention_policy_period_positive") { + return Err(MigrationContractError::MissingRetentionLegalHold); + } + if !lower.contains("constraint legal_hold_document_scope_consistent") { + return Err(MigrationContractError::MissingRetentionLegalHold); + } + Ok(()) +} + fn validate_table_body(table: &str, body: &str) -> Result<(), MigrationContractError> { let lower = body.to_ascii_lowercase(); if requires_tenant_boundary(table) && !lower.contains("tenant_record_id") { @@ -791,6 +839,84 @@ mod tests { assert_eq!(super::validate_temporal_interval_ordering(complete), Ok(())); } + #[test] + fn retention_legal_hold_contract_fails_closed() { + assert!(super::declares_retention_legal_hold( + "CREATE TABLE retention_policy (retention_policy_id uuid PRIMARY KEY)" + )); + assert!(super::declares_retention_legal_hold( + "CREATE TABLE legal_hold ()" + )); + assert!(super::declares_retention_legal_hold( + "CREATE TABLE evidence_tombstone ()" + )); + assert!(!super::declares_retention_legal_hold("CREATE TABLE x")); + + let missing_table = MigrationCatalog::from_sql( + r" + CREATE TABLE tenant_record ( + tenant_record_id uuid PRIMARY KEY, + system_time timestamptz NOT NULL + ); + CREATE TABLE retention_policy ( + retention_policy_id uuid PRIMARY KEY, + tenant_record_id uuid NOT NULL, + system_time timestamptz NOT NULL, + available_time timestamptz NOT NULL + ); + ", + "DROP TABLE tenant_record;", + ); + assert_eq!( + validate_migration_catalog(&missing_table), + Err(MigrationContractError::MissingRetentionLegalHold) + ); + + let tables_only = r" + CREATE TABLE retention_policy (x int); + CREATE TABLE legal_hold (x int); + CREATE TABLE deletion_request (x int); + CREATE TABLE evidence_tombstone (x int); + "; + assert_eq!( + super::validate_retention_legal_hold(tables_only), + Err(MigrationContractError::MissingRetentionLegalHold) + ); + let with_hold_fn = + format!("{tables_only} CREATE OR REPLACE FUNCTION reject_held_evidence_deletion()"); + assert_eq!( + super::validate_retention_legal_hold(&with_hold_fn), + Err(MigrationContractError::MissingRetentionLegalHold) + ); + let with_restore_fn = format!( + "{with_hold_fn} CREATE OR REPLACE FUNCTION reject_tombstoned_evidence_restore()" + ); + assert_eq!( + super::validate_retention_legal_hold(&with_restore_fn), + Err(MigrationContractError::MissingRetentionLegalHold) + ); + let with_hold_trigger = + format!("{with_restore_fn} CREATE TRIGGER deletion_request_reject_held_deletion"); + assert_eq!( + super::validate_retention_legal_hold(&with_hold_trigger), + Err(MigrationContractError::MissingRetentionLegalHold) + ); + let with_restore_trigger = + format!("{with_hold_trigger} CREATE TRIGGER document_record_reject_tombstone_restore"); + assert_eq!( + super::validate_retention_legal_hold(&with_restore_trigger), + Err(MigrationContractError::MissingRetentionLegalHold) + ); + let with_period = + format!("{with_restore_trigger} CONSTRAINT retention_policy_period_positive"); + assert_eq!( + super::validate_retention_legal_hold(&with_period), + Err(MigrationContractError::MissingRetentionLegalHold) + ); + let complete = format!("{with_period} CONSTRAINT legal_hold_document_scope_consistent"); + super::validate_retention_legal_hold(&complete).expect("complete 0007 contract"); + } + #[test] fn empty_and_malformed_sql_fail_closed() { let empty = MigrationCatalog::from_sql(" ", "DROP TABLE x;"); diff --git a/crates/persistence_postgres/src/retention_sql.rs b/crates/persistence_postgres/src/retention_sql.rs new file mode 100644 index 00000000..977cf2a4 --- /dev/null +++ b/crates/persistence_postgres/src/retention_sql.rs @@ -0,0 +1,1024 @@ +//! SQL contracts for retention policy, legal hold, deletion, and tombstones. + +use crate::PersistenceError; +use temporal_core::{AvailableTime, SystemTime}; +use uuid::Uuid; + +const DATA_CLASSES: [&str; 6] = [ + "raw_source", + "identity_mapping", + "derived_result", + "provider_payload", + "audit_evidence", + "export_cache", +]; +const PURPOSES: [&str; 4] = [ + "psychometric_analysis", + "legal_preservation", + "operations_audit", + "export_fulfillment", +]; +const POLICY_STATUSES: [&str; 2] = ["active", "superseded"]; +const HOLD_SCOPES: [&str; 2] = ["document", "tenant"]; +const HOLD_STATUSES: [&str; 2] = ["active", "released"]; +const DELETION_KINDS: [&str; 3] = [ + "logical_revocation", + "cache_export_removal", + "identity_tombstone", +]; +const REQUEST_STATUSES: [&str; 4] = [ + "requested", + "completed", + "blocked_by_hold", + "reproduction_limited", +]; +const REPRODUCTION_STATUSES: [&str; 3] = ["unavailable", "limited", "unaffected"]; + +/// Tenant-scoped retention policy for one data class and purpose. +#[derive(Clone, Debug, PartialEq)] +pub struct RetentionPolicyRecord { + /// Primary key for this policy identity. + pub retention_policy_id: Uuid, + /// Owning tenant boundary. + pub tenant_record_id: Uuid, + /// Governed data class (`raw_source`, `identity_mapping`, ...). + pub data_class_code: String, + /// Declared processing purpose bound to the retention period. + pub processing_purpose_code: String, + /// Positive retention period in whole days. + pub retention_period_days: i32, + /// Policy lifecycle token (`active` or `superseded`). + pub policy_status_code: String, + /// Authority citation recorded without raw source text. + pub authority_citation: String, + /// System/record time when the policy was asserted. + pub system_time: SystemTime, + /// Closed system-time upper bound; required when `policy_status_code` is + /// `superseded`, and must remain open (`None`) for `active` policies. + pub system_to: Option, + /// Availability time of the policy assertion. + pub available_time: AvailableTime, +} + +impl RetentionPolicyRecord { + /// Fail-closed vocabulary, period, window, and label validation. + /// + /// # Errors + /// + /// Returns [`PersistenceError::InvalidRetentionLifecycle`] when the period + /// is not positive, a label is empty/hostile/unknown, or the status/window + /// pair is inconsistent with migration `0007`. + pub fn validate(&self) -> Result<(), PersistenceError> { + if self.retention_period_days <= 0 { + return Err(PersistenceError::InvalidRetentionLifecycle); + } + validate_vocab(&self.data_class_code, &DATA_CLASSES)?; + validate_vocab(&self.processing_purpose_code, &PURPOSES)?; + validate_vocab(&self.policy_status_code, &POLICY_STATUSES)?; + validate_lifecycle_label(&self.authority_citation)?; + let open = self.system_to.is_none(); + let active = self.policy_status_code == "active"; + if active != open { + return Err(PersistenceError::InvalidRetentionLifecycle); + } + if let Some(until) = self.system_to.as_ref() + && until.instant() < self.system_time.instant() + { + return Err(PersistenceError::InvalidRetentionLifecycle); + } + Ok(()) + } +} + +/// Legal or contractual hold that prevents completed deletion. +#[derive(Clone, Debug, PartialEq)] +pub struct LegalHoldRecord { + /// Primary key for this hold identity. + pub legal_hold_id: Uuid, + /// Owning tenant boundary. + pub tenant_record_id: Uuid, + /// Hold scope (`document` or `tenant`). + pub hold_scope_code: String, + /// Document identity when `hold_scope_code` is `document`. + pub held_document_id: Option, + /// Authority class that imposed the hold. + pub hold_authority_code: String, + /// Hold lifecycle token (`active` or `released`). + pub hold_status_code: String, + /// Authority citation recorded without raw source text. + pub authority_citation: String, + /// System/record time when the hold was asserted. + pub system_time: SystemTime, + /// Closed system-time upper bound; required when `hold_status_code` is + /// `released`, and must remain open (`None`) for `active` holds. + pub system_to: Option, + /// Availability time of the hold assertion. + pub available_time: AvailableTime, +} + +impl LegalHoldRecord { + /// Fail-closed scope, vocabulary, window, and label validation. + /// + /// # Errors + /// + /// Returns [`PersistenceError::InvalidRetentionLifecycle`] when document + /// scope is missing an identity, tenant scope carries one, a label is + /// hostile/unknown, or the status/window pair is inconsistent. + pub fn validate(&self) -> Result<(), PersistenceError> { + validate_vocab(&self.hold_scope_code, &HOLD_SCOPES)?; + validate_vocab(&self.hold_status_code, &HOLD_STATUSES)?; + validate_lifecycle_label(&self.hold_authority_code)?; + validate_lifecycle_label(&self.authority_citation)?; + let document_scope = self.hold_scope_code == "document"; + if document_scope != self.held_document_id.is_some() { + return Err(PersistenceError::InvalidRetentionLifecycle); + } + let open = self.system_to.is_none(); + let active = self.hold_status_code == "active"; + if active != open { + return Err(PersistenceError::InvalidRetentionLifecycle); + } + if let Some(until) = self.system_to.as_ref() + && until.instant() < self.system_time.instant() + { + return Err(PersistenceError::InvalidRetentionLifecycle); + } + Ok(()) + } + + /// True when this hold blocks a completed deletion of `document_id`. + #[must_use] + pub fn blocks_deletion(&self, tenant_record_id: Uuid, document_id: Uuid) -> bool { + self.hold_status_code == "active" + && self.system_to.is_none() + && self.tenant_record_id == tenant_record_id + && (self.hold_scope_code == "tenant" || self.held_document_id == Some(document_id)) + } +} + +/// Auditable deletion request that never silently DELETE-s evidence. +#[derive(Clone, Debug, PartialEq)] +pub struct DeletionRequestRecord { + /// Primary key for this request identity. + pub deletion_request_id: Uuid, + /// Owning tenant boundary. + pub tenant_record_id: Uuid, + /// Retention policy that authorized the lifecycle action. + pub retention_policy_id: Uuid, + /// Document identity being revoked, tombstoned, or cache-cleared. + pub target_document_id: Uuid, + /// Data class being acted on. + pub target_data_class_code: String, + /// Purpose used to select the retention policy. + pub processing_purpose_code: String, + /// Deletion kind (`logical_revocation`, `cache_export_removal`, `identity_tombstone`). + pub deletion_kind_code: String, + /// Request status (`requested`, `completed`, `blocked_by_hold`, `reproduction_limited`). + pub request_status_code: String, + /// Optional hold that blocked completion. + pub legal_hold_id: Option, + /// System/record time when the request was asserted. + pub system_time: SystemTime, + /// Availability time of the request assertion. + pub available_time: AvailableTime, +} + +impl DeletionRequestRecord { + /// Fail-closed vocabulary and label validation. + /// + /// # Errors + /// + /// Returns [`PersistenceError::InvalidRetentionLifecycle`] when a code is + /// unknown or a label is empty/hostile. + pub fn validate(&self) -> Result<(), PersistenceError> { + validate_vocab(&self.target_data_class_code, &DATA_CLASSES)?; + validate_vocab(&self.processing_purpose_code, &PURPOSES)?; + validate_vocab(&self.deletion_kind_code, &DELETION_KINDS)?; + validate_vocab(&self.request_status_code, &REQUEST_STATUSES) + } + + /// Bind this request to the cited retention policy's tenant, class, and purpose. + /// + /// # Errors + /// + /// Returns [`PersistenceError::InvalidRetentionLifecycle`] when vocabulary + /// fails or the request tenant, policy identity, data class, or purpose + /// does not match the cited policy. + pub fn bind_cited_policy( + &self, + policy: &RetentionPolicyRecord, + ) -> Result<(), PersistenceError> { + self.validate()?; + if self.retention_policy_id != policy.retention_policy_id + || self.tenant_record_id != policy.tenant_record_id + || self.target_data_class_code != policy.data_class_code + || self.processing_purpose_code != policy.processing_purpose_code + { + return Err(PersistenceError::InvalidRetentionLifecycle); + } + Ok(()) + } +} + +/// Append-only tombstone that records deletion without raw source text. +#[derive(Clone, Debug, PartialEq)] +pub struct EvidenceTombstoneRecord { + /// Primary key for this tombstone identity. + pub evidence_tombstone_id: Uuid, + /// Owning tenant boundary. + pub tenant_record_id: Uuid, + /// Document identity that may no longer be restored. + pub tombstoned_document_id: Uuid, + /// Deletion request that produced the tombstone. + pub deletion_request_id: Uuid, + /// SHA-256 digest of the deletion action, not of raw source bytes. + pub evidence_digest: String, + /// Data class that was tombstoned. + pub target_data_class_code: String, + /// Deletion kind that produced the tombstone. + pub deletion_kind_code: String, + /// Reproduction consequence (`unavailable`, `limited`, `unaffected`). + pub reproduction_status_code: String, + /// System/record time when the tombstone was asserted. + pub system_time: SystemTime, + /// Availability time of the tombstone assertion. + pub available_time: AvailableTime, +} + +impl EvidenceTombstoneRecord { + /// Fail-closed digest, vocabulary, and reproduction-override validation. + /// + /// # Errors + /// + /// Returns [`PersistenceError::InvalidRetentionLifecycle`] for hostile or + /// unknown labels and [`PersistenceError::UngovernedEvidenceRestore`] when + /// raw-source deletion would silently keep reproduction available. + pub fn validate(&self) -> Result<(), PersistenceError> { + validate_sha256_hex(&self.evidence_digest)?; + validate_vocab(&self.target_data_class_code, &DATA_CLASSES)?; + validate_vocab(&self.deletion_kind_code, &DELETION_KINDS)?; + validate_vocab(&self.reproduction_status_code, &REPRODUCTION_STATUSES)?; + let source_revoked = self.target_data_class_code == "raw_source" + && (self.deletion_kind_code == "identity_tombstone" + || self.deletion_kind_code == "logical_revocation"); + if source_revoked && self.reproduction_status_code != "unavailable" { + return Err(PersistenceError::UngovernedEvidenceRestore); + } + Ok(()) + } +} + +/// Render insert SQL for a validated retention policy. +/// +/// # Errors +/// +/// Returns [`PersistenceError::InvalidRetentionLifecycle`] before any SQL is +/// produced when the record fails validation. +pub fn insert_retention_policy_sql( + record: &RetentionPolicyRecord, +) -> Result { + record.validate()?; + Ok(format!( + "INSERT INTO retention_policy (\ + retention_policy_id, tenant_record_id, data_class_code, \ + processing_purpose_code, retention_period_days, policy_status_code, \ + authority_citation, system_time, system_to, available_time\ + ) VALUES (\ + '{policy}'::uuid, '{tenant}'::uuid, '{class}', \ + '{purpose}', {days}, '{status}', \ + '{citation}', '{system}'::timestamptz, {system_to}, '{available}'::timestamptz\ + )", + policy = record.retention_policy_id, + tenant = record.tenant_record_id, + class = record.data_class_code, + purpose = record.processing_purpose_code, + days = record.retention_period_days, + status = record.policy_status_code, + citation = record.authority_citation, + system = record.system_time.to_rfc3339(), + system_to = optional_timestamptz(record.system_to.as_ref()), + available = record.available_time.to_rfc3339(), + )) +} + +/// Render SQL that atomically supersedes one active retention policy. +/// +/// The statement calls the tenant-bound `supersede_retention_policy` function +/// so the predecessor is closed and the successor is inserted in one +/// database transaction. Application code never issues a standalone UPDATE. +/// +/// # Errors +/// +/// Returns [`PersistenceError::InvalidRetentionLifecycle`] before any SQL is +/// produced when the replacement is not an active distinct policy or fails +/// vocabulary/period/label validation. +pub fn supersede_retention_policy_sql( + predecessor_retention_policy_id: Uuid, + replacement: &RetentionPolicyRecord, +) -> Result { + replacement.validate()?; + if replacement.policy_status_code != "active" + || replacement.retention_policy_id == predecessor_retention_policy_id + { + return Err(PersistenceError::InvalidRetentionLifecycle); + } + Ok(format!( + "SELECT supersede_retention_policy(\ + '{predecessor}'::uuid, '{replacement}'::uuid, {days}, \ + '{citation}', '{system}'::timestamptz, '{available}'::timestamptz\ + )", + predecessor = predecessor_retention_policy_id, + replacement = replacement.retention_policy_id, + days = replacement.retention_period_days, + citation = replacement.authority_citation, + system = replacement.system_time.to_rfc3339(), + available = replacement.available_time.to_rfc3339(), + )) +} + +/// Render insert SQL for a validated legal hold. +/// +/// # Errors +/// +/// Returns [`PersistenceError::InvalidRetentionLifecycle`] before any SQL is +/// produced when the record fails validation. +pub fn insert_legal_hold_sql(record: &LegalHoldRecord) -> Result { + record.validate()?; + Ok(format!( + "INSERT INTO legal_hold (\ + legal_hold_id, tenant_record_id, hold_scope_code, held_document_id, \ + hold_authority_code, hold_status_code, authority_citation, \ + system_time, system_to, available_time\ + ) VALUES (\ + '{hold}'::uuid, '{tenant}'::uuid, '{scope}', {document}, \ + '{authority}', '{status}', '{citation}', \ + '{system}'::timestamptz, {system_to}, '{available}'::timestamptz\ + )", + hold = record.legal_hold_id, + tenant = record.tenant_record_id, + scope = record.hold_scope_code, + document = optional_uuid(record.held_document_id), + authority = record.hold_authority_code, + status = record.hold_status_code, + citation = record.authority_citation, + system = record.system_time.to_rfc3339(), + system_to = optional_timestamptz(record.system_to.as_ref()), + available = record.available_time.to_rfc3339(), + )) +} + +/// Render SQL that releases one active legal hold under the session tenant. +/// +/// # Errors +/// +/// Returns [`PersistenceError::InvalidRetentionLifecycle`] when the hold +/// identity is nil (caller must pass a real open hold id). +pub fn release_legal_hold_sql( + legal_hold_id: Uuid, + release_system_time: &SystemTime, +) -> Result { + if legal_hold_id.is_nil() { + return Err(PersistenceError::InvalidRetentionLifecycle); + } + Ok(format!( + "SELECT release_legal_hold('{hold}'::uuid, '{release}'::timestamptz)", + hold = legal_hold_id, + release = release_system_time.to_rfc3339(), + )) +} + +/// Render insert SQL for a validated deletion request that is not a completion. +/// +/// # Errors +/// +/// Returns [`PersistenceError::InvalidRetentionLifecycle`] when the record is +/// invalid. +pub fn insert_deletion_request_sql( + record: &DeletionRequestRecord, +) -> Result { + record.validate()?; + Ok(render_deletion_request_sql(record)) +} + +/// Render insert SQL for a completed deletion after legal-hold evaluation. +/// +/// # Errors +/// +/// Returns [`PersistenceError::InvalidRetentionLifecycle`] when the record is +/// invalid and [`PersistenceError::LegalHoldBlocksDeletion`] when an active +/// hold covers the target. +pub fn insert_completed_deletion_request_sql( + record: &DeletionRequestRecord, + holds: &[LegalHoldRecord], +) -> Result { + record.validate()?; + if holds + .iter() + .any(|hold| hold.blocks_deletion(record.tenant_record_id, record.target_document_id)) + { + return Err(PersistenceError::LegalHoldBlocksDeletion); + } + Ok(render_deletion_request_sql(record)) +} + +/// Render insert SQL for a validated evidence tombstone. +/// +/// # Errors +/// +/// Returns [`PersistenceError::InvalidRetentionLifecycle`] or +/// [`PersistenceError::UngovernedEvidenceRestore`] before any SQL is produced. +pub fn insert_evidence_tombstone_sql( + record: &EvidenceTombstoneRecord, +) -> Result { + record.validate()?; + Ok(format!( + "INSERT INTO evidence_tombstone (\ + evidence_tombstone_id, tenant_record_id, tombstoned_document_id, \ + deletion_request_id, evidence_digest, target_data_class_code, \ + deletion_kind_code, reproduction_status_code, system_time, available_time\ + ) VALUES (\ + '{tombstone}'::uuid, '{tenant}'::uuid, '{document}'::uuid, \ + '{request}'::uuid, '{digest}', '{class}', \ + '{kind}', '{reproduction}', '{system}'::timestamptz, '{available}'::timestamptz\ + )", + tombstone = record.evidence_tombstone_id, + tenant = record.tenant_record_id, + document = record.tombstoned_document_id, + request = record.deletion_request_id, + digest = record.evidence_digest, + class = record.target_data_class_code, + kind = record.deletion_kind_code, + reproduction = record.reproduction_status_code, + system = record.system_time.to_rfc3339(), + available = record.available_time.to_rfc3339(), + )) +} + +/// Render selection SQL that excludes revoked or identity-tombstoned documents. +/// +/// A `cache_export_removal` tombstone or completed request does not drop the +/// document from active analysis; only `logical_revocation` and +/// `identity_tombstone` kinds remove analysis eligibility. +#[must_use] +pub fn select_active_analysis_document_sql(document_record_id: Uuid) -> String { + format!( + "SELECT document_record_id FROM document_record \ + WHERE document_record_id = '{document_record_id}'::uuid \ + AND system_to IS NULL \ + AND NOT EXISTS (\ + SELECT 1 FROM evidence_tombstone \ + WHERE tombstoned_document_id = document_record.document_record_id \ + AND deletion_kind_code IN ('logical_revocation', 'identity_tombstone')\ + ) \ + AND NOT EXISTS (\ + SELECT 1 FROM deletion_request \ + WHERE target_document_id = document_record.document_record_id \ + AND request_status_code = 'completed' \ + AND deletion_kind_code IN ('logical_revocation', 'identity_tombstone')\ + )" + ) +} + +/// Map a lifecycle SQL failure message onto a typed persistence error. +/// +/// Trigger text `legal hold blocks deletion` is +/// [`PersistenceError::LegalHoldBlocksDeletion`]. Other messages stay +/// unmapped so the transport can fail closed as a generic execution error. +#[must_use] +pub fn classify_lifecycle_sql_failure(message: &str) -> Option { + if message.contains("legal hold blocks deletion") { + Some(PersistenceError::LegalHoldBlocksDeletion) + } else { + None + } +} + +fn render_deletion_request_sql(record: &DeletionRequestRecord) -> String { + format!( + "INSERT INTO deletion_request (\ + deletion_request_id, tenant_record_id, retention_policy_id, \ + target_document_id, target_data_class_code, processing_purpose_code, \ + deletion_kind_code, request_status_code, legal_hold_id, \ + system_time, available_time\ + ) VALUES (\ + '{request}'::uuid, '{tenant}'::uuid, '{policy}'::uuid, \ + '{document}'::uuid, '{class}', '{purpose}', \ + '{kind}', '{status}', {hold}, \ + '{system}'::timestamptz, '{available}'::timestamptz\ + )", + request = record.deletion_request_id, + tenant = record.tenant_record_id, + policy = record.retention_policy_id, + document = record.target_document_id, + class = record.target_data_class_code, + purpose = record.processing_purpose_code, + kind = record.deletion_kind_code, + status = record.request_status_code, + hold = optional_uuid(record.legal_hold_id), + system = record.system_time.to_rfc3339(), + available = record.available_time.to_rfc3339(), + ) +} + +fn optional_timestamptz(value: Option<&SystemTime>) -> String { + match value { + Some(time) => format!("'{}'::timestamptz", time.to_rfc3339()), + None => "NULL".into(), + } +} + +fn optional_uuid(value: Option) -> String { + match value { + Some(id) => format!("'{id}'::uuid"), + None => "NULL".to_owned(), + } +} + +fn validate_vocab(value: &str, allowed: &[&str]) -> Result<(), PersistenceError> { + validate_lifecycle_label(value)?; + if allowed.contains(&value) { + Ok(()) + } else { + Err(PersistenceError::InvalidRetentionLifecycle) + } +} + +fn validate_lifecycle_label(value: &str) -> Result<(), PersistenceError> { + if value.is_empty() + || value + .chars() + .any(|ch| ch.is_control() || ch == '\'' || ch == ';' || ch == '\\') + { + return Err(PersistenceError::InvalidRetentionLifecycle); + } + Ok(()) +} + +fn validate_sha256_hex(value: &str) -> Result<(), PersistenceError> { + let valid_len = value.len() == 64; + let valid_hex = value + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)); + if valid_len && valid_hex { + Ok(()) + } else { + Err(PersistenceError::InvalidRetentionLifecycle) + } +} + +#[cfg(test)] +mod tests { + use super::{ + DeletionRequestRecord, EvidenceTombstoneRecord, LegalHoldRecord, RetentionPolicyRecord, + classify_lifecycle_sql_failure, insert_completed_deletion_request_sql, + insert_deletion_request_sql, insert_evidence_tombstone_sql, insert_legal_hold_sql, + insert_retention_policy_sql, release_legal_hold_sql, select_active_analysis_document_sql, + supersede_retention_policy_sql, + }; + use crate::PersistenceError; + use temporal_core::{AvailableTime, SystemTime}; + use uuid::Uuid; + + fn times() -> (AvailableTime, SystemTime) { + ( + AvailableTime::parse_rfc3339("2026-01-01T00:00:00Z").expect("available"), + SystemTime::parse_rfc3339("2026-01-01T00:00:00Z").expect("system"), + ) + } + + fn policy() -> RetentionPolicyRecord { + let (available, system) = times(); + RetentionPolicyRecord { + retention_policy_id: Uuid::nil(), + tenant_record_id: Uuid::nil(), + data_class_code: "raw_source".into(), + processing_purpose_code: "psychometric_analysis".into(), + retention_period_days: 365, + policy_status_code: "active".into(), + authority_citation: "adr-0009".into(), + system_time: system, + system_to: None, + available_time: available, + } + } + + fn hold() -> LegalHoldRecord { + let (available, system) = times(); + LegalHoldRecord { + legal_hold_id: Uuid::from_u128(2), + tenant_record_id: Uuid::nil(), + hold_scope_code: "document".into(), + held_document_id: Some(Uuid::from_u128(3)), + hold_authority_code: "contract".into(), + hold_status_code: "active".into(), + authority_citation: "hold-authority".into(), + system_time: system, + system_to: None, + available_time: available, + } + } + + fn request() -> DeletionRequestRecord { + let (available, system) = times(); + DeletionRequestRecord { + deletion_request_id: Uuid::from_u128(4), + tenant_record_id: Uuid::nil(), + retention_policy_id: Uuid::nil(), + target_document_id: Uuid::from_u128(3), + target_data_class_code: "raw_source".into(), + processing_purpose_code: "psychometric_analysis".into(), + deletion_kind_code: "identity_tombstone".into(), + request_status_code: "completed".into(), + legal_hold_id: None, + system_time: system, + available_time: available, + } + } + + fn tombstone() -> EvidenceTombstoneRecord { + let (available, system) = times(); + EvidenceTombstoneRecord { + evidence_tombstone_id: Uuid::from_u128(5), + tenant_record_id: Uuid::nil(), + tombstoned_document_id: Uuid::from_u128(3), + deletion_request_id: Uuid::from_u128(4), + evidence_digest: "ab".repeat(32), + target_data_class_code: "raw_source".into(), + deletion_kind_code: "identity_tombstone".into(), + reproduction_status_code: "unavailable".into(), + system_time: system, + available_time: available, + } + } + + #[test] + fn insert_sql_renders_policy_hold_request_and_tombstone() { + let policy_sql = insert_retention_policy_sql(&policy()).expect("policy"); + assert!(policy_sql.contains("INSERT INTO retention_policy")); + assert!(policy_sql.contains("retention_period_days")); + + let hold_sql = insert_legal_hold_sql(&hold()).expect("hold"); + assert!(hold_sql.contains("INSERT INTO legal_hold")); + assert!(hold_sql.contains("held_document_id")); + + let mut requested = request(); + requested.request_status_code = "requested".into(); + let request_sql = insert_deletion_request_sql(&requested).expect("request"); + assert!(request_sql.contains("INSERT INTO deletion_request")); + assert!(request_sql.contains("NULL")); + + let tombstone_sql = insert_evidence_tombstone_sql(&tombstone()).expect("tombstone"); + assert!(tombstone_sql.contains("INSERT INTO evidence_tombstone")); + assert!(tombstone_sql.contains("reproduction_status_code")); + assert!(!tombstone_sql.contains("content_sha256")); + } + + #[test] + fn succession_sql_calls_function_and_refuses_same_identity() { + let predecessor = Uuid::from_u128(1); + let sql = supersede_retention_policy_sql(predecessor, &policy()).expect("succession"); + assert!(sql.contains("SELECT supersede_retention_policy(")); + assert!(sql.contains(&predecessor.to_string())); + + let mut same = policy(); + same.retention_policy_id = predecessor; + assert_eq!( + supersede_retention_policy_sql(predecessor, &same), + Err(PersistenceError::InvalidRetentionLifecycle) + ); + + let mut superseded = policy(); + superseded.policy_status_code = "superseded".into(); + superseded.system_to = + Some(SystemTime::parse_rfc3339("2026-02-01T00:00:00Z").expect("closed")); + assert_eq!( + supersede_retention_policy_sql(predecessor, &superseded), + Err(PersistenceError::InvalidRetentionLifecycle) + ); + assert!(insert_retention_policy_sql(&superseded).is_ok()); + assert!( + insert_retention_policy_sql(&superseded) + .expect("historical superseded") + .contains("system_to") + ); + + let release = release_legal_hold_sql( + Uuid::from_u128(2), + &SystemTime::parse_rfc3339("2026-02-01T00:00:00Z").expect("release"), + ) + .expect("release sql"); + assert!(release.contains("SELECT release_legal_hold(")); + assert_eq!( + release_legal_hold_sql( + Uuid::nil(), + &SystemTime::parse_rfc3339("2026-02-01T00:00:00Z").expect("r") + ), + Err(PersistenceError::InvalidRetentionLifecycle) + ); + } + + #[test] + fn tenant_hold_renders_null_document_and_lookup_excludes_tombstones() { + let mut tenant_hold = hold(); + tenant_hold.hold_scope_code = "tenant".into(); + tenant_hold.held_document_id = None; + let sql = insert_legal_hold_sql(&tenant_hold).expect("tenant hold"); + assert!(sql.contains("NULL")); + let lookup = select_active_analysis_document_sql(Uuid::from_u128(3)); + assert!(lookup.contains("evidence_tombstone")); + assert!(lookup.contains("logical_revocation")); + let tombstone_clause = lookup + .split("FROM evidence_tombstone") + .nth(1) + .expect("tombstone exclusion"); + assert!( + tombstone_clause + .contains("deletion_kind_code IN ('logical_revocation', 'identity_tombstone')"), + "cache_export_removal tombstones must stay analysis-eligible" + ); + } + + #[test] + fn deletion_request_must_match_cited_retention_policy() { + assert_eq!(request().bind_cited_policy(&policy()), Ok(())); + + let mut mismatched_purpose = request(); + mismatched_purpose.processing_purpose_code = "export_fulfillment".into(); + assert_eq!( + mismatched_purpose.bind_cited_policy(&policy()), + Err(PersistenceError::InvalidRetentionLifecycle) + ); + + let mut mismatched_class = request(); + mismatched_class.target_data_class_code = "export_cache".into(); + assert_eq!( + mismatched_class.bind_cited_policy(&policy()), + Err(PersistenceError::InvalidRetentionLifecycle) + ); + + let mut mismatched_tenant = request(); + mismatched_tenant.tenant_record_id = Uuid::from_u128(9); + assert_eq!( + mismatched_tenant.bind_cited_policy(&policy()), + Err(PersistenceError::InvalidRetentionLifecycle) + ); + + let mut mismatched_policy_id = request(); + mismatched_policy_id.retention_policy_id = Uuid::from_u128(11); + assert_eq!( + mismatched_policy_id.bind_cited_policy(&policy()), + Err(PersistenceError::InvalidRetentionLifecycle) + ); + } + + #[test] + fn lifecycle_sql_message_maps_legal_hold_block() { + assert_eq!( + classify_lifecycle_sql_failure("ERROR: legal hold blocks deletion"), + Some(PersistenceError::LegalHoldBlocksDeletion) + ); + assert_eq!(classify_lifecycle_sql_failure("sql execution failed"), None); + } + + #[test] + fn status_window_pair_and_inverted_system_bounds_fail_closed() { + let closed = + SystemTime::parse_rfc3339("2026-02-01T00:00:00Z").expect("closed system bound"); + let before = + SystemTime::parse_rfc3339("2025-01-01T00:00:00Z").expect("before system bound"); + + let mut active_closed = policy(); + active_closed.system_to = Some(closed); + assert_eq!( + insert_retention_policy_sql(&active_closed), + Err(PersistenceError::InvalidRetentionLifecycle) + ); + + let mut superseded_open = policy(); + superseded_open.policy_status_code = "superseded".into(); + assert_eq!( + insert_retention_policy_sql(&superseded_open), + Err(PersistenceError::InvalidRetentionLifecycle) + ); + + let mut inverted = policy(); + inverted.policy_status_code = "superseded".into(); + inverted.system_to = Some(before); + assert_eq!( + insert_retention_policy_sql(&inverted), + Err(PersistenceError::InvalidRetentionLifecycle) + ); + + let mut hold_active_closed = hold(); + hold_active_closed.system_to = Some(closed); + assert_eq!( + insert_legal_hold_sql(&hold_active_closed), + Err(PersistenceError::InvalidRetentionLifecycle) + ); + assert!( + !hold_active_closed.blocks_deletion(Uuid::nil(), Uuid::from_u128(3)), + "active status with a closed system_to must not block deletion" + ); + + let mut hold_released_open = hold(); + hold_released_open.hold_status_code = "released".into(); + assert_eq!( + insert_legal_hold_sql(&hold_released_open), + Err(PersistenceError::InvalidRetentionLifecycle) + ); + + let mut hold_inverted = hold(); + hold_inverted.hold_status_code = "released".into(); + hold_inverted.system_to = Some(before); + assert_eq!( + insert_legal_hold_sql(&hold_inverted), + Err(PersistenceError::InvalidRetentionLifecycle) + ); + + let mut released_closed = hold(); + released_closed.hold_status_code = "released".into(); + released_closed.system_to = Some(closed); + insert_legal_hold_sql(&released_closed).expect("historical released hold"); + assert!( + !released_closed.blocks_deletion(Uuid::nil(), Uuid::from_u128(3)), + "released/closed holds must not block deletion" + ); + } + + #[test] + fn non_positive_period_unknown_vocab_and_hostile_labels_fail_closed() { + let mut zero = policy(); + zero.retention_period_days = 0; + assert_eq!( + insert_retention_policy_sql(&zero), + Err(PersistenceError::InvalidRetentionLifecycle) + ); + zero.retention_period_days = -1; + assert_eq!( + insert_retention_policy_sql(&zero), + Err(PersistenceError::InvalidRetentionLifecycle) + ); + + let mut unknown = policy(); + unknown.data_class_code = "notes".into(); + assert_eq!( + insert_retention_policy_sql(&unknown), + Err(PersistenceError::InvalidRetentionLifecycle) + ); + + let mut hostile = policy(); + hostile.authority_citation = "adr'; DROP TABLE".into(); + assert_eq!( + insert_retention_policy_sql(&hostile), + Err(PersistenceError::InvalidRetentionLifecycle) + ); + hostile.authority_citation.clear(); + assert_eq!( + insert_retention_policy_sql(&hostile), + Err(PersistenceError::InvalidRetentionLifecycle) + ); + hostile.authority_citation = "adr\\cite".into(); + assert_eq!( + insert_retention_policy_sql(&hostile), + Err(PersistenceError::InvalidRetentionLifecycle) + ); + hostile.authority_citation = "adr\ncite".into(); + assert_eq!( + insert_retention_policy_sql(&hostile), + Err(PersistenceError::InvalidRetentionLifecycle) + ); + } + + #[test] + fn hold_scope_mismatch_and_unknown_codes_fail_closed() { + let mut missing_document = hold(); + missing_document.held_document_id = None; + assert_eq!( + insert_legal_hold_sql(&missing_document), + Err(PersistenceError::InvalidRetentionLifecycle) + ); + + let mut tenant_with_document = hold(); + tenant_with_document.hold_scope_code = "tenant".into(); + assert_eq!( + insert_legal_hold_sql(&tenant_with_document), + Err(PersistenceError::InvalidRetentionLifecycle) + ); + + let mut unknown_scope = hold(); + unknown_scope.hold_scope_code = "project".into(); + assert_eq!( + insert_legal_hold_sql(&unknown_scope), + Err(PersistenceError::InvalidRetentionLifecycle) + ); + unknown_scope.hold_scope_code = "document".into(); + unknown_scope.hold_status_code = "pending".into(); + assert_eq!( + insert_legal_hold_sql(&unknown_scope), + Err(PersistenceError::InvalidRetentionLifecycle) + ); + unknown_scope.hold_status_code = "active".into(); + unknown_scope.hold_authority_code = "court;hold".into(); + assert_eq!( + insert_legal_hold_sql(&unknown_scope), + Err(PersistenceError::InvalidRetentionLifecycle) + ); + } + + #[test] + fn deletion_request_unknown_codes_fail_closed() { + let mut bad = request(); + bad.deletion_kind_code = "hard_delete".into(); + assert_eq!( + insert_deletion_request_sql(&bad), + Err(PersistenceError::InvalidRetentionLifecycle) + ); + bad.deletion_kind_code = "identity_tombstone".into(); + bad.request_status_code = "approved".into(); + assert_eq!( + insert_deletion_request_sql(&bad), + Err(PersistenceError::InvalidRetentionLifecycle) + ); + bad.request_status_code = "completed".into(); + bad.processing_purpose_code = "curiosity".into(); + assert_eq!( + insert_deletion_request_sql(&bad), + Err(PersistenceError::InvalidRetentionLifecycle) + ); + } + + #[test] + fn completed_deletion_fails_closed_under_matching_hold() { + assert_eq!( + insert_completed_deletion_request_sql(&request(), &[hold()]), + Err(PersistenceError::LegalHoldBlocksDeletion) + ); + + let mut tenant_hold = hold(); + tenant_hold.hold_scope_code = "tenant".into(); + tenant_hold.held_document_id = None; + assert_eq!( + insert_completed_deletion_request_sql(&request(), &[tenant_hold]), + Err(PersistenceError::LegalHoldBlocksDeletion) + ); + + let mut released = hold(); + released.hold_status_code = "released".into(); + insert_completed_deletion_request_sql(&request(), &[released]).expect("released"); + + let mut other_document = hold(); + other_document.held_document_id = Some(Uuid::from_u128(99)); + insert_completed_deletion_request_sql(&request(), &[other_document]).expect("other doc"); + + let mut other_tenant = hold(); + other_tenant.tenant_record_id = Uuid::from_u128(8); + insert_completed_deletion_request_sql(&request(), &[other_tenant]).expect("other tenant"); + + insert_completed_deletion_request_sql(&request(), &[]).expect("no holds"); + } + + #[test] + fn tombstone_refuses_invalid_digest_and_reproduction_override() { + let mut digest = tombstone(); + digest.evidence_digest = "zz".repeat(32); + assert_eq!( + insert_evidence_tombstone_sql(&digest), + Err(PersistenceError::InvalidRetentionLifecycle) + ); + digest.evidence_digest = "ab".repeat(16); + assert_eq!( + insert_evidence_tombstone_sql(&digest), + Err(PersistenceError::InvalidRetentionLifecycle) + ); + // 64-char non-hex and digit-only paths exercise every SHA-256 arm. + digest.evidence_digest = format!("{}G", "a".repeat(63)); + assert_eq!( + insert_evidence_tombstone_sql(&digest), + Err(PersistenceError::InvalidRetentionLifecycle) + ); + digest.evidence_digest = "0".repeat(64); + insert_evidence_tombstone_sql(&digest).expect("digit digest"); + + let mut override_status = tombstone(); + override_status.reproduction_status_code = "unaffected".into(); + assert_eq!( + insert_evidence_tombstone_sql(&override_status), + Err(PersistenceError::UngovernedEvidenceRestore) + ); + override_status.deletion_kind_code = "logical_revocation".into(); + assert_eq!( + insert_evidence_tombstone_sql(&override_status), + Err(PersistenceError::UngovernedEvidenceRestore) + ); + + let mut cache = tombstone(); + cache.deletion_kind_code = "cache_export_removal".into(); + cache.reproduction_status_code = "unaffected".into(); + insert_evidence_tombstone_sql(&cache).expect("cache removal"); + + let mut derived = tombstone(); + derived.target_data_class_code = "derived_result".into(); + derived.reproduction_status_code = "limited".into(); + insert_evidence_tombstone_sql(&derived).expect("derived limited"); + } +} diff --git a/crates/persistence_postgres/src/sqlx_live.rs b/crates/persistence_postgres/src/sqlx_live.rs index f9e1170c..57d930a9 100644 --- a/crates/persistence_postgres/src/sqlx_live.rs +++ b/crates/persistence_postgres/src/sqlx_live.rs @@ -5,6 +5,7 @@ //! for the success path. Unreachable-host failure is still unit-tested. use crate::PersistenceError; +use crate::classify_lifecycle_sql_failure; use crate::classify_write_conflict; use crate::live_pool::{LiveSqlxPool, LiveSqlxPoolOptions}; use crate::sqlx_gate::LiveSqlxConfig; @@ -67,6 +68,9 @@ impl SqlxTransport { } fn map_sqlx_error(error: &sqlx::Error) -> PersistenceError { + if let Some(lifecycle) = classify_lifecycle_sql_failure(&error.to_string()) { + return lifecycle; + } error .as_database_error() .and_then(sqlx::error::DatabaseError::code) diff --git a/crates/persistence_postgres/tests/live_postgres.rs b/crates/persistence_postgres/tests/live_postgres.rs index 2e404ad0..b1282bd3 100644 --- a/crates/persistence_postgres/tests/live_postgres.rs +++ b/crates/persistence_postgres/tests/live_postgres.rs @@ -7,11 +7,13 @@ #![cfg(feature = "live-sqlx")] use persistence_postgres::{ - AuditEvent, CorpusSplitManifestRecord, DocumentRecord, LiveDocumentRepository, - LiveSqlxPoolOptions, MembershipAssignmentRecord, MigrationCatalog, ModelArtifactRecord, - ModelRunRecord, PersistenceError, ReproducibilityManifestRecord, SqlSession, apply_sql_batch, - assume_app_runtime_role_sql, clear_session_tenant_sql, open_live_sqlx_pool, - require_live_sqlx_config, reset_app_runtime_role_sql, set_session_tenant_sql, + AuditEvent, CorpusSplitManifestRecord, DeletionRequestRecord, DocumentRecord, + EvidenceTombstoneRecord, LegalHoldRecord, LiveDocumentRepository, LiveSqlxPoolOptions, + MembershipAssignmentRecord, MigrationCatalog, ModelArtifactRecord, ModelRunRecord, + PersistenceError, ReproducibilityManifestRecord, RetentionPolicyRecord, SqlSession, + apply_sql_batch, assume_app_runtime_role_sql, clear_session_tenant_sql, open_live_sqlx_pool, + require_live_sqlx_config, reset_app_runtime_role_sql, select_active_analysis_document_sql, + set_session_tenant_sql, }; use std::sync::mpsc; use std::sync::{Arc, Barrier}; @@ -83,40 +85,14 @@ fn seed_source_artifact( } #[test] +#[allow(clippy::too_many_lines)] fn live_postgres_applies_migrations_and_document_sql() { if !live_postgres_requested() { // Offline default CI and local unit lanes stay database-free. return; } - let config = require_live_sqlx_config().expect( - "DATABASE_URL must be set and valid when TEPP_LIVE_POSTGRES=1 (live Postgres CI gate)", - ); - // Single connection so SET ROLE / tenant GUC session state survives between - // SqlSession::execute calls (pool acquire must reuse the same backend session). - let options = LiveSqlxPoolOptions::new(1, 5_000).expect("pool options"); - let pool = open_live_sqlx_pool(&config, options) - .expect("live-sqlx pool must open against the CI PostgreSQL service"); - assert!(pool.is_live()); - - let mut repo = LiveDocumentRepository::new(pool); - repo.session_mut() - .execute("SELECT 1") - .expect("SELECT 1 through live transport"); - apply_sql_timeouts(&mut repo, "5s", "60s"); - - let catalog = MigrationCatalog::from_embedded().expect("embedded foundation catalog"); - // Best-effort reset: empty service DBs lack tables/role; re-runs clean residual objects. - let _ = apply_sql_batch(repo.session_mut(), catalog.down_sql()); - let _ = repo - .session_mut() - .execute("DROP ROLE IF EXISTS tepp_app_runtime"); - let applied = repo - .apply_migrations(&catalog) - .expect("foundation+RLS migrations must apply on live PostgreSQL"); - assert!(applied >= 1); - repo.assert_restore_integrity() - .expect("empty restored catalog must pass integrity probes"); + let mut repo = open_migrated_live_repo(); let tenant_record_id = Uuid::now_v7(); let document_record_id = Uuid::now_v7(); @@ -130,11 +106,70 @@ fn live_postgres_applies_migrations_and_document_sql() { &content_digest, ); - let (available, valid, system) = sample_times(); + let (available, _valid, system) = sample_times(); + prove_document_insert_revise_and_audit( + &mut repo, + tenant_record_id, + document_record_id, + &content_digest, + available, + system, + ); + + // Owner inserts a reproducibility manifest for the same tenant, then proves + // digest lookup SQL remains executable on the live transport. + let manifest = ReproducibilityManifestRecord { + reproducibility_manifest_id: Uuid::now_v7(), + tenant_record_id, + knowledge_cutoff: available, + evidence_digest: content_digest, + code_commit_sha: "a".repeat(40), + // SHA-256 hex is always 64 characters (same contract as content_digest). + dependency_lock_digest: "b".repeat(64), + system_time: SystemTime::parse_rfc3339("2026-02-01T00:00:00Z").expect("sys"), + available_time: available, + }; + repo.insert_reproducibility_manifest(&manifest) + .expect("insert reproducibility_manifest"); + repo.submit_reproducibility_manifest_by_digests( + &manifest.evidence_digest, + &manifest.code_commit_sha, + &manifest.dependency_lock_digest, + ) + .expect("select reproducibility_manifest by digests"); + repo.submit_reproducibility_manifest_by_id(manifest.reproducibility_manifest_id) + .expect("select reproducibility_manifest by id"); + + exercise_model_run_artifact_chain(&mut repo, tenant_record_id, &manifest, available); + exercise_typed_membership_assignments(&mut repo, tenant_record_id, available, system); + prove_retention_deletion_legal_hold( + &mut repo, + tenant_record_id, + document_record_id, + source_artifact_id, + available, + system, + ); + prove_append_only_immutability(&mut repo, &manifest); + prove_temporal_interval_ordering(&mut repo, tenant_record_id, source_artifact_id); + apply_sql_timeouts(&mut repo, "3s", "30s"); + prove_concurrent_document_writes(&mut repo); + prove_tenant_rls_isolation(&mut repo); +} + +fn prove_document_insert_revise_and_audit( + repo: &mut LiveDocumentRepository, + tenant_record_id: Uuid, + document_record_id: Uuid, + content_digest: &str, + available: AvailableTime, + system: SystemTime, +) { + let valid = EventTime::parse_rfc3339("2026-01-01T00:00:00Z").expect("valid"); let record = DocumentRecord { document_record_id, tenant_record_id, - content_digest: content_digest.clone(), + content_digest: content_digest.to_owned(), available_time: available, valid_from: valid, valid_to: None, @@ -143,13 +178,11 @@ fn live_postgres_applies_migrations_and_document_sql() { revision_number: 1, }; repo.insert(&record).expect("insert document_record"); - let mut revised = record.clone(); revised.revision_number = 2; revised.system_from = SystemTime::parse_rfc3339("2026-02-01T00:00:00Z").expect("later system"); revised.content_digest = "cd".repeat(32); repo.revise(&revised).expect("revise document_record"); - repo.submit_as_known_at( document_record_id, &SystemTime::parse_rfc3339("2026-02-01T00:00:00Z").expect("known_at"), @@ -161,7 +194,6 @@ fn live_postgres_applies_migrations_and_document_sql() { &SystemTime::parse_rfc3339("2026-02-01T00:00:00Z").expect("known_at"), ) .expect("as-valid-at select"); - let audit = AuditEvent { audit_event_id: Uuid::now_v7(), tenant_record_id, @@ -170,38 +202,33 @@ fn live_postgres_applies_migrations_and_document_sql() { recorded_system_time: SystemTime::parse_rfc3339("2026-02-01T00:00:00Z").expect("audit"), }; repo.append_audit(&audit).expect("append audit_event"); +} - // Owner inserts a reproducibility manifest for the same tenant, then proves - // digest lookup SQL remains executable on the live transport. - let manifest = ReproducibilityManifestRecord { - reproducibility_manifest_id: Uuid::now_v7(), - tenant_record_id, - knowledge_cutoff: available, - evidence_digest: content_digest, - code_commit_sha: "a".repeat(40), - // SHA-256 hex is always 64 characters (same contract as content_digest). - dependency_lock_digest: "b".repeat(64), - system_time: SystemTime::parse_rfc3339("2026-02-01T00:00:00Z").expect("sys"), - available_time: available, - }; - repo.insert_reproducibility_manifest(&manifest) - .expect("insert reproducibility_manifest"); - repo.submit_reproducibility_manifest_by_digests( - &manifest.evidence_digest, - &manifest.code_commit_sha, - &manifest.dependency_lock_digest, - ) - .expect("select reproducibility_manifest by digests"); - repo.submit_reproducibility_manifest_by_id(manifest.reproducibility_manifest_id) - .expect("select reproducibility_manifest by id"); - - exercise_model_run_artifact_chain(&mut repo, tenant_record_id, &manifest, available); - exercise_typed_membership_assignments(&mut repo, tenant_record_id, available, system); - prove_append_only_immutability(&mut repo, &manifest); - prove_temporal_interval_ordering(&mut repo, tenant_record_id, source_artifact_id); - apply_sql_timeouts(&mut repo, "3s", "30s"); - prove_concurrent_document_writes(&mut repo); - prove_tenant_rls_isolation(&mut repo); +fn open_migrated_live_repo() -> LiveDocumentRepository { + let config = require_live_sqlx_config().expect( + "DATABASE_URL must be set and valid when TEPP_LIVE_POSTGRES=1 (live Postgres CI gate)", + ); + let options = LiveSqlxPoolOptions::new(1, 5_000).expect("pool options"); + let pool = open_live_sqlx_pool(&config, options) + .expect("live-sqlx pool must open against the CI PostgreSQL service"); + assert!(pool.is_live()); + let mut repo = LiveDocumentRepository::new(pool); + repo.session_mut() + .execute("SELECT 1") + .expect("SELECT 1 through live transport"); + apply_sql_timeouts(&mut repo, "5s", "60s"); + let catalog = MigrationCatalog::from_embedded().expect("embedded foundation catalog"); + let _ = apply_sql_batch(repo.session_mut(), catalog.down_sql()); + let _ = repo + .session_mut() + .execute("DROP ROLE IF EXISTS tepp_app_runtime"); + let applied = repo + .apply_migrations(&catalog) + .expect("foundation+RLS migrations must apply on live PostgreSQL"); + assert!(applied >= 1); + repo.assert_restore_integrity() + .expect("empty restored catalog must pass integrity probes"); + repo } fn apply_sql_timeouts( @@ -456,6 +483,215 @@ fn prove_concurrent_append_only_reject(source_artifact_id: Uuid) { } } +/// Legal hold blocks completed deletion; tombstones block restore and analysis. +#[allow(clippy::too_many_lines)] +fn prove_retention_deletion_legal_hold( + repo: &mut LiveDocumentRepository, + tenant_record_id: Uuid, + held_document_id: Uuid, + source_artifact_id: Uuid, + available: AvailableTime, + system: SystemTime, +) { + let policy = seed_retention_policy(repo, tenant_record_id, available, system); + let hold = + seed_document_legal_hold(repo, tenant_record_id, held_document_id, available, system); + prove_hold_blocks_completed_deletion(repo, &policy, &hold, held_document_id, available, system); + prove_tombstone_blocks_restore_and_analysis( + repo, + tenant_record_id, + source_artifact_id, + &policy, + available, + system, + ); +} + +fn seed_retention_policy( + repo: &mut LiveDocumentRepository, + tenant_record_id: Uuid, + available: AvailableTime, + system: SystemTime, +) -> RetentionPolicyRecord { + let policy = RetentionPolicyRecord { + retention_policy_id: Uuid::now_v7(), + tenant_record_id, + data_class_code: "raw_source".into(), + processing_purpose_code: "psychometric_analysis".into(), + retention_period_days: 365, + policy_status_code: "active".into(), + authority_citation: "adr-0009".into(), + system_time: system, + system_to: None, + available_time: available, + }; + repo.session_mut() + .execute(&set_session_tenant_sql(tenant_record_id)) + .expect("bind tenant session before lifecycle mutations"); + repo.insert_retention_policy(&policy) + .expect("insert retention_policy"); + policy +} + +fn seed_document_legal_hold( + repo: &mut LiveDocumentRepository, + tenant_record_id: Uuid, + held_document_id: Uuid, + available: AvailableTime, + system: SystemTime, +) -> LegalHoldRecord { + let hold = LegalHoldRecord { + legal_hold_id: Uuid::now_v7(), + tenant_record_id, + hold_scope_code: "document".into(), + held_document_id: Some(held_document_id), + hold_authority_code: "contract".into(), + hold_status_code: "active".into(), + authority_citation: "hold-authority".into(), + system_time: system, + system_to: None, + available_time: available, + }; + repo.insert_legal_hold(&hold).expect("insert legal_hold"); + hold +} + +fn prove_hold_blocks_completed_deletion( + repo: &mut LiveDocumentRepository, + policy: &RetentionPolicyRecord, + hold: &LegalHoldRecord, + held_document_id: Uuid, + available: AvailableTime, + system: SystemTime, +) { + let mut held_completion = DeletionRequestRecord { + deletion_request_id: Uuid::now_v7(), + tenant_record_id: policy.tenant_record_id, + retention_policy_id: policy.retention_policy_id, + target_document_id: held_document_id, + target_data_class_code: "raw_source".into(), + processing_purpose_code: "psychometric_analysis".into(), + deletion_kind_code: "identity_tombstone".into(), + request_status_code: "completed".into(), + legal_hold_id: None, + system_time: system, + available_time: available, + }; + assert_eq!( + repo.insert_completed_deletion_request( + &held_completion, + policy, + std::slice::from_ref(hold) + ), + Err(PersistenceError::LegalHoldBlocksDeletion), + "application layer must refuse completed deletion under an active hold" + ); + assert_eq!( + repo.insert_deletion_request(&held_completion, policy), + Err(PersistenceError::LegalHoldBlocksDeletion), + "database trigger must refuse completed deletion under an active hold" + ); + held_completion.deletion_request_id = Uuid::now_v7(); + held_completion.request_status_code = "blocked_by_hold".into(); + held_completion.legal_hold_id = Some(hold.legal_hold_id); + repo.insert_deletion_request(&held_completion, policy) + .expect("blocked_by_hold request must persist"); +} + +fn prove_tombstone_blocks_restore_and_analysis( + repo: &mut LiveDocumentRepository, + tenant_record_id: Uuid, + source_artifact_id: Uuid, + policy: &RetentionPolicyRecord, + available: AvailableTime, + system: SystemTime, +) { + let unheld_document_id = Uuid::now_v7(); + let unheld = DocumentRecord { + document_record_id: unheld_document_id, + tenant_record_id, + content_digest: "ef".repeat(32), + available_time: available, + valid_from: EventTime::parse_rfc3339("2026-01-01T00:00:00Z").expect("valid"), + valid_to: None, + system_from: system, + system_to: None, + revision_number: 1, + }; + repo.session_mut() + .execute(&format!( + "INSERT INTO document_record ( document_record_id, tenant_record_id, source_artifact_id, content_sha256, language_profile_code, assertion_time, document_time, valid_from, valid_to, system_from, system_to, available_time, revision_number ) VALUES ( '{unheld_document_id}'::uuid, '{tenant_record_id}'::uuid, '{source_artifact_id}'::uuid, '{digest}', 'und', NULL, NULL, '{valid}'::timestamptz, NULL, '{system}'::timestamptz, NULL, '{available}'::timestamptz, 1 )", + digest = unheld.content_digest, + valid = unheld.valid_from.to_rfc3339(), + system = system.to_rfc3339(), + available = available.to_rfc3339(), + )) + .expect("insert unheld document_record"); + + let completed = DeletionRequestRecord { + deletion_request_id: Uuid::now_v7(), + tenant_record_id, + retention_policy_id: policy.retention_policy_id, + target_document_id: unheld_document_id, + target_data_class_code: "raw_source".into(), + processing_purpose_code: "psychometric_analysis".into(), + deletion_kind_code: "identity_tombstone".into(), + request_status_code: "completed".into(), + legal_hold_id: None, + system_time: system, + available_time: available, + }; + repo.insert_completed_deletion_request(&completed, policy, &[]) + .expect("completed deletion without a matching hold"); + + let tombstone = EvidenceTombstoneRecord { + evidence_tombstone_id: Uuid::now_v7(), + tenant_record_id, + tombstoned_document_id: unheld_document_id, + deletion_request_id: completed.deletion_request_id, + evidence_digest: "cd".repeat(32), + target_data_class_code: "raw_source".into(), + deletion_kind_code: "identity_tombstone".into(), + reproduction_status_code: "unavailable".into(), + system_time: system, + available_time: available, + }; + repo.insert_evidence_tombstone(&tombstone) + .expect("insert evidence_tombstone"); + + let restore = format!( + "INSERT INTO document_record ( document_record_id, tenant_record_id, source_artifact_id, content_sha256, language_profile_code, assertion_time, document_time, valid_from, valid_to, system_from, system_to, available_time, revision_number ) VALUES ( '{unheld_document_id}'::uuid, '{tenant_record_id}'::uuid, '{source_artifact_id}'::uuid, '{digest}', 'und', NULL, NULL, '{valid}'::timestamptz, NULL, '2026-03-01T00:00:00Z'::timestamptz, NULL, '{available}'::timestamptz, 2 )", + digest = unheld.content_digest, + valid = unheld.valid_from.to_rfc3339(), + available = available.to_rfc3339(), + ); + assert!( + repo.session_mut().execute(&restore).is_err(), + "tombstoned document identities must not be restored" + ); + + let eligibility = format!( + "DO $tepp$ BEGIN IF EXISTS ( SELECT 1 FROM document_record WHERE document_record_id = '{unheld_document_id}'::uuid AND system_to IS NULL AND NOT EXISTS ( SELECT 1 FROM evidence_tombstone WHERE tombstoned_document_id = document_record.document_record_id AND deletion_kind_code IN ('logical_revocation', 'identity_tombstone') ) ) THEN RAISE EXCEPTION 'tombstoned document remained analysis-eligible'; END IF; IF EXISTS ( SELECT 1 FROM evidence_tombstone WHERE evidence_tombstone_id = '{tombstone}'::uuid AND reproduction_status_code <> 'unavailable' ) THEN RAISE EXCEPTION 'identity tombstone must record unavailable reproduction'; END IF; END $tepp$", + tombstone = tombstone.evidence_tombstone_id, + ); + repo.session_mut() + .execute(&eligibility) + .expect("tombstoned evidence must be analysis-ineligible"); + repo.submit_active_analysis_document(unheld_document_id) + .expect("active-analysis select must remain executable"); + let exclusion = format!( + "DO $tepp$ BEGIN \ + IF EXISTS ({select}) THEN \ + RAISE EXCEPTION 'active-analysis select returned a tombstoned document'; \ + END IF; \ + END $tepp$", + select = select_active_analysis_document_sql(unheld_document_id), + ); + repo.session_mut() + .execute(&exclusion) + .expect("active-analysis select must exclude tombstones"); +} + /// Append-only triggers must reject UPDATE/DELETE on identity tables. fn prove_append_only_immutability( repo: &mut LiveDocumentRepository, diff --git a/crates/persistence_postgres/tests/migration_ordering_contract.rs b/crates/persistence_postgres/tests/migration_ordering_contract.rs new file mode 100644 index 00000000..c1a7db25 --- /dev/null +++ b/crates/persistence_postgres/tests/migration_ordering_contract.rs @@ -0,0 +1,91 @@ +//! Execution-order contracts for lifecycle migration routines and rollback. +//! +//! `PostgreSQL` resolves routines and trigger dependencies statement by statement. +//! These tests prevent privilege statements from preceding routine definitions +//! and prevent rollback from dropping routines while dependent triggers exist. + +const RETENTION_UP_SQL: &str = + include_str!("../../../migrations/0007_retention_deletion_legal_hold.up.sql"); +const RETENTION_DOWN_SQL: &str = + include_str!("../../../migrations/0007_retention_deletion_legal_hold.down.sql"); + +fn statement_position(sql: &str, needle: &str) -> usize { + sql.find(needle) + .unwrap_or_else(|| panic!("missing migration statement: {needle}")) +} + +#[test] +fn lifecycle_routine_privileges_follow_routine_definitions() { + let ordered_pairs = [ + ( + "CREATE OR REPLACE FUNCTION enforce_retention_policy_succession", + "REVOKE ALL ON FUNCTION enforce_retention_policy_succession", + ), + ( + "CREATE OR REPLACE FUNCTION supersede_retention_policy", + "REVOKE ALL ON FUNCTION supersede_retention_policy", + ), + ( + "CREATE OR REPLACE FUNCTION supersede_retention_policy", + "GRANT EXECUTE ON FUNCTION supersede_retention_policy", + ), + ( + "CREATE OR REPLACE FUNCTION reject_held_evidence_deletion", + "REVOKE ALL ON FUNCTION reject_held_evidence_deletion", + ), + ( + "CREATE OR REPLACE FUNCTION reject_tombstoned_evidence_restore", + "REVOKE ALL ON FUNCTION reject_tombstoned_evidence_restore", + ), + ( + "CREATE OR REPLACE FUNCTION guard_legal_hold_insert", + "REVOKE ALL ON FUNCTION guard_legal_hold_insert", + ), + ( + "CREATE OR REPLACE FUNCTION guard_deletion_request_policy", + "REVOKE ALL ON FUNCTION guard_deletion_request_policy", + ), + ( + "CREATE OR REPLACE FUNCTION enforce_legal_hold_release", + "REVOKE ALL ON FUNCTION enforce_legal_hold_release", + ), + ( + "CREATE OR REPLACE FUNCTION release_legal_hold", + "REVOKE ALL ON FUNCTION release_legal_hold", + ), + ( + "CREATE OR REPLACE FUNCTION release_legal_hold", + "GRANT EXECUTE ON FUNCTION release_legal_hold", + ), + ]; + + for (definition, privilege) in ordered_pairs { + assert!( + statement_position(RETENTION_UP_SQL, definition) + < statement_position(RETENTION_UP_SQL, privilege), + "routine privileges must follow their definition: {privilege}" + ); + } +} + +#[test] +fn rollback_drops_lifecycle_routines_after_trigger_cleanup() { + let trigger_cleanup_end = statement_position(RETENTION_DOWN_SQL, "$tepp$;"); + let routine_drops = [ + "DROP FUNCTION IF EXISTS release_legal_hold", + "DROP FUNCTION IF EXISTS supersede_retention_policy", + "DROP FUNCTION IF EXISTS enforce_retention_policy_succession", + "DROP FUNCTION IF EXISTS enforce_legal_hold_release", + "DROP FUNCTION IF EXISTS reject_tombstoned_evidence_restore", + "DROP FUNCTION IF EXISTS reject_held_evidence_deletion", + "DROP FUNCTION IF EXISTS guard_legal_hold_insert", + "DROP FUNCTION IF EXISTS guard_deletion_request_policy", + ]; + + for routine_drop in routine_drops { + assert!( + trigger_cleanup_end < statement_position(RETENTION_DOWN_SQL, routine_drop), + "rollback must remove dependent triggers before dropping routines: {routine_drop}" + ); + } +} diff --git a/crates/persistence_postgres/tests/retention_deletion_legal_hold_contract.rs b/crates/persistence_postgres/tests/retention_deletion_legal_hold_contract.rs new file mode 100644 index 00000000..35c4c06b --- /dev/null +++ b/crates/persistence_postgres/tests/retention_deletion_legal_hold_contract.rs @@ -0,0 +1,159 @@ +//! Retention, deletion, and legal-hold SQL must refuse ungoverned restore. + +use persistence_postgres::{MigrationCatalog, validate_migration_catalog}; + +fn normalized(sql: &str) -> String { + sql.split_whitespace() + .collect::>() + .join(" ") + .to_ascii_lowercase() +} + +#[test] +fn embedded_catalog_declares_retention_deletion_and_legal_hold() { + let catalog = MigrationCatalog::from_embedded().expect("embedded migration catalog"); + validate_migration_catalog(&catalog).expect("embedded migration contract"); + + let up_sql = normalized(catalog.up_sql()); + assert!( + up_sql.contains("create table retention_policy"), + "policy-driven retention must be a first-class table" + ); + assert!( + up_sql.contains("create table legal_hold"), + "legal hold must be recorded with authority, not inferred" + ); + assert!( + up_sql.contains("create table deletion_request"), + "deletion must be an auditable request, not a silent DELETE" + ); + assert!( + up_sql.contains("create table evidence_tombstone"), + "source deletion must leave a tombstone without raw PII" + ); + assert!( + up_sql.contains("create or replace function reject_held_evidence_deletion"), + "an active legal hold must block completed deletion" + ); + assert!( + up_sql.contains("create or replace function reject_tombstoned_evidence_restore"), + "tombstoned document identities must not be restored" + ); + assert!( + up_sql.contains("constraint retention_policy_period_positive"), + "retention periods must be strictly positive" + ); + assert!( + up_sql.contains("constraint legal_hold_document_scope_consistent"), + "document-scoped holds must name the held document" + ); +} + +#[test] +fn retention_policy_succession_is_atomic_and_single_active() { + let catalog = MigrationCatalog::from_embedded().expect("embedded migration catalog"); + let up_sql = normalized(catalog.up_sql()); + + assert!( + up_sql.contains("system_to timestamptz"), + "policy history must close the prior system-time version" + ); + assert!( + up_sql.contains("supersedes_retention_policy_id uuid"), + "replacement policies must identify their predecessor" + ); + assert!( + up_sql.contains("create or replace function supersede_retention_policy"), + "policy succession must be one atomic database operation" + ); + assert!( + up_sql.contains("where policy_status_code = 'active' and system_to is null"), + "only the open active policy may occupy a tenant/class/purpose key" + ); + assert!( + up_sql.contains("create trigger retention_policy_enforce_succession"), + "ordinary policy mutation must remain fail closed" + ); + assert!( + up_sql.contains("create or replace function release_legal_hold"), + "legal holds must have a controlled release path" + ); + assert!( + up_sql.contains("create index evidence_tombstone_document_lookup"), + "tombstone restore checks need an indexed lookup" + ); + assert!( + up_sql.contains("pg_advisory_xact_lock"), + "hold and deletion races must be serialized" + ); +} + +#[test] +fn lifecycle_trigger_functions_are_tenant_bound_security_definers() { + let catalog = MigrationCatalog::from_embedded().expect("embedded migration catalog"); + let up_sql = normalized(catalog.up_sql()); + + for function_name in [ + "reject_held_evidence_deletion", + "reject_tombstoned_evidence_restore", + "supersede_retention_policy", + "release_legal_hold", + "guard_legal_hold_insert", + ] { + let start = up_sql + .find(&format!("create or replace function {function_name}")) + .expect("lifecycle function must exist"); + let suffix = &up_sql[start..]; + let end = suffix.find("$$;").expect("function body must close") + 3; + let function_sql = &suffix[..end]; + assert!( + function_sql.contains("security definer"), + "{function_name} must not lose protected rows behind FORCE RLS" + ); + assert!( + function_sql.contains("set search_path = public, pg_temp"), + "{function_name} must pin a trusted search path" + ); + assert!( + function_sql.contains("tepp.current_tenant_record_id"), + "{function_name} must require tenant session context" + ); + } +} + +#[test] +fn rollback_drops_retention_lifecycle_tables_and_functions() { + let catalog = MigrationCatalog::from_embedded().expect("embedded migration catalog"); + let down_sql = normalized(catalog.down_sql()); + assert!( + down_sql.contains("drop trigger if exists document_record_reject_tombstone_restore"), + "0007 rollback must detach the document restore tripwire" + ); + assert!( + down_sql.contains("drop function if exists supersede_retention_policy"), + "0007 rollback must remove the succession API" + ); + assert!( + down_sql.contains("drop function if exists enforce_retention_policy_succession"), + "0007 rollback must remove the succession mutation guard" + ); + assert!( + down_sql.contains("drop function if exists release_legal_hold"), + "0007 rollback must remove the legal-hold release API" + ); + assert!( + down_sql.contains("to_regclass('public.legal_hold')"), + "trigger drops must guard missing tables during partial rollback" + ); + for table in [ + "evidence_tombstone", + "deletion_request", + "legal_hold", + "retention_policy", + ] { + assert!( + down_sql.contains(&format!("'{table}'")), + "0007 rollback must drop {table}" + ); + } +} diff --git a/crates/persistence_postgres/tests/retention_lifecycle_window_contract.rs b/crates/persistence_postgres/tests/retention_lifecycle_window_contract.rs new file mode 100644 index 00000000..91e9b44f --- /dev/null +++ b/crates/persistence_postgres/tests/retention_lifecycle_window_contract.rs @@ -0,0 +1,112 @@ +//! Retention policy and legal-hold windows must fail closed at every boundary. + +use persistence_postgres::{ + LegalHoldRecord, PersistenceError, RetentionPolicyRecord, insert_legal_hold_sql, + insert_retention_policy_sql, +}; +use temporal_core::{AvailableTime, SystemTime}; +use uuid::Uuid; + +fn system_time(value: &str) -> SystemTime { + SystemTime::parse_rfc3339(value).expect("valid system time fixture") +} + +fn available_time() -> AvailableTime { + AvailableTime::parse_rfc3339("2026-01-01T00:00:00Z").expect("valid available time fixture") +} + +fn active_policy() -> RetentionPolicyRecord { + RetentionPolicyRecord { + retention_policy_id: Uuid::from_u128(1), + tenant_record_id: Uuid::from_u128(2), + data_class_code: "raw_source".into(), + processing_purpose_code: "psychometric_analysis".into(), + retention_period_days: 365, + policy_status_code: "active".into(), + authority_citation: "adr-0009".into(), + system_time: system_time("2026-01-01T00:00:00Z"), + system_to: None, + available_time: available_time(), + } +} + +fn active_document_hold() -> LegalHoldRecord { + LegalHoldRecord { + legal_hold_id: Uuid::from_u128(3), + tenant_record_id: Uuid::from_u128(2), + hold_scope_code: "document".into(), + held_document_id: Some(Uuid::from_u128(4)), + hold_authority_code: "contract".into(), + hold_status_code: "active".into(), + authority_citation: "hold-authority".into(), + system_time: system_time("2026-01-01T00:00:00Z"), + system_to: None, + available_time: available_time(), + } +} + +#[test] +fn retention_policy_requires_status_window_consistency_and_forward_closure() { + let mut active_with_close = active_policy(); + active_with_close.system_to = Some(system_time("2026-02-01T00:00:00Z")); + assert_eq!( + insert_retention_policy_sql(&active_with_close), + Err(PersistenceError::InvalidRetentionLifecycle) + ); + + let mut superseded_without_close = active_policy(); + superseded_without_close.policy_status_code = "superseded".into(); + assert_eq!( + insert_retention_policy_sql(&superseded_without_close), + Err(PersistenceError::InvalidRetentionLifecycle) + ); + + let mut superseded_with_reverse_close = superseded_without_close.clone(); + superseded_with_reverse_close.system_to = Some(system_time("2025-12-31T23:59:59Z")); + assert_eq!( + insert_retention_policy_sql(&superseded_with_reverse_close), + Err(PersistenceError::InvalidRetentionLifecycle) + ); + + let mut superseded_with_forward_close = superseded_without_close; + superseded_with_forward_close.system_to = Some(system_time("2026-02-01T00:00:00Z")); + insert_retention_policy_sql(&superseded_with_forward_close) + .expect("forward closed superseded policy"); +} + +#[test] +fn legal_hold_requires_status_window_consistency_and_forward_closure() { + let mut active_with_close = active_document_hold(); + active_with_close.system_to = Some(system_time("2026-02-01T00:00:00Z")); + assert_eq!( + insert_legal_hold_sql(&active_with_close), + Err(PersistenceError::InvalidRetentionLifecycle) + ); + assert!( + !active_with_close.blocks_deletion( + active_with_close.tenant_record_id, + active_with_close + .held_document_id + .expect("held document fixture") + ), + "a closed hold cannot block deletion even when its status is malformed as active" + ); + + let mut released_without_close = active_document_hold(); + released_without_close.hold_status_code = "released".into(); + assert_eq!( + insert_legal_hold_sql(&released_without_close), + Err(PersistenceError::InvalidRetentionLifecycle) + ); + + let mut released_with_reverse_close = released_without_close.clone(); + released_with_reverse_close.system_to = Some(system_time("2025-12-31T23:59:59Z")); + assert_eq!( + insert_legal_hold_sql(&released_with_reverse_close), + Err(PersistenceError::InvalidRetentionLifecycle) + ); + + let mut released_with_forward_close = released_without_close; + released_with_forward_close.system_to = Some(system_time("2026-02-01T00:00:00Z")); + insert_legal_hold_sql(&released_with_forward_close).expect("forward closed released hold"); +} diff --git a/crates/persistence_postgres/tests/retention_schema_guard_contract.rs b/crates/persistence_postgres/tests/retention_schema_guard_contract.rs new file mode 100644 index 00000000..135a156e --- /dev/null +++ b/crates/persistence_postgres/tests/retention_schema_guard_contract.rs @@ -0,0 +1,109 @@ +//! Database-layer vocabulary and tombstone/request consistency contracts. +//! +//! Application validation is not a substitute for `PostgreSQL` constraints. The +//! lifecycle migration must fail closed when callers bypass Rust renderers or +//! attempt to connect a tombstone to a different tenant, document, data class, +//! deletion kind, or incomplete deletion request. + +const RETENTION_UP_SQL: &str = + include_str!("../../../migrations/0007_retention_deletion_legal_hold.up.sql"); +const RETENTION_DOWN_SQL: &str = + include_str!("../../../migrations/0007_retention_deletion_legal_hold.down.sql"); + +#[test] +fn lifecycle_code_columns_have_database_allowlist_constraints() { + for constraint in [ + "CONSTRAINT deletion_request_kind_known", + "CONSTRAINT deletion_request_status_known", + "CONSTRAINT evidence_tombstone_kind_known", + "CONSTRAINT evidence_tombstone_reproduction_status_known", + ] { + assert!( + RETENTION_UP_SQL.contains(constraint), + "missing database allowlist constraint: {constraint}" + ); + } + + for canonical_value in [ + "'logical_revocation'", + "'cache_export_removal'", + "'identity_tombstone'", + "'requested'", + "'completed'", + "'blocked_by_hold'", + "'reproduction_limited'", + "'unavailable'", + "'limited'", + "'unaffected'", + ] { + assert!( + RETENTION_UP_SQL.contains(canonical_value), + "database vocabulary must include canonical value {canonical_value}" + ); + } +} + +#[test] +fn tombstone_insert_is_bound_to_its_completed_deletion_request() { + for required_sql in [ + "CREATE OR REPLACE FUNCTION guard_evidence_tombstone_insert", + "CREATE TRIGGER evidence_tombstone_guard_insert", + "BEFORE INSERT ON evidence_tombstone", + "deletion_request_id = NEW.deletion_request_id", + "tenant_record_id = NEW.tenant_record_id", + "target_document_id = NEW.tombstoned_document_id", + "target_data_class_code = NEW.target_data_class_code", + "deletion_kind_code = NEW.deletion_kind_code", + "request_status_code = 'completed'", + "tepp.current_tenant_record_id", + "SECURITY DEFINER", + "SET search_path = public, pg_temp", + ] { + assert!( + RETENTION_UP_SQL.contains(required_sql), + "missing fail-closed tombstone/request guard: {required_sql}" + ); + } +} + +#[test] +fn deletion_request_insert_is_bound_to_cited_retention_policy() { + for required_sql in [ + "CREATE OR REPLACE FUNCTION guard_deletion_request_policy", + "CREATE TRIGGER deletion_request_guard_policy", + "BEFORE INSERT ON deletion_request", + "retention_policy_id = NEW.retention_policy_id", + "data_class_code = NEW.target_data_class_code", + "processing_purpose_code = NEW.processing_purpose_code", + "deletion request must match cited retention policy", + ] { + assert!( + RETENTION_UP_SQL.contains(required_sql), + "missing fail-closed deletion-request/policy guard: {required_sql}" + ); + } +} + +#[test] +fn rollback_removes_tombstone_guard_before_its_function() { + let trigger = RETENTION_DOWN_SQL + .find("DROP TRIGGER IF EXISTS evidence_tombstone_guard_insert") + .expect("tombstone guard trigger cleanup"); + let function = RETENTION_DOWN_SQL + .find("DROP FUNCTION IF EXISTS guard_evidence_tombstone_insert") + .expect("tombstone guard function cleanup"); + assert!( + trigger < function, + "rollback must drop the dependent tombstone trigger before its function" + ); + let policy_trigger = RETENTION_DOWN_SQL + .find("DROP TRIGGER IF EXISTS deletion_request_guard_policy") + .expect("deletion-request policy guard trigger cleanup"); + let policy_function = RETENTION_DOWN_SQL + .find("DROP FUNCTION IF EXISTS guard_deletion_request_policy") + .expect("deletion-request policy guard function cleanup"); + assert!( + policy_trigger < policy_function, + "rollback must drop the dependent policy-guard trigger before its function" + ); +} diff --git a/crates/persistence_postgres/tests/tenant_session_binding_contract.rs b/crates/persistence_postgres/tests/tenant_session_binding_contract.rs new file mode 100644 index 00000000..1b2e55db --- /dev/null +++ b/crates/persistence_postgres/tests/tenant_session_binding_contract.rs @@ -0,0 +1,39 @@ +//! Tenant-session binding contracts for live persistence entrypoints. + +use persistence_postgres::{ + LiveDocumentRepository, RecordingSqlSession, ReproducibilityManifestRecord, +}; +use temporal_core::{AvailableTime, SystemTime}; + +#[test] +fn tenant_scoped_insert_binds_session_context() { + let tenant_record_id = uuid::Uuid::from_u128(12); + let manifest = ReproducibilityManifestRecord { + reproducibility_manifest_id: uuid::Uuid::from_u128(11), + tenant_record_id, + knowledge_cutoff: AvailableTime::parse_rfc3339("2026-01-01T00:00:00Z") + .expect("knowledge cutoff must parse"), + evidence_digest: "ab".repeat(32), + code_commit_sha: "c".repeat(40), + dependency_lock_digest: "de".repeat(32), + system_time: SystemTime::parse_rfc3339("2026-01-01T00:00:00Z") + .expect("system time must parse"), + available_time: AvailableTime::parse_rfc3339("2026-01-01T00:00:00Z") + .expect("available time must parse"), + }; + let mut repository = LiveDocumentRepository::new(RecordingSqlSession::new()); + + repository + .insert_reproducibility_manifest(&manifest) + .expect("tenant-scoped insert must succeed"); + + let executed = repository.session().executed(); + assert_eq!( + executed.len(), + 2, + "tenant-scoped insert must bind the tenant before the row mutation" + ); + assert!(executed[0].contains("tepp.current_tenant_record_id")); + assert!(executed[0].contains(&tenant_record_id.to_string())); + assert!(executed[1].contains("INSERT INTO reproducibility_manifest")); +} diff --git a/docs/ERD.md b/docs/ERD.md index 7fc04c89..f14d8eae 100644 --- a/docs/ERD.md +++ b/docs/ERD.md @@ -3,7 +3,7 @@ **Status:** Accepted logical target model with current implementation maturity explicitly marked. **Last reviewed:** 2026-08-13 -Protected main implements storage-independent domain objects plus `persistence_postgres` foundation tables (`0001`), tenant row-level security (`0002`), the model-run/artifact chain (`0003`), append-only immutability triggers (`0004`), temporal interval ordering CHECKs (`0005`), typed membership assignment (`0006`), event-relation/mention/instance SQL, source-artifact SQL, audit-event SQL, and naruon HTTP interchange contracts as executable migration/application contracts with live CI. Concurrent document-write stress (atomic revise + live multi-session proof) is on the active PR and is not implemented-main until exact-head checks, review, and protected-main integration complete. Broader planned ERD entities and backup/recovery gates remain accepted-target. +Protected main implements storage-independent domain objects plus `persistence_postgres` foundation tables (`0001`), tenant row-level security (`0002`), the model-run/artifact chain (`0003`), append-only immutability triggers (`0004`), temporal interval ordering CHECKs (`0005`), typed membership assignment (`0006`), event-relation/mention/instance SQL, source-artifact SQL, audit-event SQL, concurrent document-write stress, and naruon HTTP interchange contracts as executable migration/application contracts with live CI. Migration `0007` (active PR) adds `retention_policy`, `legal_hold`, `deletion_request`, and `evidence_tombstone` and is not implemented-main until exact-head checks, review, and protected-main integration complete. Broader planned ERD entities and backup/recovery gates remain accepted-target. ## Current domain foundation @@ -67,6 +67,11 @@ erDiagram MODEL_RUN ||--o{ FACTOR_SOLUTION : estimates FACTOR_SOLUTION ||--o{ FACTOR_LOADING : contains MODEL_RUN ||--o{ AUDIT_EVENT : evidenced_by + TENANT_RECORD ||--o{ RETENTION_POLICY : governs + TENANT_RECORD ||--o{ LEGAL_HOLD : imposes + RETENTION_POLICY ||--o{ DELETION_REQUEST : authorizes + LEGAL_HOLD ||--o{ DELETION_REQUEST : may_block + DELETION_REQUEST ||--o{ EVIDENCE_TOMBSTONE : records TENANT_RECORD { uuid tenant_record_id PK @@ -313,6 +318,46 @@ erDiagram text evidence_digest timestamptz system_time } + + RETENTION_POLICY { + uuid retention_policy_id PK + uuid tenant_record_id FK + text data_class_code + text processing_purpose_code + integer retention_period_days + text policy_status_code + text authority_citation + } + + LEGAL_HOLD { + uuid legal_hold_id PK + uuid tenant_record_id FK + text hold_scope_code + uuid held_document_id + text hold_authority_code + text hold_status_code + text authority_citation + } + + DELETION_REQUEST { + uuid deletion_request_id PK + uuid tenant_record_id FK + uuid retention_policy_id FK + uuid target_document_id + text target_data_class_code + text deletion_kind_code + text request_status_code + uuid legal_hold_id FK + } + + EVIDENCE_TOMBSTONE { + uuid evidence_tombstone_id PK + uuid tenant_record_id FK + uuid tombstoned_document_id + uuid deletion_request_id FK + text evidence_digest + text reproduction_status_code + } ``` ## Temporal/bitemporal invariants diff --git a/docs/OPERABILITY.md b/docs/OPERABILITY.md index 336fc7e6..3f0c4254 100644 --- a/docs/OPERABILITY.md +++ b/docs/OPERABILITY.md @@ -52,6 +52,8 @@ LLM-backed semantic/interpreter functions use strict bounded requests and cached ## Database target recovery +Migration `0007` (active PR) contracts policy-driven retention, legal-hold blocked deletion completion, evidence tombstones without raw-source restore, deletion requests bound to the cited policy, and analysis exclusion only for `logical_revocation`/`identity_tombstone`; live PostgreSQL evidence remains pending exact-head CI. + Before PostgreSQL becomes production state, prove migrations and rollback, tenant isolation/RLS, temporal/lineage constraints, idempotency/concurrency, backup/restore, retention/deletion, and reconstruction from immutable artifacts. Concurrent document first-insert and revise stress is implemented-main. `persistence_postgres::mark_restored_state_usable` and `assert_restore_integrity` are the current fail-closed restore gate (active PR): they revalidate tenant identity, canonical digests, same-tenant knowledge-cutoff eligibility, temporal window order, and enabled append-only triggers. They do not yet revalidate relation-aware splits or full lineage graphs; those remain separate post-restore scientific steps. The gate does not replace operator `pg_dump`/`pg_restore` runbooks. ## Model release/cutover diff --git a/docs/PRIVACY_DATA_GOVERNANCE.md b/docs/PRIVACY_DATA_GOVERNANCE.md index 429c7687..88199b21 100644 --- a/docs/PRIVACY_DATA_GOVERNANCE.md +++ b/docs/PRIVACY_DATA_GOVERNANCE.md @@ -1,7 +1,7 @@ # TEPP Privacy and Data Governance **Status:** Accepted target governance baseline; no certification claim. -**Last reviewed:** 2026-08-10 +**Last reviewed:** 2026-08-13 ## 1. Objective @@ -58,13 +58,13 @@ LLM use is optional and bounded. The default provider payload is evidence-minimi Retention is policy-driven per data class and purpose. Raw evidence, derived models, caches, provider payloads, exports, and audit records may have different retention periods. Deletion workflows distinguish: -- logical revocation from active analysis; +- logical revocation and identity tombstones from active analysis (`cache_export_removal` does not drop a document from analysis); - removal of mutable caches and exports; - deletion/tombstone of identity mappings where legally permitted; - immutable audit evidence that records the deletion action without retaining unnecessary raw PII; - legal or contractual hold that prevents deletion and records its authority. -Historical model reproducibility cannot silently override an approved deletion obligation. If source deletion makes exact reproduction unavailable, TEPP records that limitation rather than restoring deleted data from an ungoverned copy. +Historical model reproducibility cannot silently override an approved deletion obligation. If source deletion makes exact reproduction unavailable, TEPP records that limitation rather than restoring deleted data from an ungoverned copy. Migration `0007` (active PR) persists `retention_policy`, `legal_hold`, `deletion_request`, and `evidence_tombstone` so an active hold blocks completed deletion and a tombstone blocks restore of the same document identity. ## 7. Tenant and service isolation diff --git a/docs/THREAT_MODEL.md b/docs/THREAT_MODEL.md index 7b05b2d5..e041f257 100644 --- a/docs/THREAT_MODEL.md +++ b/docs/THREAT_MODEL.md @@ -46,7 +46,7 @@ No external document, model output, provider response, serialized payload, datab | Formula/active-content injection | exported artifacts execute spreadsheet/formula/HTML payloads | export escaping/sanitization, no active document execution | | Supply-chain compromise | mutable Actions/installers/dependencies alter build or release | full-SHA Actions, orphan registry-identity audit/disable, lockfiles, advisories/licenses, SBOM, SLSA-style provenance, reproducible release | | Credential exfiltration | model/test/doc obtains secrets | minimum-scope secret materialization, `NVIDIA_NIM_API_KEY` only where required, never log/prompt secrets | -| Evidence over-retention | raw source/PII remains after purpose expires | lifecycle classification, configurable retention, deletion/tombstone evidence, immutable audit digest not raw copy | +| Evidence over-retention | raw source/PII remains after purpose expires | lifecycle classification, configurable retention (`0007` `retention_policy`), deletion/tombstone evidence, legal-hold exception, immutable audit digest not raw copy | ## 4. PII handling without blanket masking diff --git a/docs/adr/0009-purpose-bound-pii-governance.md b/docs/adr/0009-purpose-bound-pii-governance.md index 26fa3ad0..229733fd 100644 --- a/docs/adr/0009-purpose-bound-pii-governance.md +++ b/docs/adr/0009-purpose-bound-pii-governance.md @@ -1,7 +1,7 @@ # ADR 0009 — Purpose-bound PII governance without blanket masking **Decision status:** Accepted -**Implementation maturity:** accepted-target +**Implementation maturity:** partial — persistence retention/deletion/legal-hold (migration `0007`) is on the active PR and is not implemented-main until exact-head checks, review, and protected-main integration complete; authorization/export/provider adapters remain accepted-target **Date:** 2026-08-10 **Supersedes:** None. diff --git a/docs/adr/README.md b/docs/adr/README.md index 1a9a7b31..5d3aa546 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -14,11 +14,11 @@ Read [`ADR_POLICY.md`](ADR_POLICY.md) first. **Decision status and implementatio | [0006](0006-vram-gpu-nvidia-orchestration.md) | VRAM-adaptive GPU compute and model-credential boundary | Accepted | accepted-target | LLM orchestration policy superseded by ADR 0010; autonomous development authority governed by ADR 0015. | | [0007](0007-rust-workspace-quality-gates.md) | Explicit Rust workspace, pinned toolchains, and exact quality gates | Accepted | implemented-main | ADR 0014 governs scientific/product claim promotion beyond repository-quality tooling. | | [0008](0008-immutable-evidence-identities-digests-and-spans.md) | Immutable evidence identities, `SHA-256` digests, exact spans, and strict wire reconstruction | Accepted | implemented-main | ADR 0013 governs future persistence/reproducibility/split authority. | -| [0009](0009-purpose-bound-pii-governance.md) | Purpose-bound PII governance without blanket masking | Accepted | accepted-target | Controls are normative architecture; deployment/control evidence is not yet a certification claim. | +| [0009](0009-purpose-bound-pii-governance.md) | Purpose-bound PII governance without blanket masking | Accepted | partial | Persistence retention/deletion/legal-hold (`0007`) is on the active PR; authorization/export/provider adapters and deployment evidence remain accepted-target. | | [0010](0010-adaptive-llm-orchestration.md) | Adaptive LLM orchestration and test-time compute | Accepted | accepted-target | Owns direct/verify/committee/conductor selection, budget, role/topology, and ablation policy. | | [0011](0011-standalone-modular-msa-boundary.md) | Standalone operation and modular CWL MSA boundary | Accepted | partial | Owns cross-service persistence/credential/API authority; no direct cross-service application-table coupling. | | [0012](0012-temporal-relational-shared-latent-topic-measurement.md) | Temporal Relational Shared-Latent Topic Measurement (TRSL-TM) | Accepted | accepted-target | Owns topic backend compatibility, global topic identity, method effects, K/model-selection prerequisites, and compositional topic coordinates. | -| [0013](0013-bitemporal-persistence-reproducibility-and-split-authority.md) | Bitemporal persistence, reproducibility manifests, and relation-aware split authority | Accepted | partial | Owns PostgreSQL adapter semantics, immutable run/split manifests, leakage-safe partitions, and recovery identity; optional `live-sqlx` `PgPool`, live PG CI, and tenant RLS implemented; full physical ERD remaining. | +| [0013](0013-bitemporal-persistence-reproducibility-and-split-authority.md) | Bitemporal persistence, reproducibility manifests, and relation-aware split authority | Accepted | partial | Owns PostgreSQL adapter semantics, immutable run/split manifests, leakage-safe partitions, and recovery identity; optional `live-sqlx` `PgPool`, live PG CI, tenant RLS, and `0006` membership implemented-main; `0007` retention/deletion/legal-hold on the active PR; remaining physical ERD/backup accepted-target. | | [0014](0014-scientific-claim-promotion-and-release-evidence.md) | Scientific claim promotion and release evidence authority | Accepted | partial | Separates design, implementation, scientific/product claim, and release authority; repository SBOM/provenance generator implemented, full release bundle remaining. | | [0015](0015-autonomous-development-review-and-merge-authority.md) | Autonomous development, review, and merge authority separation | Accepted | active-PR | Separates model proposal, deterministic verification, publication, independent review, and merge/release authority. | | [0016](0016-tdt-chronos-event-intelligence-boundary.md) | TDT, CHRONOS, and Event Ontology intelligence boundary | Accepted | accepted-target | Separates observed evidence, detection/tracking, prediction/schema inference, temporal consistency, and promoted transition authority. | diff --git a/docs/research/retention-deletion-legal-hold.md b/docs/research/retention-deletion-legal-hold.md new file mode 100644 index 00000000..e0e46969 --- /dev/null +++ b/docs/research/retention-deletion-legal-hold.md @@ -0,0 +1,37 @@ +# Retention, deletion, and legal hold (doctoring) + +## Scope + +Migration `0007` implements the persistence half of ADR 0009 / ADR 0013 lifecycle governance: + +- retention is policy-driven per tenant, data class, and purpose; +- deletion is an append-only request, not a silent `DELETE` of identity tables; +- an active legal or contractual hold blocks completed deletion; +- a tombstone records an action digest without raw source text; and +- historical reproduction cannot restore tombstoned evidence from an ungoverned copy. + +The increment does not claim GDPR, SOC 2, or CSAP sufficiency. Host/deployer authority remains outside TEPP. + +## Authority + +European Union. (2016). *Regulation (EU) 2016/679 of the European Parliament and of the Council of 27 April 2016 on the protection of natural persons with regard to the processing of personal data and on the free movement of such data (General Data Protection Regulation)*. Official Journal of the European Union, L 119, 1–88. https://eur-lex.europa.eu/eli/reg/2016/679/oj + +National Institute of Standards and Technology. (2020). *NIST privacy framework: A tool for improving privacy through enterprise risk management, version 1.0*. https://doi.org/10.6028/NIST.CSWP.01162020 + +American Institute of Certified Public Accountants. (2023). *2017 Trust services criteria for security, availability, processing integrity, confidentiality, and privacy (with revised points of focus—2022)*. AICPA & CIMA. + +Jensen, C. S., & Snodgrass, R. T. (1999). Temporal data management. *IEEE Transactions on Knowledge and Data Engineering, 11*(1), 36–44. https://doi.org/10.1109/69.755613 + +National Academies of Sciences, Engineering, and Medicine. (2019). *Reproducibility and replicability in science*. The National Academies Press. https://doi.org/10.17226/25303 + +GDPR Article 17 motivates erasure and restriction workflows, including the possibility that legal claims override erasure (European Union, 2016). The NIST Privacy Framework treats data processing, inventory, and disassociated/deleted data as risk-management outcomes rather than a single delete API (National Institute of Standards and Technology, 2020). AICPA privacy criteria require retention, disposal, and exception handling to be authorized and evidenced (American Institute of Certified Public Accountants, 2023). Bitemporal identity tables stay append-only so revocation is a later assertion, not a rewrite of valid/system history (Jensen & Snodgrass, 1999; National Academies of Sciences, Engineering, and Medicine, 2019). + +## Verification + +Defined deterministic verification includes: + +- catalog validation requiring the four lifecycle tables, hold/restore functions, triggers, period/scope constraints, policy-succession contract, fixed trigger search paths, and tenant-session guards; +- unit/contract tests refusing hostile labels, non-positive periods, mismatched hold scope, completed deletion under an active hold, invalid tombstone digests, raw-source reproduction override, invalid policy successors, and deletion requests whose tenant/class/purpose do not match the cited retention policy; and +- an exact-head live PostgreSQL procedure that is intended to insert a hold, prove application/database completion refusal as `LegalHoldBlocksDeletion`, persist a `blocked_by_hold` request, supersede one active retention policy atomically, complete deletion of an unheld document, write a tombstone with `unavailable` reproduction (never the forbidden `available` token), and prove restore and analysis eligibility fail closed for `logical_revocation`/`identity_tombstone` only. + +The live procedure is test code, not completed execution evidence. Its result remains pending until the unchanged PR head's live PostgreSQL job succeeds; queued, cancelled, predecessor-head, or local-only evidence does not promote this capability to implemented-main. \ No newline at end of file diff --git a/docs/research/standards-and-literature.md b/docs/research/standards-and-literature.md index b4b14468..75710ed3 100644 --- a/docs/research/standards-and-literature.md +++ b/docs/research/standards-and-literature.md @@ -96,6 +96,14 @@ Moreau, L., & Missier, P. (Eds.). (2013). *PROV-DM: The PROV data model*. World TEPP separates stable record identity, content equality, exact text location, wire representation, authorization, and provenance. JSON wire records are explicit versioned DTOs with unknown-field rejection and reconstruct through domain validation. `SHA-256` detects content substitution but is not treated as proof of origin, authority, or chain of custody. +## Privacy lifecycle, retention, and legal hold + +European Union. (2016). *Regulation (EU) 2016/679 of the European Parliament and of the Council of 27 April 2016 on the protection of natural persons with regard to the processing of personal data and on the free movement of such data (General Data Protection Regulation)*. Official Journal of the European Union, L 119, 1–88. https://eur-lex.europa.eu/eli/reg/2016/679/oj + +National Institute of Standards and Technology. (2020). *NIST privacy framework: A tool for improving privacy through enterprise risk management, version 1.0*. https://doi.org/10.6028/NIST.CSWP.01162020 + +TEPP uses these sources, together with the AICPA Trust Services Criteria cited below, as readiness inputs for purpose-bound retention, deletion, and legal hold. They are not self-certification authority. Persistence migration `0007` records policy, hold, deletion requests, and evidence tombstones; it does not assert that a deployment is lawful under GDPR Article 17 or attested under SOC 2. + ## AI risk, management systems, and assurance readiness International Organization for Standardization. (2023a). *Information technology—Artificial intelligence—Guidance on risk management* (ISO/IEC Standard No. 23894:2023). https://www.iso.org/standard/77304.html diff --git a/migrations/0007_retention_deletion_legal_hold.down.sql b/migrations/0007_retention_deletion_legal_hold.down.sql new file mode 100644 index 00000000..74970cde --- /dev/null +++ b/migrations/0007_retention_deletion_legal_hold.down.sql @@ -0,0 +1,65 @@ +-- Rollback for 0007_retention_deletion_legal_hold. + +DO $tepp$ +DECLARE + table_name text; +BEGIN + IF to_regclass('public.document_record') IS NOT NULL THEN + DROP TRIGGER IF EXISTS document_record_reject_tombstone_restore ON public.document_record; + END IF; + IF to_regclass('public.retention_policy') IS NOT NULL THEN + DROP TRIGGER IF EXISTS retention_policy_enforce_succession ON public.retention_policy; + DROP TRIGGER IF EXISTS retention_policy_reject_truncate ON public.retention_policy; + END IF; + IF to_regclass('public.legal_hold') IS NOT NULL THEN + DROP TRIGGER IF EXISTS legal_hold_enforce_release ON public.legal_hold; + DROP TRIGGER IF EXISTS legal_hold_guard_insert ON public.legal_hold; + DROP TRIGGER IF EXISTS legal_hold_reject_mutation ON public.legal_hold; + END IF; + IF to_regclass('public.deletion_request') IS NOT NULL THEN + DROP TRIGGER IF EXISTS deletion_request_reject_held_deletion ON public.deletion_request; + DROP TRIGGER IF EXISTS deletion_request_guard_policy ON public.deletion_request; + DROP TRIGGER IF EXISTS deletion_request_reject_mutation ON public.deletion_request; + END IF; + IF to_regclass('public.evidence_tombstone') IS NOT NULL THEN + DROP TRIGGER IF EXISTS evidence_tombstone_guard_insert ON public.evidence_tombstone; + DROP TRIGGER IF EXISTS evidence_tombstone_reject_mutation ON public.evidence_tombstone; + END IF; + + FOREACH table_name IN ARRAY ARRAY[ + 'evidence_tombstone', + 'deletion_request', + 'legal_hold', + 'retention_policy' + ] + LOOP + IF to_regclass(format('public.%I', table_name)) IS NOT NULL THEN + EXECUTE format( + 'DROP POLICY IF EXISTS %I ON public.%I', + table_name || '_tenant_isolation', + table_name + ); + EXECUTE format( + 'ALTER TABLE public.%I NO FORCE ROW LEVEL SECURITY', + table_name + ); + EXECUTE format('ALTER TABLE public.%I DISABLE ROW LEVEL SECURITY', table_name); + EXECUTE format( + 'REVOKE ALL ON TABLE public.%I FROM tepp_app_runtime', + table_name + ); + EXECUTE format('DROP TABLE IF EXISTS public.%I', table_name); + END IF; + END LOOP; +END +$tepp$; + +DROP FUNCTION IF EXISTS release_legal_hold(uuid, timestamptz); +DROP FUNCTION IF EXISTS supersede_retention_policy(uuid, uuid, integer, text, timestamptz, timestamptz); +DROP FUNCTION IF EXISTS enforce_retention_policy_succession(); +DROP FUNCTION IF EXISTS enforce_legal_hold_release(); +DROP FUNCTION IF EXISTS reject_tombstoned_evidence_restore(); +DROP FUNCTION IF EXISTS reject_held_evidence_deletion(); +DROP FUNCTION IF EXISTS guard_deletion_request_policy(); +DROP FUNCTION IF EXISTS guard_legal_hold_insert(); +DROP FUNCTION IF EXISTS guard_evidence_tombstone_insert(); diff --git a/migrations/0007_retention_deletion_legal_hold.up.sql b/migrations/0007_retention_deletion_legal_hold.up.sql new file mode 100644 index 00000000..2ffba95b --- /dev/null +++ b/migrations/0007_retention_deletion_legal_hold.up.sql @@ -0,0 +1,695 @@ +-- TEPP retention, deletion, and legal hold (ADR 0009 / ADR 0013). +-- Policy-driven lifecycle is append-only except for one controlled system-time +-- succession transition. Completed deletion cannot override an active legal +-- hold, and a tombstone blocks restore of the same document identity. +-- Tombstones store action digests, never raw source text. + +CREATE TABLE retention_policy ( + retention_policy_id uuid PRIMARY KEY, + tenant_record_id uuid NOT NULL REFERENCES tenant_record (tenant_record_id), + data_class_code text NOT NULL, + processing_purpose_code text NOT NULL, + retention_period_days integer NOT NULL, + policy_status_code text NOT NULL, + authority_citation text NOT NULL, + system_time timestamptz NOT NULL, + system_to timestamptz, + available_time timestamptz NOT NULL, + supersedes_retention_policy_id uuid REFERENCES retention_policy (retention_policy_id), + CONSTRAINT retention_policy_period_positive CHECK (retention_period_days > 0), + CONSTRAINT retention_policy_status_known CHECK ( + policy_status_code IN ('active', 'superseded') + ), + CONSTRAINT retention_policy_system_order CHECK ( + system_to IS NULL OR system_time <= system_to + ), + CONSTRAINT retention_policy_status_window_consistent CHECK ( + (policy_status_code = 'active' AND system_to IS NULL) + OR (policy_status_code = 'superseded' AND system_to IS NOT NULL) + ) +); + +CREATE TABLE legal_hold ( + legal_hold_id uuid PRIMARY KEY, + tenant_record_id uuid NOT NULL REFERENCES tenant_record (tenant_record_id), + hold_scope_code text NOT NULL, + held_document_id uuid, + hold_authority_code text NOT NULL, + hold_status_code text NOT NULL, + authority_citation text NOT NULL, + system_time timestamptz NOT NULL, + system_to timestamptz, + available_time timestamptz NOT NULL, + CONSTRAINT legal_hold_status_known CHECK ( + hold_status_code IN ('active', 'released') + ), + CONSTRAINT legal_hold_system_order CHECK ( + system_to IS NULL OR system_time <= system_to + ), + CONSTRAINT legal_hold_status_window_consistent CHECK ( + (hold_status_code = 'active' AND system_to IS NULL) + OR (hold_status_code = 'released' AND system_to IS NOT NULL) + ), + CONSTRAINT legal_hold_document_scope_consistent CHECK ( + (hold_scope_code = 'document' AND held_document_id IS NOT NULL) + OR (hold_scope_code = 'tenant' AND held_document_id IS NULL) + ) +); + +CREATE TABLE deletion_request ( + deletion_request_id uuid PRIMARY KEY, + tenant_record_id uuid NOT NULL REFERENCES tenant_record (tenant_record_id), + retention_policy_id uuid NOT NULL REFERENCES retention_policy (retention_policy_id), + target_document_id uuid NOT NULL, + target_data_class_code text NOT NULL, + processing_purpose_code text NOT NULL, + deletion_kind_code text NOT NULL, + request_status_code text NOT NULL, + legal_hold_id uuid REFERENCES legal_hold (legal_hold_id), + system_time timestamptz NOT NULL, + available_time timestamptz NOT NULL, + CONSTRAINT deletion_request_kind_known CHECK ( + deletion_kind_code IN ( + 'logical_revocation', + 'cache_export_removal', + 'identity_tombstone' + ) + ), + CONSTRAINT deletion_request_status_known CHECK ( + request_status_code IN ( + 'requested', + 'completed', + 'blocked_by_hold', + 'reproduction_limited' + ) + ) +); + +CREATE TABLE evidence_tombstone ( + evidence_tombstone_id uuid PRIMARY KEY, + tenant_record_id uuid NOT NULL REFERENCES tenant_record (tenant_record_id), + tombstoned_document_id uuid NOT NULL, + deletion_request_id uuid NOT NULL REFERENCES deletion_request (deletion_request_id), + evidence_digest text NOT NULL, + target_data_class_code text NOT NULL, + deletion_kind_code text NOT NULL, + reproduction_status_code text NOT NULL, + system_time timestamptz NOT NULL, + available_time timestamptz NOT NULL, + CONSTRAINT evidence_tombstone_kind_known CHECK ( + deletion_kind_code IN ( + 'logical_revocation', + 'cache_export_removal', + 'identity_tombstone' + ) + ), + CONSTRAINT evidence_tombstone_reproduction_status_known CHECK ( + reproduction_status_code IN ('unavailable', 'limited', 'unaffected') + ) +); + +CREATE UNIQUE INDEX retention_policy_active_purpose_unique + ON retention_policy (tenant_record_id, data_class_code, processing_purpose_code) + WHERE policy_status_code = 'active' AND system_to IS NULL; + +CREATE UNIQUE INDEX retention_policy_successor_unique + ON retention_policy (supersedes_retention_policy_id) + WHERE supersedes_retention_policy_id IS NOT NULL; + +CREATE UNIQUE INDEX legal_hold_active_document_unique + ON legal_hold (tenant_record_id, held_document_id) + WHERE hold_status_code = 'active' + AND system_to IS NULL + AND hold_scope_code = 'document' + AND held_document_id IS NOT NULL; + +CREATE UNIQUE INDEX legal_hold_active_tenant_unique + ON legal_hold (tenant_record_id) + WHERE hold_status_code = 'active' + AND system_to IS NULL + AND hold_scope_code = 'tenant'; + +CREATE INDEX evidence_tombstone_document_lookup + ON evidence_tombstone (tenant_record_id, tombstoned_document_id); + +CREATE INDEX legal_hold_active_scope_lookup + ON legal_hold (tenant_record_id, hold_scope_code, held_document_id) + WHERE hold_status_code = 'active' AND system_to IS NULL; + +CREATE INDEX deletion_request_retention_policy_id_idx + ON deletion_request (retention_policy_id); + +CREATE INDEX deletion_request_legal_hold_id_idx + ON deletion_request (legal_hold_id) + WHERE legal_hold_id IS NOT NULL; + +CREATE INDEX evidence_tombstone_deletion_request_id_idx + ON evidence_tombstone (deletion_request_id); + +CREATE INDEX deletion_request_completed_scope_lookup + ON deletion_request (tenant_record_id, system_time, target_document_id) + WHERE request_status_code = 'completed'; + +CREATE OR REPLACE FUNCTION enforce_retention_policy_succession() +RETURNS trigger +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = public, pg_temp +AS $$ +BEGIN + IF TG_OP = 'DELETE' THEN + RAISE EXCEPTION 'retention policy history is append-only' + USING ERRCODE = 'integrity_constraint_violation'; + END IF; + + IF OLD.policy_status_code <> 'active' + OR OLD.system_to IS NOT NULL + OR NEW.policy_status_code <> 'superseded' + OR NEW.system_to IS NULL + OR NEW.system_to < OLD.system_time + OR NEW.retention_policy_id IS DISTINCT FROM OLD.retention_policy_id + OR NEW.tenant_record_id IS DISTINCT FROM OLD.tenant_record_id + OR NEW.data_class_code IS DISTINCT FROM OLD.data_class_code + OR NEW.processing_purpose_code IS DISTINCT FROM OLD.processing_purpose_code + OR NEW.retention_period_days IS DISTINCT FROM OLD.retention_period_days + OR NEW.authority_citation IS DISTINCT FROM OLD.authority_citation + OR NEW.system_time IS DISTINCT FROM OLD.system_time + OR NEW.available_time IS DISTINCT FROM OLD.available_time + OR NEW.supersedes_retention_policy_id IS DISTINCT FROM OLD.supersedes_retention_policy_id + THEN + RAISE EXCEPTION 'retention policy mutation must be a controlled succession' + USING ERRCODE = 'integrity_constraint_violation'; + END IF; + + RETURN NEW; +END; +$$; + +CREATE TRIGGER retention_policy_enforce_succession + BEFORE UPDATE OR DELETE ON retention_policy + FOR EACH ROW + EXECUTE FUNCTION enforce_retention_policy_succession(); + +CREATE TRIGGER retention_policy_reject_truncate + BEFORE TRUNCATE ON retention_policy + FOR EACH STATEMENT + EXECUTE FUNCTION reject_append_only_mutation(); + +CREATE OR REPLACE FUNCTION supersede_retention_policy( + current_retention_policy_id uuid, + replacement_retention_policy_id uuid, + replacement_retention_period_days integer, + replacement_authority_citation text, + replacement_system_time timestamptz, + replacement_available_time timestamptz +) +RETURNS uuid +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = public, pg_temp +AS $$ +DECLARE + tenant_context text; + current_policy retention_policy%ROWTYPE; +BEGIN + tenant_context := nullif(current_setting('tepp.current_tenant_record_id', true), ''); + IF tenant_context IS NULL THEN + RAISE EXCEPTION 'tenant session context is required for retention policy succession' + USING ERRCODE = 'integrity_constraint_violation'; + END IF; + IF current_retention_policy_id = replacement_retention_policy_id + OR replacement_retention_period_days <= 0 + OR replacement_authority_citation IS NULL + OR btrim(replacement_authority_citation) = '' + THEN + RAISE EXCEPTION 'invalid retention policy successor' + USING ERRCODE = 'integrity_constraint_violation'; + END IF; + + SELECT * + INTO current_policy + FROM public.retention_policy + WHERE retention_policy_id = current_retention_policy_id + FOR UPDATE; + + IF NOT FOUND + OR current_policy.tenant_record_id::text <> tenant_context + OR current_policy.policy_status_code <> 'active' + OR current_policy.system_to IS NOT NULL + OR replacement_system_time < current_policy.system_time + OR replacement_available_time < current_policy.available_time + THEN + RAISE EXCEPTION 'retention policy successor is not authorized for the active version' + USING ERRCODE = 'integrity_constraint_violation'; + END IF; + + UPDATE public.retention_policy + SET policy_status_code = 'superseded', + system_to = replacement_system_time + WHERE retention_policy_id = current_retention_policy_id; + + INSERT INTO public.retention_policy ( + retention_policy_id, + tenant_record_id, + data_class_code, + processing_purpose_code, + retention_period_days, + policy_status_code, + authority_citation, + system_time, + system_to, + available_time, + supersedes_retention_policy_id + ) VALUES ( + replacement_retention_policy_id, + current_policy.tenant_record_id, + current_policy.data_class_code, + current_policy.processing_purpose_code, + replacement_retention_period_days, + 'active', + replacement_authority_citation, + replacement_system_time, + NULL, + replacement_available_time, + current_retention_policy_id + ); + + RETURN replacement_retention_policy_id; +END; +$$; + +CREATE OR REPLACE FUNCTION reject_held_evidence_deletion() +RETURNS trigger +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = public, pg_temp +AS $$ +DECLARE + tenant_context text; +BEGIN + tenant_context := nullif(current_setting('tepp.current_tenant_record_id', true), ''); + IF tenant_context IS NULL OR NEW.tenant_record_id::text <> tenant_context THEN + RAISE EXCEPTION 'tenant session context is required for lifecycle mutation' + USING ERRCODE = 'integrity_constraint_violation'; + END IF; + IF NEW.request_status_code = 'completed' THEN + -- Acquire the tenant lock first so tenant-wide and document holds share + -- one deadlock-free order. Each call uses PostgreSQL's single-bigint + -- advisory-lock overload. + PERFORM pg_advisory_xact_lock( + hashtextextended( + 'tepp:lifecycle:tenant:' || NEW.tenant_record_id::text, + 0 + ) + ); + PERFORM pg_advisory_xact_lock( + hashtextextended( + 'tepp:lifecycle:document:' + || NEW.tenant_record_id::text + || ':' + || NEW.target_document_id::text, + 0 + ) + ); + END IF; + IF NEW.request_status_code = 'completed' AND EXISTS ( + SELECT 1 + FROM public.legal_hold + WHERE tenant_record_id = NEW.tenant_record_id + AND hold_status_code = 'active' + AND system_to IS NULL + AND ( + hold_scope_code = 'tenant' + OR ( + hold_scope_code = 'document' + AND held_document_id = NEW.target_document_id + ) + ) + ) THEN + RAISE EXCEPTION 'legal hold blocks deletion' + USING ERRCODE = 'integrity_constraint_violation'; + END IF; + RETURN NEW; +END; +$$; + +CREATE TRIGGER deletion_request_reject_held_deletion + BEFORE INSERT ON deletion_request + FOR EACH ROW + EXECUTE FUNCTION reject_held_evidence_deletion(); + +CREATE OR REPLACE FUNCTION guard_deletion_request_policy() +RETURNS trigger +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = public, pg_temp +AS $$ +DECLARE + tenant_context text; +BEGIN + tenant_context := nullif(current_setting('tepp.current_tenant_record_id', true), ''); + IF tenant_context IS NULL OR NEW.tenant_record_id::text <> tenant_context THEN + RAISE EXCEPTION 'tenant session context is required for lifecycle mutation' + USING ERRCODE = 'integrity_constraint_violation'; + END IF; + IF NOT EXISTS ( + SELECT 1 + FROM public.retention_policy + WHERE retention_policy_id = NEW.retention_policy_id + AND tenant_record_id = NEW.tenant_record_id + AND data_class_code = NEW.target_data_class_code + AND processing_purpose_code = NEW.processing_purpose_code + ) THEN + RAISE EXCEPTION 'deletion request must match cited retention policy' + USING ERRCODE = 'integrity_constraint_violation'; + END IF; + RETURN NEW; +END; +$$; + +CREATE TRIGGER deletion_request_guard_policy + BEFORE INSERT ON deletion_request + FOR EACH ROW + EXECUTE FUNCTION guard_deletion_request_policy(); + +CREATE OR REPLACE FUNCTION reject_tombstoned_evidence_restore() +RETURNS trigger +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = public, pg_temp +AS $$ +DECLARE + tenant_context text; +BEGIN + tenant_context := nullif(current_setting('tepp.current_tenant_record_id', true), ''); + IF tenant_context IS NULL OR NEW.tenant_record_id::text <> tenant_context THEN + RAISE EXCEPTION 'tenant session context is required for lifecycle mutation' + USING ERRCODE = 'integrity_constraint_violation'; + END IF; + IF EXISTS ( + SELECT 1 + FROM public.evidence_tombstone + WHERE tenant_record_id = NEW.tenant_record_id + AND tombstoned_document_id = NEW.document_record_id + ) THEN + RAISE EXCEPTION 'tombstoned evidence cannot be restored' + USING ERRCODE = 'integrity_constraint_violation'; + END IF; + RETURN NEW; +END; +$$; + +CREATE TRIGGER document_record_reject_tombstone_restore + BEFORE INSERT OR UPDATE ON document_record + FOR EACH ROW + EXECUTE FUNCTION reject_tombstoned_evidence_restore(); + +CREATE OR REPLACE FUNCTION guard_evidence_tombstone_insert() +RETURNS trigger +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = public, pg_temp +AS $$ +DECLARE + tenant_context text; +BEGIN + tenant_context := nullif(current_setting('tepp.current_tenant_record_id', true), ''); + IF tenant_context IS NULL OR NEW.tenant_record_id::text <> tenant_context THEN + RAISE EXCEPTION 'tenant session context is required for lifecycle mutation' + USING ERRCODE = 'integrity_constraint_violation'; + END IF; + IF NOT EXISTS ( + SELECT 1 + FROM public.deletion_request + WHERE deletion_request_id = NEW.deletion_request_id + AND tenant_record_id = NEW.tenant_record_id + AND target_document_id = NEW.tombstoned_document_id + AND target_data_class_code = NEW.target_data_class_code + AND deletion_kind_code = NEW.deletion_kind_code + AND request_status_code = 'completed' + ) THEN + RAISE EXCEPTION 'evidence tombstone must match one completed deletion request' + USING ERRCODE = 'integrity_constraint_violation'; + END IF; + RETURN NEW; +END; +$$; + +CREATE TRIGGER evidence_tombstone_guard_insert + BEFORE INSERT ON evidence_tombstone + FOR EACH ROW + EXECUTE FUNCTION guard_evidence_tombstone_insert(); + +GRANT SELECT, INSERT ON TABLE retention_policy TO tepp_app_runtime; +GRANT SELECT, INSERT ON TABLE legal_hold TO tepp_app_runtime; +GRANT SELECT, INSERT ON TABLE deletion_request TO tepp_app_runtime; +GRANT SELECT, INSERT ON TABLE evidence_tombstone TO tepp_app_runtime; +REVOKE UPDATE, DELETE ON TABLE retention_policy FROM tepp_app_runtime; +REVOKE TRUNCATE ON TABLE retention_policy FROM tepp_app_runtime; +REVOKE UPDATE, DELETE ON TABLE legal_hold FROM tepp_app_runtime; +REVOKE TRUNCATE ON TABLE legal_hold FROM tepp_app_runtime; +REVOKE UPDATE, DELETE ON TABLE deletion_request FROM tepp_app_runtime; +REVOKE TRUNCATE ON TABLE deletion_request FROM tepp_app_runtime; +REVOKE UPDATE, DELETE ON TABLE evidence_tombstone FROM tepp_app_runtime; +REVOKE TRUNCATE ON TABLE evidence_tombstone FROM tepp_app_runtime; + +CREATE OR REPLACE FUNCTION guard_legal_hold_insert() +RETURNS trigger +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = public, pg_temp +AS $$ +DECLARE + tenant_context text; +BEGIN + tenant_context := nullif(current_setting('tepp.current_tenant_record_id', true), ''); + IF tenant_context IS NULL OR NEW.tenant_record_id::text <> tenant_context THEN + RAISE EXCEPTION 'tenant session context is required for lifecycle mutation' + USING ERRCODE = 'integrity_constraint_violation'; + END IF; + IF NEW.hold_status_code <> 'active' OR NEW.system_to IS NOT NULL THEN + RAISE EXCEPTION 'legal hold insert must be an open active hold' + USING ERRCODE = 'integrity_constraint_violation'; + END IF; + IF NEW.hold_scope_code NOT IN ('document', 'tenant') + OR (NEW.hold_scope_code = 'document' AND NEW.held_document_id IS NULL) + OR (NEW.hold_scope_code = 'tenant' AND NEW.held_document_id IS NOT NULL) + THEN + RAISE EXCEPTION 'legal hold scope is inconsistent' + USING ERRCODE = 'integrity_constraint_violation'; + END IF; + + -- Completed deletions and legal holds always acquire the tenant lock first. + -- Document-scoped operations then acquire the document lock, eliminating + -- hold/deletion TOCTOU races without relying on a nonexistent two-bigint + -- advisory-lock overload. + PERFORM pg_advisory_xact_lock( + hashtextextended( + 'tepp:lifecycle:tenant:' || NEW.tenant_record_id::text, + 0 + ) + ); + IF NEW.hold_scope_code = 'document' THEN + PERFORM pg_advisory_xact_lock( + hashtextextended( + 'tepp:lifecycle:document:' + || NEW.tenant_record_id::text + || ':' + || NEW.held_document_id::text, + 0 + ) + ); + END IF; + + -- The deletion trigger checks newly committed holds. This symmetric check + -- covers the opposite serialization order: a completed deletion that + -- committed first cannot coexist with a hold effective at or before it. + IF EXISTS ( + SELECT 1 + FROM public.deletion_request AS completed_deletion + WHERE completed_deletion.tenant_record_id = NEW.tenant_record_id + AND completed_deletion.request_status_code = 'completed' + AND completed_deletion.system_time >= NEW.system_time + AND ( + NEW.hold_scope_code = 'tenant' + OR completed_deletion.target_document_id = NEW.held_document_id + ) + ) THEN + RAISE EXCEPTION 'completed deletion blocks legal hold activation' + USING ERRCODE = 'integrity_constraint_violation'; + END IF; + + RETURN NEW; +END; +$$; + +CREATE OR REPLACE FUNCTION enforce_legal_hold_release() +RETURNS trigger +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = public, pg_temp +AS $$ +BEGIN + IF TG_OP = 'DELETE' THEN + RAISE EXCEPTION 'legal hold history is append-only' + USING ERRCODE = 'integrity_constraint_violation'; + END IF; + IF OLD.hold_status_code <> 'active' + OR OLD.system_to IS NOT NULL + OR NEW.hold_status_code <> 'released' + OR NEW.system_to IS NULL + OR NEW.system_to < OLD.system_time + OR NEW.legal_hold_id IS DISTINCT FROM OLD.legal_hold_id + OR NEW.tenant_record_id IS DISTINCT FROM OLD.tenant_record_id + OR NEW.hold_scope_code IS DISTINCT FROM OLD.hold_scope_code + OR NEW.held_document_id IS DISTINCT FROM OLD.held_document_id + OR NEW.hold_authority_code IS DISTINCT FROM OLD.hold_authority_code + OR NEW.authority_citation IS DISTINCT FROM OLD.authority_citation + OR NEW.system_time IS DISTINCT FROM OLD.system_time + OR NEW.available_time IS DISTINCT FROM OLD.available_time + THEN + RAISE EXCEPTION 'legal hold mutation must be a controlled release' + USING ERRCODE = 'integrity_constraint_violation'; + END IF; + RETURN NEW; +END; +$$; + +CREATE OR REPLACE FUNCTION release_legal_hold( + target_legal_hold_id uuid, + release_system_time timestamptz +) +RETURNS uuid +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = public, pg_temp +AS $$ +DECLARE + tenant_context text; + current_hold legal_hold%ROWTYPE; +BEGIN + tenant_context := nullif(current_setting('tepp.current_tenant_record_id', true), ''); + IF tenant_context IS NULL THEN + RAISE EXCEPTION 'tenant session context is required for legal hold release' + USING ERRCODE = 'integrity_constraint_violation'; + END IF; + + SELECT * + INTO current_hold + FROM public.legal_hold + WHERE legal_hold_id = target_legal_hold_id + FOR UPDATE; + + IF NOT FOUND + OR current_hold.tenant_record_id::text <> tenant_context + OR current_hold.hold_status_code <> 'active' + OR current_hold.system_to IS NOT NULL + OR release_system_time < current_hold.system_time + THEN + RAISE EXCEPTION 'legal hold release is not authorized for the active version' + USING ERRCODE = 'integrity_constraint_violation'; + END IF; + + PERFORM pg_advisory_xact_lock( + hashtextextended( + 'tepp:lifecycle:tenant:' || current_hold.tenant_record_id::text, + 0 + ) + ); + IF current_hold.hold_scope_code = 'document' THEN + PERFORM pg_advisory_xact_lock( + hashtextextended( + 'tepp:lifecycle:document:' + || current_hold.tenant_record_id::text + || ':' + || current_hold.held_document_id::text, + 0 + ) + ); + END IF; + + UPDATE public.legal_hold + SET hold_status_code = 'released', + system_to = release_system_time + WHERE legal_hold_id = target_legal_hold_id; + + RETURN target_legal_hold_id; +END; +$$; + +CREATE TRIGGER legal_hold_guard_insert + BEFORE INSERT ON legal_hold + FOR EACH ROW + EXECUTE FUNCTION guard_legal_hold_insert(); +CREATE TRIGGER legal_hold_enforce_release + BEFORE UPDATE OR DELETE ON legal_hold + FOR EACH ROW + EXECUTE FUNCTION enforce_legal_hold_release(); +CREATE TRIGGER legal_hold_reject_mutation + BEFORE TRUNCATE ON legal_hold + FOR EACH STATEMENT + EXECUTE FUNCTION reject_append_only_mutation(); +CREATE TRIGGER deletion_request_reject_mutation + BEFORE UPDATE OR DELETE OR TRUNCATE ON deletion_request + FOR EACH STATEMENT + EXECUTE FUNCTION reject_append_only_mutation(); +CREATE TRIGGER evidence_tombstone_reject_mutation + BEFORE UPDATE OR DELETE OR TRUNCATE ON evidence_tombstone + FOR EACH STATEMENT + EXECUTE FUNCTION reject_append_only_mutation(); + +ALTER TABLE retention_policy ENABLE ROW LEVEL SECURITY; +ALTER TABLE retention_policy FORCE ROW LEVEL SECURITY; +CREATE POLICY retention_policy_tenant_isolation ON retention_policy + FOR ALL + USING ( + tenant_record_id::text = nullif(current_setting('tepp.current_tenant_record_id', true), '') + ) + WITH CHECK ( + tenant_record_id::text = nullif(current_setting('tepp.current_tenant_record_id', true), '') + ); + +ALTER TABLE legal_hold ENABLE ROW LEVEL SECURITY; +ALTER TABLE legal_hold FORCE ROW LEVEL SECURITY; +CREATE POLICY legal_hold_tenant_isolation ON legal_hold + FOR ALL + USING ( + tenant_record_id::text = nullif(current_setting('tepp.current_tenant_record_id', true), '') + ) + WITH CHECK ( + tenant_record_id::text = nullif(current_setting('tepp.current_tenant_record_id', true), '') + ); + +ALTER TABLE deletion_request ENABLE ROW LEVEL SECURITY; +ALTER TABLE deletion_request FORCE ROW LEVEL SECURITY; +CREATE POLICY deletion_request_tenant_isolation ON deletion_request + FOR ALL + USING ( + tenant_record_id::text = nullif(current_setting('tepp.current_tenant_record_id', true), '') + ) + WITH CHECK ( + tenant_record_id::text = nullif(current_setting('tepp.current_tenant_record_id', true), '') + ); + +ALTER TABLE evidence_tombstone ENABLE ROW LEVEL SECURITY; +ALTER TABLE evidence_tombstone FORCE ROW LEVEL SECURITY; +CREATE POLICY evidence_tombstone_tenant_isolation ON evidence_tombstone + FOR ALL + USING ( + tenant_record_id::text = nullif(current_setting('tepp.current_tenant_record_id', true), '') + ) + WITH CHECK ( + tenant_record_id::text = nullif(current_setting('tepp.current_tenant_record_id', true), '') + ); + +REVOKE ALL ON FUNCTION enforce_retention_policy_succession() FROM PUBLIC; +REVOKE ALL ON FUNCTION supersede_retention_policy(uuid, uuid, integer, text, timestamptz, timestamptz) FROM PUBLIC; +REVOKE ALL ON FUNCTION reject_held_evidence_deletion() FROM PUBLIC; +REVOKE ALL ON FUNCTION reject_tombstoned_evidence_restore() FROM PUBLIC; +GRANT EXECUTE ON FUNCTION supersede_retention_policy(uuid, uuid, integer, text, timestamptz, timestamptz) TO tepp_app_runtime; +REVOKE ALL ON FUNCTION release_legal_hold(uuid, timestamptz) FROM PUBLIC; +REVOKE ALL ON FUNCTION enforce_legal_hold_release() FROM PUBLIC; +REVOKE ALL ON FUNCTION guard_legal_hold_insert() FROM PUBLIC; +GRANT EXECUTE ON FUNCTION release_legal_hold(uuid, timestamptz) TO tepp_app_runtime; +REVOKE ALL ON FUNCTION guard_evidence_tombstone_insert() FROM PUBLIC; +REVOKE ALL ON FUNCTION guard_deletion_request_policy() FROM PUBLIC; From 7aaf3baf93b612425324addf82f6611e44b2de8b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 09:17:24 +0000 Subject: [PATCH 02/62] feat(psychometric): recover multilevel event-time structure with Rubin T and strong means MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stacked on #49 psychometric_core. Not a second invariance crate. Not DSEM, RI-CLPM, MGCFA, or Mislevy PVs. - CWC within/between OLS and Kish ESS WLS (Enders & Tofighi, 2007; Curran & Bauer, 2011; Hamaker et al., 2015; Kish, 1965) - Event-time discrete lag-1 and exact scalar log-rate a = ln(φ)/Δt (Voelkle et al., 2012, Eq. 7; Driver et al., 2017, Eq. 3); difference quotient refused - CWC-then-event-time residual lag, still not DSEM - Rubin T_m = Ū_m + (1+1/m) B_m on draw-level OLS loadings (Rubin, 1996, p. 473) - Two-group OLS latent means only under strong/strict; metric/weak is not enough. #84 metric licenses shared metric meaning only. --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 1 + CLAUDE.md | 3 + DOCUMENTATION.md | 3 + README.md | 2 +- crates/psychometric_core/Cargo.toml | 2 +- crates/psychometric_core/src/cluster_mean.rs | 385 +++++++++++ crates/psychometric_core/src/error.rs | 58 ++ crates/psychometric_core/src/event_time.rs | 602 ++++++++++++++++++ crates/psychometric_core/src/latent_mean.rs | 458 +++++++++++++ crates/psychometric_core/src/lib.rs | 57 +- crates/psychometric_core/src/loading.rs | 69 +- crates/psychometric_core/src/rubin_total.rs | 148 +++++ .../tests/esem_input_recovery_contract.rs | 28 + ...multilevel_event_time_recovery_contract.rs | 144 +++++ .../tests/rubin_and_mean_gate_contract.rs | 122 ++++ docs/TRACEABILITY.md | 2 +- docs/adr/0005-posterior-esem-dsem.md | 6 +- docs/adr/README.md | 2 +- .../multilevel-event-time-recovery.md | 43 ++ ...tilevel-multiple-membership-measurement.md | 2 +- docs/research/posterior-esem-input-gates.md | 6 +- docs/research/rubin-total-variance.md | 32 + docs/research/standards-and-literature.md | 14 +- .../strong-invariance-latent-means.md | 40 ++ docs/validation/temporal-event-foundation.md | 2 +- 26 files changed, 2216 insertions(+), 17 deletions(-) create mode 100644 crates/psychometric_core/src/cluster_mean.rs create mode 100644 crates/psychometric_core/src/event_time.rs create mode 100644 crates/psychometric_core/src/latent_mean.rs create mode 100644 crates/psychometric_core/src/rubin_total.rs create mode 100644 crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs create mode 100644 crates/psychometric_core/tests/rubin_and_mean_gate_contract.rs create mode 100644 docs/research/multilevel-event-time-recovery.md create mode 100644 docs/research/rubin-total-variance.md create mode 100644 docs/research/strong-invariance-latent-means.md diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index c4e98ea4..e48d7625 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -61,7 +61,7 @@ boundaries above remain the target modular MSA architecture. | `tepp_simulation` | known-truth temporal/event data generation | | `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics | | `tepp_api` | versioned DTO, schema, and export contracts | -| `psychometric_core` | posterior-aware structural input gates and CPU `f64` loading point-estimate recovery | +| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS, event-time log-rate, Rubin `T` on OLS loadings, and strong-gated latent means | No crate exposes placeholder production behavior in Task 1. This prevents an empty façade from becoming a de facto public API before its invariants and tests diff --git a/CHANGELOG.md b/CHANGELOG.md index 3969c5e7..72e8ef62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang ### Added +- `psychometric_core` multilevel/event-time recovery on the stacked psychometric PR: cluster-mean CWC within/between OLS, Kish ESS weighted slopes, event-time-only discrete lag-1 and exact scalar local log-rate, CWC-then-event-time residual lag, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, and two-group OLS strong/strict-gated latent-mean difference. Metric/weak is not a mean license. Not DSEM, not RI-CLPM, not MGCFA, not Mislevy PVs (ADR 0005; no new migration). - `psychometric_core` posterior-aware structural input gates: construct classification, refusal of raw-proportion Pearson/OLS, explicit ALR-versus-ILR geometry boundaries, CPU `f64` OLS recovery, posterior-draw loading point-estimate averaging without Rubin uncertainty claims, invariance-gated latent-mean comparison, and causal-heuristic refusal (ADR 0005 first production slice; no new migration). - `persistence_postgres` backup/restore integrity: restored snapshots stay unusable until tenant, canonical `SHA-256`, knowledge-cutoff eligibility, temporal window order, and append-only triggers revalidate; SQL probes raise `restore integrity failed` (ADR 0013). - `persistence_postgres` concurrent document-write stress: atomic revise `DO` block that requires exactly one open `system_to` close, SQLSTATE mapping onto `ConcurrentWriteConflict` / `DuplicateDocumentRecord`, and live multi-session insert/revise/append-only proofs. No new migration number. diff --git a/CLAUDE.md b/CLAUDE.md index 42d14d84..1b89f710 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,6 +14,9 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not convert association, temporal precedence, or document links into causal language without identification evidence. - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. +- Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. +- Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. - Never use future-available evidence in historical model fits. - Do not blanket-mask PII when identity/role/linkage is scientifically required. Follow the purpose-bound separation, opaque-ID, encryption, retention, and audit contract in `docs/PRIVACY_DATA_GOVERNANCE.md`. - Treat documents and LLM outputs as untrusted. Model routing/orchestration may vary reasoning effort, decomposition, recursion and roles, but deterministic/statistical gates remain authoritative. diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 3f3318f5..10957a92 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -34,6 +34,9 @@ TEPP's approved PRD v0.4 and implementation plan are the primary product baselin | Actions workflow fleet audit | [`docs/operations/ACTIONS_WORKFLOW_FLEET.md`](docs/operations/ACTIONS_WORKFLOW_FLEET.md) | | Actions fleet research doctoring | [`docs/research/actions-workflow-fleet.md`](docs/research/actions-workflow-fleet.md) | | Posterior ESEM/DSEM input-gate doctoring | [`docs/research/posterior-esem-input-gates.md`](docs/research/posterior-esem-input-gates.md) | +| Multilevel/event-time recovery doctoring | [`docs/research/multilevel-event-time-recovery.md`](docs/research/multilevel-event-time-recovery.md) | +| Rubin total-variance doctoring | [`docs/research/rubin-total-variance.md`](docs/research/rubin-total-variance.md) | +| Strong-invariance latent-mean doctoring | [`docs/research/strong-invariance-latent-means.md`](docs/research/strong-invariance-latent-means.md) | | Hourly NIM OpenCode doctoring | [`docs/doctoring/hourly-nim-opencode-development.md`](docs/doctoring/hourly-nim-opencode-development.md) | | Change history | [`CHANGELOG.md`](CHANGELOG.md) | diff --git a/README.md b/README.md index bbf041ad..53bfbd89 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ crates/corpus_split crates/tepp_simulation crates/validation_core crates/tepp_api -crates/psychometric_core # construct/input gates; not a full ESEM/DSEM estimator +crates/psychometric_core # input gates, CWC/event-time, Rubin T, strong means; not a full ESEM/DSEM estimator ``` ## Local verification diff --git a/crates/psychometric_core/Cargo.toml b/crates/psychometric_core/Cargo.toml index 1ed33dc6..95697168 100644 --- a/crates/psychometric_core/Cargo.toml +++ b/crates/psychometric_core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "psychometric_core" -description = "Posterior-aware ESEM/DSEM input gates and CPU f64 loading recovery." +description = "Posterior-aware ESEM/DSEM input gates, multilevel/event-time recovery, Rubin T, and strong-invariance latent means." version.workspace = true edition.workspace = true rust-version.workspace = true diff --git a/crates/psychometric_core/src/cluster_mean.rs b/crates/psychometric_core/src/cluster_mean.rs new file mode 100644 index 00000000..356b40f7 --- /dev/null +++ b/crates/psychometric_core/src/cluster_mean.rs @@ -0,0 +1,385 @@ +//! Cluster-mean within/between OLS and Kish-weighted slopes. +//! +//! This is a two-level OLS decomposition after centering within cluster (CWC). +//! It is not DSEM, not RI-CLPM, and not a random-effects sampler. + +use std::collections::BTreeMap; + +use crate::error::PsychometricError; +use crate::indicator::require_finite; +use crate::loading::ordinary_least_squares_slope; + +/// One clustered predictor–outcome pair on already-mapped coordinates. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct ClusteredScore { + /// Cluster identity (person, document family, or membership unit). + pub cluster_key: u64, + /// Already-mapped predictor coordinate. + pub predictor: f64, + /// Already-mapped outcome coordinate. + pub outcome: f64, +} + +/// Recovered within-cluster and between-cluster OLS slopes. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct WithinBetweenSlopes { + /// OLS slope of cluster-mean-centered outcomes on centered predictors. + pub within_slope: f64, + /// OLS slope of cluster-mean outcomes on cluster-mean predictors. + pub between_slope: f64, +} + +/// Recover within-cluster and between-cluster OLS slopes after CWC. +/// +/// Between components use unweighted cluster means. Within components use the +/// stacked cluster-mean-centered residuals. A grand-mean pooled slope is not +/// returned because it confounds the two (Enders & Tofighi, 2007; Curran & +/// Bauer, 2011; Hamaker, Kuiper, & Grasman, 2015). +/// +/// # Errors +/// +/// Returns [`PsychometricError::InvalidNumericInput`] for empty, singleton, or +/// non-finite rows, [`PsychometricError::InsufficientClusters`] when fewer than +/// two clusters are present, and [`PsychometricError::SingularDesign`] when +/// either the within or the between predictor has zero variance. +pub fn recover_cluster_mean_within_between_slopes( + rows: &[ClusteredScore], +) -> Result { + if rows.len() < 2 { + return Err(PsychometricError::InvalidNumericInput); + } + let mut groups: BTreeMap> = BTreeMap::new(); + for row in rows { + if !row.predictor.is_finite() || !row.outcome.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + groups + .entry(row.cluster_key) + .or_default() + .push((row.predictor, row.outcome)); + } + if groups.len() < 2 { + return Err(PsychometricError::InsufficientClusters); + } + + let mut within_predictors = Vec::new(); + let mut within_outcomes = Vec::new(); + let mut between_predictors = Vec::new(); + let mut between_outcomes = Vec::new(); + for pairs in groups.values() { + let count = pairs.len() as f64; + let mut pred_sum = 0.0_f64; + let mut out_sum = 0.0_f64; + for &(predictor, outcome) in pairs { + pred_sum += predictor; + out_sum += outcome; + } + let pred_mean = pred_sum / count; + let out_mean = out_sum / count; + between_predictors.push(pred_mean); + between_outcomes.push(out_mean); + for &(predictor, outcome) in pairs { + within_predictors.push(predictor - pred_mean); + within_outcomes.push(outcome - out_mean); + } + } + + let within_slope = ordinary_least_squares_slope(&within_predictors, &within_outcomes)?; + let between_slope = ordinary_least_squares_slope(&between_predictors, &between_outcomes)?; + Ok(WithinBetweenSlopes { + within_slope, + between_slope, + }) +} + +/// Kish effective sample size `ESS = (Σ w)² / Σ w²` for non-negative weights. +/// +/// This is the same Kish (1965) formula used by `membership_core`. It is +/// reimplemented here so `psychometric_core` stays standalone. +/// +/// # Errors +/// +/// Returns [`PsychometricError::InvalidWeight`] for empty, negative, non-finite, +/// or all-zero weights. +pub fn kish_effective_sample_size(weights: &[f64]) -> Result { + if weights.is_empty() { + return Err(PsychometricError::InvalidWeight); + } + let mut sum = 0.0_f64; + let mut sum_sq = 0.0_f64; + for &weight in weights { + if !weight.is_finite() || weight < 0.0 { + return Err(PsychometricError::InvalidWeight); + } + sum += weight; + sum_sq += weight * weight; + } + if sum <= 0.0 { + return Err(PsychometricError::InvalidWeight); + } + require_finite((sum * sum) / sum_sq) +} + +/// Weighted least-squares slope using Kish membership/survey weights. +/// +/// The slope is the ordinary WLS estimator. Kish ESS is the information +/// diagnostic, not a second slope. +/// +/// # Errors +/// +/// Returns [`PsychometricError::InvalidNumericInput`] for length or finiteness +/// failures, [`PsychometricError::InvalidWeight`] for invalid weights, and +/// [`PsychometricError::SingularDesign`] when the weighted predictor has zero +/// variance. +pub fn recover_kish_weighted_slope( + predictor: &[f64], + outcome: &[f64], + weights: &[f64], +) -> Result { + if predictor.len() < 2 || predictor.len() != outcome.len() || predictor.len() != weights.len() { + return Err(PsychometricError::InvalidNumericInput); + } + let _ess = kish_effective_sample_size(weights)?; + let mut weight_sum = 0.0_f64; + let mut pred_sum = 0.0_f64; + let mut out_sum = 0.0_f64; + for index in 0..predictor.len() { + let pred = predictor[index]; + let out = outcome[index]; + let weight = weights[index]; + if !pred.is_finite() || !out.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + weight_sum += weight; + pred_sum += weight * pred; + out_sum += weight * out; + } + let pred_mean = pred_sum / weight_sum; + let out_mean = out_sum / weight_sum; + let mut cross = 0.0_f64; + let mut pred_ss = 0.0_f64; + for index in 0..predictor.len() { + let pred_dev = predictor[index] - pred_mean; + let out_dev = outcome[index] - out_mean; + let weight = weights[index]; + cross += weight * pred_dev * out_dev; + pred_ss += weight * pred_dev * pred_dev; + } + if pred_ss <= 0.0 { + return Err(PsychometricError::SingularDesign); + } + require_finite(cross / pred_ss) +} + +#[cfg(test)] +mod tests { + use super::{ + ClusteredScore, kish_effective_sample_size, recover_cluster_mean_within_between_slopes, + recover_kish_weighted_slope, + }; + use crate::error::PsychometricError; + + #[test] + fn noiseless_cwc_recovers_distinct_within_and_between_slopes() { + let rows = [ + ClusteredScore { + cluster_key: 1, + predictor: 0.0, + outcome: 2.0, + }, + ClusteredScore { + cluster_key: 1, + predictor: 2.0, + outcome: 3.0, + }, + ClusteredScore { + cluster_key: 2, + predictor: 4.0, + outcome: 10.0, + }, + ClusteredScore { + cluster_key: 2, + predictor: 6.0, + outcome: 11.0, + }, + ]; + // cluster 1 mean x=1 y=2.5; cluster 2 mean x=5 y=10.5 → between = 2 + // within: (-1,-0.5),(1,0.5) and (-1,-0.5),(1,0.5) → within = 0.5 + let recovered = recover_cluster_mean_within_between_slopes(&rows).expect("cwc"); + assert!((recovered.within_slope - 0.5).abs() < 1e-12); + assert!((recovered.between_slope - 2.0).abs() < 1e-12); + } + + #[test] + fn empty_or_one_cluster_or_nonfinite_rows_fail_closed() { + assert_eq!( + recover_cluster_mean_within_between_slopes(&[]), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_cluster_mean_within_between_slopes(&[ClusteredScore { + cluster_key: 1, + predictor: 0.0, + outcome: 1.0, + }]), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_cluster_mean_within_between_slopes(&[ + ClusteredScore { + cluster_key: 1, + predictor: 0.0, + outcome: 1.0, + }, + ClusteredScore { + cluster_key: 1, + predictor: 1.0, + outcome: 2.0, + }, + ]), + Err(PsychometricError::InsufficientClusters) + ); + assert_eq!( + recover_cluster_mean_within_between_slopes(&[ + ClusteredScore { + cluster_key: 1, + predictor: f64::NAN, + outcome: 1.0, + }, + ClusteredScore { + cluster_key: 2, + predictor: 1.0, + outcome: 2.0, + }, + ]), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_cluster_mean_within_between_slopes(&[ + ClusteredScore { + cluster_key: 1, + predictor: 0.0, + outcome: f64::INFINITY, + }, + ClusteredScore { + cluster_key: 2, + predictor: 1.0, + outcome: 2.0, + }, + ]), + Err(PsychometricError::InvalidNumericInput) + ); + } + + #[test] + fn singular_within_or_between_predictor_fails() { + let no_within = [ + ClusteredScore { + cluster_key: 1, + predictor: 0.0, + outcome: 1.0, + }, + ClusteredScore { + cluster_key: 1, + predictor: 0.0, + outcome: 2.0, + }, + ClusteredScore { + cluster_key: 2, + predictor: 1.0, + outcome: 3.0, + }, + ClusteredScore { + cluster_key: 2, + predictor: 1.0, + outcome: 4.0, + }, + ]; + assert_eq!( + recover_cluster_mean_within_between_slopes(&no_within), + Err(PsychometricError::SingularDesign) + ); + let no_between = [ + ClusteredScore { + cluster_key: 1, + predictor: 0.0, + outcome: 1.0, + }, + ClusteredScore { + cluster_key: 1, + predictor: 2.0, + outcome: 2.0, + }, + ClusteredScore { + cluster_key: 2, + predictor: 0.0, + outcome: 3.0, + }, + ClusteredScore { + cluster_key: 2, + predictor: 2.0, + outcome: 4.0, + }, + ]; + assert_eq!( + recover_cluster_mean_within_between_slopes(&no_between), + Err(PsychometricError::SingularDesign) + ); + } + + #[test] + fn kish_ess_and_weighted_slope_oracles() { + let ess = kish_effective_sample_size(&[1.0, 1.0, 1.0, 1.0]).expect("eq"); + assert!((ess - 4.0).abs() < 1e-12); + let unequal = kish_effective_sample_size(&[1.0, 0.0, 0.0, 0.0]).expect("one"); + assert!((unequal - 1.0).abs() < 1e-12); + let slope = + recover_kish_weighted_slope(&[0.0, 1.0, 2.0], &[0.0, 2.0, 4.0], &[1.0, 1.0, 1.0]) + .expect("wls"); + assert!((slope - 2.0).abs() < 1e-12); + assert_eq!( + kish_effective_sample_size(&[]), + Err(PsychometricError::InvalidWeight) + ); + assert_eq!( + kish_effective_sample_size(&[-0.1]), + Err(PsychometricError::InvalidWeight) + ); + assert_eq!( + kish_effective_sample_size(&[f64::NAN]), + Err(PsychometricError::InvalidWeight) + ); + assert_eq!( + kish_effective_sample_size(&[0.0, 0.0]), + Err(PsychometricError::InvalidWeight) + ); + assert_eq!( + recover_kish_weighted_slope(&[0.0], &[1.0], &[1.0]), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_kish_weighted_slope(&[0.0, 1.0], &[1.0], &[1.0, 1.0]), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_kish_weighted_slope(&[0.0, 1.0], &[1.0, 2.0], &[1.0]), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_kish_weighted_slope(&[0.0, f64::NAN], &[1.0, 2.0], &[1.0, 1.0]), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_kish_weighted_slope(&[0.0, 1.0], &[1.0, f64::INFINITY], &[1.0, 1.0]), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_kish_weighted_slope(&[0.0, 1.0], &[1.0, 2.0], &[-1.0, 1.0]), + Err(PsychometricError::InvalidWeight) + ); + assert_eq!( + recover_kish_weighted_slope(&[1.0, 1.0], &[2.0, 3.0], &[1.0, 1.0]), + Err(PsychometricError::SingularDesign) + ); + } +} diff --git a/crates/psychometric_core/src/error.rs b/crates/psychometric_core/src/error.rs index 337ac5fa..341370b0 100644 --- a/crates/psychometric_core/src/error.rs +++ b/crates/psychometric_core/src/error.rs @@ -24,6 +24,21 @@ pub enum PsychometricError { /// Latent-mean or path comparison was requested without invariance /// evidence. InvarianceRequired, + /// A structural lag or local log-rate was requested on a non-event clock. + EventTimeRequired, + /// The Voelkle–Oud difference quotient was offered as a continuous-time + /// rate. + DifferenceQuotientForbidden, + /// Fewer than two clusters were supplied for a within/between decomposition. + InsufficientClusters, + /// A membership or survey weight is empty, negative, or non-finite. + InvalidWeight, + /// An event-time interval is non-positive. + NonPositiveInterval, + /// Fewer than two posterior draws were supplied for Rubin combining. + InsufficientDraws, + /// Latent-mean comparison was requested at metric/weak invariance. + StrongInvarianceRequired, } impl fmt::Display for PsychometricError { @@ -40,6 +55,21 @@ impl fmt::Display for PsychometricError { Self::CausalUnderidentified => "temporal precedence is not causal identification", Self::UnresolvedConstruct => "construct class is unresolved", Self::InvarianceRequired => "latent-mean comparison requires invariance evidence", + Self::EventTimeRequired => { + "discrete lag and local log-rate require event time, not another clock" + } + Self::DifferenceQuotientForbidden => { + "the difference quotient is not the local continuous-time rate" + } + Self::InsufficientClusters => "within/between recovery requires at least two clusters", + Self::InvalidWeight => "invalid non-negative finite psychometric weight", + Self::NonPositiveInterval => "event-time interval must be strictly positive", + Self::InsufficientDraws => { + "Rubin total variance requires at least two complete-data draws" + } + Self::StrongInvarianceRequired => { + "latent-mean comparison requires strong or strict invariance; metric/weak is not enough" + } }; formatter.write_str(message) } @@ -81,5 +111,33 @@ mod tests { PsychometricError::InvarianceRequired.to_string(), "latent-mean comparison requires invariance evidence" ); + assert_eq!( + PsychometricError::EventTimeRequired.to_string(), + "discrete lag and local log-rate require event time, not another clock" + ); + assert_eq!( + PsychometricError::DifferenceQuotientForbidden.to_string(), + "the difference quotient is not the local continuous-time rate" + ); + assert_eq!( + PsychometricError::InsufficientClusters.to_string(), + "within/between recovery requires at least two clusters" + ); + assert_eq!( + PsychometricError::InvalidWeight.to_string(), + "invalid non-negative finite psychometric weight" + ); + assert_eq!( + PsychometricError::NonPositiveInterval.to_string(), + "event-time interval must be strictly positive" + ); + assert_eq!( + PsychometricError::InsufficientDraws.to_string(), + "Rubin total variance requires at least two complete-data draws" + ); + assert_eq!( + PsychometricError::StrongInvarianceRequired.to_string(), + "latent-mean comparison requires strong or strict invariance; metric/weak is not enough" + ); } } diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs new file mode 100644 index 00000000..fe418c3a --- /dev/null +++ b/crates/psychometric_core/src/event_time.rs @@ -0,0 +1,602 @@ +//! Event-time discrete lag-1 and exact scalar local log-rate. +//! +//! Voelkle, Oud, Davidov, and Schmidt (2012, Eq. 7) and Driver, Oud, and +//! Voelkle (2017, Eq. 3) map the continuous-time drift by +//! `A*(Δt) = exp(A Δt)`. The noiseless scalar inverse is +//! `a = ln(φ) / Δt` with `φ = A*(Δt)`. The difference quotient +//! `(x(t+Δt) − x(t)) / Δt` (their Eqs. 3–4) is refused. This is not DSEM. + +use std::collections::BTreeMap; + +use crate::error::PsychometricError; +use crate::indicator::require_finite; + +/// Clock on which a structural lag may be computed. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum LagClock { + /// Event / valid time. The only clock that licenses a structural lag. + EventTime, + /// System / transaction time. + SystemTime, + /// Assertion time. + AssertionTime, + /// Document time. + DocumentTime, + /// Availability time. + AvailabilityTime, + /// Knowledge cutoff. + KnowledgeCutoff, +} + +impl LagClock { + /// Stable wire name for the lag clock. + #[must_use] + pub const fn as_str(self) -> &'static str { + match self { + Self::EventTime => "event_time", + Self::SystemTime => "system_time", + Self::AssertionTime => "assertion_time", + Self::DocumentTime => "document_time", + Self::AvailabilityTime => "availability_time", + Self::KnowledgeCutoff => "knowledge_cutoff", + } + } + + /// Return whether this clock may carry a discrete lag or local log-rate. + #[must_use] + pub const fn admits_structural_lag(self) -> bool { + matches!(self, Self::EventTime) + } +} + +/// One occasion on an event-time series. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct EventOccasion { + /// Event / valid time of the observation. + pub event_time: f64, + /// Already-mapped score. + pub score: f64, +} + +/// One clustered event-time score used for CWC-then-lag recovery. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct ClusteredEventScore { + /// Cluster identity. + pub cluster_key: u64, + /// Event / valid time. + pub event_time: f64, + /// Already-mapped score. + pub score: f64, +} + +/// Discrete lag-1 coefficient and its exact local log-rate. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct DiscreteLagAndLogRate { + /// Discrete-time lag `φ = exp(a Δt)`. + pub discrete_lag: f64, + /// Local log-rate `a = ln(φ) / Δt`. + pub log_rate: f64, + /// Positive event-time interval. + pub event_delta: f64, +} + +/// Recover the noiseless scalar discrete lag `later / earlier`. +/// +/// # Errors +/// +/// Returns [`PsychometricError::InvalidNumericInput`] when either score is +/// non-finite or the earlier score is zero (the ratio is undefined). +pub fn recover_discrete_lag_one(earlier: f64, later: f64) -> Result { + if !earlier.is_finite() || !later.is_finite() || earlier == 0.0 { + return Err(PsychometricError::InvalidNumericInput); + } + require_finite(later / earlier) +} + +/// Map a discrete lag through the exact scalar exponential inverse. +/// +/// `a = ln(φ) / Δt`. The clock must be event time. `φ` must be strictly +/// positive so the real logarithm exists. +/// +/// # Errors +/// +/// Returns [`PsychometricError::EventTimeRequired`] for any non-event clock, +/// [`PsychometricError::NonPositiveInterval`] when `event_delta` is not +/// strictly positive, and [`PsychometricError::InvalidNumericInput`] when the +/// discrete lag is non-finite or not strictly positive. +pub fn recover_local_log_rate( + discrete_lag: f64, + event_delta: f64, + clock: LagClock, +) -> Result { + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + if !event_delta.is_finite() || event_delta <= 0.0 { + return Err(PsychometricError::NonPositiveInterval); + } + if !discrete_lag.is_finite() || discrete_lag <= 0.0 { + return Err(PsychometricError::InvalidNumericInput); + } + require_finite(discrete_lag.ln() / event_delta) +} + +/// Recover the exact scalar pair `(φ, a)` on event time. +/// +/// # Errors +/// +/// Propagates [`recover_discrete_lag_one`] and [`recover_local_log_rate`]. +pub fn recover_event_time_discrete_lag_and_log_rate( + earlier: f64, + later: f64, + event_delta: f64, + clock: LagClock, +) -> Result { + let discrete_lag = recover_discrete_lag_one(earlier, later)?; + let log_rate = recover_local_log_rate(discrete_lag, event_delta, clock)?; + Ok(DiscreteLagAndLogRate { + discrete_lag, + log_rate, + event_delta, + }) +} + +/// Refuse the difference quotient as a continuous-time rate. +/// +/// Voelkle et al. (2012) discourage `(x(t+Δt) − x(t)) / Δt` as the drift. +/// +/// # Errors +/// +/// Always returns [`PsychometricError::DifferenceQuotientForbidden`]. +pub fn refuse_difference_quotient_as_local_rate( + earlier: f64, + later: f64, + delta: f64, +) -> Result { + let _ = (earlier, later, delta); + Err(PsychometricError::DifferenceQuotientForbidden) +} + +/// Mean local log-rate across consecutive event-time pairs. +/// +/// Occasions are sorted by event time. Each pair uses the exact scalar map. +/// Equal or inverted times fail closed. +/// +/// # Errors +/// +/// Returns [`PsychometricError::EventTimeRequired`] for a non-event clock, +/// [`PsychometricError::InvalidNumericInput`] for fewer than two occasions or +/// non-finite values, and [`PsychometricError::NonPositiveInterval`] when +/// consecutive times are not strictly increasing. +pub fn recover_event_series_mean_log_rate( + occasions: &[EventOccasion], + clock: LagClock, +) -> Result { + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + if occasions.len() < 2 { + return Err(PsychometricError::InvalidNumericInput); + } + let mut ordered = occasions.to_vec(); + ordered.sort_by(|left, right| { + left.event_time + .partial_cmp(&right.event_time) + .unwrap_or(std::cmp::Ordering::Equal) + }); + let mut rates = Vec::new(); + for window in ordered.windows(2) { + let earlier = window[0]; + let later = window[1]; + if !earlier.event_time.is_finite() + || !later.event_time.is_finite() + || !earlier.score.is_finite() + || !later.score.is_finite() + { + return Err(PsychometricError::InvalidNumericInput); + } + let delta = later.event_time - earlier.event_time; + let recovered = + recover_event_time_discrete_lag_and_log_rate(earlier.score, later.score, delta, clock)?; + rates.push(recovered.log_rate); + } + let count = rates.len() as f64; + require_finite(rates.iter().sum::() / count) +} + +/// Local log-rate of cluster-mean-centered residuals on event time. +/// +/// Stable between-cluster means are removed first (CWC). Consecutive +/// within-cluster residuals then use the exact scalar map. This is not DSEM. +/// +/// # Errors +/// +/// Returns [`PsychometricError::EventTimeRequired`] for a non-event clock, +/// [`PsychometricError::InvalidNumericInput`] for empty, singleton, or +/// non-finite rows, [`PsychometricError::InsufficientClusters`] when fewer +/// than two clusters appear, and interval/lag errors from the scalar map. +pub fn recover_within_residual_event_time_log_rate( + rows: &[ClusteredEventScore], + clock: LagClock, +) -> Result { + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + if rows.len() < 2 { + return Err(PsychometricError::InvalidNumericInput); + } + let mut groups: BTreeMap> = BTreeMap::new(); + for &row in rows { + if !row.event_time.is_finite() || !row.score.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + groups.entry(row.cluster_key).or_default().push(row); + } + if groups.len() < 2 { + return Err(PsychometricError::InsufficientClusters); + } + let mut pairs = Vec::new(); + for occasions in groups.values_mut() { + if occasions.len() < 2 { + continue; + } + let count = occasions.len() as f64; + let mean = occasions.iter().map(|row| row.score).sum::() / count; + occasions.sort_by(|left, right| { + left.event_time + .partial_cmp(&right.event_time) + .unwrap_or(std::cmp::Ordering::Equal) + }); + for window in occasions.windows(2) { + let earlier_resid = window[0].score - mean; + let later_resid = window[1].score - mean; + let delta = window[1].event_time - window[0].event_time; + if !delta.is_finite() || delta <= 0.0 { + return Err(PsychometricError::NonPositiveInterval); + } + if !earlier_resid.is_finite() || !later_resid.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + pairs.push((earlier_resid, later_resid, delta)); + } + } + fit_scalar_log_rate(&pairs) +} + +fn fit_scalar_log_rate(pairs: &[(f64, f64, f64)]) -> Result { + if pairs.is_empty() { + return Err(PsychometricError::InvalidNumericInput); + } + let mut start_sum = 0.0_f64; + let mut start_count = 0.0_f64; + for &(earlier, later, delta) in pairs { + if earlier != 0.0 { + let discrete_lag = later / earlier; + if discrete_lag.is_finite() && discrete_lag > 0.0 { + start_sum += discrete_lag.ln() / delta; + start_count += 1.0; + } + } + } + if start_count <= 0.0 { + return Err(PsychometricError::InvalidNumericInput); + } + let mut log_rate = start_sum / start_count; + for _ in 0..16 { + let mut score = 0.0_f64; + let mut derivative = 0.0_f64; + for &(earlier, later, delta) in pairs { + let mapped = (log_rate * delta).exp(); + if !mapped.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + let weight = delta * earlier; + score += weight * mapped * later - delta * mapped * mapped * earlier * earlier; + derivative += delta * weight * mapped * later + - 2.0 * delta * delta * mapped * mapped * earlier * earlier; + } + if !score.is_finite() || !derivative.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + if derivative.abs() <= 1e-18 { + break; + } + let next = log_rate - score / derivative; + if !next.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + if (next - log_rate).abs() < 1e-14 { + log_rate = next; + break; + } + log_rate = next; + } + require_finite(log_rate) +} + +#[cfg(test)] +mod tests { + use super::{ + ClusteredEventScore, EventOccasion, LagClock, recover_discrete_lag_one, + recover_event_series_mean_log_rate, recover_event_time_discrete_lag_and_log_rate, + recover_local_log_rate, recover_within_residual_event_time_log_rate, + refuse_difference_quotient_as_local_rate, + }; + use crate::error::PsychometricError; + + #[test] + fn exact_scalar_map_inverts_exponential_drift() { + let drift = -0.5_f64; + let delta = 2.0_f64; + let earlier = 1.5_f64; + let later = earlier * (drift * delta).exp(); + let recovered = recover_event_time_discrete_lag_and_log_rate( + earlier, + later, + delta, + LagClock::EventTime, + ) + .expect("exact"); + assert!((recovered.log_rate - drift).abs() < 1e-12); + assert!((recovered.discrete_lag - (drift * delta).exp()).abs() < 1e-12); + assert!((recovered.event_delta - delta).abs() < 1e-15); + } + + #[test] + fn non_event_clocks_and_difference_quotient_fail_closed() { + for clock in [ + LagClock::SystemTime, + LagClock::AssertionTime, + LagClock::DocumentTime, + LagClock::AvailabilityTime, + LagClock::KnowledgeCutoff, + ] { + assert_eq!( + recover_local_log_rate(0.5, 1.0, clock), + Err(PsychometricError::EventTimeRequired) + ); + assert!(!clock.admits_structural_lag()); + assert!(!clock.as_str().is_empty()); + } + assert!(LagClock::EventTime.admits_structural_lag()); + assert_eq!(LagClock::EventTime.as_str(), "event_time"); + assert_eq!( + refuse_difference_quotient_as_local_rate(1.0, 0.5, 1.0), + Err(PsychometricError::DifferenceQuotientForbidden) + ); + } + + #[test] + fn invalid_lag_inputs_fail_closed() { + assert_eq!( + recover_discrete_lag_one(0.0, 1.0), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_lag_one(f64::NAN, 1.0), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_lag_one(1.0, f64::INFINITY), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_local_log_rate(0.5, 0.0, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_local_log_rate(0.5, -1.0, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_local_log_rate(0.5, f64::NAN, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_local_log_rate(0.0, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_local_log_rate(-0.2, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_local_log_rate(f64::NAN, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + } + + #[test] + fn series_mean_log_rate_recovers_and_refuses() { + let drift = -0.25_f64; + let occasions = [ + EventOccasion { + event_time: 0.0, + score: 2.0, + }, + EventOccasion { + event_time: 1.0, + score: 2.0 * drift.exp(), + }, + EventOccasion { + event_time: 3.0, + score: 2.0 * (drift * 3.0).exp(), + }, + ]; + let series = + recover_event_series_mean_log_rate(&occasions, LagClock::EventTime).expect("series"); + assert!((series - drift).abs() < 1e-12); + assert_eq!( + recover_event_series_mean_log_rate(&occasions, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_event_series_mean_log_rate(&[], LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_event_series_mean_log_rate( + &[EventOccasion { + event_time: 0.0, + score: 1.0, + }], + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_event_series_mean_log_rate( + &[ + EventOccasion { + event_time: f64::NAN, + score: 1.0, + }, + EventOccasion { + event_time: 1.0, + score: 0.5, + }, + ], + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_event_series_mean_log_rate( + &[ + EventOccasion { + event_time: 0.0, + score: 1.0, + }, + EventOccasion { + event_time: 0.0, + score: 0.5, + }, + ], + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + } + + fn clustered(cluster_key: u64, event_time: f64, score: f64) -> ClusteredEventScore { + ClusteredEventScore { + cluster_key, + event_time, + score, + } + } + + fn occasion(event_time: f64, score: f64) -> EventOccasion { + EventOccasion { event_time, score } + } + + fn decaying_clustered_scores(drift: f64) -> [ClusteredEventScore; 12] { + [ + clustered(1, 0.0, 10.0 + 1.0), + clustered(1, 1.0, 10.0 + drift.exp()), + clustered(1, 2.0, 10.0 + (drift * 2.0).exp()), + clustered(1, 3.0, 10.0 + (drift * 3.0).exp()), + clustered(1, 4.0, 10.0 + (drift * 4.0).exp()), + clustered(1, 5.0, 10.0 + (drift * 5.0).exp()), + clustered(2, 0.0, -6.0 + 1.2), + clustered(2, 1.0, -6.0 + 1.2 * drift.exp()), + clustered(2, 2.0, -6.0 + 1.2 * (drift * 2.0).exp()), + clustered(2, 3.0, -6.0 + 1.2 * (drift * 3.0).exp()), + clustered(2, 4.0, -6.0 + 1.2 * (drift * 4.0).exp()), + clustered(2, 5.0, -6.0 + 1.2 * (drift * 5.0).exp()), + ] + } + + #[test] + fn within_residual_paths_recover_and_refuse() { + let drift = -0.25_f64; + let clustered = decaying_clustered_scores(drift); + let within = recover_within_residual_event_time_log_rate(&clustered, LagClock::EventTime) + .expect("cwc lag"); + let pooled_scores = clustered.map(|row| EventOccasion { + event_time: row.event_time, + score: row.score, + }); + let pooled = recover_event_series_mean_log_rate(&pooled_scores, LagClock::EventTime); + let within_error = (within - drift).abs(); + match pooled { + Ok(pooled_rate) => { + let pooled_error = (pooled_rate - drift).abs(); + assert!( + within_error < pooled_error, + "CWC log-rate error {within_error} should beat pooled {pooled_error}" + ); + } + Err(_) => { + assert!(within_error.is_finite()); + } + } + assert!(within_error.is_finite()); + } + + #[test] + fn within_residual_invalid_rows_fail_closed() { + let rows = decaying_clustered_scores(-0.25); + assert_eq!( + recover_within_residual_event_time_log_rate(&rows, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_within_residual_event_time_log_rate(&[], LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_within_residual_event_time_log_rate( + &[clustered(1, 0.0, 1.0), clustered(1, 1.0, 0.5)], + LagClock::EventTime + ), + Err(PsychometricError::InsufficientClusters) + ); + assert_eq!( + recover_within_residual_event_time_log_rate( + &[clustered(1, f64::NAN, 1.0), clustered(2, 1.0, 0.5)], + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_event_series_mean_log_rate( + &[occasion(0.0, 1.0), occasion(1.0, f64::NAN)], + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_within_residual_event_time_log_rate( + &[ + clustered(1, 0.0, 1.0), + clustered(1, 1.0, 0.5), + clustered(2, 0.0, 2.0), + clustered(2, 1.0, 1.0), + ], + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_within_residual_event_time_log_rate( + &[clustered(1, 0.0, 1.0), clustered(2, 1.0, f64::INFINITY)], + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_within_residual_event_time_log_rate( + &[ + clustered(1, 0.0, 1.0), + clustered(1, 0.0, 1.2), + clustered(2, 0.0, 2.0), + clustered(2, 1.0, 1.5), + ], + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + } +} diff --git a/crates/psychometric_core/src/latent_mean.rs b/crates/psychometric_core/src/latent_mean.rs new file mode 100644 index 00000000..fa3f3a98 --- /dev/null +++ b/crates/psychometric_core/src/latent_mean.rs @@ -0,0 +1,458 @@ +//! Two-group OLS strong/strict-gated latent-mean difference. +//! +//! Metric/weak invariance (equal loadings only) licenses shared *metric* +//! meaning. It does not license latent-mean comparison. Strong invariance +//! (equal loading and intercept) is required for means; strict additionally +//! equalizes residual variances. This is two-group OLS, not MGCFA. +//! +//! Wire names `configural` / `metric` / `scalar` match the unpublished +//! `measurement_invariance` crate (#84) without importing it. That crate's +//! `Metric` gate is not used here for latent means. `#84` `scalar` is the +//! strong/scalar status. Meredith (1993) names weak/strong/strict are used +//! only as conventional labels; that PDF was not opened. + +use crate::error::PsychometricError; +use crate::indicator::{IndicatorKind, require_finite, require_valid_indicator}; +use crate::loading::ordinary_least_squares_fit; + +/// Two-group OLS invariance status for a mean comparison. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum MeanInvarianceStatus { + /// Same regression form only. Loadings need not match. + Configural, + /// Equal loadings. `#84` wire name `metric`. Does not license means. + Metric, + /// Equal loadings and intercepts. `#84` wire name `scalar`. + Strong, + /// Strong plus equal residual variances. + Strict, +} + +impl MeanInvarianceStatus { + /// Local status name (`strong` / `strict` keep Meredith's mean hierarchy). + #[must_use] + pub const fn as_str(self) -> &'static str { + match self { + Self::Configural => "configural", + Self::Metric => "metric", + Self::Strong => "strong", + Self::Strict => "strict", + } + } + + /// `#84` `measurement_invariance` wire name without importing that crate. + #[must_use] + pub const fn as_measurement_invariance_wire_name(self) -> &'static str { + match self { + Self::Configural => "configural", + Self::Metric => "metric", + Self::Strong => "scalar", + Self::Strict => "strict", + } + } + + /// Return whether `#84` would license shared *metric* meaning. + #[must_use] + pub const fn licenses_shared_metric_meaning(self) -> bool { + matches!(self, Self::Metric | Self::Strong | Self::Strict) + } + + /// Return whether latent-mean comparison is licensed. + #[must_use] + pub const fn licenses_latent_mean_comparison(self) -> bool { + matches!(self, Self::Strong | Self::Strict) + } +} + +/// One group's factor-score and indicator series. +#[derive(Clone, Debug, PartialEq)] +pub struct GroupIndicatorSeries { + /// Factor scores for the group. + pub factor_scores: Vec, + /// Indicator coordinates for the group. + pub indicators: Vec, +} + +/// Two-group OLS measurement parameters and status. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct TwoGroupMeasurement { + /// Reference-group intercept. + pub reference_intercept: f64, + /// Reference-group loading. + pub reference_loading: f64, + /// Comparison-group intercept. + pub comparison_intercept: f64, + /// Comparison-group loading. + pub comparison_loading: f64, + /// Reference residual variance. + pub reference_residual_variance: f64, + /// Comparison residual variance. + pub comparison_residual_variance: f64, + /// Classified invariance status. + pub status: MeanInvarianceStatus, +} + +/// Classify two-group OLS invariance from loadings, intercepts, and residuals. +/// +/// # Errors +/// +/// Returns indicator-kind or OLS errors from either group, and +/// [`PsychometricError::InvalidNumericInput`] when a tolerance is non-finite +/// or negative. +pub fn classify_two_group_ols_invariance( + reference: &GroupIndicatorSeries, + comparison: &GroupIndicatorSeries, + kind: IndicatorKind, + loading_tolerance: f64, + intercept_tolerance: f64, + residual_tolerance: f64, +) -> Result { + require_valid_indicator(kind)?; + if !loading_tolerance.is_finite() + || loading_tolerance < 0.0 + || !intercept_tolerance.is_finite() + || intercept_tolerance < 0.0 + || !residual_tolerance.is_finite() + || residual_tolerance < 0.0 + { + return Err(PsychometricError::InvalidNumericInput); + } + let reference_fit = + ordinary_least_squares_fit(&reference.factor_scores, &reference.indicators)?; + let comparison_fit = + ordinary_least_squares_fit(&comparison.factor_scores, &comparison.indicators)?; + let loading_gap = (reference_fit.slope - comparison_fit.slope).abs(); + let intercept_gap = (reference_fit.intercept - comparison_fit.intercept).abs(); + let residual_gap = (reference_fit.residual_variance - comparison_fit.residual_variance).abs(); + let status = if loading_gap > loading_tolerance { + MeanInvarianceStatus::Configural + } else if intercept_gap > intercept_tolerance { + MeanInvarianceStatus::Metric + } else if residual_gap > residual_tolerance { + MeanInvarianceStatus::Strong + } else { + MeanInvarianceStatus::Strict + }; + Ok(TwoGroupMeasurement { + reference_intercept: reference_fit.intercept, + reference_loading: reference_fit.slope, + comparison_intercept: comparison_fit.intercept, + comparison_loading: comparison_fit.slope, + reference_residual_variance: reference_fit.residual_variance, + comparison_residual_variance: comparison_fit.residual_variance, + status, + }) +} + +/// Recover `(ȳ_c − ȳ_r) / λ` only under strong or strict invariance. +/// +/// Metric/weak (equal loading, different intercept) fails closed. +/// +/// # Errors +/// +/// Returns [`PsychometricError::StrongInvarianceRequired`] when the classified +/// status is configural or metric, [`PsychometricError::SingularDesign`] when +/// the common loading is zero, and otherwise the classification errors. +pub fn recover_strong_gated_latent_mean_difference( + reference: &GroupIndicatorSeries, + comparison: &GroupIndicatorSeries, + kind: IndicatorKind, + loading_tolerance: f64, + intercept_tolerance: f64, + residual_tolerance: f64, +) -> Result { + let measurement = classify_two_group_ols_invariance( + reference, + comparison, + kind, + loading_tolerance, + intercept_tolerance, + residual_tolerance, + )?; + if !measurement.status.licenses_latent_mean_comparison() { + return Err(PsychometricError::StrongInvarianceRequired); + } + let loading = require_finite(f64::midpoint( + measurement.reference_loading, + measurement.comparison_loading, + ))?; + if loading == 0.0 { + return Err(PsychometricError::SingularDesign); + } + let reference_mean = series_mean(&reference.indicators)?; + let comparison_mean = series_mean(&comparison.indicators)?; + require_finite((comparison_mean - reference_mean) / loading) +} + +fn series_mean(values: &[f64]) -> Result { + let mut sum = 0.0_f64; + for &value in values { + sum += value; + } + require_finite(sum / values.len() as f64) +} + +#[cfg(test)] +mod tests { + use super::{ + GroupIndicatorSeries, MeanInvarianceStatus, classify_two_group_ols_invariance, + recover_strong_gated_latent_mean_difference, + }; + use crate::error::PsychometricError; + use crate::indicator::IndicatorKind; + + fn series(factors: &[f64], intercept: f64, loading: f64) -> GroupIndicatorSeries { + GroupIndicatorSeries { + factor_scores: factors.to_vec(), + indicators: factors + .iter() + .map(|score| intercept + loading * score) + .collect(), + } + } + + #[test] + fn hash84_metric_wire_name_does_not_license_latent_means() { + assert_eq!( + MeanInvarianceStatus::Metric.as_measurement_invariance_wire_name(), + "metric" + ); + assert!(MeanInvarianceStatus::Metric.licenses_shared_metric_meaning()); + assert!(!MeanInvarianceStatus::Metric.licenses_latent_mean_comparison()); + assert_eq!(MeanInvarianceStatus::Metric.as_str(), "metric"); + } + + #[test] + fn hash84_scalar_wire_name_is_strong_and_licenses_means() { + assert_eq!( + MeanInvarianceStatus::Strong.as_measurement_invariance_wire_name(), + "scalar" + ); + assert!(MeanInvarianceStatus::Strong.licenses_shared_metric_meaning()); + assert!(MeanInvarianceStatus::Strong.licenses_latent_mean_comparison()); + assert_eq!(MeanInvarianceStatus::Strong.as_str(), "strong"); + assert!(MeanInvarianceStatus::Strict.licenses_latent_mean_comparison()); + assert_eq!( + MeanInvarianceStatus::Strict.as_measurement_invariance_wire_name(), + "strict" + ); + assert!(!MeanInvarianceStatus::Configural.licenses_shared_metric_meaning()); + assert!(!MeanInvarianceStatus::Configural.licenses_latent_mean_comparison()); + assert_eq!( + MeanInvarianceStatus::Configural.as_measurement_invariance_wire_name(), + "configural" + ); + } + + #[test] + fn strong_invariance_recovers_latent_mean_difference() { + let reference = series(&[-1.0, 0.0, 1.0], 0.5, 1.2); + let comparison = series(&[1.0, 2.0, 3.0], 0.5, 1.2); + let difference = recover_strong_gated_latent_mean_difference( + &reference, + &comparison, + IndicatorKind::AdditiveLogRatio, + 1e-9, + 1e-9, + 1e-9, + ) + .expect("strong"); + // ȳ_r = 0.5, ȳ_c = 0.5+1.2*2 = 2.9, diff/λ = 2.4/1.2 = 2.0 + assert!((difference - 2.0).abs() < 1e-12); + let classified = classify_two_group_ols_invariance( + &reference, + &comparison, + IndicatorKind::AdditiveLogRatio, + 1e-9, + 1e-9, + 1e-9, + ) + .expect("class"); + assert_eq!(classified.status, MeanInvarianceStatus::Strict); + } + + #[test] + fn metric_only_and_configural_refuse_latent_means() { + let reference = series(&[-1.0, 0.0, 1.0], 0.5, 1.2); + let metric_only = series(&[1.0, 2.0, 3.0], 1.5, 1.2); + assert_eq!( + recover_strong_gated_latent_mean_difference( + &reference, + &metric_only, + IndicatorKind::AdditiveLogRatio, + 1e-9, + 1e-9, + 1e-9, + ), + Err(PsychometricError::StrongInvarianceRequired) + ); + let classified = classify_two_group_ols_invariance( + &reference, + &metric_only, + IndicatorKind::AdditiveLogRatio, + 1e-9, + 1e-9, + 1e-9, + ) + .expect("metric"); + assert_eq!(classified.status, MeanInvarianceStatus::Metric); + + let configural = series(&[1.0, 2.0, 3.0], 0.5, 0.4); + assert_eq!( + recover_strong_gated_latent_mean_difference( + &reference, + &configural, + IndicatorKind::AdditiveLogRatio, + 1e-9, + 1e-9, + 1e-9, + ), + Err(PsychometricError::StrongInvarianceRequired) + ); + let classified = classify_two_group_ols_invariance( + &reference, + &configural, + IndicatorKind::AdditiveLogRatio, + 1e-9, + 1e-9, + 1e-9, + ) + .expect("configural"); + assert_eq!(classified.status, MeanInvarianceStatus::Configural); + } + + #[test] + fn strong_but_not_strict_still_licenses_means() { + let reference = GroupIndicatorSeries { + factor_scores: vec![-2.0, -1.0, 0.0, 1.0, 2.0], + indicators: vec![0.5 - 2.4, 0.5 - 1.2, 0.5, 0.5 + 1.2, 0.5 + 2.4], + }; + let comparison = GroupIndicatorSeries { + factor_scores: vec![-2.0, -1.0, 0.0, 1.0, 2.0], + indicators: vec![ + 0.5 - 2.4 + 0.2, + 0.5 - 1.2 - 0.4, + 0.5, + 0.5 + 1.2 + 0.4, + 0.5 + 2.4 - 0.2, + ], + }; + let classified = classify_two_group_ols_invariance( + &reference, + &comparison, + IndicatorKind::LogisticNormal, + 0.05, + 0.2, + 1e-12, + ) + .expect("strong"); + assert_eq!(classified.status, MeanInvarianceStatus::Strong); + let difference = recover_strong_gated_latent_mean_difference( + &reference, + &comparison, + IndicatorKind::LogisticNormal, + 0.05, + 0.2, + 1e-12, + ) + .expect("licensed"); + assert!(difference.is_finite()); + } + + #[test] + fn invalid_tolerance_raw_kind_and_zero_loading_fail() { + let reference = series(&[-1.0, 0.0, 1.0], 0.0, 1.0); + let comparison = series(&[0.0, 1.0, 2.0], 0.0, 1.0); + assert_eq!( + classify_two_group_ols_invariance( + &reference, + &comparison, + IndicatorKind::AdditiveLogRatio, + f64::NAN, + 1e-9, + 1e-9, + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + classify_two_group_ols_invariance( + &reference, + &comparison, + IndicatorKind::AdditiveLogRatio, + -0.1, + 1e-9, + 1e-9, + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + classify_two_group_ols_invariance( + &reference, + &comparison, + IndicatorKind::AdditiveLogRatio, + 1e-9, + f64::INFINITY, + 1e-9, + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + classify_two_group_ols_invariance( + &reference, + &comparison, + IndicatorKind::AdditiveLogRatio, + 1e-9, + -0.01, + 1e-9, + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + classify_two_group_ols_invariance( + &reference, + &comparison, + IndicatorKind::AdditiveLogRatio, + 1e-9, + 1e-9, + f64::NAN, + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + classify_two_group_ols_invariance( + &reference, + &comparison, + IndicatorKind::AdditiveLogRatio, + 1e-9, + 1e-9, + -1.0, + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_strong_gated_latent_mean_difference( + &reference, + &comparison, + IndicatorKind::RawProportion, + 1e-9, + 1e-9, + 1e-9, + ), + Err(PsychometricError::RawProportionForbidden) + ); + let zero = series(&[-1.0, 0.0, 1.0], 2.0, 0.0); + let other = series(&[0.0, 1.0, 2.0], 2.0, 0.0); + assert_eq!( + recover_strong_gated_latent_mean_difference( + &zero, + &other, + IndicatorKind::IsometricLogRatio, + 1e-9, + 1e-9, + 1e-9, + ), + Err(PsychometricError::SingularDesign) + ); + } +} diff --git a/crates/psychometric_core/src/lib.rs b/crates/psychometric_core/src/lib.rs index 3476e0b9..90ea4393 100644 --- a/crates/psychometric_core/src/lib.rs +++ b/crates/psychometric_core/src/lib.rs @@ -7,19 +7,36 @@ //! crate classifies constructs, admits mapped log-ratio/logistic-normal inputs, //! distinguishes ALR from orthonormal ILR geometry, averages loading point //! estimates across posterior draws on a CPU `f64` path without claiming Rubin -//! uncertainty pooling, and refuses causal language from non-identifying cues. +//! uncertainty pooling, combines draw-level OLS loadings with Rubin `T`, +//! decomposes cluster-mean within/between OLS, maps event-time discrete lags +//! through the exact scalar exponential, and refuses latent-mean comparison +//! below strong invariance. mod causality; +mod cluster_mean; mod construct; mod error; +mod event_time; mod indicator; +mod latent_mean; 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 and between-cluster OLS slopes. +pub use cluster_mean::WithinBetweenSlopes; +/// Kish effective sample size on psychometric weights. +pub use cluster_mean::kish_effective_sample_size; +/// Cluster-mean within/between OLS after CWC. +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; /// Permit latent-mean comparison only with invariance evidence. @@ -28,12 +45,46 @@ pub use construct::compare_latent_means; pub use construct::interpret_as_reflective; /// 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; +/// Noiseless scalar discrete lag `later / earlier`. +pub use event_time::recover_discrete_lag_one; +/// Mean local log-rate on a sorted event-time series. +pub use event_time::recover_event_series_mean_log_rate; +/// Exact scalar pair `(φ, a)` on event time. +pub use event_time::recover_event_time_discrete_lag_and_log_rate; +/// Exact scalar inverse `a = ln(φ) / Δt`. +pub use event_time::recover_local_log_rate; +/// CWC-then-event-time local log-rate (not DSEM). +pub use event_time::recover_within_residual_event_time_log_rate; +/// Refuse the difference quotient as a continuous-time rate. +pub use event_time::refuse_difference_quotient_as_local_rate; /// 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; +/// 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. @@ -42,3 +93,7 @@ pub use loading::recover_reflective_loading; 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; diff --git a/crates/psychometric_core/src/loading.rs b/crates/psychometric_core/src/loading.rs index a45dd328..11165d2d 100644 --- a/crates/psychometric_core/src/loading.rs +++ b/crates/psychometric_core/src/loading.rs @@ -3,6 +3,19 @@ use crate::error::PsychometricError; use crate::indicator::{IndicatorKind, centered_pairs, require_finite, require_valid_indicator}; +/// Ordinary least-squares intercept, slope, and residual variance. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct OrdinaryLeastSquaresFit { + /// Intercept `ν = ȳ − λ x̄`. + pub intercept: f64, + /// Slope `λ`. + pub slope: f64, + /// Residual variance `SSE / df` with `df = n − 2` when `n > 2`, else `0`. + pub residual_variance: f64, + /// Predictor sum of squared deviations `Σ (x − x̄)²`. + pub predictor_sum_of_squares: f64, +} + /// Ordinary least-squares slope of `outcome` on `predictor`. /// /// # Errors @@ -14,7 +27,23 @@ pub fn ordinary_least_squares_slope( predictor: &[f64], outcome: &[f64], ) -> Result { - let (pred_dev, out_dev, _) = centered_pairs(predictor, outcome)?; + Ok(ordinary_least_squares_fit(predictor, outcome)?.slope) +} + +/// Ordinary least-squares intercept and slope with residual variance. +/// +/// Two-point lines have residual variance `0` because they fit exactly. +/// +/// # Errors +/// +/// Returns [`PsychometricError::InvalidNumericInput`] for empty, singleton, +/// unequal-length, or non-finite vectors and +/// [`PsychometricError::SingularDesign`] when the predictor has zero variance. +pub fn ordinary_least_squares_fit( + predictor: &[f64], + outcome: &[f64], +) -> Result { + let (pred_dev, out_dev, n) = centered_pairs(predictor, outcome)?; let mut cross = 0.0_f64; let mut pred_ss = 0.0_f64; for (pred, out) in pred_dev.iter().zip(&out_dev) { @@ -24,7 +53,30 @@ pub fn ordinary_least_squares_slope( if pred_ss <= 0.0 { return Err(PsychometricError::SingularDesign); } - require_finite(cross / pred_ss) + let slope = require_finite(cross / pred_ss)?; + let mut outcome_sum = 0.0_f64; + let mut predictor_sum = 0.0_f64; + for (&pred, &out) in predictor.iter().zip(outcome) { + predictor_sum += pred; + outcome_sum += out; + } + let intercept = require_finite(outcome_sum / n - slope * (predictor_sum / n))?; + let mut sse = 0.0_f64; + for (&pred, &out) in predictor.iter().zip(outcome) { + let residual = out - (intercept + slope * pred); + sse += residual * residual; + } + let residual_variance = if n > 2.0 { + require_finite(sse / (n - 2.0))? + } else { + 0.0 + }; + Ok(OrdinaryLeastSquaresFit { + intercept, + slope, + residual_variance, + predictor_sum_of_squares: pred_ss, + }) } /// Recover a single reflective loading from factor scores and an indicator. @@ -44,7 +96,9 @@ pub fn recover_reflective_loading( #[cfg(test)] mod tests { - use super::{ordinary_least_squares_slope, recover_reflective_loading}; + use super::{ + ordinary_least_squares_fit, ordinary_least_squares_slope, recover_reflective_loading, + }; use crate::error::PsychometricError; use crate::indicator::IndicatorKind; @@ -52,6 +106,11 @@ mod tests { fn unit_slope_recovers_and_empty_or_overflow_input_fails() { let slope = ordinary_least_squares_slope(&[0.0, 1.0], &[0.0, 1.0]).expect("unit"); assert!((slope - 1.0).abs() < 1e-15); + let fit = ordinary_least_squares_fit(&[0.0, 1.0, 2.0], &[1.0, 3.0, 5.0]).expect("line"); + assert!((fit.slope - 2.0).abs() < 1e-12); + assert!((fit.intercept - 1.0).abs() < 1e-12); + assert!(fit.residual_variance.abs() < 1e-12); + assert!(fit.predictor_sum_of_squares > 0.0); assert_eq!( recover_reflective_loading(&[], &[], IndicatorKind::AdditiveLogRatio), Err(PsychometricError::InvalidNumericInput) @@ -60,5 +119,9 @@ mod tests { ordinary_least_squares_slope(&[0.0, f64::MAX], &[0.0, f64::MAX]), Err(PsychometricError::InvalidNumericInput) ); + assert_eq!( + ordinary_least_squares_fit(&[1.0, 1.0], &[2.0, 3.0]), + Err(PsychometricError::SingularDesign) + ); } } diff --git a/crates/psychometric_core/src/rubin_total.rs b/crates/psychometric_core/src/rubin_total.rs new file mode 100644 index 00000000..ba0f1b2d --- /dev/null +++ b/crates/psychometric_core/src/rubin_total.rs @@ -0,0 +1,148 @@ +//! Rubin total variance for draw-level OLS loadings. +//! +//! Rubin (1996, p. 473), restating Rubin (1987): +//! `T_m = Ū_m + (1 + 1/m) B_m`, where `Ū_m` is the mean complete-data +//! sampling variance and `B_m` is the between-draw variance of the point +//! estimates. This combines complete-data OLS loadings. It is not Mislevy +//! plausible-value draws. + +use crate::error::PsychometricError; +use crate::indicator::{IndicatorKind, require_finite, require_valid_indicator}; +use crate::loading::ordinary_least_squares_fit; + +/// Rubin-combined OLS loading and total variance. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct RubinCombinedLoading { + /// Mean complete-data loading `Q̄`. + pub mean_loading: f64, + /// Mean complete-data sampling variance `Ū`. + pub within_variance: f64, + /// Between-draw variance `B`. + pub between_variance: f64, + /// Total variance `T = Ū + (1 + 1/m) B`. + pub total_variance: f64, + /// Number of complete-data draws `m`. + pub draw_count: usize, +} + +/// Combine OLS loadings across posterior indicator draws with Rubin `T`. +/// +/// Each draw contributes `Q̂_ℓ = λ_ℓ` and +/// `U_ℓ = σ̂²_ℓ / Σ (f − f̄)²`. The helper does not treat the draws as +/// Mislevy person-level plausible values. +/// +/// # Errors +/// +/// Returns [`PsychometricError::InvalidNumericInput`] when a draw is empty or +/// non-finite, [`PsychometricError::InsufficientDraws`] when fewer than two +/// draws are supplied, and indicator-kind or OLS errors from a draw. +pub fn combine_draw_level_ols_loadings( + factor_scores: &[f64], + indicator_draws: &[Vec], + kind: IndicatorKind, +) -> Result { + require_valid_indicator(kind)?; + if indicator_draws.len() < 2 { + return Err(PsychometricError::InsufficientDraws); + } + let draw_count = indicator_draws.len(); + let mut loadings = Vec::with_capacity(draw_count); + let mut within = Vec::with_capacity(draw_count); + for draw in indicator_draws { + let fit = ordinary_least_squares_fit(factor_scores, draw)?; + if fit.predictor_sum_of_squares <= 0.0 { + return Err(PsychometricError::SingularDesign); + } + let sampling_variance = + require_finite(fit.residual_variance / fit.predictor_sum_of_squares)?; + loadings.push(fit.slope); + within.push(sampling_variance); + } + let count = draw_count as f64; + let mut loading_sum = 0.0_f64; + let mut within_sum = 0.0_f64; + for index in 0..draw_count { + loading_sum += loadings[index]; + within_sum += within[index]; + } + let mean_loading = require_finite(loading_sum / count)?; + let within_variance = require_finite(within_sum / count)?; + let mut between_ss = 0.0_f64; + for loading in &loadings { + let deviation = loading - mean_loading; + between_ss += deviation * deviation; + } + let between_variance = require_finite(between_ss / (count - 1.0))?; + let total_variance = require_finite(within_variance + (1.0 + 1.0 / count) * between_variance)?; + Ok(RubinCombinedLoading { + mean_loading, + within_variance, + between_variance, + total_variance, + draw_count, + }) +} + +#[cfg(test)] +mod tests { + use super::combine_draw_level_ols_loadings; + use crate::error::PsychometricError; + use crate::indicator::IndicatorKind; + + #[test] + fn rubin_t_matches_mean_plus_inflated_between() { + let factors = [-1.0_f64, 0.0, 1.0]; + let draws = [vec![-0.7, 0.0, 0.7], vec![-0.9, 0.0, 0.9]]; + let combined = + combine_draw_level_ols_loadings(&factors, &draws, IndicatorKind::AdditiveLogRatio) + .expect("rubin"); + assert!((combined.mean_loading - 0.8).abs() < 1e-12); + assert_eq!(combined.draw_count, 2); + let expected_total = + combined.within_variance + (1.0 + 1.0 / 2.0) * combined.between_variance; + assert!((combined.total_variance - expected_total).abs() < 1e-15); + assert!(combined.between_variance > 0.0); + assert!(combined.within_variance.abs() < 1e-12); + } + + #[test] + fn raw_proportion_single_draw_and_bad_numeric_fail() { + let factors = [0.0_f64, 1.0, 2.0]; + assert_eq!( + combine_draw_level_ols_loadings( + &factors, + &[vec![0.0, 1.0, 2.0]], + IndicatorKind::AdditiveLogRatio + ), + Err(PsychometricError::InsufficientDraws) + ); + assert_eq!( + combine_draw_level_ols_loadings(&factors, &[], IndicatorKind::AdditiveLogRatio), + Err(PsychometricError::InsufficientDraws) + ); + assert_eq!( + combine_draw_level_ols_loadings( + &factors, + &[vec![0.0, 1.0, 2.0], vec![0.0, 1.0, 2.0]], + IndicatorKind::RawProportion + ), + Err(PsychometricError::RawProportionForbidden) + ); + assert_eq!( + combine_draw_level_ols_loadings( + &factors, + &[vec![0.0, f64::NAN, 2.0], vec![0.0, 1.0, 2.0]], + IndicatorKind::IsometricLogRatio + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + combine_draw_level_ols_loadings( + &[1.0, 1.0, 1.0], + &[vec![0.0, 1.0, 2.0], vec![0.0, 1.0, 2.0]], + IndicatorKind::LogisticNormal + ), + Err(PsychometricError::SingularDesign) + ); + } +} diff --git a/crates/psychometric_core/tests/esem_input_recovery_contract.rs b/crates/psychometric_core/tests/esem_input_recovery_contract.rs index de3fddc8..2b694ddd 100644 --- a/crates/psychometric_core/tests/esem_input_recovery_contract.rs +++ b/crates/psychometric_core/tests/esem_input_recovery_contract.rs @@ -264,4 +264,32 @@ fn finite_alr_correlation_and_error_messages_are_stable() { PsychometricError::InvarianceRequired.to_string(), "latent-mean comparison requires invariance evidence" ); + assert_eq!( + PsychometricError::EventTimeRequired.to_string(), + "discrete lag and local log-rate require event time, not another clock" + ); + assert_eq!( + PsychometricError::DifferenceQuotientForbidden.to_string(), + "the difference quotient is not the local continuous-time rate" + ); + assert_eq!( + PsychometricError::InsufficientClusters.to_string(), + "within/between recovery requires at least two clusters" + ); + assert_eq!( + PsychometricError::InvalidWeight.to_string(), + "invalid non-negative finite psychometric weight" + ); + assert_eq!( + PsychometricError::NonPositiveInterval.to_string(), + "event-time interval must be strictly positive" + ); + assert_eq!( + PsychometricError::InsufficientDraws.to_string(), + "Rubin total variance requires at least two complete-data draws" + ); + assert_eq!( + PsychometricError::StrongInvarianceRequired.to_string(), + "latent-mean comparison requires strong or strict invariance; metric/weak is not enough" + ); } diff --git a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs new file mode 100644 index 00000000..7359a35d --- /dev/null +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -0,0 +1,144 @@ +//! True-parameter recovery for multilevel OLS, event-time log-rate, and CWC lags. +#![allow(clippy::cast_precision_loss)] + +use psychometric_core::{ + ClusteredEventScore, ClusteredScore, EventOccasion, IndicatorKind, LagClock, PsychometricError, + ordinary_least_squares_slope, recover_cluster_mean_within_between_slopes, + recover_event_series_mean_log_rate, recover_event_time_discrete_lag_and_log_rate, + recover_kish_weighted_slope, recover_within_residual_event_time_log_rate, + refuse_difference_quotient_as_local_rate, +}; + +fn rmse(truth: &[f64], recovered: &[f64]) -> f64 { + let n = truth.len() as f64; + let sum_sq: f64 = truth + .iter() + .zip(recovered) + .map(|(left, right)| { + let residual = left - right; + residual * residual + }) + .sum(); + (sum_sq / n).sqrt() +} + +#[test] +fn cluster_mean_cwc_recovers_known_within_and_between_better_than_pooled() { + let true_within = 0.5_f64; + let true_between = 2.0_f64; + let mut rows = Vec::new(); + for cluster in 0..6_u64 { + let cluster_mean = f64::from(u32::try_from(cluster).expect("tiny")) * 2.0; + for occasion in 0..4 { + let within = f64::from(occasion) - 1.5; + let predictor = cluster_mean + within; + let outcome = true_between * cluster_mean + true_within * within; + rows.push(ClusteredScore { + cluster_key: cluster, + predictor, + outcome, + }); + } + } + let recovered = recover_cluster_mean_within_between_slopes(&rows).expect("cwc"); + let within_error = rmse(&[true_within], &[recovered.within_slope]); + let between_error = rmse(&[true_between], &[recovered.between_slope]); + assert!(within_error < 1e-12, "within RMSE {within_error}"); + assert!(between_error < 1e-12, "between RMSE {between_error}"); + + let predictors: Vec = rows.iter().map(|row| row.predictor).collect(); + let outcomes: Vec = rows.iter().map(|row| row.outcome).collect(); + let pooled = ordinary_least_squares_slope(&predictors, &outcomes).expect("pooled"); + let pooled_within_error = rmse(&[true_within], &[pooled]); + let pooled_between_error = rmse(&[true_between], &[pooled]); + assert!( + pooled_within_error > within_error, + "pooled RMSE {pooled_within_error} should exceed CWC within {within_error}" + ); + assert!( + pooled_between_error > between_error, + "pooled RMSE {pooled_between_error} should exceed CWC between {between_error}" + ); +} + +#[test] +fn kish_weighted_slope_recovers_known_loading() { + let true_slope = 0.75_f64; + let predictor = [0.0_f64, 1.0, 2.0, 3.0]; + let outcome = [0.0, true_slope, 2.0 * true_slope, 3.0 * true_slope]; + let weights = [1.0_f64, 2.0, 1.0, 0.5]; + let recovered = recover_kish_weighted_slope(&predictor, &outcome, &weights).expect("wls"); + let error = rmse(&[true_slope], &[recovered]); + assert!(error < 1e-12, "Kish WLS RMSE {error}"); +} + +#[test] +fn event_time_log_rate_recovers_known_drift_and_refuses_quotient() { + let true_drift = -0.4_f64; + let earlier = 1.25_f64; + let delta = 1.5_f64; + let later = earlier * (true_drift * delta).exp(); + let recovered = + recover_event_time_discrete_lag_and_log_rate(earlier, later, delta, LagClock::EventTime) + .expect("exact map"); + let error = rmse(&[true_drift], &[recovered.log_rate]); + assert!(error < 1e-12, "log-rate RMSE {error}"); + assert_eq!( + refuse_difference_quotient_as_local_rate(earlier, later, delta), + Err(PsychometricError::DifferenceQuotientForbidden) + ); + assert_eq!( + recover_event_time_discrete_lag_and_log_rate(earlier, later, delta, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); +} + +#[test] +fn within_residual_event_time_log_rate_beats_pooled_levels() { + let true_drift = -0.3_f64; + let mut rows = Vec::new(); + for (cluster, person_mean, start) in [(1_u64, 8.0_f64, 1.0_f64), (2, -5.0, 1.4)] { + for step in 0..6 { + let time = f64::from(step); + rows.push(ClusteredEventScore { + cluster_key: cluster, + event_time: time, + score: person_mean + start * (true_drift * time).exp(), + }); + } + } + let recovered = + recover_within_residual_event_time_log_rate(&rows, LagClock::EventTime).expect("cwc lag"); + let within_error = rmse(&[true_drift], &[recovered]); + + let mut pooled = Vec::new(); + for (cluster, person_mean, start) in [(1_u64, 8.0_f64, 1.0_f64), (2, -5.0, 1.4)] { + let time = cluster as f64; + pooled.push(EventOccasion { + event_time: time, + score: person_mean + start, + }); + } + let pooled_rate = recover_event_series_mean_log_rate(&pooled, LagClock::EventTime); + match pooled_rate { + Ok(rate) => { + let pooled_error = rmse(&[true_drift], &[rate]); + assert!( + within_error < pooled_error, + "CWC lag RMSE {within_error} should beat pooled {pooled_error}" + ); + } + Err(PsychometricError::InvalidNumericInput | PsychometricError::NonPositiveInterval) => {} + Err(other) => panic!("unexpected pooled error {other}"), + } + assert!(within_error < 0.25, "CWC lag RMSE {within_error} too large"); +} + +#[test] +fn admitted_coordinates_still_required_for_multilevel_weights() { + assert_eq!( + recover_kish_weighted_slope(&[0.0, 1.0], &[0.2, 0.3], &[1.0, 1.0]), + ordinary_least_squares_slope(&[0.0, 1.0], &[0.2, 0.3]) + ); + let _ = IndicatorKind::AdditiveLogRatio; +} diff --git a/crates/psychometric_core/tests/rubin_and_mean_gate_contract.rs b/crates/psychometric_core/tests/rubin_and_mean_gate_contract.rs new file mode 100644 index 00000000..2fb644fb --- /dev/null +++ b/crates/psychometric_core/tests/rubin_and_mean_gate_contract.rs @@ -0,0 +1,122 @@ +//! Rubin T combining and strong-invariance latent-mean claim boundaries. +#![allow(clippy::cast_precision_loss)] + +use psychometric_core::{ + GroupIndicatorSeries, IndicatorKind, MeanInvarianceStatus, PsychometricError, + classify_two_group_ols_invariance, combine_draw_level_ols_loadings, + recover_loading_point_estimate_mean, recover_strong_gated_latent_mean_difference, +}; + +fn rmse(truth: &[f64], recovered: &[f64]) -> f64 { + let n = truth.len() as f64; + let sum_sq: f64 = truth + .iter() + .zip(recovered) + .map(|(left, right)| { + let residual = left - right; + residual * residual + }) + .sum(); + (sum_sq / n).sqrt() +} + +#[test] +fn rubin_t_mean_recovers_true_loading_and_is_not_a_point_estimate_alias() { + let true_loading = 0.8_f64; + let factors = [-2.0_f64, -1.0, 0.0, 1.0, 2.0]; + let draws = [ + factors + .iter() + .map(|score| (true_loading - 0.05) * score) + .collect::>(), + factors + .iter() + .map(|score| (true_loading + 0.05) * score) + .collect::>(), + factors + .iter() + .map(|score| true_loading * score) + .collect::>(), + ]; + let combined = combine_draw_level_ols_loadings(&factors, &draws, IndicatorKind::LogisticNormal) + .expect("rubin"); + let point = + recover_loading_point_estimate_mean(&factors, &draws, IndicatorKind::LogisticNormal) + .expect("point"); + let error = rmse(&[true_loading], &[combined.mean_loading]); + assert!(error < 1e-12, "Rubin mean RMSE {error}"); + assert!((combined.mean_loading - point).abs() < 1e-15); + let expected = combined.within_variance + + (1.0 + 1.0 / (combined.draw_count as f64)) * combined.between_variance; + assert!((combined.total_variance - expected).abs() < 1e-15); + assert!(combined.between_variance > 0.0); +} + +#[test] +fn metric_status_matches_hash84_metric_and_refuses_latent_means() { + assert_eq!( + MeanInvarianceStatus::Metric.as_measurement_invariance_wire_name(), + "metric" + ); + assert!(MeanInvarianceStatus::Metric.licenses_shared_metric_meaning()); + assert!(!MeanInvarianceStatus::Metric.licenses_latent_mean_comparison()); + + let reference = GroupIndicatorSeries { + factor_scores: vec![-1.0, 0.0, 1.0], + indicators: vec![0.2, 1.0, 1.8], + }; + let comparison = GroupIndicatorSeries { + factor_scores: vec![-1.0, 0.0, 1.0], + indicators: vec![1.2, 2.0, 2.8], + }; + let classified = classify_two_group_ols_invariance( + &reference, + &comparison, + IndicatorKind::AdditiveLogRatio, + 1e-9, + 1e-9, + 1e-9, + ) + .expect("metric"); + assert_eq!(classified.status, MeanInvarianceStatus::Metric); + assert_eq!( + recover_strong_gated_latent_mean_difference( + &reference, + &comparison, + IndicatorKind::AdditiveLogRatio, + 1e-9, + 1e-9, + 1e-9, + ), + Err(PsychometricError::StrongInvarianceRequired) + ); +} + +#[test] +fn strong_status_matches_hash84_scalar_and_recovers_mean_difference() { + assert_eq!( + MeanInvarianceStatus::Strong.as_measurement_invariance_wire_name(), + "scalar" + ); + assert!(MeanInvarianceStatus::Strong.licenses_latent_mean_comparison()); + + let reference = GroupIndicatorSeries { + factor_scores: vec![-1.0, 0.0, 1.0], + indicators: vec![-0.8, 0.0, 0.8], + }; + let comparison = GroupIndicatorSeries { + factor_scores: vec![1.0, 2.0, 3.0], + indicators: vec![0.8, 1.6, 2.4], + }; + let difference = recover_strong_gated_latent_mean_difference( + &reference, + &comparison, + IndicatorKind::IsometricLogRatio, + 1e-9, + 1e-9, + 1e-9, + ) + .expect("strong"); + let error = rmse(&[2.0], &[difference]); + assert!(error < 1e-12, "latent-mean RMSE {error}"); +} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index f012e93a..ccd19e94 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -28,7 +28,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | report template/section/copied/style/modality method effects | ADR 0004/0012; PRD/TRD | simulation truth factors implemented; estimator-side method model remains future | partial | | candidate K statistical/Pareto gates + blinded LLM review | ADR 0012; research | future `model_selection` | accepted-target | | compositional topic correlation / stable clustering | ADR 0005/0012; research | future `network_analysis` | accepted-target | -| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, and posterior-draw point-estimate averaging on the active PR; full ESEM/DSEM and Rubin/joint uncertainty propagation remaining | partial | +| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS, event-time log-rate, and strong/strict-gated latent means on the stacked psychometric PR; full ESEM/DSEM remaining | partial | | CPU bounded multithreading + GPU/VRAM streaming/parity | ADR 0001/0006 | future `compute_backend` | accepted-target | | TDT detection/tracking vs CHRONOS schema/prediction/temporal consistency | ADR 0016; PRD/research | future `event_intelligence` | accepted-target | | evidence-bounded LLM interpretation | ADR 0010/0012; PRD | future `interpretation_gateway` | accepted-target | diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index 3eb214a0..96d1b096 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -1,7 +1,7 @@ # ADR 0005 — Posterior-aware ESEM/DSEM and structural interpretation **Decision status:** Accepted -**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging (not Rubin variance pooling), invariance-gated mean comparison, and causal-heuristic refusal are implemented on the active PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and continuous-time dynamics remain accepted-target +**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, CWC-then-event-time residual lag, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target **Date:** 2026-08-05 **Supersedes:** None. ADR 0012 governs upstream topic measurement/network coordinates; this ADR governs higher-order psychometric structure and longitudinal interpretation. @@ -14,12 +14,14 @@ TEPP also needs to distinguish stable between-unit differences from within-unit ## Decision Topic proportions are not treated as error-free ordinary indicators. TEPP uses logistic-normal latent coordinates or valid orthonormal log-ratio coordinates and propagates topic posterior uncertainty through plausible values or a joint text-measurement/structural model. -The current executable slice only averages loading point estimates across posterior draws. It does not yet pool within-draw and between-draw uncertainty and therefore does not satisfy the full posterior-propagation decision by itself. +The current executable slice averages loading point estimates across posterior draws and, separately, combines those complete-data OLS loadings with Rubin total variance. The point-estimate helper still does not by itself satisfy the full posterior-propagation decision. The Rubin helper uses complete-data OLS sampling variances; it does not treat the draws as Mislevy person-level plausible values. Before ESEM/SEM interpretation, each higher-order construct is classified as reflective, formative/composite, network, or unresolved. Reflective indicators may use ESEM/set-ESEM; formative structures use composite/formative models; interacting structures use network models. A good global fit statistic is not authority to reinterpret a formative/network structure as reflective. Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS and Kish-weighted slopes. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3) and refuses the difference quotient. Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. + Input/process/intervention/outcome paths obey event-time order. Temporal precedence, document linkage, event tracking, or model prediction alone do not justify causal language. ## Non-goals diff --git a/docs/adr/README.md b/docs/adr/README.md index 18c019d9..1cb26798 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -10,7 +10,7 @@ Read [`ADR_POLICY.md`](ADR_POLICY.md) first. **Decision status and implementatio | [0002](0002-six-clock-temporal-semantics.md) | Six-clock temporal semantics and fail-closed historical leakage prevention | Accepted | active-PR | Unmerged PR #8 is the canonical Task 3 replacement implementing typed clocks/intervals against the current protected-main lineage; conflicted PR #5 is superseded lineage. Later graph/split enforcement remains target work. | | [0003](0003-relational-event-multiple-membership.md) | Relational event ontology and time-varying cross-classified multiple membership | Accepted | partial | Weighted time-varying membership network/roles are active-PR (PR #12); full multilevel estimators, graph ontology, and persistence remain accepted-target. ADR 0016 owns event-intelligence tasks. | | [0004](0004-shared-multilingual-latent-space.md) | One shared multilingual latent space with explicit invariance status | Accepted | accepted-target | ADR 0012 owns the full topic-estimator/backend/global-topic contract. | -| [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | partial | Input gates, posterior-draw loading point-estimate averaging, and causal-refusal are on the active PR; Rubin uncertainty pooling remains target work; full ESEM/DSEM estimator remains accepted-target. | +| [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | partial | Input gates, posterior-draw point estimates, Rubin `T` on OLS loadings, CWC within/between OLS, event-time log-rate, and strong-gated latent means are on the stacked psychometric PR; full ESEM/DSEM estimator remains accepted-target. | | [0006](0006-vram-gpu-nvidia-orchestration.md) | VRAM-adaptive GPU compute and model-credential boundary | Accepted | accepted-target | LLM orchestration policy superseded by ADR 0010; autonomous development authority governed by ADR 0015. | | [0007](0007-rust-workspace-quality-gates.md) | Explicit Rust workspace, pinned toolchains, and exact quality gates | Accepted | implemented-main | ADR 0014 governs scientific/product claim promotion beyond repository-quality tooling. | | [0008](0008-immutable-evidence-identities-digests-and-spans.md) | Immutable evidence identities, `SHA-256` digests, exact spans, and strict wire reconstruction | Accepted | implemented-main | ADR 0013 governs future persistence/reproducibility/split authority. | diff --git a/docs/research/multilevel-event-time-recovery.md b/docs/research/multilevel-event-time-recovery.md new file mode 100644 index 00000000..bf80fa66 --- /dev/null +++ b/docs/research/multilevel-event-time-recovery.md @@ -0,0 +1,43 @@ +# Multilevel cluster-mean OLS and event-time log-rate + +## Scope + +This slice stays inside `psychometric_core`. It does not add a second invariance crate and does not recreate `#78` `longitudinal_core` or `#80` `irregular_time`. + +1. recover within-cluster and between-cluster OLS slopes after centering within cluster (CWC); +2. recover a Kish-weighted least-squares slope and report Kish ESS as the information diagnostic; +3. map a discrete lag-1 coefficient through the exact scalar exponential on **event time only**; +4. refuse the difference quotient as a continuous-time rate; +5. apply the same event-time map to CWC residuals (still not DSEM). + +## Claim boundary + +This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, and not a matrix `expm` implementation. + +## Authoritative sources + +Enders, C. K., & Tofighi, D. (2007). Centering predictor variables in cross-sectional multilevel models: A new look at an old issue. *Psychological Methods, 12*(2), 121–138. https://doi.org/10.1037/1082-989X.12.2.121 + +Curran, P. J., & Bauer, D. J. (2011). The disaggregation of within-person and between-person effects in longitudinal models of change. *Annual Review of Psychology, 62*, 583–619. https://doi.org/10.1146/annurev.psych.093008.100356 + +Hamaker, E. L., Kuiper, R. M., & Grasman, R. P. P. P. (2015). A critique of the cross-lagged panel model. *Psychological Methods, 20*(1), 102–116. https://doi.org/10.1037/a0038889 + +Voelkle, M. C., Oud, J. H. L., Davidov, E., & Schmidt, P. (2012). An SEM approach to continuous time modeling of panel data: Relating authoritarianism and anomia. *Psychological Methods, 17*(2), 176–192. https://doi.org/10.1037/a0027543 + +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 + +Kish, L. (1965). *Survey sampling*. John Wiley & Sons. + +## Formula notes + +- **CWC.** For cluster \(i\) and occasion \(t\), \(x_{it}^{w} = x_{it}-\bar x_{i}\) and \(y_{it}^{w} = y_{it}-\bar y_{i}\). The within slope is OLS of \(y^{w}\) on \(x^{w}\). The between slope is OLS of the cluster means. A grand-mean pooled slope confounds the two. +- **Kish ESS.** \(\mathrm{ESS}=(\sum w)^{2}/\sum w^{2}\) on non-negative finite weights. WLS uses the weights in the slope; ESS is not a second slope. +- **Exact scalar map.** Voelkle et al. (2012, Eq. 7) and Driver et al. (2017, Eq. 3): \(\varphi = A^{*}(\Delta t)=\exp(a\,\Delta t)\). The inverse is \(a=\ln\varphi/\Delta t\). The difference quotient \((x(t+\Delta t)-x(t))/\Delta t\) is refused. +- **CWC-then-lag.** Sample cluster means are removed first. Consecutive within residuals are then fitted by least squares to \(r_{t+\Delta t}\approx\exp(a\Delta t)\,r_{t}\) on event time. Same-sign pair-wise logs initialize the scalar Newton step. Sign-flipping \(T=2\) CWC pairs have no real logarithm and fail closed. + +## Verification + +- noiseless CWC recovers known within and between slopes with smaller computed RMSE than a pooled OLS collapse; +- Kish WLS recovers a known slope; +- the exact scalar map recovers a known drift on event time and refuses every other clock plus the difference quotient; +- CWC-then-lag on a two-cluster decaying series has smaller computed RMSE than a level-pooled series when the latter is identified. diff --git a/docs/research/multilevel-multiple-membership-measurement.md b/docs/research/multilevel-multiple-membership-measurement.md index 3b529b53..5cc47c3a 100644 --- a/docs/research/multilevel-multiple-membership-measurement.md +++ b/docs/research/multilevel-multiple-membership-measurement.md @@ -12,7 +12,7 @@ TEPP documents and events may simultaneously belong to authors, departments, cus ## Estimator target (future) -Production multilevel estimators (cross-classified / multiple-membership ESEM/DSEM) remain accepted-target in `psychometric_core`. Recovery studies must use realistic synthetic truth with known multilevel structure and report RMSE, bias, and coverage via `validation_core`; those metrics are a non-exhaustive subset of the acceptance contract. ADR 0005 additionally requires posterior-uncertainty propagation, construct-specific model classification, measurement invariance, within/between separation, irregular-time handling, and event-time ordering. +Production multilevel estimators (cross-classified / multiple-membership ESEM/DSEM) remain accepted-target in `psychometric_core`. The current executable slice recovers two-level CWC OLS and Kish-weighted slopes only. Recovery studies must use realistic synthetic truth with known multilevel structure and report RMSE, bias, and coverage via `validation_core`; those metrics are a non-exhaustive subset of the acceptance contract. ADR 0005 additionally requires posterior-uncertainty propagation, construct-specific model classification, measurement invariance, within/between separation, irregular-time handling, and event-time ordering. ## Authority sources diff --git a/docs/research/posterior-esem-input-gates.md b/docs/research/posterior-esem-input-gates.md index b431aae1..a56ab190 100644 --- a/docs/research/posterior-esem-input-gates.md +++ b/docs/research/posterior-esem-input-gates.md @@ -8,11 +8,11 @@ This slice delivers the first executable ADR 0005 contract in `psychometric_core 2. refuse raw topic-proportion Pearson correlations and OLS loadings as psychometric inputs; 3. admit ALR, ILR, or logistic-normal coordinates as unconstrained structural inputs while reserving orthonormal Aitchison-distance claims for ILR; 4. recover a reflective loading point estimate by ordinary least squares on a CPU `f64` path; -5. average recovered loading point estimates across posterior indicator draws without claiming Rubin within/between uncertainty pooling; -6. refuse latent-mean comparison without invariance evidence; +5. average recovered loading point estimates across posterior indicator draws without claiming Rubin within/between uncertainty pooling (the Rubin `T` helper is a separate API; see `docs/research/rubin-total-variance.md`); +6. refuse latent-mean comparison without invariance evidence, and recover a mean difference only under strong or strict two-group OLS status; 7. refuse causal language that rests only on temporal precedence, document linkage, event tracking, or model prediction. -Full ESEM/set-ESEM, formative composites, DSEM, and continuous-time dynamics remain accepted-target. +Cluster-mean CWC, Kish WLS, event-time log-rate, and CWC-then-lag live in the same crate and are doctored in `docs/research/multilevel-event-time-recovery.md`. Full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target. ## Authoritative sources diff --git a/docs/research/rubin-total-variance.md b/docs/research/rubin-total-variance.md new file mode 100644 index 00000000..28a44191 --- /dev/null +++ b/docs/research/rubin-total-variance.md @@ -0,0 +1,32 @@ +# Rubin total variance on draw-level OLS loadings + +## Scope + +Adds Rubin combining for complete-data OLS loadings across posterior indicator draws. The existing arithmetic-mean helper remains a point-estimate summary and still must not be described as Rubin pooling. + +## Claim boundary + +`T_m = \bar U_m + (1+1/m)B_m` is the complete-data combining rule. This slice does **not** implement Mislevy plausible values. The 1991 *Psychometrika* paper was not opened in this cycle (Cambridge/Springer/ETS copies were HTML stubs or paywalled). Do not cite that paper as having been read. + +## Authoritative sources + +Rubin, D. B. (1996). Multiple imputation after 18+ years. *Journal of the American Statistical Association, 91*(434), 473–489. https://doi.org/10.1080/01621459.1996.10476908 + +The scanned page 473 restates \(T_m=\bar U_m+(1+1/m)B_m\) from Rubin (1987). The 1987 book itself was not opened. + +## Formula notes + +For \(m\ge 2\) complete-data OLS loadings \(\hat Q_\ell\) with sampling variances \(U_\ell=\hat\sigma^{2}_\ell/\sum(f-\bar f)^{2}\): + +- \(\bar Q_m=m^{-1}\sum\hat Q_\ell\) +- \(\bar U_m=m^{-1}\sum U_\ell\) +- \(B_m=(m-1)^{-1}\sum(\hat Q_\ell-\bar Q_m)^{2}\) +- \(T_m=\bar U_m+(1+1/m)B_m\) + +Two-point lines have residual variance 0 and therefore \(U_\ell=0\). Symmetric draw noise can still produce \(B_m>0\). + +## Verification + +- the combined mean recovers a known loading with machine-scale computed RMSE and matches the point-estimate mean; +- reported \(T\) equals \(\bar U+(1+1/m)B\) on the same draws; +- raw-proportion, singleton-draw, and singular designs fail closed. diff --git a/docs/research/standards-and-literature.md b/docs/research/standards-and-literature.md index 3f7c083d..a2737525 100644 --- a/docs/research/standards-and-literature.md +++ b/docs/research/standards-and-literature.md @@ -14,11 +14,23 @@ Marsh, H. W., Morin, A. J. S., Parker, P. D., & Kaur, G. (2014). Exploratory str Bollen, K., & Lennox, R. (1991). Conventional wisdom on measurement: A structural equation perspective. *Psychological Bulletin, 110*(2), 305–314. https://doi.org/10.1037/0033-2909.110.2.305 +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 + +Enders, C. K., & Tofighi, D. (2007). Centering predictor variables in cross-sectional multilevel models: A new look at an old issue. *Psychological Methods, 12*(2), 121–138. https://doi.org/10.1037/1082-989X.12.2.121 + +Curran, P. J., & Bauer, D. J. (2011). The disaggregation of within-person and between-person effects in longitudinal models of change. *Annual Review of Psychology, 62*, 583–619. https://doi.org/10.1146/annurev.psych.093008.100356 + +Hamaker, E. L., Kuiper, R. M., & Grasman, R. P. P. P. (2015). A critique of the cross-lagged panel model. *Psychological Methods, 20*(1), 102–116. https://doi.org/10.1037/a0038889 + +Voelkle, M. C., Oud, J. H. L., Davidov, E., & Schmidt, P. (2012). An SEM approach to continuous time modeling of panel data: Relating authoritarianism and anomia. *Psychological Methods, 17*(2), 176–192. https://doi.org/10.1037/a0027543 + +Rubin, D. B. (1996). Multiple imputation after 18+ years. *Journal of the American Statistical Association, 91*(434), 473–489. https://doi.org/10.1080/01621459.1996.10476908 + Mislevy, R. J. (1991). Randomization-based inference about latent variables from complex samples. *Psychometrika, 56*(2), 177–196. https://doi.org/10.1007/BF02294457 Holland, P. W. (1986). Statistics and causal inference. *Journal of the American Statistical Association, 81*(396), 945–960. https://doi.org/10.1080/01621459.1986.10478354 -TEPP applies these sources to construct definition, score interpretation, reliability, validity evidence, uncertainty, consequences, longitudinal invariance, ESEM cross-loadings, and DSEM. Topic outputs are treated as fallible indicators or components only after their construct role is evaluated. Reflective, formative, and network classes remain distinct (Bollen & Lennox, 1991). Posterior uncertainty is propagated by averaging structural estimates across plausible values (Mislevy, 1991). Temporal precedence is not causal identification (Holland, 1986). +TEPP applies these sources to construct definition, score interpretation, reliability, validity evidence, uncertainty, consequences, longitudinal invariance, ESEM cross-loadings, and DSEM. Topic outputs are treated as fallible indicators or components only after their construct role is evaluated. Reflective, formative, and network classes remain distinct (Bollen & Lennox, 1991). Complete-data OLS loadings across posterior indicator draws are combined with Rubin (1996) \(T_m\); the arithmetic-mean helper remains a point estimate. Mislevy (1991) is listed as a future plausible-value source and was not opened in this cycle. Temporal precedence is not causal identification (Holland, 1986). Within/between OLS follows Enders and Tofighi (2007), Curran and Bauer (2011), and Hamaker et al. (2015). The scalar continuous-time map is Voelkle et al. (2012, Eq. 7) / Driver et al. (2017, Eq. 3). Metric/weak invariance does not license latent-mean comparison. ## Structural, correlated, dynamic, relational, and multilingual topic models diff --git a/docs/research/strong-invariance-latent-means.md b/docs/research/strong-invariance-latent-means.md new file mode 100644 index 00000000..c7dd9756 --- /dev/null +++ b/docs/research/strong-invariance-latent-means.md @@ -0,0 +1,40 @@ +# Strong-invariance gate for two-group OLS latent means + +## Scope + +Adds a two-group OLS classification of configural / metric / strong / strict status and recovers \((\bar y_c-\bar y_r)/\lambda\) only when strong or strict holds. The boolean `compare_latent_means` helper is unchanged. + +This slice does **not** import the unpublished `measurement_invariance` crate on `#84`. Claim-boundary tests use that crate's wire names (`configural`, `metric`, `scalar`) as documented strings only. + +## Claim boundary + +- `#84` `metric` licenses shared **metric** meaning. It does **not** license latent means. +- `#84` `scalar` is the strong/scalar status (equal loading and intercept). That status licenses latent means. +- Strict (also equal residual variance) also licenses latent means. +- This is two-group OLS, not MGCFA, not partial invariance, and not alignment optimization. +- Meredith (1993) names weak/strong/strict are used only as conventional labels. That PDF was not opened (Springer remains closed; Unpaywall reported no OA location). Do not cite Meredith equations as having been read. + +## Authoritative sources used for the mean gate + +The executable gate follows the ADR 0005 rule that mean comparison requires the invariance level needed for that claim, and the `#84` terminology split between metric meaning and scalar/strong means. Opened sources that constrain the surrounding longitudinal/invariance stance: + +Asparouhov, T., & Muthén, B. (2009). Exploratory structural equation modeling. *Structural Equation Modeling: A Multidisciplinary Journal, 16*(3), 397–438. https://doi.org/10.1080/10705510903008204 + +Hamaker, E. L., Kuiper, R. M., & Grasman, R. P. P. P. (2015). A critique of the cross-lagged panel model. *Psychological Methods, 20*(1), 102–116. https://doi.org/10.1037/a0038889 + +## Formula notes + +Per group, \(y=\nu+\lambda f+e\) is fit by OLS. Status is: + +- configural when \(|\lambda_r-\lambda_c|\) exceeds tolerance; +- metric when loadings match and intercepts differ; +- strong when loadings and intercepts match and residual variances differ; +- strict when loadings, intercepts, and residual variances match. + +The latent-mean difference is \((\bar y_c-\bar y_r)/\lambda\) with \(\lambda\) the midpoint of the two loadings, and only after strong or strict. + +## Verification + +- strong/strict series recover a known mean difference with computed RMSE; +- metric-only (equal loading, shifted intercept) and configural series return `StrongInvarianceRequired`; +- `#84` wire-name tests: `metric` licenses shared metric meaning and refuses means; `scalar` is strong and licenses means. diff --git a/docs/validation/temporal-event-foundation.md b/docs/validation/temporal-event-foundation.md index ac659ebc..1e4cc529 100644 --- a/docs/validation/temporal-event-foundation.md +++ b/docs/validation/temporal-event-foundation.md @@ -23,7 +23,7 @@ This report tracks exact-head scientific and engineering evidence required befor | Truth corpora / manifests | `tepp_simulation` | implemented-main | — | deterministic generator tests | Task 10 / PR #18 | | Recovery metrics | `validation_core` | implemented-main | — | RMSE/bias/coverage/MC gates | Task 11 / PR #19 | | Versioned API/export contracts | `tepp_api` | implemented-main | naruon HTTP interchange | unknown-field/version/limit + naruon HTTPS interchange tests | Task 12 / PR #21; live HTTP service remaining | -| Psychometric structural input gates | `psychometric_core` | accepted-target | active PR | construct-class refusal + ALR/ILR boundary + true-loading RMSE + posterior-draw point-estimate mean; full ESEM/DSEM/Rubin uncertainty remaining | ADR 0005; `docs/research/posterior-esem-input-gates.md` | +| Psychometric structural input gates | `psychometric_core` | accepted-target | stacked psychometric PR | construct-class refusal + ALR/ILR boundary + true-loading RMSE + posterior-draw point-estimate mean + Rubin `T` + CWC within/between + event-time log-rate + strong-gated latent means; full ESEM/DSEM remaining | ADR 0005; `docs/research/posterior-esem-input-gates.md`; `docs/research/multilevel-event-time-recovery.md`; `docs/research/rubin-total-variance.md`; `docs/research/strong-invariance-latent-means.md` | | CWL modular connectors | `docs/connectors/*` | implemented-main | — | contract docs + examples | PR #22; live HTTP ports remaining | | Release SBOM/provenance generator | `scripts/release_evidence.py` | partial | — | generate+validate in CI | Task 13 partial / PR #28 | From 14a28f0e61da2a5bd68fa292a1c3cadf9fa7326f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 09:35:59 +0000 Subject: [PATCH 03/62] feat(psychometric): map irregular already-centered residuals without re-centering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Curran and Bauer (2011, pp. 607–608) show that subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect. Caller-supplied residuals with irregular event intervals recover a = ln(φ)/Δt. Still not DSEM. Meredith (1993) and Mislevy (1991) remain unread (Unpaywall closed). --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 1 + CLAUDE.md | 2 +- README.md | 2 +- crates/psychometric_core/src/event_time.rs | 157 +++++++++++++++++- crates/psychometric_core/src/lib.rs | 9 +- ...multilevel_event_time_recovery_contract.rs | 55 +++++- .../scientific_claim_boundary_contract.rs | 57 ++++++- docs/TRACEABILITY.md | 2 +- docs/adr/0005-posterior-esem-dsem.md | 4 +- docs/adr/README.md | 2 +- .../multilevel-event-time-recovery.md | 9 +- docs/research/posterior-esem-input-gates.md | 2 +- docs/research/rubin-total-variance.md | 2 +- docs/research/standards-and-literature.md | 4 +- .../strong-invariance-latent-means.md | 2 +- docs/validation/temporal-event-foundation.md | 2 +- 17 files changed, 291 insertions(+), 23 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index e48d7625..e8fbaad3 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -61,7 +61,7 @@ boundaries above remain the target modular MSA architecture. | `tepp_simulation` | known-truth temporal/event data generation | | `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics | | `tepp_api` | versioned DTO, schema, and export contracts | -| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS, event-time log-rate, Rubin `T` on OLS loadings, and strong-gated latent means | +| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS, event-time log-rate, irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means | No crate exposes placeholder production behavior in Task 1. This prevents an empty façade from becoming a de facto public API before its invariants and tests diff --git a/CHANGELOG.md b/CHANGELOG.md index 72e8ef62..1844d9b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang ### Added +- Irregular already-centered residual event-time log-rate in `psychometric_core`: caller-supplied within residuals with irregular `Δt` recover `a = ln(φ)/Δt`. Cluster-mean subtraction on a raw autoregressive series is not that estimand (Curran & Bauer, 2011, pp. 607–608). Still not DSEM. - `psychometric_core` multilevel/event-time recovery on the stacked psychometric PR: cluster-mean CWC within/between OLS, Kish ESS weighted slopes, event-time-only discrete lag-1 and exact scalar local log-rate, CWC-then-event-time residual lag, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, and two-group OLS strong/strict-gated latent-mean difference. Metric/weak is not a mean license. Not DSEM, not RI-CLPM, not MGCFA, not Mislevy PVs (ADR 0005; no new migration). - `psychometric_core` posterior-aware structural input gates: construct classification, refusal of raw-proportion Pearson/OLS, explicit ALR-versus-ILR geometry boundaries, CPU `f64` OLS recovery, posterior-draw loading point-estimate averaging without Rubin uncertainty claims, invariance-gated latent-mean comparison, and causal-heuristic refusal (ADR 0005 first production slice; no new migration). - `persistence_postgres` backup/restore integrity: restored snapshots stay unusable until tenant, canonical `SHA-256`, knowledge-cutoff eligibility, temporal window order, and append-only triggers revalidate; SQL probes raise `restore integrity failed` (ADR 0013). diff --git a/CLAUDE.md b/CLAUDE.md index 1b89f710..99f293bf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,7 +16,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. - Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. -- Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. +- Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Never use future-available evidence in historical model fits. - Do not blanket-mask PII when identity/role/linkage is scientifically required. Follow the purpose-bound separation, opaque-ID, encryption, retention, and audit contract in `docs/PRIVACY_DATA_GOVERNANCE.md`. - Treat documents and LLM outputs as untrusted. Model routing/orchestration may vary reasoning effort, decomposition, recursion and roles, but deterministic/statistical gates remain authoritative. diff --git a/README.md b/README.md index 53bfbd89..25357b64 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ crates/corpus_split crates/tepp_simulation crates/validation_core crates/tepp_api -crates/psychometric_core # input gates, CWC/event-time, Rubin T, strong means; not a full ESEM/DSEM estimator +crates/psychometric_core # input gates, CWC/event-time, irregular residual lag, Rubin T, strong means; not a full ESEM/DSEM estimator ``` ## Local verification diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index fe418c3a..5d51890b 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -70,6 +70,20 @@ pub struct ClusteredEventScore { pub score: f64, } +/// One already-centered lagged residual pair on event time. +/// +/// `earlier_residual` and `later_residual` are within residuals the caller +/// already formed. This type is not a raw score and is not re-centered. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct LaggedWithinResidual { + /// Earlier within residual. + pub earlier_residual: f64, + /// Later within residual. + pub later_residual: f64, + /// Strictly positive event-time interval. Intervals may be irregular. + pub event_delta: f64, +} + /// Discrete lag-1 coefficient and its exact local log-rate. #[derive(Clone, Copy, Debug, PartialEq)] pub struct DiscreteLagAndLogRate { @@ -210,6 +224,13 @@ pub fn recover_event_series_mean_log_rate( /// Stable between-cluster means are removed first (CWC). Consecutive /// within-cluster residuals then use the exact scalar map. This is not DSEM. /// +/// Curran and Bauer (2011, pp. 607–608) show that subtracting the observed +/// person-specific mean from a raw autoregressive series does **not** isolate +/// the lagged within-person effect. This helper therefore does not claim to +/// recover the raw-process drift `a` from CWC of a raw AR path. For that +/// estimand, supply already-centered lagged residuals to +/// [`recover_irregular_centered_residual_log_rate`]. +/// /// # Errors /// /// Returns [`PsychometricError::EventTimeRequired`] for a non-event clock, @@ -264,6 +285,50 @@ pub fn recover_within_residual_event_time_log_rate( fit_scalar_log_rate(&pairs) } +/// Mean exact scalar log-rate on already-centered residuals with irregular intervals. +/// +/// Each pair is `a = ln(later / earlier) / Δt` (Voelkle et al., 2012, Eq. 7). +/// The function does **not** center again. Curran and Bauer (2011, pp. 607–608) +/// reject person-mean subtraction on a raw autoregressive series as the +/// lagged within-person residual. Intervals may be irregular. This is not DSEM. +/// +/// # Errors +/// +/// Returns [`PsychometricError::EventTimeRequired`] for a non-event clock, +/// [`PsychometricError::InvalidNumericInput`] for an empty series or a +/// non-finite / non-positive residual ratio, and +/// [`PsychometricError::NonPositiveInterval`] when any interval is not +/// strictly positive. +pub fn recover_irregular_centered_residual_log_rate( + pairs: &[LaggedWithinResidual], + clock: LagClock, +) -> Result { + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + if pairs.is_empty() { + return Err(PsychometricError::InvalidNumericInput); + } + let mut sum = 0.0_f64; + for pair in pairs { + if !pair.earlier_residual.is_finite() + || !pair.later_residual.is_finite() + || !pair.event_delta.is_finite() + { + return Err(PsychometricError::InvalidNumericInput); + } + let recovered = recover_event_time_discrete_lag_and_log_rate( + pair.earlier_residual, + pair.later_residual, + pair.event_delta, + clock, + )?; + sum += recovered.log_rate; + } + let count = pairs.len() as f64; + require_finite(sum / count) +} + fn fit_scalar_log_rate(pairs: &[(f64, f64, f64)]) -> Result { if pairs.is_empty() { return Err(PsychometricError::InvalidNumericInput); @@ -318,8 +383,9 @@ fn fit_scalar_log_rate(pairs: &[(f64, f64, f64)]) -> Result LaggedWithinResidual { + LaggedWithinResidual { + earlier_residual, + later_residual, + event_delta, + } + } + + #[test] + fn irregular_centered_residuals_recover_exact_drift() { + let drift = -0.4_f64; + let pairs = [ + lagged(1.2, 1.2 * (drift * 0.5).exp(), 0.5), + lagged(0.8, 0.8 * (drift * 1.75).exp(), 1.75), + lagged(-1.1, -1.1 * (drift * 2.25).exp(), 2.25), + ]; + let recovered = recover_irregular_centered_residual_log_rate(&pairs, LagClock::EventTime) + .expect("irregular"); + assert!((recovered - drift).abs() < 1e-12); + } + + #[test] + fn irregular_centered_residuals_fail_closed() { + let ok = lagged(1.0, 0.8, 1.0); + assert_eq!( + recover_irregular_centered_residual_log_rate(&[ok], LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_irregular_centered_residual_log_rate(&[], LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_irregular_centered_residual_log_rate( + &[lagged(f64::NAN, 0.8, 1.0)], + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_irregular_centered_residual_log_rate( + &[lagged(1.0, f64::INFINITY, 1.0)], + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_irregular_centered_residual_log_rate( + &[lagged(1.0, 0.8, f64::NAN)], + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_irregular_centered_residual_log_rate( + &[lagged(0.0, 0.8, 1.0)], + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_irregular_centered_residual_log_rate( + &[lagged(1.0, -0.8, 1.0)], + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_irregular_centered_residual_log_rate( + &[lagged(1.0, 0.8, 0.0)], + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_irregular_centered_residual_log_rate( + &[lagged(1.0, 0.8, -0.5)], + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + } } diff --git a/crates/psychometric_core/src/lib.rs b/crates/psychometric_core/src/lib.rs index 90ea4393..1095d79b 100644 --- a/crates/psychometric_core/src/lib.rs +++ b/crates/psychometric_core/src/lib.rs @@ -9,7 +9,8 @@ //! estimates across posterior draws on a CPU `f64` path without claiming Rubin //! uncertainty pooling, combines draw-level OLS loadings with Rubin `T`, //! decomposes cluster-mean within/between OLS, maps event-time discrete lags -//! through the exact scalar exponential, and refuses latent-mean comparison +//! through the exact scalar exponential, maps already-centered irregular +//! residuals without re-centering, and refuses latent-mean comparison //! below strong invariance. mod causality; @@ -53,15 +54,19 @@ pub use event_time::DiscreteLagAndLogRate; 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; /// Noiseless scalar discrete lag `later / earlier`. pub use event_time::recover_discrete_lag_one; /// Mean local log-rate on a sorted event-time series. pub use event_time::recover_event_series_mean_log_rate; /// Exact scalar pair `(φ, a)` on event time. pub use event_time::recover_event_time_discrete_lag_and_log_rate; +/// Mean exact log-rate on already-centered irregular residuals. +pub use event_time::recover_irregular_centered_residual_log_rate; /// Exact scalar inverse `a = ln(φ) / Δt`. pub use event_time::recover_local_log_rate; -/// CWC-then-event-time local log-rate (not DSEM). +/// CWC-then-event-time local log-rate (not DSEM; not raw-process AR drift). pub use event_time::recover_within_residual_event_time_log_rate; /// Refuse the difference quotient as a continuous-time rate. pub use event_time::refuse_difference_quotient_as_local_rate; 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 7359a35d..c9f8d166 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -2,9 +2,10 @@ #![allow(clippy::cast_precision_loss)] use psychometric_core::{ - ClusteredEventScore, ClusteredScore, EventOccasion, IndicatorKind, LagClock, PsychometricError, - ordinary_least_squares_slope, recover_cluster_mean_within_between_slopes, - recover_event_series_mean_log_rate, recover_event_time_discrete_lag_and_log_rate, + ClusteredEventScore, ClusteredScore, EventOccasion, IndicatorKind, LagClock, + LaggedWithinResidual, PsychometricError, ordinary_least_squares_slope, + recover_cluster_mean_within_between_slopes, recover_event_series_mean_log_rate, + recover_event_time_discrete_lag_and_log_rate, recover_irregular_centered_residual_log_rate, recover_kish_weighted_slope, recover_within_residual_event_time_log_rate, refuse_difference_quotient_as_local_rate, }; @@ -134,6 +135,54 @@ fn within_residual_event_time_log_rate_beats_pooled_levels() { assert!(within_error < 0.25, "CWC lag RMSE {within_error} too large"); } +#[test] +fn irregular_centered_residuals_recover_known_drift_better_than_cwc_of_raw_ar() { + let true_drift = -0.35_f64; + let pairs = [ + LaggedWithinResidual { + earlier_residual: 1.4, + later_residual: 1.4 * (true_drift * 0.4).exp(), + event_delta: 0.4, + }, + LaggedWithinResidual { + earlier_residual: 0.9, + later_residual: 0.9 * (true_drift * 1.6).exp(), + event_delta: 1.6, + }, + LaggedWithinResidual { + earlier_residual: -0.7, + later_residual: -0.7 * (true_drift * 2.2).exp(), + event_delta: 2.2, + }, + ]; + let centered = recover_irregular_centered_residual_log_rate(&pairs, LagClock::EventTime) + .expect("centered residual"); + let centered_error = rmse(&[true_drift], &[centered]); + assert!( + centered_error < 1e-12, + "already-centered irregular RMSE {centered_error}" + ); + + let mut raw_ar = Vec::new(); + for (cluster, person_mean, start) in [(1_u64, 7.5_f64, 1.1_f64), (2, -4.0, 0.8)] { + for step in 0..6 { + let time = f64::from(step); + raw_ar.push(ClusteredEventScore { + cluster_key: cluster, + event_time: time, + score: person_mean + start * (true_drift * time).exp(), + }); + } + } + let cwc = + recover_within_residual_event_time_log_rate(&raw_ar, LagClock::EventTime).expect("cwc ar"); + let cwc_error = rmse(&[true_drift], &[cwc]); + assert!( + cwc_error > centered_error, + "Curran & Bauer: CWC of raw AR RMSE {cwc_error} must exceed already-centered {centered_error}" + ); +} + #[test] fn admitted_coordinates_still_required_for_multilevel_weights() { assert_eq!( diff --git a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs index 91240def..c1667cec 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -1,7 +1,9 @@ //! Scientific claim boundaries for compositional coordinates and posterior draws. use psychometric_core::{ - IndicatorKind, posterior_draw_point_estimate_mean, recover_loading_point_estimate_mean, + ClusteredEventScore, IndicatorKind, LagClock, LaggedWithinResidual, + posterior_draw_point_estimate_mean, recover_irregular_centered_residual_log_rate, + recover_loading_point_estimate_mean, recover_within_residual_event_time_log_rate, }; #[test] @@ -32,3 +34,56 @@ fn posterior_draw_helpers_report_point_estimates_without_rubin_variance_claims() .expect("posterior-draw point-estimate mean"); assert!((loading - 0.8).abs() < 1e-15); } + +#[test] +fn person_mean_subtraction_on_raw_ar_is_not_the_lagged_within_effect() { + let drift = -0.28_f64; + let centered = recover_irregular_centered_residual_log_rate( + &[LaggedWithinResidual { + earlier_residual: 1.0, + later_residual: (drift * 1.3).exp(), + event_delta: 1.3, + }], + LagClock::EventTime, + ) + .expect("already centered"); + assert!((centered - drift).abs() < 1e-12); + + let raw = [ + ClusteredEventScore { + cluster_key: 1, + event_time: 0.0, + score: 6.0 + 1.0, + }, + ClusteredEventScore { + cluster_key: 1, + event_time: 1.0, + score: 6.0 + drift.exp(), + }, + ClusteredEventScore { + cluster_key: 1, + event_time: 2.0, + score: 6.0 + (drift * 2.0).exp(), + }, + ClusteredEventScore { + cluster_key: 2, + event_time: 0.0, + score: -3.0 + 1.2, + }, + ClusteredEventScore { + cluster_key: 2, + event_time: 1.0, + score: -3.0 + 1.2 * drift.exp(), + }, + ClusteredEventScore { + cluster_key: 2, + event_time: 2.0, + score: -3.0 + 1.2 * (drift * 2.0).exp(), + }, + ]; + let cwc = recover_within_residual_event_time_log_rate(&raw, LagClock::EventTime).expect("cwc"); + assert!( + (cwc - drift).abs() > 1e-6, + "Curran & Bauer (2011, pp. 607–608): CWC of raw AR recovered {cwc}, which must not equal drift {drift}" + ); +} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index ccd19e94..9931472d 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -28,7 +28,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | report template/section/copied/style/modality method effects | ADR 0004/0012; PRD/TRD | simulation truth factors implemented; estimator-side method model remains future | partial | | candidate K statistical/Pareto gates + blinded LLM review | ADR 0012; research | future `model_selection` | accepted-target | | compositional topic correlation / stable clustering | ADR 0005/0012; research | future `network_analysis` | accepted-target | -| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS, event-time log-rate, and strong/strict-gated latent means on the stacked psychometric PR; full ESEM/DSEM remaining | partial | +| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS, event-time log-rate, irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR; full ESEM/DSEM remaining | partial | | CPU bounded multithreading + GPU/VRAM streaming/parity | ADR 0001/0006 | future `compute_backend` | accepted-target | | TDT detection/tracking vs CHRONOS schema/prediction/temporal consistency | ADR 0016; PRD/research | future `event_intelligence` | accepted-target | | evidence-bounded LLM interpretation | ADR 0010/0012; PRD | future `interpretation_gateway` | accepted-target | diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index 96d1b096..86ac01e9 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -1,7 +1,7 @@ # ADR 0005 — Posterior-aware ESEM/DSEM and structural interpretation **Decision status:** Accepted -**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, CWC-then-event-time residual lag, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target +**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target **Date:** 2026-08-05 **Supersedes:** None. ADR 0012 governs upstream topic measurement/network coordinates; this ADR governs higher-order psychometric structure and longitudinal interpretation. @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS and Kish-weighted slopes. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3) and refuses the difference quotient. Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS and Kish-weighted slopes. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3) and refuses the difference quotient. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. 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/adr/README.md b/docs/adr/README.md index 1cb26798..bdf6b44d 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -10,7 +10,7 @@ Read [`ADR_POLICY.md`](ADR_POLICY.md) first. **Decision status and implementatio | [0002](0002-six-clock-temporal-semantics.md) | Six-clock temporal semantics and fail-closed historical leakage prevention | Accepted | active-PR | Unmerged PR #8 is the canonical Task 3 replacement implementing typed clocks/intervals against the current protected-main lineage; conflicted PR #5 is superseded lineage. Later graph/split enforcement remains target work. | | [0003](0003-relational-event-multiple-membership.md) | Relational event ontology and time-varying cross-classified multiple membership | Accepted | partial | Weighted time-varying membership network/roles are active-PR (PR #12); full multilevel estimators, graph ontology, and persistence remain accepted-target. ADR 0016 owns event-intelligence tasks. | | [0004](0004-shared-multilingual-latent-space.md) | One shared multilingual latent space with explicit invariance status | Accepted | accepted-target | ADR 0012 owns the full topic-estimator/backend/global-topic contract. | -| [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | partial | Input gates, posterior-draw point estimates, Rubin `T` on OLS loadings, CWC within/between OLS, event-time log-rate, and strong-gated latent means are on the stacked psychometric PR; full ESEM/DSEM estimator remains accepted-target. | +| [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | partial | Input gates, posterior-draw point estimates, Rubin `T` on OLS loadings, CWC within/between OLS, event-time log-rate, irregular already-centered residual lag, and strong-gated latent means are on the stacked psychometric PR; full ESEM/DSEM estimator remains accepted-target. | | [0006](0006-vram-gpu-nvidia-orchestration.md) | VRAM-adaptive GPU compute and model-credential boundary | Accepted | accepted-target | LLM orchestration policy superseded by ADR 0010; autonomous development authority governed by ADR 0015. | | [0007](0007-rust-workspace-quality-gates.md) | Explicit Rust workspace, pinned toolchains, and exact quality gates | Accepted | implemented-main | ADR 0014 governs scientific/product claim promotion beyond repository-quality tooling. | | [0008](0008-immutable-evidence-identities-digests-and-spans.md) | Immutable evidence identities, `SHA-256` digests, exact spans, and strict wire reconstruction | Accepted | implemented-main | ADR 0013 governs future persistence/reproducibility/split authority. | diff --git a/docs/research/multilevel-event-time-recovery.md b/docs/research/multilevel-event-time-recovery.md index bf80fa66..67024866 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -8,7 +8,8 @@ This slice stays inside `psychometric_core`. It does not add a second invariance 2. recover a Kish-weighted least-squares slope and report Kish ESS as the information diagnostic; 3. map a discrete lag-1 coefficient through the exact scalar exponential on **event time only**; 4. refuse the difference quotient as a continuous-time rate; -5. apply the same event-time map to CWC residuals (still not DSEM). +5. apply the same event-time map to CWC residuals (still not DSEM); +6. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). ## Claim boundary @@ -33,11 +34,13 @@ Kish, L. (1965). *Survey sampling*. John Wiley & Sons. - **CWC.** For cluster \(i\) and occasion \(t\), \(x_{it}^{w} = x_{it}-\bar x_{i}\) and \(y_{it}^{w} = y_{it}-\bar y_{i}\). The within slope is OLS of \(y^{w}\) on \(x^{w}\). The between slope is OLS of the cluster means. A grand-mean pooled slope confounds the two. - **Kish ESS.** \(\mathrm{ESS}=(\sum w)^{2}/\sum w^{2}\) on non-negative finite weights. WLS uses the weights in the slope; ESS is not a second slope. - **Exact scalar map.** Voelkle et al. (2012, Eq. 7) and Driver et al. (2017, Eq. 3): \(\varphi = A^{*}(\Delta t)=\exp(a\,\Delta t)\). The inverse is \(a=\ln\varphi/\Delta t\). The difference quotient \((x(t+\Delta t)-x(t))/\Delta t\) is refused. -- **CWC-then-lag.** Sample cluster means are removed first. Consecutive within residuals are then fitted by least squares to \(r_{t+\Delta t}\approx\exp(a\Delta t)\,r_{t}\) on event time. Same-sign pair-wise logs initialize the scalar Newton step. Sign-flipping \(T=2\) CWC pairs have no real logarithm and fail closed. +- **CWC-then-lag.** Sample cluster means are removed first. Consecutive within residuals are then fitted by least squares to \(r_{t+\Delta t}\approx\exp(a\Delta t)\,r_{t}\) on event time. Same-sign pair-wise logs initialize the scalar Newton step. Sign-flipping \(T=2\) CWC pairs have no real logarithm and fail closed. Curran and Bauer (2011, pp. 607–608) show that this person-mean subtraction on a raw autoregressive series does **not** isolate the lagged within-person effect; the helper therefore does not claim to recover the raw-process drift. +- **Already-centered irregular residual.** The caller supplies lagged within residuals. The mean of \(a=\ln(r_{t+\Delta t}/r_t)/\Delta t\) is the exact scalar map. Intervals may be irregular. The helper does not center again. This is not DSEM. ## Verification - noiseless CWC recovers known within and between slopes with smaller computed RMSE than a pooled OLS collapse; - Kish WLS recovers a known slope; - the exact scalar map recovers a known drift on event time and refuses every other clock plus the difference quotient; -- CWC-then-lag on a two-cluster decaying series has smaller computed RMSE than a level-pooled series when the latter is identified. +- CWC-then-lag on a two-cluster decaying series has smaller computed RMSE than a level-pooled series when the latter is identified; +- already-centered irregular residuals recover a known drift at machine-scale RMSE, and that RMSE is smaller than CWC of the corresponding raw autoregressive series (Curran & Bauer, 2011, pp. 607–608). diff --git a/docs/research/posterior-esem-input-gates.md b/docs/research/posterior-esem-input-gates.md index a56ab190..22ee69fb 100644 --- a/docs/research/posterior-esem-input-gates.md +++ b/docs/research/posterior-esem-input-gates.md @@ -12,7 +12,7 @@ This slice delivers the first executable ADR 0005 contract in `psychometric_core 6. refuse latent-mean comparison without invariance evidence, and recover a mean difference only under strong or strict two-group OLS status; 7. refuse causal language that rests only on temporal precedence, document linkage, event tracking, or model prediction. -Cluster-mean CWC, Kish WLS, event-time log-rate, and CWC-then-lag live in the same crate and are doctored in `docs/research/multilevel-event-time-recovery.md`. Full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target. +Cluster-mean CWC, Kish WLS, event-time log-rate, CWC-then-lag, and irregular already-centered residual log-rate live in the same crate and are doctored in `docs/research/multilevel-event-time-recovery.md`. Full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target. ## Authoritative sources diff --git a/docs/research/rubin-total-variance.md b/docs/research/rubin-total-variance.md index 28a44191..49bf0a24 100644 --- a/docs/research/rubin-total-variance.md +++ b/docs/research/rubin-total-variance.md @@ -6,7 +6,7 @@ Adds Rubin combining for complete-data OLS loadings across posterior indicator d ## Claim boundary -`T_m = \bar U_m + (1+1/m)B_m` is the complete-data combining rule. This slice does **not** implement Mislevy plausible values. The 1991 *Psychometrika* paper was not opened in this cycle (Cambridge/Springer/ETS copies were HTML stubs or paywalled). Do not cite that paper as having been read. +`T_m = \bar U_m + (1+1/m)B_m` is the complete-data combining rule. This slice does **not** implement Mislevy plausible values. The 1991 *Psychometrika* paper was not opened in this cycle (Unpaywall 2026-08-17: closed; Cambridge/Springer/ETS copies were HTML stubs or paywalled). Do not cite that paper as having been read. ## Authoritative sources diff --git a/docs/research/standards-and-literature.md b/docs/research/standards-and-literature.md index a2737525..586e1fbf 100644 --- a/docs/research/standards-and-literature.md +++ b/docs/research/standards-and-literature.md @@ -28,9 +28,11 @@ Rubin, D. B. (1996). Multiple imputation after 18+ years. *Journal of the Americ Mislevy, R. J. (1991). Randomization-based inference about latent variables from complex samples. *Psychometrika, 56*(2), 177–196. https://doi.org/10.1007/BF02294457 +Meredith, W. (1993). Measurement invariance, factor analysis and factorial invariance. *Psychometrika, 58*(4), 525–543. https://doi.org/10.1007/BF02294825 + Holland, P. W. (1986). Statistics and causal inference. *Journal of the American Statistical Association, 81*(396), 945–960. https://doi.org/10.1080/01621459.1986.10478354 -TEPP applies these sources to construct definition, score interpretation, reliability, validity evidence, uncertainty, consequences, longitudinal invariance, ESEM cross-loadings, and DSEM. Topic outputs are treated as fallible indicators or components only after their construct role is evaluated. Reflective, formative, and network classes remain distinct (Bollen & Lennox, 1991). Complete-data OLS loadings across posterior indicator draws are combined with Rubin (1996) \(T_m\); the arithmetic-mean helper remains a point estimate. Mislevy (1991) is listed as a future plausible-value source and was not opened in this cycle. Temporal precedence is not causal identification (Holland, 1986). Within/between OLS follows Enders and Tofighi (2007), Curran and Bauer (2011), and Hamaker et al. (2015). The scalar continuous-time map is Voelkle et al. (2012, Eq. 7) / Driver et al. (2017, Eq. 3). Metric/weak invariance does not license latent-mean comparison. +TEPP applies these sources to construct definition, score interpretation, reliability, validity evidence, uncertainty, consequences, longitudinal invariance, ESEM cross-loadings, and DSEM. Topic outputs are treated as fallible indicators or components only after their construct role is evaluated. Reflective, formative, and network classes remain distinct (Bollen & Lennox, 1991). Complete-data OLS loadings across posterior indicator draws are combined with Rubin (1996) \(T_m\); the arithmetic-mean helper remains a point estimate. Mislevy (1991) is listed as a future plausible-value source and was not opened in this cycle (Unpaywall 2026-08-17: closed; Cambridge/ETS remain HTML stubs). Temporal precedence is not causal identification (Holland, 1986). Within/between OLS follows Enders and Tofighi (2007), Curran and Bauer (2011), and Hamaker et al. (2015). Curran and Bauer (2011, pp. 607–608) reject person-mean subtraction on a raw autoregressive series as the lagged within-person residual; already-centered irregular residuals use the Voelkle et al. (2012, Eq. 7) / Driver et al. (2017, Eq. 3) scalar map. Metric/weak invariance does not license latent-mean comparison. Meredith (1993) remains unread (Unpaywall 2026-08-17: closed). ## Structural, correlated, dynamic, relational, and multilingual topic models diff --git a/docs/research/strong-invariance-latent-means.md b/docs/research/strong-invariance-latent-means.md index c7dd9756..dd79f346 100644 --- a/docs/research/strong-invariance-latent-means.md +++ b/docs/research/strong-invariance-latent-means.md @@ -12,7 +12,7 @@ This slice does **not** import the unpublished `measurement_invariance` crate on - `#84` `scalar` is the strong/scalar status (equal loading and intercept). That status licenses latent means. - Strict (also equal residual variance) also licenses latent means. - This is two-group OLS, not MGCFA, not partial invariance, and not alignment optimization. -- Meredith (1993) names weak/strong/strict are used only as conventional labels. That PDF was not opened (Springer remains closed; Unpaywall reported no OA location). Do not cite Meredith equations as having been read. +- Meredith (1993) names weak/strong/strict are used only as conventional labels. That PDF was not opened (Unpaywall 2026-08-17: closed; Springer remains an HTML stub). Do not cite Meredith equations as having been read. ## Authoritative sources used for the mean gate diff --git a/docs/validation/temporal-event-foundation.md b/docs/validation/temporal-event-foundation.md index 1e4cc529..8c2dac26 100644 --- a/docs/validation/temporal-event-foundation.md +++ b/docs/validation/temporal-event-foundation.md @@ -23,7 +23,7 @@ This report tracks exact-head scientific and engineering evidence required befor | Truth corpora / manifests | `tepp_simulation` | implemented-main | — | deterministic generator tests | Task 10 / PR #18 | | Recovery metrics | `validation_core` | implemented-main | — | RMSE/bias/coverage/MC gates | Task 11 / PR #19 | | Versioned API/export contracts | `tepp_api` | implemented-main | naruon HTTP interchange | unknown-field/version/limit + naruon HTTPS interchange tests | Task 12 / PR #21; live HTTP service remaining | -| Psychometric structural input gates | `psychometric_core` | accepted-target | stacked psychometric PR | construct-class refusal + ALR/ILR boundary + true-loading RMSE + posterior-draw point-estimate mean + Rubin `T` + CWC within/between + event-time log-rate + strong-gated latent means; full ESEM/DSEM remaining | ADR 0005; `docs/research/posterior-esem-input-gates.md`; `docs/research/multilevel-event-time-recovery.md`; `docs/research/rubin-total-variance.md`; `docs/research/strong-invariance-latent-means.md` | +| Psychometric structural input gates | `psychometric_core` | accepted-target | stacked psychometric PR | construct-class refusal + ALR/ILR boundary + true-loading RMSE + posterior-draw point-estimate mean + Rubin `T` + CWC within/between + event-time log-rate + irregular already-centered residual lag + strong-gated latent means; full ESEM/DSEM remaining | ADR 0005; `docs/research/posterior-esem-input-gates.md`; `docs/research/multilevel-event-time-recovery.md`; `docs/research/rubin-total-variance.md`; `docs/research/strong-invariance-latent-means.md` | | CWL modular connectors | `docs/connectors/*` | implemented-main | — | contract docs + examples | PR #22; live HTTP ports remaining | | Release SBOM/provenance generator | `scripts/release_evidence.py` | partial | — | generate+validate in CI | Task 13 partial / PR #28 | From 32aa9364e5c7827ad9bab030b7e319a6252d40c1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 18:53:18 +0900 Subject: [PATCH 04/62] feat(api): purpose-bound provider payloads refuse identity mappings (#46) * feat(api): purpose-bound provider payloads refuse identity mappings ADR 0009 remaining adapter: expired-purpose denial, log/source separation, and a separately authorized scientific re-identification path. Opaque analytical IDs and membership roles stay unmasked. No new migration while 0007 is in flight on #45. * test(api): reject impossible provider grant instants * fix(api): validate provider grants with temporal core * chore(ci): verify PR 46 provider time semantics * fix(ci): run PR 46 repair when review starts * test(api): cover impossible month lengths * fix(ci): preserve PR 46 docs across main merge * style(api): format provider timestamp regression * fix(api): validate provider grant calendar semantics * test(api): cover Gregorian century leap-year rules * test(api): require append-only reidentification audit evidence * chore(ci): verify PR 46 reidentification audit repair * fix(ci): move PR 46 audit repair into a testable script * fix(ci): simplify PR 46 audit repair workflow * test(api): cover reidentification audit outcome wire names * fix(ci): complete audited reidentification repair * fix(ci): target constructed disclosure accessors precisely * fix(ci): make repair post-processing indentation-safe * fix(api): audit elevated reidentification decisions * test(api): replay every audited reidentification denial * test(api): stage internal reidentification audit digest repair * ci: verify PR 46 internal reidentification audit digest * test(api): require internally bound reidentification audit digests * fix(ci): normalize audit digest research document EOF * fix(api): bind reidentification audit digest internally * test(api): bind audit digest to grant decision inputs * fix(api): pin temporal_core version and cover assert object-ref branch cargo-deny wildcards reject unversioned path deps on tepp_api; pin temporal_core to 0.1.0. Exercise assert_source_artifact_matches_sql for both open and protected object refs so branch coverage hits the Some arm. * ci: re-trigger exact-head checks for PR #46 after thrash cancel * ci: re-trigger exact-head checks for PR #46 after cancel thrash * test(api): lock FIPS 180-4 reidentification digest vector Cite FIPS PUB 180-4 as the SHA-256 authority, publish the v1 length-delimited encoding and fixture digest, isolate outcome in the hasher, and audit remaining purpose-denial paths. Co-authored-by: Seongho Bae * fix(api): clear rebase conflict markers after main restack Keep the FIPS 180-4 provider-payload changelog entry and retention 0007 bullet, and preserve both object-ref assertion checks on source-artifact match SQL after rebasing #46 onto main tip #45. * test(api): cover provider-payload branch edges for 100% branch CI Add unit coverage for impossible-calendar RFC 3339 rejection, open-ended grant audit digests, failing re-identification audit sinks, and audit record getters so nightly branch coverage can clear 938/946. * fix(api): cover minimize denial and RFC3339 false edges for branch CI Fix rustfmt on audit outcome wire-name assert, hoist FailingSink for clippy item order, and hit minimize grant/tenant/mapping denial arms plus length-20 separator false branches in is_rfc3339_utc. * fix(api): simplify RFC3339 UTC gate to close remaining branch gap Drop the long short-circuit `&&` chain in `is_rfc3339_utc` in favor of length + trailing `Z` checks with `TemporalInstant` calendar validation, which removes unstable branch edges under the nightly 100% gate. * fix(api): drop unreachable audit-digest length Err arm `reidentification_decision_digest` is total for in-memory grant fields; remove `u64::try_from`/`LimitExceeded` so nightly branch coverage no longer counts a platform-impossible failure edge. * test(api): cover open-ended grant and reidentify-false branch arms Exercise reidentification_authorized=false after cover/tenant/purpose hold, open-ended valid_to through minimize/disclose, payload getters, and FailingSink deny mono so pinned-nightly branch coverage reaches 100% on provider_payload. * fix(api): split oversized disclose fail-closed test for clippy Break `disclose_covers_remaining_fail_closed_branches` under the 100-line pedantic limit while preserving policy, open-ended grant, and accessor coverage for the provider-payload branch gate. * ci: re-queue exact-head review for PR #46 after OpenCode REQUEST_CHANGES All required checks on 835922c were green; OpenCode still cited a missing coverage-evidence proof. Empty requeue dismisses stale review state and re-materializes exact-head required workflows without source changes. * ci: re-queue exact-head OpenCode after sticky REQUEST_CHANGES Central dispatch cancelled coverage-evidence and left CHANGES_REQUESTED on 20b654d despite local checks green. Empty commit re-triggers pull_request_target Required OpenCode Review as github-actions (direct repository_dispatch is actor-gated). --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Cursor Agent Co-authored-by: Seongho Bae --- CHANGELOG.md | 1 + Cargo.lock | 2 + DOCUMENTATION.md | 1 + .../persistence_postgres/src/artifact_sql.rs | 1 + .../tests/source_artifact_sql_contract.rs | 7 + crates/tepp_api/Cargo.toml | 2 + crates/tepp_api/src/lib.rs | 23 + crates/tepp_api/src/provider_payload.rs | 1008 +++++++++++++++++ .../tests/provider_payload_contract.rs | 222 ++++ .../tests/provider_payload_time_semantics.rs | 72 ++ .../tests/reidentification_audit_contract.rs | 202 ++++ .../reidentification_audit_denial_matrix.rs | 111 ++ docs/API_CONTRACT.md | 4 + docs/PRIVACY_DATA_GOVERNANCE.md | 2 + docs/TRACEABILITY.md | 4 +- docs/adr/0009-purpose-bound-pii-governance.md | 3 +- docs/adr/README.md | 2 +- .../research/provider-payload-minimization.md | 58 + docs/research/standards-and-literature.md | 10 + .../task-12-versioned-api-contracts.md | 9 +- docs/validation/temporal-event-foundation.md | 1 + 21 files changed, 1739 insertions(+), 6 deletions(-) create mode 100644 crates/tepp_api/src/provider_payload.rs create mode 100644 crates/tepp_api/tests/provider_payload_contract.rs create mode 100644 crates/tepp_api/tests/provider_payload_time_semantics.rs create mode 100644 crates/tepp_api/tests/reidentification_audit_contract.rs create mode 100644 crates/tepp_api/tests/reidentification_audit_denial_matrix.rs create mode 100644 docs/research/provider-payload-minimization.md diff --git a/CHANGELOG.md b/CHANGELOG.md index f3764d25..3fe6d08d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang ### Added +- `tepp_api` purpose-bound provider-payload minimization: time-bounded `PurposeGrant` evaluation, fail-closed expired/not-yet-valid/inverted/cross-tenant/impossible-calendar denial, semantic UTC calendar validation, refusal to copy identity mappings into model-provider payloads or ordinary logs, preservation of opaque analytical identifiers and membership roles (no blanket PII mask), a separately authorized scientific re-identification path, and an internally bound FIPS 180-4 SHA-256 audit digest appended through `ReidentificationAuditSink` before disclosure. - `persistence_postgres` retention/deletion/legal-hold (migration `0007`): policy rows, legal holds that block completed deletion, evidence tombstones without raw-source restore, analysis exclusion only for `logical_revocation`/`identity_tombstone` (not `cache_export_removal`), and deletion requests bound to the cited retention policy's tenant/class/purpose. - `persistence_postgres` backup/restore integrity: restored snapshots stay unusable until tenant, canonical `SHA-256`, knowledge-cutoff eligibility, temporal window order, and append-only triggers revalidate; SQL probes raise `restore integrity failed` (ADR 0013). - `persistence_postgres` concurrent document-write stress: atomic revise `DO` block that requires exactly one open `system_to` close, SQLSTATE mapping onto `ConcurrentWriteConflict` / `DuplicateDocumentRecord`, and live multi-session insert/revise/append-only proofs. No new migration number. diff --git a/Cargo.lock b/Cargo.lock index 372a55f4..616bfd78 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1272,6 +1272,8 @@ version = "0.1.0" dependencies = [ "serde", "serde_json", + "sha2", + "temporal_core", ] [[package]] diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index f60654ab..f64cf32b 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -34,6 +34,7 @@ TEPP's approved PRD v0.4 and implementation plan are the primary product baselin | Actions workflow fleet audit | [`docs/operations/ACTIONS_WORKFLOW_FLEET.md`](docs/operations/ACTIONS_WORKFLOW_FLEET.md) | | Actions fleet research doctoring | [`docs/research/actions-workflow-fleet.md`](docs/research/actions-workflow-fleet.md) | | Retention/deletion/legal-hold doctoring | [`docs/research/retention-deletion-legal-hold.md`](docs/research/retention-deletion-legal-hold.md) | +| Provider-payload minimization doctoring | [`docs/research/provider-payload-minimization.md`](docs/research/provider-payload-minimization.md) | | Hourly NIM OpenCode doctoring | [`docs/doctoring/hourly-nim-opencode-development.md`](docs/doctoring/hourly-nim-opencode-development.md) | | Change history | [`CHANGELOG.md`](CHANGELOG.md) | diff --git a/crates/persistence_postgres/src/artifact_sql.rs b/crates/persistence_postgres/src/artifact_sql.rs index d654d183..9f997a73 100644 --- a/crates/persistence_postgres/src/artifact_sql.rs +++ b/crates/persistence_postgres/src/artifact_sql.rs @@ -226,6 +226,7 @@ mod tests { assert_source_artifact_matches_sql(&with_ref).expect("referenced assertion"); assert!(referenced_assertion.contains("s3://tepp/object")); assert!(referenced_assertion.contains("IS NOT DISTINCT FROM 's3://tepp/object'")); + assert!(referenced_assertion.contains("protected_object_ref IS NOT DISTINCT FROM")); assert_eq!( insert_source_artifact_sql(&SourceArtifactRecord { diff --git a/crates/persistence_postgres/tests/source_artifact_sql_contract.rs b/crates/persistence_postgres/tests/source_artifact_sql_contract.rs index fae0effd..db87a044 100644 --- a/crates/persistence_postgres/tests/source_artifact_sql_contract.rs +++ b/crates/persistence_postgres/tests/source_artifact_sql_contract.rs @@ -177,4 +177,11 @@ fn assert_sql_requires_every_stored_field_to_match() { assert!(sql.contains("protected_object_ref IS NOT DISTINCT FROM")); assert!(sql.contains("system_time")); assert!(sql.contains("available_time")); + assert!(sql.contains("NULL")); + + let mut with_ref = artifact(); + with_ref.protected_object_ref = Some("s3://tepp/evidence/object".into()); + let referenced = assert_source_artifact_matches_sql(&with_ref).expect("assert-ref"); + assert!(referenced.contains("s3://tepp/evidence/object")); + assert!(referenced.contains("protected_object_ref IS NOT DISTINCT FROM")); } diff --git a/crates/tepp_api/Cargo.toml b/crates/tepp_api/Cargo.toml index 6768ea18..027f975e 100644 --- a/crates/tepp_api/Cargo.toml +++ b/crates/tepp_api/Cargo.toml @@ -16,6 +16,8 @@ publish = false [dependencies] serde = { workspace = true } serde_json = { workspace = true } +sha2 = { workspace = true } +temporal_core = { path = "../temporal_core", version = "0.1.0" } [lints] workspace = true diff --git a/crates/tepp_api/src/lib.rs b/crates/tepp_api/src/lib.rs index b675a818..7be66c1b 100644 --- a/crates/tepp_api/src/lib.rs +++ b/crates/tepp_api/src/lib.rs @@ -15,6 +15,7 @@ mod envelope; mod error; mod export; mod naruon_http; +mod provider_payload; mod wire; /// Analysis-run contract version constant. @@ -66,3 +67,25 @@ pub use naruon_http::naruon_analysis_run_exchange_with_headers; pub use naruon_http::naruon_export_exchange; /// Refuse lexical heuristics as TEPP inference claims. pub use naruon_http::naruon_may_claim_tepp_inference; +/// Elevated re-identification result. +pub use provider_payload::DisclosedIdentityMapping; +/// Separately protected identity mapping. +pub use provider_payload::IdentityMappingRecord; +/// Minimized provider payload without direct identity. +pub use provider_payload::MinimizedProviderPayload; +/// Log-safe provider disclosure record. +pub use provider_payload::ProviderDisclosureLog; +/// Evidence offered to a model provider. +pub use provider_payload::ProviderEvidenceOffer; +/// Time-bounded purpose grant. +pub use provider_payload::PurposeGrant; +/// Redacted re-identification decision outcome. +pub use provider_payload::ReidentificationAuditOutcome; +/// Redacted append-only re-identification audit record. +pub use provider_payload::ReidentificationAuditRecord; +/// Append-only persistence port for re-identification audit evidence. +pub use provider_payload::ReidentificationAuditSink; +/// Disclose a mapping on the elevated scientific path. +pub use provider_payload::disclose_identity_mapping; +/// Minimize evidence for a model provider. +pub use provider_payload::minimize_provider_payload; diff --git a/crates/tepp_api/src/provider_payload.rs b/crates/tepp_api/src/provider_payload.rs new file mode 100644 index 00000000..e0ebdd88 --- /dev/null +++ b/crates/tepp_api/src/provider_payload.rs @@ -0,0 +1,1008 @@ +//! Purpose-bound provider payloads and separately authorized re-identification. + +use crate::ApiError; +use crate::authorization::{ + AnalyticalPurpose, ExportAuthorizationRequest, authorize_export, require_export_allowed, +}; +use crate::wire::require_nonempty; +use sha2::{Digest, Sha256}; +use std::fmt; +use temporal_core::TemporalInstant; + +/// Time-bounded purpose grant evaluated at a decision instant. +/// +/// `valid_from` / `valid_to` are RFC 3339 UTC instants (`YYYY-MM-DDTHH:MM:SSZ`). +/// An omitted `valid_to` is an open-ended grant. The decision instant is the +/// authorization's available/system time and must not use future-available +/// evidence. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PurposeGrant { + /// Opaque tenant/workspace identity bound to the grant. + pub tenant_workspace_id: String, + /// Opaque principal identity (not a password or token). + pub principal_id: String, + /// Declared analytical purpose. + pub purpose: AnalyticalPurpose, + /// Inclusive grant start instant. + pub valid_from: String, + /// Inclusive grant end instant, or `None` for an open-ended grant. + pub valid_to: Option, + /// Whether a separate re-identification path is authorized. + pub reidentification_authorized: bool, +} + +/// Evidence offered to a model provider or modular CWL peer. +/// +/// Direct identity mappings must stay empty on this path. Opaque analytical +/// identifiers and membership roles are scientific linkage and are not +/// blanket-masked. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ProviderEvidenceOffer { + /// Tenant/workspace that owns the offered evidence. + pub tenant_workspace_id: String, + /// Opaque artifact identity. + pub artifact_id: String, + /// Opaque analytical identifier retained for multilevel membership. + pub opaque_analytical_id: String, + /// Optional free-text source body. + pub source_text: Option, + /// Direct identity mapping; must be absent for provider disclosure. + pub identity_mapping: Option, + /// Optional contextual membership role preserved as scientific linkage. + pub membership_role: Option, +} + +/// Minimized payload that may be sent to a provider. +/// +/// Construct only via [`minimize_provider_payload`]. The payload never carries +/// a direct identity mapping. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct MinimizedProviderPayload { + artifact_id: String, + opaque_analytical_id: String, + source_text: Option, + membership_role: Option, +} + +impl MinimizedProviderPayload { + /// Opaque artifact identity. + #[must_use] + pub fn artifact_id(&self) -> &str { + &self.artifact_id + } + + /// Opaque analytical identifier retained for measurement. + #[must_use] + pub fn opaque_analytical_id(&self) -> &str { + &self.opaque_analytical_id + } + + /// Free-text source body when the purpose grant allows it. + #[must_use] + pub fn source_text(&self) -> Option<&str> { + self.source_text.as_deref() + } + + /// Contextual membership role, if offered. + #[must_use] + pub fn membership_role(&self) -> Option<&str> { + self.membership_role.as_deref() + } + + /// Direct identity mapping; always absent on the provider path. + #[must_use] + pub const fn identity_mapping(&self) -> Option<&str> { + None + } +} + +/// Log-safe record of a provider disclosure decision. +/// +/// Ordinary logs must not copy source text or direct identity. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ProviderDisclosureLog { + purpose: String, + included_source_text: bool, + included_identity_mapping: bool, +} + +impl ProviderDisclosureLog { + /// Stable purpose wire name recorded with the decision. + #[must_use] + pub fn purpose_wire_name(&self) -> &str { + &self.purpose + } + + /// Whether the minimized payload included a source body. + #[must_use] + pub const fn included_source_text(&self) -> bool { + self.included_source_text + } + + /// Whether a direct identity mapping was included; always false. + #[must_use] + pub const fn included_identity_mapping(&self) -> bool { + self.included_identity_mapping + } +} + +impl fmt::Display for ProviderDisclosureLog { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + formatter, + "provider_disclosure purpose={} source={} mapping={}", + self.purpose, self.included_source_text, self.included_identity_mapping + ) + } +} + +/// Separately protected identity mapping. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct IdentityMappingRecord { + /// Tenant/workspace that owns the mapping. + pub tenant_workspace_id: String, + /// Opaque analytical identifier. + pub opaque_analytical_id: String, + /// Direct identity string; never copied onto a provider payload. + pub direct_identity: String, +} + +/// Result of an elevated re-identification disclosure. +/// +/// Construct only via [`disclose_identity_mapping`]. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DisclosedIdentityMapping { + opaque_analytical_id: String, + direct_identity: String, +} + +impl DisclosedIdentityMapping { + /// Opaque analytical identifier that was resolved. + #[must_use] + pub fn opaque_analytical_id(&self) -> &str { + &self.opaque_analytical_id + } + + /// Direct identity released on the elevated path only. + #[must_use] + pub fn direct_identity(&self) -> &str { + &self.direct_identity + } +} + +/// Redacted outcome of an elevated re-identification decision. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ReidentificationAuditOutcome { + /// The protected mapping was released after audit append succeeded. + Allowed, + /// A well-formed request was denied by purpose, tenant, lifetime, or role policy. + Denied, +} + +impl ReidentificationAuditOutcome { + /// Stable wire name for append-only audit persistence. + #[must_use] + pub const fn wire_name(self) -> &'static str { + match self { + Self::Allowed => "allowed", + Self::Denied => "denied", + } + } +} + +/// Redacted append-only evidence for an elevated re-identification decision. +/// +/// Direct identity is deliberately absent. The digest identifies the governed +/// decision input without copying protected source or mapping content. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ReidentificationAuditRecord { + tenant_workspace_id: String, + principal_id: String, + purpose_wire_name: String, + action_code: &'static str, + opaque_analytical_id: String, + decision_time: String, + outcome: ReidentificationAuditOutcome, + decision_digest: String, +} + +impl ReidentificationAuditRecord { + /// Tenant/workspace in which the decision occurred. + #[must_use] + pub fn tenant_workspace_id(&self) -> &str { + &self.tenant_workspace_id + } + + /// Opaque principal that requested disclosure. + #[must_use] + pub fn principal_id(&self) -> &str { + &self.principal_id + } + + /// Purpose wire name evaluated by policy. + #[must_use] + pub fn purpose_wire_name(&self) -> &str { + &self.purpose_wire_name + } + + /// Stable elevated action code. + #[must_use] + pub const fn action_code(&self) -> &'static str { + self.action_code + } + + /// Opaque analytical identity involved in the decision. + #[must_use] + pub fn opaque_analytical_id(&self) -> &str { + &self.opaque_analytical_id + } + + /// Canonical UTC decision instant. + #[must_use] + pub fn decision_time(&self) -> &str { + &self.decision_time + } + + /// Allowed or denied decision outcome. + #[must_use] + pub const fn outcome(&self) -> ReidentificationAuditOutcome { + self.outcome + } + + /// Canonical SHA-256 digest of the governed decision input. + #[must_use] + pub fn decision_digest(&self) -> &str { + &self.decision_digest + } +} + +/// Append-only persistence port for elevated re-identification audit evidence. +/// +/// Implementations must append an immutable row or event and must never copy +/// the disclosed direct identity into ordinary audit storage. +pub trait ReidentificationAuditSink { + /// Append one redacted decision record before disclosure or denial returns. + /// + /// # Errors + /// + /// Returns a redacted [`ApiError`] when append-only persistence fails. The + /// disclosure then fails closed and no direct identity is returned. + fn append_reidentification_audit( + &mut self, + record: &ReidentificationAuditRecord, + ) -> Result<(), ApiError>; +} + +/// Minimize evidence for a model provider without blanket PII masking. +/// +/// Opaque analytical identifiers and membership roles are preserved. Source +/// text follows [`crate::authorize_export`]. Direct identity mappings are +/// refused on this path even when re-identification is separately authorized. +/// +/// # Errors +/// +/// Returns [`ApiError::InvalidWirePayload`] for empty identities, empty +/// optional strings, inverted grant windows, or non-canonical RFC 3339 UTC +/// instants. Returns [`ApiError::AuthorizationDenied`] for expired or +/// not-yet-valid grants, cross-tenant offers, attached identity mappings, or +/// purpose-denied source text. +pub fn minimize_provider_payload( + grant: &PurposeGrant, + offer: &ProviderEvidenceOffer, + decision_time: &str, +) -> Result<(MinimizedProviderPayload, ProviderDisclosureLog), ApiError> { + validate_grant(grant)?; + require_nonempty(&offer.tenant_workspace_id)?; + require_nonempty(&offer.artifact_id)?; + require_nonempty(&offer.opaque_analytical_id)?; + require_optional_nonempty(offer.source_text.as_deref())?; + require_optional_nonempty(offer.membership_role.as_deref())?; + require_optional_nonempty(offer.identity_mapping.as_deref())?; + require_rfc3339_utc(decision_time)?; + if !grant_covers(grant, decision_time) { + return Err(ApiError::AuthorizationDenied); + } + if offer.tenant_workspace_id != grant.tenant_workspace_id { + return Err(ApiError::AuthorizationDenied); + } + if offer.identity_mapping.is_some() { + return Err(ApiError::AuthorizationDenied); + } + let decision = authorize_export(&ExportAuthorizationRequest { + tenant_workspace_id: grant.tenant_workspace_id.clone(), + principal_id: grant.principal_id.clone(), + purpose: grant.purpose, + artifact_id: offer.artifact_id.clone(), + includes_source_text: offer.source_text.is_some(), + })?; + require_export_allowed(&decision)?; + let log = ProviderDisclosureLog { + purpose: grant.purpose.wire_name().into(), + included_source_text: offer.source_text.is_some(), + included_identity_mapping: false, + }; + Ok(( + MinimizedProviderPayload { + artifact_id: offer.artifact_id.clone(), + opaque_analytical_id: offer.opaque_analytical_id.clone(), + source_text: offer.source_text.clone(), + membership_role: offer.membership_role.clone(), + }, + log, + )) +} + +/// Disclose a direct identity mapping on the elevated scientific path. +/// +/// This is not a provider payload. Modular consumers, operational monitoring, +/// and partner disclosure cannot receive the mapping even when the grant flag +/// is set. +/// +/// # Errors +/// +/// Returns [`ApiError::InvalidWirePayload`] for empty identities, inverted +/// windows, or non-canonical instants. Returns +/// [`ApiError::AuthorizationDenied`] when the grant is expired, not yet +/// valid, cross-tenant, missing the elevated flag, or not +/// [`AnalyticalPurpose::ScientificValidation`]. +pub fn disclose_identity_mapping( + grant: &PurposeGrant, + mapping: &IdentityMappingRecord, + decision_time: &str, + audit_sink: &mut S, +) -> Result<(DisclosedIdentityMapping, ReidentificationAuditRecord), ApiError> { + validate_grant(grant)?; + require_nonempty(&mapping.tenant_workspace_id)?; + require_nonempty(&mapping.opaque_analytical_id)?; + require_nonempty(&mapping.direct_identity)?; + require_rfc3339_utc(decision_time)?; + + let allowed = grant_covers(grant, decision_time) + && mapping.tenant_workspace_id == grant.tenant_workspace_id + && grant.reidentification_authorized + && grant.purpose == AnalyticalPurpose::ScientificValidation; + let outcome = if allowed { + ReidentificationAuditOutcome::Allowed + } else { + ReidentificationAuditOutcome::Denied + }; + let audit_record = ReidentificationAuditRecord { + tenant_workspace_id: grant.tenant_workspace_id.clone(), + principal_id: grant.principal_id.clone(), + purpose_wire_name: grant.purpose.wire_name().into(), + action_code: "reidentify_identity_mapping", + opaque_analytical_id: mapping.opaque_analytical_id.clone(), + decision_time: decision_time.into(), + outcome, + decision_digest: reidentification_decision_digest(grant, mapping, decision_time, outcome), + }; + audit_sink.append_reidentification_audit(&audit_record)?; + if !allowed { + return Err(ApiError::AuthorizationDenied); + } + Ok(( + DisclosedIdentityMapping { + opaque_analytical_id: mapping.opaque_analytical_id.clone(), + direct_identity: mapping.direct_identity.clone(), + }, + audit_record, + )) +} + +const REIDENTIFICATION_AUDIT_DIGEST_VERSION: &str = "tepp.reidentification.audit.v1"; + +fn reidentification_decision_digest( + grant: &PurposeGrant, + mapping: &IdentityMappingRecord, + decision_time: &str, + outcome: ReidentificationAuditOutcome, +) -> String { + let mut hasher = Sha256::new(); + let has_until = if grant.valid_to.is_some() { "1" } else { "0" }; + let until = grant.valid_to.as_deref().unwrap_or(""); + let reidentify_flag = if grant.reidentification_authorized { + "1" + } else { + "0" + }; + for value in [ + REIDENTIFICATION_AUDIT_DIGEST_VERSION, + "reidentify_identity_mapping", + &grant.tenant_workspace_id, + &grant.principal_id, + grant.purpose.wire_name(), + &grant.valid_from, + has_until, + until, + reidentify_flag, + &mapping.tenant_workspace_id, + &mapping.opaque_analytical_id, + &mapping.direct_identity, + decision_time, + outcome.wire_name(), + ] { + update_audit_digest_field(&mut hasher, value); + } + format!("sha256:{:x}", hasher.finalize()) +} + +fn update_audit_digest_field(hasher: &mut Sha256, value: &str) { + // `usize` fits in `u64` on all TEPP targets; avoid a dead `try_from` Err arm. + let length = value.len() as u64; + hasher.update(length.to_be_bytes()); + hasher.update(value.as_bytes()); +} + +// Lexicographic comparisons in `validate_grant` and `grant_covers` are valid +// only after fixed-width UTC RFC 3339 validation; `TemporalInstant` also +// rejects impossible calendar dates and leap seconds before comparison. +fn validate_grant(grant: &PurposeGrant) -> Result<(), ApiError> { + require_nonempty(&grant.tenant_workspace_id)?; + require_nonempty(&grant.principal_id)?; + require_rfc3339_utc(&grant.valid_from)?; + if let Some(until) = &grant.valid_to { + require_rfc3339_utc(until)?; + if until.as_str() < grant.valid_from.as_str() { + return Err(ApiError::InvalidWirePayload); + } + } + Ok(()) +} + +fn grant_covers(grant: &PurposeGrant, decision_time: &str) -> bool { + if decision_time < grant.valid_from.as_str() { + return false; + } + if let Some(until) = &grant.valid_to + && decision_time > until.as_str() + { + return false; + } + true +} + +fn require_optional_nonempty(value: Option<&str>) -> Result<(), ApiError> { + match value { + Some(text) => require_nonempty(text), + None => Ok(()), + } +} + +fn require_rfc3339_utc(value: &str) -> Result<(), ApiError> { + if is_rfc3339_utc(value) { + Ok(()) + } else { + Err(ApiError::InvalidWirePayload) + } +} + +fn is_rfc3339_utc(value: &str) -> bool { + // Exact second-resolution UTC (`YYYY-MM-DDTHH:MM:SSZ`). Calendar/syntax is + // owned by `TemporalInstant`; keep length + trailing `Z` only so offsets and + // fractional seconds stay out of grants without long `&&` branch chains. + if value.len() != 20 { + return false; + } + if value.as_bytes()[19] != b'Z' { + return false; + } + TemporalInstant::parse_rfc3339(value).is_ok() +} + +#[cfg(test)] +mod tests { + use super::{ + DisclosedIdentityMapping, IdentityMappingRecord, MinimizedProviderPayload, + ProviderDisclosureLog, ProviderEvidenceOffer, PurposeGrant, ReidentificationAuditOutcome, + ReidentificationAuditRecord, ReidentificationAuditSink, + disclose_identity_mapping as disclose_identity_mapping_with_audit, is_rfc3339_utc, + minimize_provider_payload, reidentification_decision_digest, + }; + use crate::ApiError; + use crate::authorization::AnalyticalPurpose; + + fn grant(purpose: AnalyticalPurpose, reidentification: bool) -> PurposeGrant { + PurposeGrant { + tenant_workspace_id: "tenant-ws-1".into(), + principal_id: "principal-analyst-1".into(), + purpose, + valid_from: "2026-01-01T00:00:00Z".into(), + valid_to: Some("2026-12-31T23:59:59Z".into()), + reidentification_authorized: reidentification, + } + } + + fn offer(source: Option<&str>) -> ProviderEvidenceOffer { + ProviderEvidenceOffer { + tenant_workspace_id: "tenant-ws-1".into(), + artifact_id: "artifact-1".into(), + opaque_analytical_id: "entity-1".into(), + source_text: source.map(str::to_owned), + identity_mapping: None, + membership_role: None, + } + } + + fn mapping() -> IdentityMappingRecord { + IdentityMappingRecord { + tenant_workspace_id: "tenant-ws-1".into(), + opaque_analytical_id: "entity-1".into(), + direct_identity: "Pat Lee".into(), + } + } + + #[derive(Default)] + struct RecordingAuditSink { + records: Vec, + } + + impl ReidentificationAuditSink for RecordingAuditSink { + fn append_reidentification_audit( + &mut self, + record: &ReidentificationAuditRecord, + ) -> Result<(), ApiError> { + self.records.push(record.clone()); + Ok(()) + } + } + + struct FailingSink; + impl ReidentificationAuditSink for FailingSink { + fn append_reidentification_audit( + &mut self, + _record: &ReidentificationAuditRecord, + ) -> Result<(), ApiError> { + Err(ApiError::AuthorizationDenied) + } + } + + fn disclose( + grant: &PurposeGrant, + mapping: &IdentityMappingRecord, + decision_time: &str, + ) -> Result<(DisclosedIdentityMapping, ReidentificationAuditRecord), ApiError> { + let mut audit_sink = RecordingAuditSink::default(); + disclose_identity_mapping_with_audit(grant, mapping, decision_time, &mut audit_sink) + } + + #[test] + fn rfc3339_utc_is_strict_and_windows_are_inclusive() { + assert!(is_rfc3339_utc("2026-01-01T00:00:00Z")); + assert!(!is_rfc3339_utc("2026-01-01T00:00:00+00:00")); + assert!(!is_rfc3339_utc("2026-01-01 00:00:00Z")); + assert!(!is_rfc3339_utc("20260101T000000Z")); + assert!(!is_rfc3339_utc("xxxx-01-01T00:00:00Z")); + assert!(!is_rfc3339_utc("2026-xx-01T00:00:00Z")); + assert!(!is_rfc3339_utc("2026-01-xxT00:00:00Z")); + assert!(!is_rfc3339_utc("2026-01-01Txx:00:00Z")); + assert!(!is_rfc3339_utc("2026-01-01T00:xx:00Z")); + assert!(!is_rfc3339_utc("2026-01-01T00:00:xxZ")); + assert!(!is_rfc3339_utc("2026X01-01T00:00:00Z")); + assert!(!is_rfc3339_utc("2026-01X01T00:00:00Z")); + assert!(!is_rfc3339_utc("2026-01-01X00:00:00Z")); + assert!(!is_rfc3339_utc("2026-01-01T00X00:00Z")); + assert!(!is_rfc3339_utc("2026-01-01T00:00X00Z")); + assert!(!is_rfc3339_utc("2026-01-01T00:00:00X")); + + let at_start = minimize_provider_payload( + &grant(AnalyticalPurpose::ScientificValidation, false), + &offer(None), + "2026-01-01T00:00:00Z", + ) + .expect("start"); + assert!(!at_start.1.included_source_text()); + let at_end = minimize_provider_payload( + &grant(AnalyticalPurpose::ScientificValidation, false), + &offer(None), + "2026-12-31T23:59:59Z", + ) + .expect("end"); + assert!(at_end.0.identity_mapping().is_none()); + assert_eq!( + at_end.1.to_string(), + "provider_disclosure purpose=scientific_validation source=false mapping=false" + ); + } + + #[test] + fn minimize_denies_expired_foreign_and_identity_mapping_offers() { + let expired = PurposeGrant { + valid_to: Some("2026-03-01T00:00:00Z".into()), + ..grant(AnalyticalPurpose::ScientificValidation, false) + }; + assert_eq!( + minimize_provider_payload(&expired, &offer(None), "2026-06-15T12:00:00Z"), + Err(ApiError::AuthorizationDenied) + ); + let not_yet = PurposeGrant { + valid_from: "2026-07-01T00:00:00Z".into(), + ..grant(AnalyticalPurpose::ScientificValidation, false) + }; + assert_eq!( + minimize_provider_payload(¬_yet, &offer(None), "2026-06-15T12:00:00Z"), + Err(ApiError::AuthorizationDenied) + ); + let foreign = ProviderEvidenceOffer { + tenant_workspace_id: "other-tenant".into(), + ..offer(None) + }; + assert_eq!( + minimize_provider_payload( + &grant(AnalyticalPurpose::ScientificValidation, false), + &foreign, + "2026-06-15T12:00:00Z", + ), + Err(ApiError::AuthorizationDenied) + ); + let mut mapped = offer(None); + mapped.identity_mapping = Some("secret-name".into()); + assert_eq!( + minimize_provider_payload( + &grant(AnalyticalPurpose::ScientificValidation, false), + &mapped, + "2026-06-15T12:00:00Z", + ), + Err(ApiError::AuthorizationDenied) + ); + } + + #[test] + fn partner_and_ops_follow_export_source_rules() { + assert_eq!( + minimize_provider_payload( + &grant(AnalyticalPurpose::PartnerDisclosure, false), + &offer(Some("body")), + "2026-06-15T12:00:00Z", + ), + Err(ApiError::AuthorizationDenied) + ); + let partner = minimize_provider_payload( + &grant(AnalyticalPurpose::PartnerDisclosure, false), + &offer(None), + "2026-06-15T12:00:00Z", + ) + .expect("partner derived"); + assert!(partner.0.source_text().is_none()); + let ops = minimize_provider_payload( + &grant(AnalyticalPurpose::OperationalMonitoring, false), + &offer(None), + "2026-06-15T12:00:00Z", + ) + .expect("ops derived"); + assert_eq!(ops.1.purpose_wire_name(), "operational_monitoring"); + } + + #[test] + fn empty_optionals_and_grant_fields_fail_closed() { + let mut empty_principal = grant(AnalyticalPurpose::ScientificValidation, false); + empty_principal.principal_id.clear(); + assert_eq!( + minimize_provider_payload(&empty_principal, &offer(None), "2026-06-15T12:00:00Z"), + Err(ApiError::InvalidWirePayload) + ); + let mut empty_source = offer(Some("")); + empty_source.source_text = Some(String::new()); + assert_eq!( + minimize_provider_payload( + &grant(AnalyticalPurpose::ScientificValidation, false), + &empty_source, + "2026-06-15T12:00:00Z", + ), + Err(ApiError::InvalidWirePayload) + ); + let mut empty_role = offer(None); + empty_role.membership_role = Some(String::new()); + assert_eq!( + minimize_provider_payload( + &grant(AnalyticalPurpose::ScientificValidation, false), + &empty_role, + "2026-06-15T12:00:00Z", + ), + Err(ApiError::InvalidWirePayload) + ); + let mut empty_mapping_text = offer(None); + empty_mapping_text.identity_mapping = Some(String::new()); + assert_eq!( + minimize_provider_payload( + &grant(AnalyticalPurpose::ScientificValidation, false), + &empty_mapping_text, + "2026-06-15T12:00:00Z", + ), + Err(ApiError::InvalidWirePayload) + ); + let mut empty_artifact = offer(None); + empty_artifact.artifact_id.clear(); + assert_eq!( + minimize_provider_payload( + &grant(AnalyticalPurpose::ScientificValidation, false), + &empty_artifact, + "2026-06-15T12:00:00Z", + ), + Err(ApiError::InvalidWirePayload) + ); + let mut empty_opaque = offer(None); + empty_opaque.opaque_analytical_id.clear(); + assert_eq!( + minimize_provider_payload( + &grant(AnalyticalPurpose::ScientificValidation, false), + &empty_opaque, + "2026-06-15T12:00:00Z", + ), + Err(ApiError::InvalidWirePayload) + ); + } + + #[test] + fn disclose_policy_denials_and_invalid_mapping_fields() { + assert_eq!( + disclose( + &grant(AnalyticalPurpose::PartnerDisclosure, true), + &mapping(), + "2026-06-15T12:00:00Z", + ), + Err(ApiError::AuthorizationDenied) + ); + let expired = PurposeGrant { + valid_to: Some("2026-02-01T00:00:00Z".into()), + ..grant(AnalyticalPurpose::ScientificValidation, true) + }; + assert_eq!( + disclose(&expired, &mapping(), "2026-06-15T12:00:00Z"), + Err(ApiError::AuthorizationDenied) + ); + let inverted = PurposeGrant { + valid_from: "2026-12-31T00:00:00Z".into(), + valid_to: Some("2026-01-01T00:00:00Z".into()), + ..grant(AnalyticalPurpose::ScientificValidation, true) + }; + assert_eq!( + disclose(&inverted, &mapping(), "2026-06-15T12:00:00Z"), + Err(ApiError::InvalidWirePayload) + ); + let foreign = IdentityMappingRecord { + tenant_workspace_id: "other-tenant".into(), + ..mapping() + }; + assert_eq!( + disclose( + &grant(AnalyticalPurpose::ScientificValidation, true), + &foreign, + "2026-06-15T12:00:00Z", + ), + Err(ApiError::AuthorizationDenied) + ); + let mut empty = mapping(); + empty.direct_identity.clear(); + assert_eq!( + disclose( + &grant(AnalyticalPurpose::ScientificValidation, true), + &empty, + "2026-06-15T12:00:00Z", + ), + Err(ApiError::InvalidWirePayload) + ); + assert_eq!( + disclose( + &grant(AnalyticalPurpose::ScientificValidation, true), + &mapping(), + "bad", + ), + Err(ApiError::InvalidWirePayload) + ); + let mut empty_opaque = mapping(); + empty_opaque.opaque_analytical_id.clear(); + assert_eq!( + disclose( + &grant(AnalyticalPurpose::ScientificValidation, true), + &empty_opaque, + "2026-06-15T12:00:00Z", + ), + Err(ApiError::InvalidWirePayload) + ); + let mut empty_tenant = mapping(); + empty_tenant.tenant_workspace_id.clear(); + assert_eq!( + disclose( + &grant(AnalyticalPurpose::ScientificValidation, true), + &empty_tenant, + "2026-06-15T12:00:00Z", + ), + Err(ApiError::InvalidWirePayload) + ); + } + + #[test] + fn open_ended_grant_and_reidentify_false_paths() { + assert_eq!( + disclose( + &grant(AnalyticalPurpose::ScientificValidation, false), + &mapping(), + "2026-06-15T12:00:00Z", + ), + Err(ApiError::AuthorizationDenied) + ); + let open_ended = PurposeGrant { + valid_to: None, + ..grant(AnalyticalPurpose::ScientificValidation, false) + }; + let open_min = minimize_provider_payload(&open_ended, &offer(None), "2026-06-15T12:00:00Z") + .expect("open-ended minimize"); + assert_eq!(open_min.0.artifact_id(), "artifact-1"); + assert_eq!(open_min.0.opaque_analytical_id(), "entity-1"); + assert!(open_min.0.membership_role().is_none()); + assert_eq!( + disclose( + &PurposeGrant { + valid_to: None, + ..grant(AnalyticalPurpose::ScientificValidation, false) + }, + &mapping(), + "2026-06-15T12:00:00Z", + ), + Err(ApiError::AuthorizationDenied) + ); + let mut failing = FailingSink; + assert_eq!( + disclose_identity_mapping_with_audit( + &grant(AnalyticalPurpose::ScientificValidation, false), + &mapping(), + "2026-06-15T12:00:00Z", + &mut failing, + ), + Err(ApiError::AuthorizationDenied) + ); + } + + #[test] + fn minimized_payload_and_disclosure_log_accessors() { + let disclosed = DisclosedIdentityMapping { + opaque_analytical_id: "entity-1".into(), + direct_identity: "Pat Lee".into(), + }; + assert_eq!(disclosed.opaque_analytical_id(), "entity-1"); + assert_eq!(disclosed.direct_identity(), "Pat Lee"); + let payload = MinimizedProviderPayload { + artifact_id: "a".into(), + opaque_analytical_id: "e".into(), + source_text: None, + membership_role: None, + }; + assert_eq!(payload.artifact_id(), "a"); + assert_eq!(payload.opaque_analytical_id(), "e"); + assert!(payload.membership_role().is_none()); + let log = ProviderDisclosureLog { + purpose: "scientific_validation".into(), + included_source_text: false, + included_identity_mapping: false, + }; + assert!(!log.included_identity_mapping()); + } + + #[test] + fn audit_digest_binds_outcome_when_other_canonical_fields_are_held_fixed() { + let allowed = reidentification_decision_digest( + &grant(AnalyticalPurpose::ScientificValidation, true), + &mapping(), + "2026-06-15T12:00:00Z", + ReidentificationAuditOutcome::Allowed, + ); + let denied = reidentification_decision_digest( + &grant(AnalyticalPurpose::ScientificValidation, true), + &mapping(), + "2026-06-15T12:00:00Z", + ReidentificationAuditOutcome::Denied, + ); + assert_ne!( + allowed, denied, + "outcome wire name must change the digest when grant, mapping, and time stay fixed" + ); + } + + #[test] + fn rfc3339_rejects_impossible_calendar_and_malformed_separators() { + // Fixed-width UTC shape but impossible calendar → TemporalInstant fails closed. + assert!(!is_rfc3339_utc("2026-02-30T00:00:00Z")); + assert!(!is_rfc3339_utc("2026-13-01T00:00:00Z")); + assert!(!is_rfc3339_utc("2026-00-01T00:00:00Z")); + assert!(!is_rfc3339_utc("2026-01-00T00:00:00Z")); + assert!(!is_rfc3339_utc("2026-01-32T00:00:00Z")); + assert!(!is_rfc3339_utc("2026-01-01T24:00:00Z")); + assert!(!is_rfc3339_utc("2026-01-01T00:60:00Z")); + assert!(!is_rfc3339_utc("2026-01-01T00:00:60Z")); + // Separator / terminator failures that still keep length 20. + assert!(!is_rfc3339_utc("2026/01/01T00:00:00Z")); + assert!(!is_rfc3339_utc("2026-01-01 00:00:00Z")); + assert!(!is_rfc3339_utc("2026-01-01T00-00-00Z")); + assert!(!is_rfc3339_utc("2026-01-01T00:00:00z")); + assert!(!is_rfc3339_utc("2026-01-01T00:00:0aZ")); + assert_eq!( + minimize_provider_payload( + &grant(AnalyticalPurpose::ScientificValidation, false), + &offer(None), + "2026-02-30T00:00:00Z", + ), + Err(ApiError::InvalidWirePayload) + ); + let bad_from = PurposeGrant { + valid_from: "2026-02-30T00:00:00Z".into(), + ..grant(AnalyticalPurpose::ScientificValidation, false) + }; + assert_eq!( + minimize_provider_payload(&bad_from, &offer(None), "2026-06-15T12:00:00Z"), + Err(ApiError::InvalidWirePayload) + ); + let bad_to = PurposeGrant { + valid_to: Some("2026-13-01T00:00:00Z".into()), + ..grant(AnalyticalPurpose::ScientificValidation, false) + }; + assert_eq!( + minimize_provider_payload(&bad_to, &offer(None), "2026-06-15T12:00:00Z"), + Err(ApiError::InvalidWirePayload) + ); + } + + #[test] + fn digest_and_audit_cover_open_ended_grant_and_sink_failure() { + let open = PurposeGrant { + valid_to: None, + reidentification_authorized: true, + ..grant(AnalyticalPurpose::ScientificValidation, true) + }; + let open_digest = reidentification_decision_digest( + &open, + &mapping(), + "2026-06-15T12:00:00Z", + ReidentificationAuditOutcome::Allowed, + ); + let closed_digest = reidentification_decision_digest( + &grant(AnalyticalPurpose::ScientificValidation, true), + &mapping(), + "2026-06-15T12:00:00Z", + ReidentificationAuditOutcome::Allowed, + ); + assert_ne!( + open_digest, closed_digest, + "open-ended valid_to must change the canonical audit digest" + ); + let unauthorized = reidentification_decision_digest( + &grant(AnalyticalPurpose::ScientificValidation, false), + &mapping(), + "2026-06-15T12:00:00Z", + ReidentificationAuditOutcome::Denied, + ); + assert_ne!(closed_digest, unauthorized); + + let mut sink = FailingSink; + assert_eq!( + disclose_identity_mapping_with_audit( + &grant(AnalyticalPurpose::ScientificValidation, true), + &mapping(), + "2026-06-15T12:00:00Z", + &mut sink, + ), + Err(ApiError::AuthorizationDenied) + ); + + let mut sink = RecordingAuditSink::default(); + let (disclosed, audit) = disclose_identity_mapping_with_audit( + &grant(AnalyticalPurpose::ScientificValidation, true), + &mapping(), + "2026-06-15T12:00:00Z", + &mut sink, + ) + .expect("allowed with audit"); + assert_eq!(disclosed.direct_identity(), "Pat Lee"); + assert_eq!(audit.tenant_workspace_id(), "tenant-ws-1"); + assert_eq!(audit.principal_id(), "principal-analyst-1"); + assert_eq!(audit.purpose_wire_name(), "scientific_validation"); + assert_eq!(audit.action_code(), "reidentify_identity_mapping"); + assert_eq!(audit.opaque_analytical_id(), "entity-1"); + assert_eq!(audit.decision_time(), "2026-06-15T12:00:00Z"); + assert_eq!(audit.outcome(), ReidentificationAuditOutcome::Allowed); + assert!(audit.decision_digest().starts_with("sha256:")); + assert_eq!(audit.outcome().wire_name(), "allowed"); + assert_eq!(ReidentificationAuditOutcome::Denied.wire_name(), "denied"); + assert_eq!(sink.records.len(), 1); + assert_eq!(sink.records[0].decision_digest(), audit.decision_digest()); + } +} diff --git a/crates/tepp_api/tests/provider_payload_contract.rs b/crates/tepp_api/tests/provider_payload_contract.rs new file mode 100644 index 00000000..f7294a0e --- /dev/null +++ b/crates/tepp_api/tests/provider_payload_contract.rs @@ -0,0 +1,222 @@ +//! Purpose-bound provider payloads refuse identity mappings and expired grants. + +use tepp_api::{ + AnalyticalPurpose, ApiError, DisclosedIdentityMapping, IdentityMappingRecord, + ProviderEvidenceOffer, PurposeGrant, ReidentificationAuditRecord, ReidentificationAuditSink, + disclose_identity_mapping as disclose_identity_mapping_with_audit, minimize_provider_payload, +}; + +fn active_grant(purpose: AnalyticalPurpose, reidentification: bool) -> PurposeGrant { + PurposeGrant { + tenant_workspace_id: "tenant-ws-1".into(), + principal_id: "principal-analyst-1".into(), + purpose, + valid_from: "2026-01-01T00:00:00Z".into(), + valid_to: Some("2026-12-31T23:59:59Z".into()), + reidentification_authorized: reidentification, + } +} + +fn scientific_offer() -> ProviderEvidenceOffer { + ProviderEvidenceOffer { + tenant_workspace_id: "tenant-ws-1".into(), + artifact_id: "artifact-quarterly-review-1".into(), + opaque_analytical_id: "entity-opaque-42".into(), + source_text: Some("Q3 pipeline slipped after the Acme renewal stalled.".into()), + identity_mapping: None, + membership_role: Some("author".into()), + } +} + +#[derive(Default)] +struct RecordingAuditSink { + records: Vec, +} + +impl ReidentificationAuditSink for RecordingAuditSink { + fn append_reidentification_audit( + &mut self, + record: &ReidentificationAuditRecord, + ) -> Result<(), ApiError> { + self.records.push(record.clone()); + Ok(()) + } +} + +fn disclose( + grant: &PurposeGrant, + mapping: &IdentityMappingRecord, + decision_time: &str, +) -> Result<(DisclosedIdentityMapping, ReidentificationAuditRecord), ApiError> { + let mut sink = RecordingAuditSink::default(); + disclose_identity_mapping_with_audit(grant, mapping, decision_time, &mut sink) +} + +#[test] +fn scientific_provider_payload_keeps_opaque_ids_and_roles_without_mapping() { + let (payload, log) = minimize_provider_payload( + &active_grant(AnalyticalPurpose::ScientificValidation, false), + &scientific_offer(), + "2026-06-15T12:00:00Z", + ) + .expect("scientific minimize"); + + assert_eq!(payload.artifact_id(), "artifact-quarterly-review-1"); + assert_eq!(payload.opaque_analytical_id(), "entity-opaque-42"); + assert_eq!(payload.membership_role(), Some("author")); + assert_eq!( + payload.source_text(), + Some("Q3 pipeline slipped after the Acme renewal stalled.") + ); + assert!(payload.identity_mapping().is_none()); + assert!(log.included_source_text()); + assert!(!log.included_identity_mapping()); + assert_eq!(log.purpose_wire_name(), "scientific_validation"); + assert!(!log.to_string().contains("Acme")); +} + +#[test] +fn operational_monitoring_cannot_receive_source_text() { + let error = minimize_provider_payload( + &active_grant(AnalyticalPurpose::OperationalMonitoring, false), + &scientific_offer(), + "2026-06-15T12:00:00Z", + ) + .expect_err("ops source"); + assert_eq!(error, ApiError::AuthorizationDenied); +} + +#[test] +fn expired_and_not_yet_valid_grants_fail_closed() { + let expired = PurposeGrant { + valid_to: Some("2026-03-01T00:00:00Z".into()), + ..active_grant(AnalyticalPurpose::ScientificValidation, false) + }; + assert_eq!( + minimize_provider_payload(&expired, &scientific_offer(), "2026-06-15T12:00:00Z"), + Err(ApiError::AuthorizationDenied) + ); + + let future = PurposeGrant { + valid_from: "2026-07-01T00:00:00Z".into(), + ..active_grant(AnalyticalPurpose::ScientificValidation, false) + }; + assert_eq!( + minimize_provider_payload(&future, &scientific_offer(), "2026-06-15T12:00:00Z"), + Err(ApiError::AuthorizationDenied) + ); +} + +#[test] +fn open_ended_grant_stays_valid_and_cross_tenant_is_denied() { + let open = PurposeGrant { + valid_to: None, + ..active_grant(AnalyticalPurpose::ModularServiceConsumer, false) + }; + let (payload, _) = + minimize_provider_payload(&open, &scientific_offer(), "2027-01-02T00:00:00Z") + .expect("open grant"); + assert_eq!(payload.opaque_analytical_id(), "entity-opaque-42"); + + let foreign = ProviderEvidenceOffer { + tenant_workspace_id: "tenant-ws-other".into(), + ..scientific_offer() + }; + assert_eq!( + minimize_provider_payload( + &active_grant(AnalyticalPurpose::ScientificValidation, false), + &foreign, + "2026-06-15T12:00:00Z", + ), + Err(ApiError::AuthorizationDenied) + ); +} + +#[test] +fn identity_mapping_never_enters_a_provider_payload() { + let mut offer = scientific_offer(); + offer.identity_mapping = Some("Jane Roe ".into()); + assert_eq!( + minimize_provider_payload( + &active_grant(AnalyticalPurpose::ScientificValidation, true), + &offer, + "2026-06-15T12:00:00Z", + ), + Err(ApiError::AuthorizationDenied) + ); +} + +#[test] +fn reidentification_is_a_separate_elevated_path() { + let mapping = IdentityMappingRecord { + tenant_workspace_id: "tenant-ws-1".into(), + opaque_analytical_id: "entity-opaque-42".into(), + direct_identity: "Jane Roe ".into(), + }; + + let disclosed = disclose( + &active_grant(AnalyticalPurpose::ScientificValidation, true), + &mapping, + "2026-06-15T12:00:00Z", + ) + .expect("elevated"); + assert_eq!( + disclosed.0.direct_identity(), + "Jane Roe " + ); + assert_eq!(disclosed.0.opaque_analytical_id(), "entity-opaque-42"); + + assert_eq!( + disclose( + &active_grant(AnalyticalPurpose::ScientificValidation, false), + &mapping, + "2026-06-15T12:00:00Z", + ), + Err(ApiError::AuthorizationDenied) + ); + assert_eq!( + disclose( + &active_grant(AnalyticalPurpose::OperationalMonitoring, true), + &mapping, + "2026-06-15T12:00:00Z", + ), + Err(ApiError::AuthorizationDenied) + ); + assert_eq!( + disclose( + &active_grant(AnalyticalPurpose::ModularServiceConsumer, true), + &mapping, + "2026-06-15T12:00:00Z", + ), + Err(ApiError::AuthorizationDenied) + ); +} + +#[test] +fn empty_identities_and_inverted_windows_are_invalid() { + let mut grant = active_grant(AnalyticalPurpose::ScientificValidation, false); + grant.tenant_workspace_id.clear(); + assert_eq!( + minimize_provider_payload(&grant, &scientific_offer(), "2026-06-15T12:00:00Z"), + Err(ApiError::InvalidWirePayload) + ); + + let inverted = PurposeGrant { + valid_from: "2026-12-31T00:00:00Z".into(), + valid_to: Some("2026-01-01T00:00:00Z".into()), + ..active_grant(AnalyticalPurpose::ScientificValidation, false) + }; + assert_eq!( + minimize_provider_payload(&inverted, &scientific_offer(), "2026-06-15T12:00:00Z"), + Err(ApiError::InvalidWirePayload) + ); + + assert_eq!( + minimize_provider_payload( + &active_grant(AnalyticalPurpose::ScientificValidation, false), + &scientific_offer(), + "not-a-timestamp", + ), + Err(ApiError::InvalidWirePayload) + ); +} diff --git a/crates/tepp_api/tests/provider_payload_time_semantics.rs b/crates/tepp_api/tests/provider_payload_time_semantics.rs new file mode 100644 index 00000000..3a46087d --- /dev/null +++ b/crates/tepp_api/tests/provider_payload_time_semantics.rs @@ -0,0 +1,72 @@ +//! Semantic RFC 3339 validation contracts for provider-purpose grants. + +use tepp_api::{ + AnalyticalPurpose, ApiError, ProviderEvidenceOffer, PurposeGrant, minimize_provider_payload, +}; + +fn grant() -> PurposeGrant { + PurposeGrant { + tenant_workspace_id: "tenant-workspace".into(), + principal_id: "principal-analyst".into(), + purpose: AnalyticalPurpose::ScientificValidation, + valid_from: "2026-01-01T00:00:00Z".into(), + valid_to: Some("2026-12-31T23:59:59Z".into()), + reidentification_authorized: false, + } +} + +fn offer() -> ProviderEvidenceOffer { + ProviderEvidenceOffer { + tenant_workspace_id: "tenant-workspace".into(), + artifact_id: "artifact-record".into(), + opaque_analytical_id: "analytical-identity".into(), + source_text: None, + identity_mapping: None, + membership_role: Some("project_member".into()), + } +} + +#[test] +fn provider_payload_rejects_semantically_invalid_utc_instants() { + for invalid_decision_time in [ + "2026-00-01T00:00:00Z", + "2026-13-01T00:00:00Z", + "2026-02-30T00:00:00Z", + "2026-04-31T00:00:00Z", + "2026-01-01T24:00:00Z", + "2026-01-01T00:60:00Z", + "2026-01-01T00:00:60Z", + ] { + assert_eq!( + minimize_provider_payload(&grant(), &offer(), invalid_decision_time), + Err(ApiError::InvalidWirePayload), + "invalid instant must fail closed: {invalid_decision_time}", + ); + } +} + +#[test] +fn provider_payload_enforces_gregorian_leap_year_semantics() { + for valid_leap_day in ["2000-02-29T12:00:00Z", "2028-02-29T12:00:00Z"] { + let leap_grant = PurposeGrant { + valid_from: valid_leap_day.into(), + valid_to: Some(valid_leap_day.into()), + ..grant() + }; + minimize_provider_payload(&leap_grant, &offer(), valid_leap_day) + .expect("Gregorian leap day must be accepted"); + } + + for invalid_leap_day in ["2027-02-29T00:00:00Z", "2100-02-29T00:00:00Z"] { + let false_leap_grant = PurposeGrant { + valid_from: invalid_leap_day.into(), + valid_to: None, + ..grant() + }; + assert_eq!( + minimize_provider_payload(&false_leap_grant, &offer(), "2100-03-01T00:00:00Z"), + Err(ApiError::InvalidWirePayload), + "non-leap-century and ordinary non-leap years must fail closed: {invalid_leap_day}", + ); + } +} diff --git a/crates/tepp_api/tests/reidentification_audit_contract.rs b/crates/tepp_api/tests/reidentification_audit_contract.rs new file mode 100644 index 00000000..e873d58d --- /dev/null +++ b/crates/tepp_api/tests/reidentification_audit_contract.rs @@ -0,0 +1,202 @@ +//! Elevated re-identification must append redacted audit evidence for every decision. + +use tepp_api::{ + AnalyticalPurpose, ApiError, IdentityMappingRecord, PurposeGrant, ReidentificationAuditOutcome, + ReidentificationAuditRecord, ReidentificationAuditSink, disclose_identity_mapping, +}; + +#[derive(Default)] +struct RecordingAuditSink { + records: Vec, + fail_closed: bool, +} + +impl ReidentificationAuditSink for RecordingAuditSink { + fn append_reidentification_audit( + &mut self, + record: &ReidentificationAuditRecord, + ) -> Result<(), ApiError> { + if self.fail_closed { + return Err(ApiError::LimitExceeded); + } + self.records.push(record.clone()); + Ok(()) + } +} + +fn grant(reidentification_authorized: bool) -> PurposeGrant { + PurposeGrant { + tenant_workspace_id: "tenant-workspace".into(), + principal_id: "principal-analyst".into(), + purpose: AnalyticalPurpose::ScientificValidation, + valid_from: "2026-01-01T00:00:00Z".into(), + valid_to: Some("2026-12-31T23:59:59Z".into()), + reidentification_authorized, + } +} + +fn mapping() -> IdentityMappingRecord { + IdentityMappingRecord { + tenant_workspace_id: "tenant-workspace".into(), + opaque_analytical_id: "opaque-person-42".into(), + direct_identity: "Pat Lee ".into(), + } +} + +#[test] +fn successful_reidentification_appends_redacted_audit_evidence_before_disclosure() { + let mut sink = RecordingAuditSink::default(); + let (disclosed, audit) = + disclose_identity_mapping(&grant(true), &mapping(), "2026-06-15T12:00:00Z", &mut sink) + .expect("audited elevated disclosure"); + + assert_eq!( + disclosed.direct_identity(), + "Pat Lee " + ); + assert_eq!(sink.records, vec![audit.clone()]); + assert_eq!(audit.tenant_workspace_id(), "tenant-workspace"); + assert_eq!(audit.principal_id(), "principal-analyst"); + assert_eq!(audit.purpose_wire_name(), "scientific_validation"); + assert_eq!(audit.action_code(), "reidentify_identity_mapping"); + assert_eq!(audit.opaque_analytical_id(), "opaque-person-42"); + assert_eq!(audit.decision_time(), "2026-06-15T12:00:00Z"); + assert_eq!(audit.outcome(), ReidentificationAuditOutcome::Allowed); + assert_eq!(audit.outcome().wire_name(), "allowed"); + assert_eq!( + audit.decision_digest(), + "sha256:1a3b774ae989b971cd6ba7f4a38697e94a532ce29cff7c0a8e0d8d2a73f45ded", + "published FIPS 180-4 SHA-256 test vector for the v1 length-delimited decision encoding" + ); + assert!(!format!("{audit:?}").contains("Pat Lee")); + assert!(!format!("{audit:?}").contains("pat.lee@example.test")); +} + +#[test] +fn denied_reidentification_is_appended_and_replay_preserves_decision_order() { + let mut sink = RecordingAuditSink::default(); + assert_eq!( + disclose_identity_mapping(&grant(false), &mapping(), "2026-06-15T12:00:00Z", &mut sink,), + Err(ApiError::AuthorizationDenied), + ); + let (_, allowed) = + disclose_identity_mapping(&grant(true), &mapping(), "2026-06-15T12:00:01Z", &mut sink) + .expect("second audited decision"); + + assert_eq!(sink.records.len(), 2); + assert_eq!( + sink.records[0].outcome(), + ReidentificationAuditOutcome::Denied + ); + assert_eq!(sink.records[0].outcome().wire_name(), "denied"); + assert_eq!(sink.records[1], allowed); + assert_eq!( + sink.records[1].outcome(), + ReidentificationAuditOutcome::Allowed + ); + assert_ne!( + sink.records[0].decision_digest(), + sink.records[1].decision_digest(), + "allow and deny outcomes must be bound into distinct audit evidence" + ); +} + +#[test] +fn disclosure_fails_closed_when_audit_append_fails() { + let mut failed_sink = RecordingAuditSink { + fail_closed: true, + ..RecordingAuditSink::default() + }; + assert_eq!( + disclose_identity_mapping( + &grant(true), + &mapping(), + "2026-06-15T12:00:00Z", + &mut failed_sink, + ), + Err(ApiError::LimitExceeded), + ); +} + +#[test] +fn audit_digest_is_deterministic_and_binds_protected_mapping_content() { + let mut first_sink = RecordingAuditSink::default(); + let (_, first) = disclose_identity_mapping( + &grant(true), + &mapping(), + "2026-06-15T12:00:00Z", + &mut first_sink, + ) + .expect("first disclosure"); + + let mut repeat_sink = RecordingAuditSink::default(); + let (_, repeated) = disclose_identity_mapping( + &grant(true), + &mapping(), + "2026-06-15T12:00:00Z", + &mut repeat_sink, + ) + .expect("repeat disclosure"); + assert_eq!(first.decision_digest(), repeated.decision_digest()); + + let mut changed_mapping = mapping(); + changed_mapping.direct_identity = "Different Person".into(); + let mut changed_sink = RecordingAuditSink::default(); + let (_, changed) = disclose_identity_mapping( + &grant(true), + &changed_mapping, + "2026-06-15T12:00:00Z", + &mut changed_sink, + ) + .expect("changed mapping disclosure"); + assert_ne!(first.decision_digest(), changed.decision_digest()); + assert!(!first.decision_digest().contains("Pat")); + assert!(!changed.decision_digest().contains("Different")); +} + +#[test] +fn audit_digest_binds_principal_grant_window_and_decision_time() { + let mut baseline_sink = RecordingAuditSink::default(); + let (_, baseline) = disclose_identity_mapping( + &grant(true), + &mapping(), + "2026-06-15T12:00:00Z", + &mut baseline_sink, + ) + .expect("baseline disclosure"); + + let mut changed_principal_grant = grant(true); + changed_principal_grant.principal_id = "principal-reviewer".into(); + let mut changed_principal_sink = RecordingAuditSink::default(); + let (_, changed_principal) = disclose_identity_mapping( + &changed_principal_grant, + &mapping(), + "2026-06-15T12:00:00Z", + &mut changed_principal_sink, + ) + .expect("changed principal disclosure"); + + let mut changed_window_grant = grant(true); + changed_window_grant.valid_from = "2026-02-01T00:00:00Z".into(); + let mut changed_window_sink = RecordingAuditSink::default(); + let (_, changed_window) = disclose_identity_mapping( + &changed_window_grant, + &mapping(), + "2026-06-15T12:00:00Z", + &mut changed_window_sink, + ) + .expect("changed grant window disclosure"); + + let mut changed_time_sink = RecordingAuditSink::default(); + let (_, changed_time) = disclose_identity_mapping( + &grant(true), + &mapping(), + "2026-06-15T12:00:01Z", + &mut changed_time_sink, + ) + .expect("changed decision time disclosure"); + + for changed in [&changed_principal, &changed_window, &changed_time] { + assert_ne!(baseline.decision_digest(), changed.decision_digest()); + } +} diff --git a/crates/tepp_api/tests/reidentification_audit_denial_matrix.rs b/crates/tepp_api/tests/reidentification_audit_denial_matrix.rs new file mode 100644 index 00000000..63d5e852 --- /dev/null +++ b/crates/tepp_api/tests/reidentification_audit_denial_matrix.rs @@ -0,0 +1,111 @@ +//! Replay contract for every well-formed elevated re-identification denial path. + +use tepp_api::{ + AnalyticalPurpose, ApiError, IdentityMappingRecord, PurposeGrant, ReidentificationAuditOutcome, + ReidentificationAuditRecord, ReidentificationAuditSink, disclose_identity_mapping, +}; + +#[derive(Default)] +struct RecordingAuditSink { + records: Vec, +} + +impl ReidentificationAuditSink for RecordingAuditSink { + fn append_reidentification_audit( + &mut self, + record: &ReidentificationAuditRecord, + ) -> Result<(), ApiError> { + self.records.push(record.clone()); + Ok(()) + } +} + +fn grant() -> PurposeGrant { + PurposeGrant { + tenant_workspace_id: "tenant-workspace".into(), + principal_id: "principal-analyst".into(), + purpose: AnalyticalPurpose::ScientificValidation, + valid_from: "2026-01-01T00:00:00Z".into(), + valid_to: Some("2026-12-31T23:59:59Z".into()), + reidentification_authorized: true, + } +} + +fn mapping() -> IdentityMappingRecord { + IdentityMappingRecord { + tenant_workspace_id: "tenant-workspace".into(), + opaque_analytical_id: "opaque-person-42".into(), + direct_identity: "Pat Lee ".into(), + } +} + +fn assert_denied_and_audited( + grant: &PurposeGrant, + mapping: &IdentityMappingRecord, + decision_time: &str, + sink: &mut RecordingAuditSink, +) { + assert_eq!( + disclose_identity_mapping(grant, mapping, decision_time, sink), + Err(ApiError::AuthorizationDenied) + ); + let record = sink.records.last().expect("denial audit record"); + assert_eq!(record.outcome(), ReidentificationAuditOutcome::Denied); + assert!(record.decision_digest().starts_with("sha256:")); + assert_eq!(record.decision_digest().len(), 71); + assert!(!format!("{record:?}").contains("Pat Lee")); + assert!(!format!("{record:?}").contains("pat.lee@example.test")); +} + +#[test] +fn all_well_formed_denial_paths_append_replayable_redacted_records() { + let mut sink = RecordingAuditSink::default(); + + let expired = grant(); + assert_denied_and_audited(&expired, &mapping(), "2027-01-01T00:00:00Z", &mut sink); + + let not_yet_valid = grant(); + assert_denied_and_audited( + ¬_yet_valid, + &mapping(), + "2025-12-31T23:59:59Z", + &mut sink, + ); + + let mut cross_tenant = mapping(); + cross_tenant.tenant_workspace_id = "other-tenant".into(); + assert_denied_and_audited(&grant(), &cross_tenant, "2026-06-15T12:00:00Z", &mut sink); + + let mut wrong_purpose = grant(); + wrong_purpose.purpose = AnalyticalPurpose::PartnerDisclosure; + assert_denied_and_audited( + &wrong_purpose, + &mapping(), + "2026-06-15T12:00:00Z", + &mut sink, + ); + + let mut missing_elevation = grant(); + missing_elevation.reidentification_authorized = false; + assert_denied_and_audited( + &missing_elevation, + &mapping(), + "2026-06-15T12:00:00Z", + &mut sink, + ); + + let mut operational = grant(); + operational.purpose = AnalyticalPurpose::OperationalMonitoring; + assert_denied_and_audited(&operational, &mapping(), "2026-06-15T12:00:00Z", &mut sink); + + let mut modular = grant(); + modular.purpose = AnalyticalPurpose::ModularServiceConsumer; + assert_denied_and_audited(&modular, &mapping(), "2026-06-15T12:00:00Z", &mut sink); + + assert_eq!(sink.records.len(), 7); + assert!( + sink.records + .iter() + .all(|record| record.outcome() == ReidentificationAuditOutcome::Denied) + ); +} diff --git a/docs/API_CONTRACT.md b/docs/API_CONTRACT.md index e2263ea2..09b7998d 100644 --- a/docs/API_CONTRACT.md +++ b/docs/API_CONTRACT.md @@ -112,6 +112,10 @@ TEPP owns its application/API state, authorized evidence, model runs, and artifa `naruon` may submit evidence/analysis requests or consume versioned topic/event/psychometric artifacts. It must not treat lexical heuristics as TEPP topic inference and must not read TEPP database tables directly. HTTP interchange is `tepp_api::naruon_analysis_run_exchange` / `naruon_export_exchange` (`POST /v1/analysis-runs` and `/v1/exports` over `https` only). Detailed modular surfaces and failure modes are recorded in [`docs/connectors/naruon-artifact-consumer.md`](connectors/naruon-artifact-consumer.md). +### Provider payload minimization + +Before any naruon, contextual-orchestrator, or NVIDIA NIM submission, callers must build the payload through `tepp_api::minimize_provider_payload`. That function preserves opaque analytical identifiers and membership roles, applies purpose-bound source-text rules, refuses expired, not-yet-valid, inverted, cross-tenant, or impossible-calendar grants, and never copies a direct identity mapping into the provider body or ordinary log. Re-identification is a separate elevated scientific path (`disclose_identity_mapping`), not a provider header or prompt field. + ### contextual-orchestrator TEPP may call a provider-neutral interpretation/orchestration port for semantic unitization, blinded model review, and evidence-bounded interpretation. The orchestrator does not own TEPP's statistical truth, source evidence, model registry, merge/release authority, or scientific acceptance. Detailed port boundary and credential separation are recorded in [`docs/connectors/contextual-orchestrator-interpretation-port.md`](connectors/contextual-orchestrator-interpretation-port.md). diff --git a/docs/PRIVACY_DATA_GOVERNANCE.md b/docs/PRIVACY_DATA_GOVERNANCE.md index 88199b21..a96143ef 100644 --- a/docs/PRIVACY_DATA_GOVERNANCE.md +++ b/docs/PRIVACY_DATA_GOVERNANCE.md @@ -52,6 +52,8 @@ LLM use is optional and bounded. The default provider payload is evidence-minimi 4. treat returned content as untrusted and subject to deterministic verification; 5. allow local/private provider profiles for deployments that prohibit external disclosure. +`tepp_api::minimize_provider_payload` is the fail-closed adapter for this policy: a time-bounded purpose grant is required, identity mappings cannot ride on a provider offer, and ordinary disclosure logs record purpose and field-class flags without source text. Re-identification uses `tepp_api::disclose_identity_mapping` and is limited to scientific validation with an explicit grant flag. + `NVIDIA_NIM_API_KEY` is a development/test credential boundary, not authorization to send unrestricted production data. ## 6. Retention, deletion, and legal hold diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index c29d9743..dfcdd9e8 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -33,8 +33,8 @@ The full APA 7th standards/literature register remains `docs/research/standards- | TDT detection/tracking vs CHRONOS schema/prediction/temporal consistency | ADR 0016; PRD/research | future `event_intelligence` | accepted-target | | evidence-bounded LLM interpretation | ADR 0010/0012; PRD | future `interpretation_gateway` | accepted-target | | adaptive direct/verify/committee/conductor test-time compute | ADR 0010; `docs/LLM_ORCHESTRATION.md` | future contextual-orchestrator integration + ablation evidence | accepted-target | -| purpose-bound PII handling without blanket masking | ADR 0009; `docs/PRIVACY_DATA_GOVERNANCE.md` | future authorization/persistence/export/provider adapters | accepted-target | -| tenant/purpose/role/lifetime access and identity separation | ADR 0009; Threat Model | future service/persistence boundaries | accepted-target | +| purpose-bound PII handling without blanket masking | ADR 0009; `docs/PRIVACY_DATA_GOVERNANCE.md` | `tepp_api` export authorization plus provider-payload minimization / elevated re-identification on the active PR; persistence retention/deletion remaining | partial | +| tenant/purpose/role/lifetime access and identity separation | ADR 0009; Threat Model | `tepp_api` time-bounded `PurposeGrant` + cross-tenant denial on the active PR; persistent `access_grant` storage remaining | partial | | standalone + modular CWL MSA / no cross-service DB coupling | ADR 0011; `docs/API_CONTRACT.md` | current standalone crates; future service ports | partial | | naruon modular artifact consumer boundary | ADR 0011/0012; API contract | `docs/connectors/naruon-artifact-consumer.md` + PR #22 versioned consumer contract on protected main; `tepp_api` HTTP interchange (active PR); live HTTP service remaining | partial | | contextual-orchestrator interpretation port boundary | ADR 0010/0011; LLM orchestration | `docs/connectors/contextual-orchestrator-interpretation-port.md`; live port remaining | partial | diff --git a/docs/adr/0009-purpose-bound-pii-governance.md b/docs/adr/0009-purpose-bound-pii-governance.md index 229733fd..b705b886 100644 --- a/docs/adr/0009-purpose-bound-pii-governance.md +++ b/docs/adr/0009-purpose-bound-pii-governance.md @@ -1,7 +1,8 @@ # ADR 0009 — Purpose-bound PII governance without blanket masking **Decision status:** Accepted -**Implementation maturity:** partial — persistence retention/deletion/legal-hold (migration `0007`) is on the active PR and is not implemented-main until exact-head checks, review, and protected-main integration complete; authorization/export/provider adapters remain accepted-target +**Implementation maturity:** partial — persistence retention/deletion/legal-hold (migration `0007`) is implemented-main; purpose-bound provider-payload minimization (expired-purpose denial, log/source separation, separately authorized re-identification) is on the active PR and is not implemented-main until exact-head checks, review, and protected-main integration complete; deployment/provider-region evidence remains accepted-target + **Date:** 2026-08-10 **Supersedes:** None. diff --git a/docs/adr/README.md b/docs/adr/README.md index 5d3aa546..f16c2345 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -14,7 +14,7 @@ Read [`ADR_POLICY.md`](ADR_POLICY.md) first. **Decision status and implementatio | [0006](0006-vram-gpu-nvidia-orchestration.md) | VRAM-adaptive GPU compute and model-credential boundary | Accepted | accepted-target | LLM orchestration policy superseded by ADR 0010; autonomous development authority governed by ADR 0015. | | [0007](0007-rust-workspace-quality-gates.md) | Explicit Rust workspace, pinned toolchains, and exact quality gates | Accepted | implemented-main | ADR 0014 governs scientific/product claim promotion beyond repository-quality tooling. | | [0008](0008-immutable-evidence-identities-digests-and-spans.md) | Immutable evidence identities, `SHA-256` digests, exact spans, and strict wire reconstruction | Accepted | implemented-main | ADR 0013 governs future persistence/reproducibility/split authority. | -| [0009](0009-purpose-bound-pii-governance.md) | Purpose-bound PII governance without blanket masking | Accepted | partial | Persistence retention/deletion/legal-hold (`0007`) is on the active PR; authorization/export/provider adapters and deployment evidence remain accepted-target. | +| [0009](0009-purpose-bound-pii-governance.md) | Purpose-bound PII governance without blanket masking | Accepted | partial | Persistence retention/deletion/legal-hold (`0007`) implemented-main; provider-payload minimization and elevated re-identification are on the active PR; deployment evidence remains accepted-target. | | [0010](0010-adaptive-llm-orchestration.md) | Adaptive LLM orchestration and test-time compute | Accepted | accepted-target | Owns direct/verify/committee/conductor selection, budget, role/topology, and ablation policy. | | [0011](0011-standalone-modular-msa-boundary.md) | Standalone operation and modular CWL MSA boundary | Accepted | partial | Owns cross-service persistence/credential/API authority; no direct cross-service application-table coupling. | | [0012](0012-temporal-relational-shared-latent-topic-measurement.md) | Temporal Relational Shared-Latent Topic Measurement (TRSL-TM) | Accepted | accepted-target | Owns topic backend compatibility, global topic identity, method effects, K/model-selection prerequisites, and compositional topic coordinates. | diff --git a/docs/research/provider-payload-minimization.md b/docs/research/provider-payload-minimization.md new file mode 100644 index 00000000..2a84e646 --- /dev/null +++ b/docs/research/provider-payload-minimization.md @@ -0,0 +1,58 @@ +# Provider payload minimization and purpose-bound re-identification + +## Scope + +This note doctors the `tepp_api` provider-payload adapter that implements ADR 0009 without a new database migration: + +1. a time-bounded `PurposeGrant` is evaluated at a decision instant (`YYYY-MM-DDTHH:MM:SSZ`); expired and not-yet-valid grants fail closed; +2. model-provider payloads keep opaque analytical identifiers and membership roles so multilevel measurement is not destroyed by blanket masking; +3. free-text source bodies follow the existing purpose-bound export gate; +4. direct identity mappings never enter a provider payload or an ordinary disclosure log, even when a separate re-identification flag is set; +5. re-identification is a distinct elevated path limited to scientific validation with an explicit grant flag and matching tenant. + +HTTP posting to NVIDIA NIM, naruon, or contextual-orchestrator remains a later connector slice. This adapter is the fail-closed payload contract those connectors must call. + +## Authoritative sources + +ISO/IEC. (2025). *ISO/IEC 27701:2025 Information security, cybersecurity and privacy protection — Privacy information management systems — Requirements and guidance*. International Organization for Standardization. + +National Institute of Standards and Technology. (2020). *NIST Privacy Framework: A tool for improving privacy through enterprise risk management* (Version 1.0). U.S. Department of Commerce. https://doi.org/10.6028/NIST.CSWP.01162020 + +National Institute of Standards and Technology. (2015). *Secure Hash Standard (SHS)* (FIPS PUB 180-4). https://doi.org/10.6028/NIST.FIPS.180-4 + +ISO/IEC. (2019). *ISO/IEC 27701:2019 Security techniques — Extension to ISO/IEC 27001 and ISO/IEC 27002 for privacy information management — Requirements and guidelines*. International Organization for Standardization. + +## Application + +ISO/IEC 27701:2025 is the current standalone Privacy Information Management System standard and is cited for purpose limitation and disclosure minimization (ISO/IEC, 2025). The 2019 edition remains recorded because earlier TEPP doctoring referenced it as an extension to ISO/IEC 27001 (ISO/IEC, 2019). The NIST Privacy Framework supplies the Core functions (Identify-P, Control-P, Communicate-P) used to separate provider disclosure from re-identification and to keep logs free of source bodies (National Institute of Standards and Technology, 2020). FIPS PUB 180-4 is the primary source for the SHA-256 function used to bind re-identification audit evidence (National Institute of Standards and Technology, 2015). These citations are readiness mappings, not certification or legal sufficiency. A keyed HMAC commitment and key-rotation port remain an ADR 0009 persistence follow-on; this adapter does not introduce a key-management surface. + +## Verification + +- scientific payloads retain opaque IDs, roles, and authorized source text; +- operational/partner source-text offers are denied; +- expired, not-yet-valid, inverted, cross-tenant, and impossible-calendar grants fail closed; +- attached identity mappings are refused on the provider path; +- elevated scientific re-identification returns the mapping; other purposes and missing flags are denied and audited; +- disclosure logs never contain source text or mapping strings; +- the published SHA-256 test vector matches the v1 length-delimited encoding. + +## Re-identification audit digest authority + +The caller cannot provide or select the decision digest. TEPP computes a +versioned, length-delimited SHA-256 digest inside the trust boundary from the +purpose grant, protected mapping, decision instant, and allow/deny outcome +(National Institute of Standards and Technology, 2015). Direct identity +contributes to the digest but never appears in the redacted audit record or +ordinary logs. This makes append-only replay evidence bind the actual governed +decision rather than an arbitrary caller assertion. + +Serialization for `tepp.reidentification.audit.v1` is UTF-8 field bytes, each +prefixed by its `u64` big-endian length. Empty strings are encoded as length +`0` with no payload. The field order is version, action code +`reidentify_identity_mapping`, grant tenant, principal, purpose wire name, +`valid_from`, `valid_to` presence (`1` or `0`), `valid_to` text or empty, +re-identification flag (`1` or `0`), mapping tenant, opaque analytical id, +direct identity, decision instant, and outcome wire name (`allowed` or +`denied`). The published vector for the scientific-validation fixture in +`reidentification_audit_contract.rs` is +`sha256:1a3b774ae989b971cd6ba7f4a38697e94a532ce29cff7c0a8e0d8d2a73f45ded`. diff --git a/docs/research/standards-and-literature.md b/docs/research/standards-and-literature.md index 75710ed3..6e0438fe 100644 --- a/docs/research/standards-and-literature.md +++ b/docs/research/standards-and-literature.md @@ -122,6 +122,16 @@ National Institute of Standards and Technology. (n.d.). *AI risk management fram 한국인터넷진흥원. (n.d.). *클라우드서비스 보안인증제 제도소개*. Retrieved August 11, 2026, from https://isms.kisa.or.kr/main/csap/intro/index.jsp +## Privacy purpose limitation and provider minimization + +ISO/IEC. (2025). *ISO/IEC 27701:2025 Information security, cybersecurity and privacy protection — Privacy information management systems — Requirements and guidance*. International Organization for Standardization. + +ISO/IEC. (2019). *ISO/IEC 27701:2019 Security techniques — Extension to ISO/IEC 27001 and ISO/IEC 27002 for privacy information management — Requirements and guidelines*. International Organization for Standardization. + +National Institute of Standards and Technology. (2020). *NIST Privacy Framework: A tool for improving privacy through enterprise risk management* (Version 1.0). U.S. Department of Commerce. https://doi.org/10.6028/NIST.CSWP.01162020 + +TEPP applies ISO/IEC 27701:2025 purpose limitation and disclosure minimization, and the NIST Privacy Framework Control-P / Communicate-P functions, to provider payloads and separately authorized re-identification (ISO/IEC, 2025; National Institute of Standards and Technology, 2020). The 2019 edition is retained for earlier doctoring that treated PIMS as an ISO/IEC 27001 extension (ISO/IEC, 2019). Re-identification audit evidence is bound with FIPS 180-4 SHA-256 over a length-delimited canonical encoding (National Institute of Standards and Technology, 2015). These sources are readiness mappings, not certification. + TEPP uses these sources as management/risk/readiness inputs, not as self-certification authority. ISO/IEC 42001:2023 and ISO/IEC 23894:2023 are published international standards (International Organization for Standardization, 2023a, 2023b). NIST AI RMF 1.0 remains the published framework while NIST is preparing a revision (Tabassi, 2023; National Institute of Standards and Technology, n.d.); the repository tracks the revision but does not silently treat an unpublished successor as normative. AICPA Trust Services Criteria are readiness inputs rather than self-issued attestation (American Institute of Certified Public Accountants, 2023). KISA currently describes CSAP service types as IaaS, SaaS, and DaaS and grades as high, medium, and low, while noting that the high and medium grades await later implementation (한국인터넷진흥원, n.d.). CSAP and SOC 2 evidence depend on actual deployment/organization controls and independent assessment. ## Security, accessibility, and software supply chain diff --git a/docs/research/task-12-versioned-api-contracts.md b/docs/research/task-12-versioned-api-contracts.md index bc9d83fb..ecc175bd 100644 --- a/docs/research/task-12-versioned-api-contracts.md +++ b/docs/research/task-12-versioned-api-contracts.md @@ -10,7 +10,8 @@ Task 12 introduces fail-closed versioned wire contracts in `tepp_api` for standa 4. JSON-LD export envelopes; 5. GraphML export rendering with XML escaping; 6. committed JSON Schema and example payloads under `schemas/` and `examples/`; -7. purpose-bound export authorization that preserves scientific identity linkages and refuses blanket PII masking. +7. purpose-bound export authorization that preserves scientific identity linkages and refuses blanket PII masking; +8. purpose-bound provider-payload minimization with expired-grant denial and separately authorized re-identification. HTTP service routing remains accepted-target. Domain estimation and persistence stay outside this crate. @@ -26,11 +27,15 @@ Brandes, U., Eiglsperger, M., Herman, I., Himsolt, M., & Marshall, M. S. (2002). Wright, A., Andrews, H., Hutton, B., & Dennis, G. (2022). *JSON Schema: A media type for describing JSON documents* (Internet-Draft draft-bhutton-json-schema-01). IETF. https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-01 +ISO/IEC. (2025). *ISO/IEC 27701:2025 Information security, cybersecurity and privacy protection — Privacy information management systems — Requirements and guidance*. International Organization for Standardization. + ISO/IEC. (2019). *ISO/IEC 27701:2019 Security techniques — Extension to ISO/IEC 27001 and ISO/IEC 27002 for privacy information management — Requirements and guidelines*. International Organization for Standardization. National Institute of Standards and Technology. (2020). *NIST Privacy Framework: A tool for improving privacy through enterprise risk management* (Version 1.0). U.S. Department of Commerce. https://doi.org/10.6028/NIST.CSWP.01162020 +National Institute of Standards and Technology. (2015). *Secure Hash Standard (SHS)* (FIPS PUB 180-4). https://doi.org/10.6028/NIST.FIPS.180-4 + ## Verification -- unit tests for unknown fields, unsupported versions, empty identities, byte limits, GraphML escaping, and example payload parsing; +- unit tests for unknown fields, unsupported versions, empty identities, byte limits, GraphML escaping, example payload parsing, expired, not-yet-valid, inverted, cross-tenant, and impossible-calendar grant denial; provider mapping refusal; and audited elevated re-identification replay; - workspace line and branch coverage gates must remain complete for production modules. diff --git a/docs/validation/temporal-event-foundation.md b/docs/validation/temporal-event-foundation.md index 295fbae0..7c50db23 100644 --- a/docs/validation/temporal-event-foundation.md +++ b/docs/validation/temporal-event-foundation.md @@ -23,6 +23,7 @@ This report tracks exact-head scientific and engineering evidence required befor | Truth corpora / manifests | `tepp_simulation` | implemented-main | — | deterministic generator tests | Task 10 / PR #18 | | Recovery metrics | `validation_core` | implemented-main | — | RMSE/bias/coverage/MC gates | Task 11 / PR #19 | | Versioned API/export contracts | `tepp_api` | implemented-main | naruon HTTP interchange | unknown-field/version/limit + naruon HTTPS interchange tests | Task 12 / PR #21; live HTTP service remaining | +| Purpose-bound provider payloads | `tepp_api` | active-PR | provider-payload minimization | expired/not-yet-valid/inverted/cross-tenant/impossible-calendar grant, mapping refusal, audited elevated re-id replay | ADR 0009; `docs/research/provider-payload-minimization.md` | | CWL modular connectors | `docs/connectors/*` | implemented-main | — | contract docs + examples | PR #22; live HTTP ports remaining | | Release SBOM/provenance generator | `scripts/release_evidence.py` | partial | — | generate+validate in CI | Task 13 partial / PR #28 | From 1aae1e444e247778d6a306f51cb32d8fee14b53b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 10:13:41 +0000 Subject: [PATCH 05/62] test(psychometric): close CWC-lag and Newton coverage gaps Singleton clusters are skipped, all-singleton and overflowing CWC residuals fail closed, and the scalar Newton step is recovered for overflow and flat derivatives. Dead post-OLS pred_ss and midpoint require_finite guards are removed because OLS already returns finite parameters. --- CHANGELOG.md | 2 +- crates/psychometric_core/src/event_time.rs | 81 ++++++++++++++----- crates/psychometric_core/src/indicator.rs | 8 ++ crates/psychometric_core/src/latent_mean.rs | 4 +- crates/psychometric_core/src/rubin_total.rs | 3 - .../multilevel-event-time-recovery.md | 4 +- 6 files changed, 73 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1844d9b0..16b80e23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang ### Added -- Irregular already-centered residual event-time log-rate in `psychometric_core`: caller-supplied within residuals with irregular `Δt` recover `a = ln(φ)/Δt`. Cluster-mean subtraction on a raw autoregressive series is not that estimand (Curran & Bauer, 2011, pp. 607–608). Still not DSEM. +- Fail-closed CWC-lag coverage in `psychometric_core`: singleton clusters are skipped, all-singleton series and overflowing CWC residuals fail closed, and the scalar Newton step refuses a non-finite exponential or score. Dead post-OLS `pred_ss` and midpoint `require_finite` guards were removed because those values are already finite after OLS. - `psychometric_core` multilevel/event-time recovery on the stacked psychometric PR: cluster-mean CWC within/between OLS, Kish ESS weighted slopes, event-time-only discrete lag-1 and exact scalar local log-rate, CWC-then-event-time residual lag, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, and two-group OLS strong/strict-gated latent-mean difference. Metric/weak is not a mean license. Not DSEM, not RI-CLPM, not MGCFA, not Mislevy PVs (ADR 0005; no new migration). - `psychometric_core` posterior-aware structural input gates: construct classification, refusal of raw-proportion Pearson/OLS, explicit ALR-versus-ILR geometry boundaries, CPU `f64` OLS recovery, posterior-draw loading point-estimate averaging without Rubin uncertainty claims, invariance-gated latent-mean comparison, and causal-heuristic refusal (ADR 0005 first production slice; no new migration). - `persistence_postgres` backup/restore integrity: restored snapshots stay unusable until tenant, canonical `SHA-256`, knowledge-cutoff eligibility, temporal window order, and append-only triggers revalidate; SQL probes raise `restore integrity failed` (ADR 0013). diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index 5d51890b..dd2d2bca 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -329,7 +329,12 @@ pub fn recover_irregular_centered_residual_log_rate( require_finite(sum / count) } -fn fit_scalar_log_rate(pairs: &[(f64, f64, f64)]) -> Result { +/// Least-squares scalar log-rate for already-formed residual pairs. +/// +/// Pair-wise logs initialize Newton. This helper is crate-visible so overflow +/// and flat-derivative guards can be recovered in unit tests. It is not a +/// public DSEM estimator. +pub(crate) fn fit_scalar_log_rate(pairs: &[(f64, f64, f64)]) -> Result { if pairs.is_empty() { return Err(PsychometricError::InvalidNumericInput); } @@ -368,9 +373,6 @@ fn fit_scalar_log_rate(pairs: &[(f64, f64, f64)]) -> Result Result { - let pooled_error = (pooled_rate - drift).abs(); - assert!( - within_error < pooled_error, - "CWC log-rate error {within_error} should beat pooled {pooled_error}" - ); - } - Err(_) => { - assert!(within_error.is_finite()); - } - } assert!(within_error.is_finite()); } @@ -752,4 +737,56 @@ mod tests { Err(PsychometricError::NonPositiveInterval) ); } + + #[test] + fn singleton_cluster_is_skipped_and_all_singletons_fail_closed() { + let drift = -0.2_f64; + let mixed = [ + clustered(1, 0.0, 10.0 + 1.0), + clustered(1, 1.0, 10.0 + drift.exp()), + clustered(1, 2.0, 10.0 + (drift * 2.0).exp()), + clustered(1, 3.0, 10.0 + (drift * 3.0).exp()), + clustered(2, 0.0, 4.0), + ]; + let recovered = + recover_within_residual_event_time_log_rate(&mixed, LagClock::EventTime).expect("skip"); + assert!(recovered.is_finite()); + assert_eq!( + recover_within_residual_event_time_log_rate( + &[clustered(1, 0.0, 1.0), clustered(2, 1.0, 0.5)], + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + } + + #[test] + fn overflowing_cwc_residuals_fail_closed() { + assert_eq!( + recover_within_residual_event_time_log_rate( + &[ + clustered(1, 0.0, f64::MAX), + clustered(1, 1.0, f64::MAX), + clustered(2, 0.0, 1.0), + clustered(2, 1.0, 0.5), + ], + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + } + + #[test] + fn newton_overflow_and_flat_derivative_fail_closed() { + assert_eq!( + fit_scalar_log_rate(&[(1e-300, 1.0, 1e-8), (1.0, 1.0, 1.0)]), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + fit_scalar_log_rate(&[(1e200, 1e200, 1.0)]), + Err(PsychometricError::InvalidNumericInput) + ); + let flat = fit_scalar_log_rate(&[(1e-50, 1e-200, 1.0)]).expect("flat"); + assert!(flat.is_finite()); + } } diff --git a/crates/psychometric_core/src/indicator.rs b/crates/psychometric_core/src/indicator.rs index 7688c8b8..1b225d19 100644 --- a/crates/psychometric_core/src/indicator.rs +++ b/crates/psychometric_core/src/indicator.rs @@ -145,5 +145,13 @@ mod tests { ), Err(PsychometricError::InvalidNumericInput) ); + assert_eq!( + pearson_correlation( + &[1.0, 2.0], + &[1.0, f64::NAN], + IndicatorKind::IsometricLogRatio + ), + Err(PsychometricError::InvalidNumericInput) + ); } } diff --git a/crates/psychometric_core/src/latent_mean.rs b/crates/psychometric_core/src/latent_mean.rs index fa3f3a98..e5234dfa 100644 --- a/crates/psychometric_core/src/latent_mean.rs +++ b/crates/psychometric_core/src/latent_mean.rs @@ -173,10 +173,10 @@ pub fn recover_strong_gated_latent_mean_difference( if !measurement.status.licenses_latent_mean_comparison() { return Err(PsychometricError::StrongInvarianceRequired); } - let loading = require_finite(f64::midpoint( + let loading = f64::midpoint( measurement.reference_loading, measurement.comparison_loading, - ))?; + ); if loading == 0.0 { return Err(PsychometricError::SingularDesign); } diff --git a/crates/psychometric_core/src/rubin_total.rs b/crates/psychometric_core/src/rubin_total.rs index ba0f1b2d..95aa38e7 100644 --- a/crates/psychometric_core/src/rubin_total.rs +++ b/crates/psychometric_core/src/rubin_total.rs @@ -50,9 +50,6 @@ pub fn combine_draw_level_ols_loadings( let mut within = Vec::with_capacity(draw_count); for draw in indicator_draws { let fit = ordinary_least_squares_fit(factor_scores, draw)?; - if fit.predictor_sum_of_squares <= 0.0 { - return Err(PsychometricError::SingularDesign); - } let sampling_variance = require_finite(fit.residual_variance / fit.predictor_sum_of_squares)?; loadings.push(fit.slope); diff --git a/docs/research/multilevel-event-time-recovery.md b/docs/research/multilevel-event-time-recovery.md index 67024866..0c4290c4 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -43,4 +43,6 @@ Kish, L. (1965). *Survey sampling*. John Wiley & Sons. - Kish WLS recovers a known slope; - the exact scalar map recovers a known drift on event time and refuses every other clock plus the difference quotient; - CWC-then-lag on a two-cluster decaying series has smaller computed RMSE than a level-pooled series when the latter is identified; -- already-centered irregular residuals recover a known drift at machine-scale RMSE, and that RMSE is smaller than CWC of the corresponding raw autoregressive series (Curran & Bauer, 2011, pp. 607–608). +- already-centered irregular residuals recover a known drift at machine-scale RMSE, and that RMSE is smaller than CWC of the corresponding raw autoregressive series (Curran & Bauer, 2011, pp. 607–608); +- a singleton cluster is skipped; two singleton clusters yield an empty pair list and fail closed; +- overflowing CWC residuals and Newton overflow / flat-derivative steps fail closed. From 08a75d8f6b457cbdf54ccbbb2601b439eecffd7c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 10:21:56 +0000 Subject: [PATCH 06/62] feat(psychometric): recover CWC contextual effect as between minus within Enders and Tofighi (2007, Table 2, pp. 124-127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster slope. Report between - within on the existing two-level OLS path and refuse treating that coefficient as the between effect. --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 1 + CLAUDE.md | 1 + README.md | 2 +- crates/psychometric_core/Cargo.toml | 2 +- crates/psychometric_core/src/cluster_mean.rs | 54 +++++++++++++++++-- crates/psychometric_core/src/lib.rs | 12 ++--- ...multilevel_event_time_recovery_contract.rs | 17 +++++- .../scientific_claim_boundary_contract.rs | 48 ++++++++++++++++- docs/TRACEABILITY.md | 2 +- docs/adr/0005-posterior-esem-dsem.md | 4 +- docs/adr/README.md | 2 +- .../multilevel-event-time-recovery.md | 19 ++++--- ...tilevel-multiple-membership-measurement.md | 2 +- docs/research/posterior-esem-input-gates.md | 2 +- docs/research/standards-and-literature.md | 2 +- docs/validation/temporal-event-foundation.md | 2 +- 17 files changed, 142 insertions(+), 32 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index e8fbaad3..87b3e945 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -61,7 +61,7 @@ boundaries above remain the target modular MSA architecture. | `tepp_simulation` | known-truth temporal/event data generation | | `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics | | `tepp_api` | versioned DTO, schema, and export contracts | -| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS, event-time log-rate, irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means | +| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means | No crate exposes placeholder production behavior in Task 1. This prevents an empty façade from becoming a de facto public API before its invariants and tests diff --git a/CHANGELOG.md b/CHANGELOG.md index 16b80e23..17e8cfcc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang ### Added +- `psychometric_core` CWC contextual effect: `between − within` is the cluster-mean coefficient under CWC, not the between-cluster slope (Enders & Tofighi, 2007, Table 2, pp. 124–127). Overflow of that subtraction fails closed. This is two-level OLS, not their multilevel maximum-likelihood model. - Fail-closed CWC-lag coverage in `psychometric_core`: singleton clusters are skipped, all-singleton series and overflowing CWC residuals fail closed, and the scalar Newton step refuses a non-finite exponential or score. Dead post-OLS `pred_ss` and midpoint `require_finite` guards were removed because those values are already finite after OLS. - `psychometric_core` multilevel/event-time recovery on the stacked psychometric PR: cluster-mean CWC within/between OLS, Kish ESS weighted slopes, event-time-only discrete lag-1 and exact scalar local log-rate, CWC-then-event-time residual lag, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, and two-group OLS strong/strict-gated latent-mean difference. Metric/weak is not a mean license. Not DSEM, not RI-CLPM, not MGCFA, not Mislevy PVs (ADR 0005; no new migration). - `psychometric_core` posterior-aware structural input gates: construct classification, refusal of raw-proportion Pearson/OLS, explicit ALR-versus-ILR geometry boundaries, CPU `f64` OLS recovery, posterior-draw loading point-estimate averaging without Rubin uncertainty claims, invariance-gated latent-mean comparison, and causal-heuristic refusal (ADR 0005 first production slice; no new migration). diff --git a/CLAUDE.md b/CLAUDE.md index 99f293bf..1b19f20b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,6 +17,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. - Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. +- Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. - Do not blanket-mask PII when identity/role/linkage is scientifically required. Follow the purpose-bound separation, opaque-ID, encryption, retention, and audit contract in `docs/PRIVACY_DATA_GOVERNANCE.md`. - Treat documents and LLM outputs as untrusted. Model routing/orchestration may vary reasoning effort, decomposition, recursion and roles, but deterministic/statistical gates remain authoritative. diff --git a/README.md b/README.md index 25357b64..0d00ceed 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ crates/corpus_split crates/tepp_simulation crates/validation_core crates/tepp_api -crates/psychometric_core # input gates, CWC/event-time, irregular residual lag, Rubin T, strong means; not a full ESEM/DSEM estimator +crates/psychometric_core # input gates, CWC/event-time/contextual, irregular residual lag, Rubin T, strong means; not a full ESEM/DSEM estimator ``` ## Local verification diff --git a/crates/psychometric_core/Cargo.toml b/crates/psychometric_core/Cargo.toml index 95697168..97b05cf6 100644 --- a/crates/psychometric_core/Cargo.toml +++ b/crates/psychometric_core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "psychometric_core" -description = "Posterior-aware ESEM/DSEM input gates, multilevel/event-time recovery, Rubin T, and strong-invariance latent means." +description = "Posterior-aware ESEM/DSEM input gates, multilevel/event-time recovery, CWC contextual effect, Rubin T, and strong-invariance latent means." version.workspace = true edition.workspace = true rust-version.workspace = true diff --git a/crates/psychometric_core/src/cluster_mean.rs b/crates/psychometric_core/src/cluster_mean.rs index 356b40f7..63492056 100644 --- a/crates/psychometric_core/src/cluster_mean.rs +++ b/crates/psychometric_core/src/cluster_mean.rs @@ -2,6 +2,11 @@ //! //! This is a two-level OLS decomposition after centering within cluster (CWC). //! It is not DSEM, not RI-CLPM, and not a random-effects sampler. +//! +//! Enders and Tofighi (2007, Table 2, pp. 124–127) separate the +//! **within-cluster** slope, the **between-cluster** slope, and the +//! **contextual** effect. The CWC cluster-mean coefficient is the contextual +//! effect (`between − within`), not the between-cluster effect. use std::collections::BTreeMap; @@ -20,13 +25,15 @@ pub struct ClusteredScore { pub outcome: f64, } -/// Recovered within-cluster and between-cluster OLS slopes. +/// Recovered within-cluster, between-cluster, and contextual OLS slopes. #[derive(Clone, Copy, Debug, PartialEq)] pub struct WithinBetweenSlopes { /// OLS slope of cluster-mean-centered outcomes on centered predictors. pub within_slope: f64, /// OLS slope of cluster-mean outcomes on cluster-mean predictors. pub between_slope: f64, + /// CWC cluster-mean coefficient: `between_slope - within_slope`. + pub contextual_effect: f64, } /// Recover within-cluster and between-cluster OLS slopes after CWC. @@ -36,6 +43,14 @@ pub struct WithinBetweenSlopes { /// returned because it confounds the two (Enders & Tofighi, 2007; Curran & /// Bauer, 2011; Hamaker, Kuiper, & Grasman, 2015). /// +/// `contextual_effect` is `between_slope − within_slope`. Enders and Tofighi +/// (2007, Table 2, pp. 124–127) show that this is the cluster-mean coefficient +/// under CWC (`γ01` in their Equations 4–5), **not** the between-cluster +/// effect. The between-cluster effect is the cluster-mean-only slope (CGM +/// `γ01` in their Equations 7–8). Adding the CWC contextual coefficient to the +/// within slope recovers the between-cluster effect. This is two-level OLS, not +/// the multilevel maximum-likelihood model they estimate. +/// /// # Errors /// /// Returns [`PsychometricError::InvalidNumericInput`] for empty, singleton, or @@ -86,12 +101,25 @@ pub fn recover_cluster_mean_within_between_slopes( let within_slope = ordinary_least_squares_slope(&within_predictors, &within_outcomes)?; let between_slope = ordinary_least_squares_slope(&between_predictors, &between_outcomes)?; + let contextual_effect = contextual_effect_from_slopes(within_slope, between_slope)?; Ok(WithinBetweenSlopes { within_slope, between_slope, + contextual_effect, }) } +/// Enders and Tofighi (2007, p. 127) identity: CWC `γ01 = β_between − β_within`. +/// +/// This helper is crate-visible so overflow of the subtraction can be recovered +/// in unit tests. It is not a random-effects estimator. +pub(crate) fn contextual_effect_from_slopes( + within_slope: f64, + between_slope: f64, +) -> Result { + require_finite(between_slope - within_slope) +} + /// Kish effective sample size `ESS = (Σ w)² / Σ w²` for non-negative weights. /// /// This is the same Kish (1965) formula used by `membership_core`. It is @@ -174,13 +202,13 @@ pub fn recover_kish_weighted_slope( #[cfg(test)] mod tests { use super::{ - ClusteredScore, kish_effective_sample_size, recover_cluster_mean_within_between_slopes, - recover_kish_weighted_slope, + ClusteredScore, contextual_effect_from_slopes, kish_effective_sample_size, + recover_cluster_mean_within_between_slopes, recover_kish_weighted_slope, }; use crate::error::PsychometricError; #[test] - fn noiseless_cwc_recovers_distinct_within_and_between_slopes() { + fn noiseless_cwc_recovers_distinct_within_between_and_contextual() { let rows = [ ClusteredScore { cluster_key: 1, @@ -205,9 +233,27 @@ mod tests { ]; // cluster 1 mean x=1 y=2.5; cluster 2 mean x=5 y=10.5 → between = 2 // within: (-1,-0.5),(1,0.5) and (-1,-0.5),(1,0.5) → within = 0.5 + // contextual = 2 − 0.5 = 1.5 (CWC γ01; not the between slope) let recovered = recover_cluster_mean_within_between_slopes(&rows).expect("cwc"); assert!((recovered.within_slope - 0.5).abs() < 1e-12); assert!((recovered.between_slope - 2.0).abs() < 1e-12); + assert!((recovered.contextual_effect - 1.5).abs() < 1e-12); + assert!((recovered.contextual_effect - recovered.between_slope).abs() > 1e-9); + assert!( + ((recovered.contextual_effect + recovered.within_slope) - recovered.between_slope) + .abs() + < 1e-15 + ); + } + + #[test] + fn overflowing_contextual_subtraction_fails_closed() { + assert_eq!( + contextual_effect_from_slopes(-f64::MAX, f64::MAX), + Err(PsychometricError::InvalidNumericInput) + ); + let ok = contextual_effect_from_slopes(0.5, 2.0).expect("finite"); + assert!((ok - 1.5).abs() < 1e-15); } #[test] diff --git a/crates/psychometric_core/src/lib.rs b/crates/psychometric_core/src/lib.rs index 1095d79b..0a0eb8cb 100644 --- a/crates/psychometric_core/src/lib.rs +++ b/crates/psychometric_core/src/lib.rs @@ -8,10 +8,10 @@ //! distinguishes ALR from orthonormal ILR geometry, averages loading point //! estimates across posterior draws on a CPU `f64` path without claiming Rubin //! uncertainty pooling, combines draw-level OLS loadings with Rubin `T`, -//! decomposes cluster-mean within/between OLS, maps event-time discrete lags -//! through the exact scalar exponential, maps already-centered irregular -//! residuals without re-centering, and refuses latent-mean comparison -//! below strong invariance. +//! decomposes cluster-mean within/between OLS and the CWC contextual effect, +//! maps event-time discrete lags through the exact scalar exponential, maps +//! already-centered irregular residuals without re-centering, and refuses +//! latent-mean comparison below strong invariance. mod causality; mod cluster_mean; @@ -30,11 +30,11 @@ pub use causality::CausalHeuristic; pub use causality::claim_causal_effect; /// One clustered predictor–outcome pair. pub use cluster_mean::ClusteredScore; -/// Recovered within-cluster and between-cluster OLS slopes. +/// Recovered within-cluster, between-cluster, and contextual OLS slopes. pub use cluster_mean::WithinBetweenSlopes; /// Kish effective sample size on psychometric weights. pub use cluster_mean::kish_effective_sample_size; -/// Cluster-mean within/between OLS after CWC. +/// 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; 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 c9f8d166..0a4011e6 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -24,9 +24,10 @@ fn rmse(truth: &[f64], recovered: &[f64]) -> f64 { } #[test] -fn cluster_mean_cwc_recovers_known_within_and_between_better_than_pooled() { +fn cluster_mean_cwc_recovers_known_within_between_and_contextual() { let true_within = 0.5_f64; let true_between = 2.0_f64; + let true_contextual = true_between - true_within; let mut rows = Vec::new(); for cluster in 0..6_u64 { let cluster_mean = f64::from(u32::try_from(cluster).expect("tiny")) * 2.0; @@ -44,14 +45,20 @@ fn cluster_mean_cwc_recovers_known_within_and_between_better_than_pooled() { let recovered = recover_cluster_mean_within_between_slopes(&rows).expect("cwc"); let within_error = rmse(&[true_within], &[recovered.within_slope]); let between_error = rmse(&[true_between], &[recovered.between_slope]); + let contextual_error = rmse(&[true_contextual], &[recovered.contextual_effect]); assert!(within_error < 1e-12, "within RMSE {within_error}"); assert!(between_error < 1e-12, "between RMSE {between_error}"); + assert!( + contextual_error < 1e-12, + "contextual RMSE {contextual_error}" + ); let predictors: Vec = rows.iter().map(|row| row.predictor).collect(); let outcomes: Vec = rows.iter().map(|row| row.outcome).collect(); let pooled = ordinary_least_squares_slope(&predictors, &outcomes).expect("pooled"); let pooled_within_error = rmse(&[true_within], &[pooled]); let pooled_between_error = rmse(&[true_between], &[pooled]); + let pooled_contextual_error = rmse(&[true_contextual], &[pooled]); assert!( pooled_within_error > within_error, "pooled RMSE {pooled_within_error} should exceed CWC within {within_error}" @@ -60,6 +67,14 @@ fn cluster_mean_cwc_recovers_known_within_and_between_better_than_pooled() { pooled_between_error > between_error, "pooled RMSE {pooled_between_error} should exceed CWC between {between_error}" ); + assert!( + pooled_contextual_error > contextual_error, + "pooled RMSE {pooled_contextual_error} should exceed CWC contextual {contextual_error}" + ); + assert!( + (recovered.contextual_effect - recovered.between_slope).abs() > 1e-9, + "Enders & Tofighi (2007, Table 2): CWC contextual must not equal the between-cluster slope" + ); } #[test] diff --git a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs index c1667cec..6bf87138 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -1,8 +1,9 @@ //! Scientific claim boundaries for compositional coordinates and posterior draws. use psychometric_core::{ - ClusteredEventScore, IndicatorKind, LagClock, LaggedWithinResidual, - posterior_draw_point_estimate_mean, recover_irregular_centered_residual_log_rate, + ClusteredEventScore, ClusteredScore, IndicatorKind, LagClock, LaggedWithinResidual, + ordinary_least_squares_slope, posterior_draw_point_estimate_mean, + recover_cluster_mean_within_between_slopes, recover_irregular_centered_residual_log_rate, recover_loading_point_estimate_mean, recover_within_residual_event_time_log_rate, }; @@ -87,3 +88,46 @@ fn person_mean_subtraction_on_raw_ar_is_not_the_lagged_within_effect() { "Curran & Bauer (2011, pp. 607–608): CWC of raw AR recovered {cwc}, which must not equal drift {drift}" ); } + +#[test] +fn cwc_cluster_mean_coefficient_is_not_the_between_cluster_effect() { + let rows = [ + ClusteredScore { + cluster_key: 1, + predictor: 0.0, + outcome: 2.0, + }, + ClusteredScore { + cluster_key: 1, + predictor: 2.0, + outcome: 3.0, + }, + ClusteredScore { + cluster_key: 2, + predictor: 4.0, + outcome: 10.0, + }, + ClusteredScore { + cluster_key: 2, + predictor: 6.0, + outcome: 11.0, + }, + ]; + let recovered = recover_cluster_mean_within_between_slopes(&rows).expect("cwc"); + let predictors: Vec = rows.iter().map(|row| row.predictor).collect(); + let outcomes: Vec = rows.iter().map(|row| row.outcome).collect(); + let pooled = ordinary_least_squares_slope(&predictors, &outcomes).expect("pooled"); + assert!( + (recovered.contextual_effect - recovered.between_slope).abs() > 1e-9, + "Enders & Tofighi (2007, Table 2, pp. 124–127): CWC γ01 is contextual, not between" + ); + assert!( + (recovered.contextual_effect - pooled).abs() > 1e-9, + "pooled OLS must not be treated as the CWC contextual effect" + ); + assert!( + ((recovered.contextual_effect + recovered.within_slope) - recovered.between_slope).abs() + < 1e-15, + "adding CWC γ01 to γ10 must recover the between-cluster slope" + ); +} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 9931472d..b94878ba 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -28,7 +28,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | report template/section/copied/style/modality method effects | ADR 0004/0012; PRD/TRD | simulation truth factors implemented; estimator-side method model remains future | partial | | candidate K statistical/Pareto gates + blinded LLM review | ADR 0012; research | future `model_selection` | accepted-target | | compositional topic correlation / stable clustering | ADR 0005/0012; research | future `network_analysis` | accepted-target | -| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS, event-time log-rate, irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR; full ESEM/DSEM remaining | partial | +| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR; full ESEM/DSEM remaining | partial | | CPU bounded multithreading + GPU/VRAM streaming/parity | ADR 0001/0006 | future `compute_backend` | accepted-target | | TDT detection/tracking vs CHRONOS schema/prediction/temporal consistency | ADR 0016; PRD/research | future `event_intelligence` | accepted-target | | evidence-bounded LLM interpretation | ADR 0010/0012; PRD | future `interpretation_gateway` | accepted-target | diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index 86ac01e9..2a55603f 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -1,7 +1,7 @@ # ADR 0005 — Posterior-aware ESEM/DSEM and structural interpretation **Decision status:** Accepted -**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target +**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target **Date:** 2026-08-05 **Supersedes:** None. ADR 0012 governs upstream topic measurement/network coordinates; this ADR governs higher-order psychometric structure and longitudinal interpretation. @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS and Kish-weighted slopes. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3) and refuses the difference quotient. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3) and refuses the difference quotient. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. 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/adr/README.md b/docs/adr/README.md index bdf6b44d..b277ad91 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -10,7 +10,7 @@ Read [`ADR_POLICY.md`](ADR_POLICY.md) first. **Decision status and implementatio | [0002](0002-six-clock-temporal-semantics.md) | Six-clock temporal semantics and fail-closed historical leakage prevention | Accepted | active-PR | Unmerged PR #8 is the canonical Task 3 replacement implementing typed clocks/intervals against the current protected-main lineage; conflicted PR #5 is superseded lineage. Later graph/split enforcement remains target work. | | [0003](0003-relational-event-multiple-membership.md) | Relational event ontology and time-varying cross-classified multiple membership | Accepted | partial | Weighted time-varying membership network/roles are active-PR (PR #12); full multilevel estimators, graph ontology, and persistence remain accepted-target. ADR 0016 owns event-intelligence tasks. | | [0004](0004-shared-multilingual-latent-space.md) | One shared multilingual latent space with explicit invariance status | Accepted | accepted-target | ADR 0012 owns the full topic-estimator/backend/global-topic contract. | -| [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | partial | Input gates, posterior-draw point estimates, Rubin `T` on OLS loadings, CWC within/between OLS, event-time log-rate, irregular already-centered residual lag, and strong-gated latent means are on the stacked psychometric PR; full ESEM/DSEM estimator remains accepted-target. | +| [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | partial | Input gates, posterior-draw point estimates, Rubin `T` on OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, irregular already-centered residual lag, and strong-gated latent means are on the stacked psychometric PR; full ESEM/DSEM estimator remains accepted-target. | | [0006](0006-vram-gpu-nvidia-orchestration.md) | VRAM-adaptive GPU compute and model-credential boundary | Accepted | accepted-target | LLM orchestration policy superseded by ADR 0010; autonomous development authority governed by ADR 0015. | | [0007](0007-rust-workspace-quality-gates.md) | Explicit Rust workspace, pinned toolchains, and exact quality gates | Accepted | implemented-main | ADR 0014 governs scientific/product claim promotion beyond repository-quality tooling. | | [0008](0008-immutable-evidence-identities-digests-and-spans.md) | Immutable evidence identities, `SHA-256` digests, exact spans, and strict wire reconstruction | Accepted | implemented-main | ADR 0013 governs future persistence/reproducibility/split authority. | diff --git a/docs/research/multilevel-event-time-recovery.md b/docs/research/multilevel-event-time-recovery.md index 0c4290c4..73b7ea4c 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -5,15 +5,16 @@ This slice stays inside `psychometric_core`. It does not add a second invariance crate and does not recreate `#78` `longitudinal_core` or `#80` `irregular_time`. 1. recover within-cluster and between-cluster OLS slopes after centering within cluster (CWC); -2. recover a Kish-weighted least-squares slope and report Kish ESS as the information diagnostic; -3. map a discrete lag-1 coefficient through the exact scalar exponential on **event time only**; -4. refuse the difference quotient as a continuous-time rate; -5. apply the same event-time map to CWC residuals (still not DSEM); -6. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). +2. recover the CWC contextual effect as `between − within` (Enders & Tofighi, 2007, Table 2); +3. recover a Kish-weighted least-squares slope and report Kish ESS as the information diagnostic; +4. map a discrete lag-1 coefficient through the exact scalar exponential on **event time only**; +5. refuse the difference quotient as a continuous-time rate; +6. apply the same event-time map to CWC residuals (still not DSEM); +7. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). ## Claim boundary -This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, and not a matrix `expm` implementation. +This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. ## Authoritative sources @@ -32,6 +33,7 @@ Kish, L. (1965). *Survey sampling*. John Wiley & Sons. ## Formula notes - **CWC.** For cluster \(i\) and occasion \(t\), \(x_{it}^{w} = x_{it}-\bar x_{i}\) and \(y_{it}^{w} = y_{it}-\bar y_{i}\). The within slope is OLS of \(y^{w}\) on \(x^{w}\). The between slope is OLS of the cluster means. A grand-mean pooled slope confounds the two. +- **Contextual effect.** Enders and Tofighi (2007, Table 2, pp. 124–127): under CWC, the cluster-mean coefficient \(\gamma_{01}\) is the contextual effect (expected difference between two people with the same individual \(X\) from groups one unit apart on \(\bar X\)). Under CGM the same symbol is the between-cluster effect. The OLS identity is \(\gamma_{01}^{\mathrm{CWC}}=\beta_{\mathrm{between}}-\beta_{\mathrm{within}}\). Adding the CWC contextual coefficient to the within slope recovers the between-cluster slope. This crate reports the OLS analogue; it does not estimate their multilevel maximum-likelihood model. - **Kish ESS.** \(\mathrm{ESS}=(\sum w)^{2}/\sum w^{2}\) on non-negative finite weights. WLS uses the weights in the slope; ESS is not a second slope. - **Exact scalar map.** Voelkle et al. (2012, Eq. 7) and Driver et al. (2017, Eq. 3): \(\varphi = A^{*}(\Delta t)=\exp(a\,\Delta t)\). The inverse is \(a=\ln\varphi/\Delta t\). The difference quotient \((x(t+\Delta t)-x(t))/\Delta t\) is refused. - **CWC-then-lag.** Sample cluster means are removed first. Consecutive within residuals are then fitted by least squares to \(r_{t+\Delta t}\approx\exp(a\Delta t)\,r_{t}\) on event time. Same-sign pair-wise logs initialize the scalar Newton step. Sign-flipping \(T=2\) CWC pairs have no real logarithm and fail closed. Curran and Bauer (2011, pp. 607–608) show that this person-mean subtraction on a raw autoregressive series does **not** isolate the lagged within-person effect; the helper therefore does not claim to recover the raw-process drift. @@ -39,10 +41,11 @@ Kish, L. (1965). *Survey sampling*. John Wiley & Sons. ## Verification -- noiseless CWC recovers known within and between slopes with smaller computed RMSE than a pooled OLS collapse; +- noiseless CWC recovers known within, between, and contextual slopes with smaller computed RMSE than a pooled OLS collapse; +- the CWC contextual effect is not equal to the between-cluster slope when the within slope is nonzero, and contextual + within recovers between; - Kish WLS recovers a known slope; - the exact scalar map recovers a known drift on event time and refuses every other clock plus the difference quotient; - CWC-then-lag on a two-cluster decaying series has smaller computed RMSE than a level-pooled series when the latter is identified; - already-centered irregular residuals recover a known drift at machine-scale RMSE, and that RMSE is smaller than CWC of the corresponding raw autoregressive series (Curran & Bauer, 2011, pp. 607–608); - a singleton cluster is skipped; two singleton clusters yield an empty pair list and fail closed; -- overflowing CWC residuals and Newton overflow / flat-derivative steps fail closed. +- overflowing CWC residuals, overflowing contextual subtraction, and Newton overflow / flat-derivative steps fail closed. diff --git a/docs/research/multilevel-multiple-membership-measurement.md b/docs/research/multilevel-multiple-membership-measurement.md index 5cc47c3a..23fccf82 100644 --- a/docs/research/multilevel-multiple-membership-measurement.md +++ b/docs/research/multilevel-multiple-membership-measurement.md @@ -12,7 +12,7 @@ TEPP documents and events may simultaneously belong to authors, departments, cus ## Estimator target (future) -Production multilevel estimators (cross-classified / multiple-membership ESEM/DSEM) remain accepted-target in `psychometric_core`. The current executable slice recovers two-level CWC OLS and Kish-weighted slopes only. Recovery studies must use realistic synthetic truth with known multilevel structure and report RMSE, bias, and coverage via `validation_core`; those metrics are a non-exhaustive subset of the acceptance contract. ADR 0005 additionally requires posterior-uncertainty propagation, construct-specific model classification, measurement invariance, within/between separation, irregular-time handling, and event-time ordering. +Production multilevel estimators (cross-classified / multiple-membership ESEM/DSEM) remain accepted-target in `psychometric_core`. The current executable slice recovers two-level CWC OLS, the CWC contextual effect, and Kish-weighted slopes only. Recovery studies must use realistic synthetic truth with known multilevel structure and report RMSE, bias, and coverage via `validation_core`; those metrics are a non-exhaustive subset of the acceptance contract. ADR 0005 additionally requires posterior-uncertainty propagation, construct-specific model classification, measurement invariance, within/between separation, irregular-time handling, and event-time ordering. ## Authority sources diff --git a/docs/research/posterior-esem-input-gates.md b/docs/research/posterior-esem-input-gates.md index 22ee69fb..502f0fce 100644 --- a/docs/research/posterior-esem-input-gates.md +++ b/docs/research/posterior-esem-input-gates.md @@ -12,7 +12,7 @@ This slice delivers the first executable ADR 0005 contract in `psychometric_core 6. refuse latent-mean comparison without invariance evidence, and recover a mean difference only under strong or strict two-group OLS status; 7. refuse causal language that rests only on temporal precedence, document linkage, event tracking, or model prediction. -Cluster-mean CWC, Kish WLS, event-time log-rate, CWC-then-lag, and irregular already-centered residual log-rate live in the same crate and are doctored in `docs/research/multilevel-event-time-recovery.md`. Full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target. +Cluster-mean CWC, the CWC contextual effect, Kish WLS, event-time log-rate, CWC-then-lag, and irregular already-centered residual log-rate live in the same crate and are doctored in `docs/research/multilevel-event-time-recovery.md`. Full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target. ## Authoritative sources diff --git a/docs/research/standards-and-literature.md b/docs/research/standards-and-literature.md index 586e1fbf..06c341a1 100644 --- a/docs/research/standards-and-literature.md +++ b/docs/research/standards-and-literature.md @@ -32,7 +32,7 @@ Meredith, W. (1993). Measurement invariance, factor analysis and factorial invar Holland, P. W. (1986). Statistics and causal inference. *Journal of the American Statistical Association, 81*(396), 945–960. https://doi.org/10.1080/01621459.1986.10478354 -TEPP applies these sources to construct definition, score interpretation, reliability, validity evidence, uncertainty, consequences, longitudinal invariance, ESEM cross-loadings, and DSEM. Topic outputs are treated as fallible indicators or components only after their construct role is evaluated. Reflective, formative, and network classes remain distinct (Bollen & Lennox, 1991). Complete-data OLS loadings across posterior indicator draws are combined with Rubin (1996) \(T_m\); the arithmetic-mean helper remains a point estimate. Mislevy (1991) is listed as a future plausible-value source and was not opened in this cycle (Unpaywall 2026-08-17: closed; Cambridge/ETS remain HTML stubs). Temporal precedence is not causal identification (Holland, 1986). Within/between OLS follows Enders and Tofighi (2007), Curran and Bauer (2011), and Hamaker et al. (2015). Curran and Bauer (2011, pp. 607–608) reject person-mean subtraction on a raw autoregressive series as the lagged within-person residual; already-centered irregular residuals use the Voelkle et al. (2012, Eq. 7) / Driver et al. (2017, Eq. 3) scalar map. Metric/weak invariance does not license latent-mean comparison. Meredith (1993) remains unread (Unpaywall 2026-08-17: closed). +TEPP applies these sources to construct definition, score interpretation, reliability, validity evidence, uncertainty, consequences, longitudinal invariance, ESEM cross-loadings, and DSEM. Topic outputs are treated as fallible indicators or components only after their construct role is evaluated. Reflective, formative, and network classes remain distinct (Bollen & Lennox, 1991). Complete-data OLS loadings across posterior indicator draws are combined with Rubin (1996) \(T_m\); the arithmetic-mean helper remains a point estimate. Mislevy (1991) is listed as a future plausible-value source and was not opened in this cycle (Unpaywall/OpenAlex/archive.org 2026-08-17: closed; Cambridge/ETS remain HTML stubs). Temporal precedence is not causal identification (Holland, 1986). Within/between OLS follows Enders and Tofighi (2007), Curran and Bauer (2011), and Hamaker et al. (2015). Enders and Tofighi (2007, Table 2, pp. 124–127; PDF opened 2026-08-17) show that the CWC cluster-mean coefficient is the contextual effect (`between − within`), not the between-cluster effect. Curran and Bauer (2011, pp. 607–608) reject person-mean subtraction on a raw autoregressive series as the lagged within-person residual; already-centered irregular residuals use the Voelkle et al. (2012, Eq. 7) / Driver et al. (2017, Eq. 3) scalar map. Metric/weak invariance does not license latent-mean comparison. Meredith (1993) remains unread (Unpaywall/OpenAlex/archive.org 2026-08-17: closed). ## Structural, correlated, dynamic, relational, and multilingual topic models diff --git a/docs/validation/temporal-event-foundation.md b/docs/validation/temporal-event-foundation.md index 8c2dac26..9fed4a77 100644 --- a/docs/validation/temporal-event-foundation.md +++ b/docs/validation/temporal-event-foundation.md @@ -23,7 +23,7 @@ This report tracks exact-head scientific and engineering evidence required befor | Truth corpora / manifests | `tepp_simulation` | implemented-main | — | deterministic generator tests | Task 10 / PR #18 | | Recovery metrics | `validation_core` | implemented-main | — | RMSE/bias/coverage/MC gates | Task 11 / PR #19 | | Versioned API/export contracts | `tepp_api` | implemented-main | naruon HTTP interchange | unknown-field/version/limit + naruon HTTPS interchange tests | Task 12 / PR #21; live HTTP service remaining | -| Psychometric structural input gates | `psychometric_core` | accepted-target | stacked psychometric PR | construct-class refusal + ALR/ILR boundary + true-loading RMSE + posterior-draw point-estimate mean + Rubin `T` + CWC within/between + event-time log-rate + irregular already-centered residual lag + strong-gated latent means; full ESEM/DSEM remaining | ADR 0005; `docs/research/posterior-esem-input-gates.md`; `docs/research/multilevel-event-time-recovery.md`; `docs/research/rubin-total-variance.md`; `docs/research/strong-invariance-latent-means.md` | +| Psychometric structural input gates | `psychometric_core` | accepted-target | stacked psychometric PR | construct-class refusal + ALR/ILR boundary + true-loading RMSE + posterior-draw point-estimate mean + Rubin `T` + CWC within/between + CWC contextual effect + event-time log-rate + irregular already-centered residual lag + strong-gated latent means; full ESEM/DSEM remaining | ADR 0005; `docs/research/posterior-esem-input-gates.md`; `docs/research/multilevel-event-time-recovery.md`; `docs/research/rubin-total-variance.md`; `docs/research/strong-invariance-latent-means.md` | | CWL modular connectors | `docs/connectors/*` | implemented-main | — | contract docs + examples | PR #22; live HTTP ports remaining | | Release SBOM/provenance generator | `scripts/release_evidence.py` | partial | — | generate+validate in CI | Task 13 partial / PR #28 | From 6f74e460798dc955ca2ad82fd859c116f8eafcf9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 10:23:52 +0000 Subject: [PATCH 07/62] test(psychometric): close residual-overflow and Pearson branch gaps Bitwise-and the CWC residual finite checks so both arms execute. Recover Newton start-skip / deriv-INF, series NaN, and Pearson empty/mismatch/left-INF paths. Mean-gate and PV math are unchanged. --- CHANGELOG.md | 2 +- crates/psychometric_core/src/event_time.rs | 64 ++++++++++++++++++- crates/psychometric_core/src/indicator.rs | 31 +++++++++ .../multilevel-event-time-recovery.md | 2 +- 4 files changed, 96 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17e8cfcc..97255630 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang ### Added - `psychometric_core` CWC contextual effect: `between − within` is the cluster-mean coefficient under CWC, not the between-cluster slope (Enders & Tofighi, 2007, Table 2, pp. 124–127). Overflow of that subtraction fails closed. This is two-level OLS, not their multilevel maximum-likelihood model. -- Fail-closed CWC-lag coverage in `psychometric_core`: singleton clusters are skipped, all-singleton series and overflowing CWC residuals fail closed, and the scalar Newton step refuses a non-finite exponential or score. Dead post-OLS `pred_ss` and midpoint `require_finite` guards were removed because those values are already finite after OLS. +- Fail-closed CWC-lag coverage in `psychometric_core`: singleton clusters are skipped, all-singleton series and overflowing CWC residuals fail closed, later-only residual overflow is checked with bitwise `is_finite`, the scalar Newton step refuses a non-finite exponential, score, start-skip, or deriv-INF, and Pearson empty/mismatch/left-INF paths are recovered. Dead post-OLS `pred_ss` and midpoint `require_finite` guards were removed because those values are already finite after OLS. - `psychometric_core` multilevel/event-time recovery on the stacked psychometric PR: cluster-mean CWC within/between OLS, Kish ESS weighted slopes, event-time-only discrete lag-1 and exact scalar local log-rate, CWC-then-event-time residual lag, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, and two-group OLS strong/strict-gated latent-mean difference. Metric/weak is not a mean license. Not DSEM, not RI-CLPM, not MGCFA, not Mislevy PVs (ADR 0005; no new migration). - `psychometric_core` posterior-aware structural input gates: construct classification, refusal of raw-proportion Pearson/OLS, explicit ALR-versus-ILR geometry boundaries, CPU `f64` OLS recovery, posterior-draw loading point-estimate averaging without Rubin uncertainty claims, invariance-gated latent-mean comparison, and causal-heuristic refusal (ADR 0005 first production slice; no new migration). - `persistence_postgres` backup/restore integrity: restored snapshots stay unusable until tenant, canonical `SHA-256`, knowledge-cutoff eligibility, temporal window order, and append-only triggers revalidate; SQL probes raise `restore integrity failed` (ADR 0013). diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index dd2d2bca..2ac3c5b8 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -276,7 +276,7 @@ pub fn recover_within_residual_event_time_log_rate( if !delta.is_finite() || delta <= 0.0 { return Err(PsychometricError::NonPositiveInterval); } - if !earlier_resid.is_finite() || !later_resid.is_finite() { + if !(earlier_resid.is_finite() & later_resid.is_finite()) { return Err(PsychometricError::InvalidNumericInput); } pairs.push((earlier_resid, later_resid, delta)); @@ -529,6 +529,27 @@ mod tests { ), Err(PsychometricError::InvalidNumericInput) ); + assert_eq!( + recover_event_series_mean_log_rate( + &[occasion(0.0, 1.0), occasion(f64::NAN, 0.5)], + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_event_series_mean_log_rate( + &[occasion(0.0, f64::NAN), occasion(1.0, 0.5)], + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_event_series_mean_log_rate( + &[occasion(0.0, 1.0), occasion(1.0, f64::NAN)], + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); assert_eq!( recover_event_series_mean_log_rate( &[ @@ -788,5 +809,46 @@ mod tests { ); let flat = fit_scalar_log_rate(&[(1e-50, 1e-200, 1.0)]).expect("flat"); assert!(flat.is_finite()); + assert_eq!( + fit_scalar_log_rate(&[(0.0, 1.0, 1.0), (1.0, -1.0, 1.0)]), + Err(PsychometricError::InvalidNumericInput) + ); + let skipped_start = + fit_scalar_log_rate(&[(1e-320, 1.0, 1.0), (1.0, 0.5, 1.0)]).expect("skip inf ratio"); + assert!(skipped_start.is_finite()); + assert_eq!( + fit_scalar_log_rate(&[(1e154, 1e154, 1.0)]), + Err(PsychometricError::InvalidNumericInput) + ); + } + + #[test] + fn one_sided_residual_overflow_and_nonfinite_interval_fail_closed() { + assert_eq!( + recover_within_residual_event_time_log_rate( + &[ + clustered(1, 0.0, -f64::MAX), + clustered(1, 1.0, -f64::MAX), + clustered(1, 2.0, -f64::MAX), + clustered(1, 3.0, f64::MAX), + clustered(2, 0.0, 1.0), + clustered(2, 1.0, 0.8), + ], + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_within_residual_event_time_log_rate( + &[ + clustered(1, f64::MAX, 1.0), + clustered(1, -f64::MAX, 0.5), + clustered(2, 0.0, 1.0), + clustered(2, 1.0, 0.5), + ], + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); } } diff --git a/crates/psychometric_core/src/indicator.rs b/crates/psychometric_core/src/indicator.rs index 1b225d19..230c8011 100644 --- a/crates/psychometric_core/src/indicator.rs +++ b/crates/psychometric_core/src/indicator.rs @@ -133,6 +133,17 @@ mod tests { #[test] fn valid_kinds_pass_and_zero_right_variance_is_singular() { require_valid_indicator(IndicatorKind::IsometricLogRatio).expect("ilr"); + let correlation = pearson_correlation( + &[0.0, 1.0, 2.0], + &[0.0, 2.0, 4.0], + IndicatorKind::AdditiveLogRatio, + ) + .expect("line"); + assert!((correlation - 1.0).abs() < 1e-12); + assert_eq!( + pearson_correlation(&[1.0, 1.0], &[2.0, 3.0], IndicatorKind::LogisticNormal), + Err(PsychometricError::SingularDesign) + ); assert_eq!( pearson_correlation(&[1.0, 2.0], &[3.0, 3.0], IndicatorKind::LogisticNormal), Err(PsychometricError::SingularDesign) @@ -153,5 +164,25 @@ mod tests { ), Err(PsychometricError::InvalidNumericInput) ); + assert_eq!( + pearson_correlation( + &[f64::INFINITY, 2.0], + &[1.0, 3.0], + IndicatorKind::LogisticNormal + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + pearson_correlation(&[], &[], IndicatorKind::AdditiveLogRatio), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + pearson_correlation( + &[1.0, 2.0, 3.0], + &[1.0, 2.0], + IndicatorKind::AdditiveLogRatio + ), + Err(PsychometricError::InvalidNumericInput) + ); } } diff --git a/docs/research/multilevel-event-time-recovery.md b/docs/research/multilevel-event-time-recovery.md index 73b7ea4c..5b02bdc1 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -48,4 +48,4 @@ Kish, L. (1965). *Survey sampling*. John Wiley & Sons. - CWC-then-lag on a two-cluster decaying series has smaller computed RMSE than a level-pooled series when the latter is identified; - already-centered irregular residuals recover a known drift at machine-scale RMSE, and that RMSE is smaller than CWC of the corresponding raw autoregressive series (Curran & Bauer, 2011, pp. 607–608); - a singleton cluster is skipped; two singleton clusters yield an empty pair list and fail closed; -- overflowing CWC residuals, overflowing contextual subtraction, and Newton overflow / flat-derivative steps fail closed. +- overflowing CWC residuals, overflowing contextual subtraction, later-only residual overflow, non-finite intervals, Newton overflow / start-skip / deriv-INF, and Pearson empty/mismatch paths fail closed. From 67d32bb13b6fa2a098878c8eaf13b9d87bb34c7b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 11:14:35 +0000 Subject: [PATCH 08/62] feat(psychometric): remap discrete lags across unequal event intervals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Voelkle et al. (2012, Eq. 7; ZORA accepted manuscript pp. 2, 16, 33 and Appendix B) show that discrete-time autoregressive coefficients from different intervals are not comparable. Recover φ(Δt)=exp(a Δt) and map φ_src at Δt_src onto φ_ref through a=ln(φ_src)/Δt_src. Pooling those discrete lags fails closed. Still not DSEM, not matrix expm. Meredith (1993) and Mislevy (1991) remain unread. --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 2 +- CLAUDE.md | 2 +- crates/psychometric_core/src/error.rs | 9 + crates/psychometric_core/src/event_time.rs | 161 +++++++++++++++++- crates/psychometric_core/src/lib.rs | 9 +- .../tests/esem_input_recovery_contract.rs | 4 + ...multilevel_event_time_recovery_contract.rs | 31 +++- docs/adr/0005-posterior-esem-dsem.md | 4 +- .../multilevel-event-time-recovery.md | 17 +- docs/research/standards-and-literature.md | 2 +- 11 files changed, 224 insertions(+), 19 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 87b3e945..4a0c5d2f 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -61,7 +61,7 @@ boundaries above remain the target modular MSA architecture. | `tepp_simulation` | known-truth temporal/event data generation | | `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics | | `tepp_api` | versioned DTO, schema, and export contracts | -| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means | +| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means | No crate exposes placeholder production behavior in Task 1. This prevents an empty façade from becoming a de facto public API before its invariants and tests diff --git a/CHANGELOG.md b/CHANGELOG.md index 97255630..ac43e456 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang ### Added -- `psychometric_core` CWC contextual effect: `between − within` is the cluster-mean coefficient under CWC, not the between-cluster slope (Enders & Tofighi, 2007, Table 2, pp. 124–127). Overflow of that subtraction fails closed. This is two-level OLS, not their multilevel maximum-likelihood model. +- `psychometric_core` exact scalar forward map `φ(Δt) = exp(a Δt)` and interval remapping: a discrete lag at one event interval maps onto another through the Voelkle et al. (2012, Eq. 7) log-rate. Pooling discrete lags from unequal intervals fails closed. Still not a matrix `expm` and not DSEM. - Fail-closed CWC-lag coverage in `psychometric_core`: singleton clusters are skipped, all-singleton series and overflowing CWC residuals fail closed, later-only residual overflow is checked with bitwise `is_finite`, the scalar Newton step refuses a non-finite exponential, score, start-skip, or deriv-INF, and Pearson empty/mismatch/left-INF paths are recovered. Dead post-OLS `pred_ss` and midpoint `require_finite` guards were removed because those values are already finite after OLS. - `psychometric_core` multilevel/event-time recovery on the stacked psychometric PR: cluster-mean CWC within/between OLS, Kish ESS weighted slopes, event-time-only discrete lag-1 and exact scalar local log-rate, CWC-then-event-time residual lag, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, and two-group OLS strong/strict-gated latent-mean difference. Metric/weak is not a mean license. Not DSEM, not RI-CLPM, not MGCFA, not Mislevy PVs (ADR 0005; no new migration). - `psychometric_core` posterior-aware structural input gates: construct classification, refusal of raw-proportion Pearson/OLS, explicit ALR-versus-ILR geometry boundaries, CPU `f64` OLS recovery, posterior-draw loading point-estimate averaging without Rubin uncertainty claims, invariance-gated latent-mean comparison, and causal-heuristic refusal (ADR 0005 first production slice; no new migration). diff --git a/CLAUDE.md b/CLAUDE.md index 1b19f20b..af56f41b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. -- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. diff --git a/crates/psychometric_core/src/error.rs b/crates/psychometric_core/src/error.rs index 341370b0..fba15858 100644 --- a/crates/psychometric_core/src/error.rs +++ b/crates/psychometric_core/src/error.rs @@ -29,6 +29,8 @@ pub enum PsychometricError { /// The Voelkle–Oud difference quotient was offered as a continuous-time /// rate. DifferenceQuotientForbidden, + /// Discrete lags from unequal event intervals were treated as one coefficient. + UnequalIntervalPoolingForbidden, /// Fewer than two clusters were supplied for a within/between decomposition. InsufficientClusters, /// A membership or survey weight is empty, negative, or non-finite. @@ -61,6 +63,9 @@ impl fmt::Display for PsychometricError { Self::DifferenceQuotientForbidden => { "the difference quotient is not the local continuous-time rate" } + Self::UnequalIntervalPoolingForbidden => { + "discrete lags from unequal event intervals are not one coefficient" + } Self::InsufficientClusters => "within/between recovery requires at least two clusters", Self::InvalidWeight => "invalid non-negative finite psychometric weight", Self::NonPositiveInterval => "event-time interval must be strictly positive", @@ -119,6 +124,10 @@ mod tests { PsychometricError::DifferenceQuotientForbidden.to_string(), "the difference quotient is not the local continuous-time rate" ); + assert_eq!( + PsychometricError::UnequalIntervalPoolingForbidden.to_string(), + "discrete lags from unequal event intervals are not one coefficient" + ); assert_eq!( PsychometricError::InsufficientClusters.to_string(), "within/between recovery requires at least two clusters" diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index 2ac3c5b8..bf500417 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -1,10 +1,14 @@ //! Event-time discrete lag-1 and exact scalar local log-rate. //! -//! Voelkle, Oud, Davidov, and Schmidt (2012, Eq. 7) and Driver, Oud, and -//! Voelkle (2017, Eq. 3) map the continuous-time drift by +//! Voelkle, Oud, Davidov, and Schmidt (2012, Eq. 7; ZORA accepted +//! manuscript, Continuous Time Modeling p. 16 and Appendix B) and Driver, +//! Oud, and Voelkle (2017, Eq. 3) map the continuous-time drift by //! `A*(Δt) = exp(A Δt)`. The noiseless scalar inverse is -//! `a = ln(φ) / Δt` with `φ = A*(Δt)`. The difference quotient -//! `(x(t+Δt) − x(t)) / Δt` (their Eqs. 3–4) is refused. This is not DSEM. +//! `a = ln(φ) / Δt` with `φ = A*(Δt)`. The forward map is +//! `φ(Δt) = exp(a Δt)`. Discrete lags from unequal event intervals are +//! not one coefficient; they map through `a` first. The difference +//! quotient `(x(t+Δt) − x(t)) / Δt` (their Eqs. 3–4) is refused. This is +//! not DSEM and not a matrix `expm`. use std::collections::BTreeMap; @@ -136,6 +140,34 @@ pub fn recover_local_log_rate( require_finite(discrete_lag.ln() / event_delta) } +/// Exact scalar forward map `φ(Δt) = exp(a Δt)` (Voelkle et al., 2012, Eq. 7). +/// +/// This is the inverse of [`recover_local_log_rate`]. It is the scalar case of +/// `A*(Δt) = exp(A Δt)`, not a matrix `expm`. +/// +/// # Errors +/// +/// Returns [`PsychometricError::EventTimeRequired`] for any non-event clock, +/// [`PsychometricError::NonPositiveInterval`] when `event_delta` is not +/// strictly positive, and [`PsychometricError::InvalidNumericInput`] when the +/// log-rate is non-finite or the exponential overflows. +pub fn recover_discrete_lag_from_log_rate( + log_rate: f64, + event_delta: f64, + clock: LagClock, +) -> Result { + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + if !event_delta.is_finite() || event_delta <= 0.0 { + return Err(PsychometricError::NonPositiveInterval); + } + if !log_rate.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + require_finite((log_rate * event_delta).exp()) +} + /// Recover the exact scalar pair `(φ, a)` on event time. /// /// # Errors @@ -156,6 +188,44 @@ pub fn recover_event_time_discrete_lag_and_log_rate( }) } +/// Map a discrete lag from one event interval onto another through `a`. +/// +/// Voelkle et al. (2012, ZORA accepted manuscript pp. 2, 16, 33) show that +/// discrete-time autoregressive coefficients from different intervals are +/// not comparable. The licensed path is `a = ln(φ_src) / Δt_src` then +/// `φ_ref = exp(a Δt_ref)`. Equal source and reference intervals still go +/// through that map. This is not DSEM. +/// +/// # Errors +/// +/// Propagates [`recover_local_log_rate`] and +/// [`recover_discrete_lag_from_log_rate`]. +pub fn map_discrete_lag_across_event_intervals( + discrete_lag: f64, + source_delta: f64, + reference_delta: f64, + clock: LagClock, +) -> Result { + let log_rate = recover_local_log_rate(discrete_lag, source_delta, clock)?; + recover_discrete_lag_from_log_rate(log_rate, reference_delta, clock) +} + +/// Refuse treating discrete lags from unequal event intervals as one coefficient. +/// +/// Always fails closed. Map each lag through +/// [`map_discrete_lag_across_event_intervals`] instead. +/// +/// # Errors +/// +/// Always returns [`PsychometricError::UnequalIntervalPoolingForbidden`]. +pub fn refuse_pooled_discrete_lag_across_unequal_intervals( + first_delta: f64, + second_delta: f64, +) -> Result { + let _ = (first_delta, second_delta); + Err(PsychometricError::UnequalIntervalPoolingForbidden) +} + /// Refuse the difference quotient as a continuous-time rate. /// /// Voelkle et al. (2012) discourage `(x(t+Δt) − x(t)) / Δt` as the drift. @@ -386,10 +456,12 @@ pub(crate) fn fit_scalar_log_rate(pairs: &[(f64, f64, f64)]) -> Result 1e-9); + assert_eq!( + refuse_pooled_discrete_lag_across_unequal_intervals(source_delta, reference_delta), + Err(PsychometricError::UnequalIntervalPoolingForbidden) + ); + assert_eq!( + refuse_pooled_discrete_lag_across_unequal_intervals(source_delta, source_delta), + Err(PsychometricError::UnequalIntervalPoolingForbidden) + ); + } + + #[test] + fn forward_map_and_interval_remap_fail_closed() { + assert_eq!( + recover_discrete_lag_from_log_rate(-0.2, 1.0, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_discrete_lag_from_log_rate(-0.2, 0.0, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_lag_from_log_rate(-0.2, -1.0, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_lag_from_log_rate(-0.2, f64::NAN, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_lag_from_log_rate(f64::NAN, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_lag_from_log_rate(800.0, 10.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + map_discrete_lag_across_event_intervals(0.5, 1.0, 2.0, LagClock::AssertionTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + map_discrete_lag_across_event_intervals(0.5, 0.0, 2.0, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + map_discrete_lag_across_event_intervals(0.5, 1.0, 0.0, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + map_discrete_lag_across_event_intervals(-0.2, 1.0, 2.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + } + #[test] fn non_event_clocks_and_difference_quotient_fail_closed() { for clock in [ diff --git a/crates/psychometric_core/src/lib.rs b/crates/psychometric_core/src/lib.rs index 0a0eb8cb..39a81be5 100644 --- a/crates/psychometric_core/src/lib.rs +++ b/crates/psychometric_core/src/lib.rs @@ -10,7 +10,8 @@ //! uncertainty pooling, combines draw-level OLS loadings with Rubin `T`, //! decomposes cluster-mean within/between OLS and the CWC contextual effect, //! maps event-time discrete lags through the exact scalar exponential, maps -//! already-centered irregular residuals without re-centering, and refuses +//! already-centered irregular residuals without re-centering, remaps discrete +//! lags across unequal event intervals through that log-rate, and refuses //! latent-mean comparison below strong invariance. mod causality; @@ -56,6 +57,10 @@ pub use event_time::EventOccasion; 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 forward map `φ = exp(a Δt)`. +pub use event_time::recover_discrete_lag_from_log_rate; /// Noiseless scalar discrete lag `later / earlier`. pub use event_time::recover_discrete_lag_one; /// Mean local log-rate on a sorted event-time series. @@ -70,6 +75,8 @@ pub use event_time::recover_local_log_rate; pub use event_time::recover_within_residual_event_time_log_rate; /// Refuse the difference quotient as a continuous-time rate. pub use event_time::refuse_difference_quotient_as_local_rate; +/// Refuse pooling discrete lags from unequal event intervals. +pub use event_time::refuse_pooled_discrete_lag_across_unequal_intervals; /// Indicator coordinate kind. pub use indicator::IndicatorKind; /// Pearson correlation on valid coordinates. diff --git a/crates/psychometric_core/tests/esem_input_recovery_contract.rs b/crates/psychometric_core/tests/esem_input_recovery_contract.rs index 2b694ddd..19bd1e1e 100644 --- a/crates/psychometric_core/tests/esem_input_recovery_contract.rs +++ b/crates/psychometric_core/tests/esem_input_recovery_contract.rs @@ -272,6 +272,10 @@ fn finite_alr_correlation_and_error_messages_are_stable() { PsychometricError::DifferenceQuotientForbidden.to_string(), "the difference quotient is not the local continuous-time rate" ); + assert_eq!( + PsychometricError::UnequalIntervalPoolingForbidden.to_string(), + "discrete lags from unequal event intervals are not one coefficient" + ); assert_eq!( PsychometricError::InsufficientClusters.to_string(), "within/between recovery requires at least two clusters" 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 0a4011e6..c4f52908 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -3,11 +3,12 @@ use psychometric_core::{ ClusteredEventScore, ClusteredScore, EventOccasion, IndicatorKind, LagClock, - LaggedWithinResidual, PsychometricError, ordinary_least_squares_slope, - recover_cluster_mean_within_between_slopes, recover_event_series_mean_log_rate, + LaggedWithinResidual, PsychometricError, map_discrete_lag_across_event_intervals, + ordinary_least_squares_slope, recover_cluster_mean_within_between_slopes, + recover_discrete_lag_from_log_rate, recover_event_series_mean_log_rate, recover_event_time_discrete_lag_and_log_rate, recover_irregular_centered_residual_log_rate, recover_kish_weighted_slope, recover_within_residual_event_time_log_rate, - refuse_difference_quotient_as_local_rate, + refuse_difference_quotient_as_local_rate, refuse_pooled_discrete_lag_across_unequal_intervals, }; fn rmse(truth: &[f64], recovered: &[f64]) -> f64 { @@ -109,6 +110,30 @@ fn event_time_log_rate_recovers_known_drift_and_refuses_quotient() { ); } +#[test] +fn discrete_lag_remaps_across_unequal_event_intervals() { + let true_drift = -0.35_f64; + let month = 1.0_f64; + let two_months = 2.0_f64; + let month_lag = + recover_discrete_lag_from_log_rate(true_drift, month, LagClock::EventTime).expect("φ(1)"); + let two_month_truth = (true_drift * two_months).exp(); + let remapped = + map_discrete_lag_across_event_intervals(month_lag, month, two_months, LagClock::EventTime) + .expect("φ(2)"); + let error = rmse(&[two_month_truth], &[remapped]); + assert!(error < 1e-12, "interval-remap RMSE {error}"); + let pooled_error = rmse(&[two_month_truth], &[month_lag]); + assert!( + pooled_error > error, + "Voelkle: pooling φ(1) as φ(2) RMSE {pooled_error} must exceed remap {error}" + ); + assert_eq!( + refuse_pooled_discrete_lag_across_unequal_intervals(month, two_months), + Err(PsychometricError::UnequalIntervalPoolingForbidden) + ); +} + #[test] fn within_residual_event_time_log_rate_beats_pooled_levels() { let true_drift = -0.3_f64; diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index 2a55603f..3df71991 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -1,7 +1,7 @@ # ADR 0005 — Posterior-aware ESEM/DSEM and structural interpretation **Decision status:** Accepted -**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target +**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target **Date:** 2026-08-05 **Supersedes:** None. ADR 0012 governs upstream topic measurement/network coordinates; this ADR governs higher-order psychometric structure and longitudinal interpretation. @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3) and refuses the difference quotient. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, and refuses both the difference quotient and pooling discrete lags from unequal intervals. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. 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 5b02bdc1..2e401415 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -8,13 +8,15 @@ This slice stays inside `psychometric_core`. It does not add a second invariance 2. recover the CWC contextual effect as `between − within` (Enders & Tofighi, 2007, Table 2); 3. recover a Kish-weighted least-squares slope and report Kish ESS as the information diagnostic; 4. map a discrete lag-1 coefficient through the exact scalar exponential on **event time only**; -5. refuse the difference quotient as a continuous-time rate; -6. apply the same event-time map to CWC residuals (still not DSEM); -7. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). +5. recover the exact scalar forward map `φ(Δt) = exp(a Δt)` and remap a discrete lag onto another event interval through that log-rate; +6. refuse pooling discrete lags from unequal event intervals as one coefficient; +7. refuse the difference quotient as a continuous-time rate; +8. apply the same event-time map to CWC residuals (still not DSEM); +9. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). ## Claim boundary -This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. +This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. ## Authoritative sources @@ -30,12 +32,15 @@ Driver, C. C., Oud, J. H. L., & Voelkle, M. C. (2017). Continuous time structura Kish, L. (1965). *Survey sampling*. John Wiley & Sons. +The Voelkle et al. (2012) ZORA accepted manuscript was opened 2026-08-17 (https://www.zora.uzh.ch/handle/20.500.14742/72792). Equation 7 is the exact discrete–continuous map; Equations 3–4 are the discouraged difference-quotient approximation; Appendix B sets Equation 6 equal to Equation 2 to obtain Equation 7. Meredith (1993) and Mislevy (1991) remain unread (Unpaywall/OpenAlex/Semantic Scholar/archive.org 2026-08-17: closed). + ## Formula notes - **CWC.** For cluster \(i\) and occasion \(t\), \(x_{it}^{w} = x_{it}-\bar x_{i}\) and \(y_{it}^{w} = y_{it}-\bar y_{i}\). The within slope is OLS of \(y^{w}\) on \(x^{w}\). The between slope is OLS of the cluster means. A grand-mean pooled slope confounds the two. - **Contextual effect.** Enders and Tofighi (2007, Table 2, pp. 124–127): under CWC, the cluster-mean coefficient \(\gamma_{01}\) is the contextual effect (expected difference between two people with the same individual \(X\) from groups one unit apart on \(\bar X\)). Under CGM the same symbol is the between-cluster effect. The OLS identity is \(\gamma_{01}^{\mathrm{CWC}}=\beta_{\mathrm{between}}-\beta_{\mathrm{within}}\). Adding the CWC contextual coefficient to the within slope recovers the between-cluster slope. This crate reports the OLS analogue; it does not estimate their multilevel maximum-likelihood model. - **Kish ESS.** \(\mathrm{ESS}=(\sum w)^{2}/\sum w^{2}\) on non-negative finite weights. WLS uses the weights in the slope; ESS is not a second slope. -- **Exact scalar map.** Voelkle et al. (2012, Eq. 7) and Driver et al. (2017, Eq. 3): \(\varphi = A^{*}(\Delta t)=\exp(a\,\Delta t)\). The inverse is \(a=\ln\varphi/\Delta t\). The difference quotient \((x(t+\Delta t)-x(t))/\Delta t\) is refused. +- **Exact scalar map.** Voelkle et al. (2012, Eq. 7) and Driver et al. (2017, Eq. 3): \(\varphi = A^{*}(\Delta t)=\exp(a\,\Delta t)\). The inverse is \(a=\ln\varphi/\Delta t\). The forward map is the same equation. The difference quotient \((x(t+\Delta t)-x(t))/\Delta t\) is refused. +- **Unequal-interval remap.** Discrete \(\varphi(\Delta t_1)\) and \(\varphi(\Delta t_2)\) are not comparable when \(\Delta t_1\neq\Delta t_2\) (Voelkle et al., 2012, ZORA manuscript pp. 2, 16, 33). The licensed path is \(a=\ln\varphi_{\mathrm{src}}/\Delta t_{\mathrm{src}}\) then \(\varphi_{\mathrm{ref}}=\exp(a\,\Delta t_{\mathrm{ref}})\). Pooling those discrete lags fails closed. - **CWC-then-lag.** Sample cluster means are removed first. Consecutive within residuals are then fitted by least squares to \(r_{t+\Delta t}\approx\exp(a\Delta t)\,r_{t}\) on event time. Same-sign pair-wise logs initialize the scalar Newton step. Sign-flipping \(T=2\) CWC pairs have no real logarithm and fail closed. Curran and Bauer (2011, pp. 607–608) show that this person-mean subtraction on a raw autoregressive series does **not** isolate the lagged within-person effect; the helper therefore does not claim to recover the raw-process drift. - **Already-centered irregular residual.** The caller supplies lagged within residuals. The mean of \(a=\ln(r_{t+\Delta t}/r_t)/\Delta t\) is the exact scalar map. Intervals may be irregular. The helper does not center again. This is not DSEM. @@ -45,6 +50,8 @@ Kish, L. (1965). *Survey sampling*. John Wiley & Sons. - the CWC contextual effect is not equal to the between-cluster slope when the within slope is nonzero, and contextual + within recovers between; - Kish WLS recovers a known slope; - the exact scalar map recovers a known drift on event time and refuses every other clock plus the difference quotient; +- the forward map inverts the log-rate, remaps \(\varphi(1)\) onto \(\varphi(2)\) at machine-scale RMSE, and that RMSE is smaller than treating \(\varphi(1)\) as \(\varphi(2)\); +- pooling discrete lags from unequal intervals fails closed; - CWC-then-lag on a two-cluster decaying series has smaller computed RMSE than a level-pooled series when the latter is identified; - already-centered irregular residuals recover a known drift at machine-scale RMSE, and that RMSE is smaller than CWC of the corresponding raw autoregressive series (Curran & Bauer, 2011, pp. 607–608); - a singleton cluster is skipped; two singleton clusters yield an empty pair list and fail closed; diff --git a/docs/research/standards-and-literature.md b/docs/research/standards-and-literature.md index 06c341a1..350b60d3 100644 --- a/docs/research/standards-and-literature.md +++ b/docs/research/standards-and-literature.md @@ -32,7 +32,7 @@ Meredith, W. (1993). Measurement invariance, factor analysis and factorial invar Holland, P. W. (1986). Statistics and causal inference. *Journal of the American Statistical Association, 81*(396), 945–960. https://doi.org/10.1080/01621459.1986.10478354 -TEPP applies these sources to construct definition, score interpretation, reliability, validity evidence, uncertainty, consequences, longitudinal invariance, ESEM cross-loadings, and DSEM. Topic outputs are treated as fallible indicators or components only after their construct role is evaluated. Reflective, formative, and network classes remain distinct (Bollen & Lennox, 1991). Complete-data OLS loadings across posterior indicator draws are combined with Rubin (1996) \(T_m\); the arithmetic-mean helper remains a point estimate. Mislevy (1991) is listed as a future plausible-value source and was not opened in this cycle (Unpaywall/OpenAlex/archive.org 2026-08-17: closed; Cambridge/ETS remain HTML stubs). Temporal precedence is not causal identification (Holland, 1986). Within/between OLS follows Enders and Tofighi (2007), Curran and Bauer (2011), and Hamaker et al. (2015). Enders and Tofighi (2007, Table 2, pp. 124–127; PDF opened 2026-08-17) show that the CWC cluster-mean coefficient is the contextual effect (`between − within`), not the between-cluster effect. Curran and Bauer (2011, pp. 607–608) reject person-mean subtraction on a raw autoregressive series as the lagged within-person residual; already-centered irregular residuals use the Voelkle et al. (2012, Eq. 7) / Driver et al. (2017, Eq. 3) scalar map. Metric/weak invariance does not license latent-mean comparison. Meredith (1993) remains unread (Unpaywall/OpenAlex/archive.org 2026-08-17: closed). +TEPP applies these sources to construct definition, score interpretation, reliability, validity evidence, uncertainty, consequences, longitudinal invariance, ESEM cross-loadings, and DSEM. Topic outputs are treated as fallible indicators or components only after their construct role is evaluated. Reflective, formative, and network classes remain distinct (Bollen & Lennox, 1991). Complete-data OLS loadings across posterior indicator draws are combined with Rubin (1996) \(T_m\); the arithmetic-mean helper remains a point estimate. Mislevy (1991) is listed as a future plausible-value source and was not opened in this cycle (Unpaywall/OpenAlex/archive.org 2026-08-17: closed; Cambridge/ETS remain HTML stubs). Temporal precedence is not causal identification (Holland, 1986). Within/between OLS follows Enders and Tofighi (2007), Curran and Bauer (2011), and Hamaker et al. (2015). Enders and Tofighi (2007, Table 2, pp. 124–127; PDF opened 2026-08-17) show that the CWC cluster-mean coefficient is the contextual effect (`between − within`), not the between-cluster effect. Curran and Bauer (2011, pp. 607–608) reject person-mean subtraction on a raw autoregressive series as the lagged within-person residual; already-centered irregular residuals use the Voelkle et al. (2012, Eq. 7) / Driver et al. (2017, Eq. 3) scalar map. Discrete lags from unequal event intervals are remapped through that log-rate (Voelkle et al., 2012, ZORA accepted manuscript opened 2026-08-17, Eq. 7 and Appendix B) and are not pooled. Metric/weak invariance does not license latent-mean comparison. Meredith (1993) remains unread (Unpaywall/OpenAlex/archive.org 2026-08-17: closed). ## Structural, correlated, dynamic, relational, and multilingual topic models From 63a7a48c40d6ecc7377d12191d2fe0e2a873a9da Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 11:21:04 +0000 Subject: [PATCH 09/62] feat(psychometric): refuse binary64 underflow of the scalar forward map MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Voelkle et al. (2012, Eq. 7; ZORA accepted manuscript p. 16) write discrete auto-effects as e^{a Δt}. Those quantities are strictly positive; a = ln(φ)/Δt requires φ > 0. Binary64 exp of a large negative argument is +0 and is not a discrete lag. Direct overflow already failed closed. Newton mapped +0 is refused the same way. Meredith (1993) and Mislevy (1991) remain unread. --- CHANGELOG.md | 1 + CLAUDE.md | 2 +- crates/psychometric_core/src/event_time.rs | 45 ++++++++++++++----- ...multilevel_event_time_recovery_contract.rs | 29 +++++++++--- docs/adr/0005-posterior-esem-dsem.md | 2 +- .../multilevel-event-time-recovery.md | 14 +++--- docs/research/rubin-total-variance.md | 2 +- docs/research/standards-and-literature.md | 2 +- .../strong-invariance-latent-means.md | 2 +- 9 files changed, 71 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac43e456..87f0ee95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,6 +76,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang ### Changed +- `psychometric_core` scalar forward map `φ(Δt) = exp(a Δt)` now refuses binary64 underflow to `+0`. Voelkle et al. (2012, Eq. 7; ZORA accepted manuscript p. 16) write discrete auto-effects as `e^{a Δt}`, which are strictly positive; `a = ln(φ) / Δt` requires `φ > 0`. Direct overflow already failed closed. The Newton residual path refuses a mapped `+0` the same way. - Clarified ADR 0001 so it owns Rust-first numerical/reference-backend authority while ADR 0011 owns cross-service MSA/service authority. - Clarified ADR 0006 so it owns GPU/VRAM and model-credential boundaries; ADR 0010 now owns LLM orchestration policy and ADR 0015 owns autonomous repository-write/review/merge authority. - Expanded ADR 0002–0005 and 0009–0011 with explicit implementation maturity, alternatives, failure/recovery, compatibility/migration, verification, and rollback/supersession boundaries where they were previously implicit. diff --git a/CLAUDE.md b/CLAUDE.md index af56f41b..ccda6ef7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. -- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index bf500417..d775c4ff 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -150,7 +150,9 @@ pub fn recover_local_log_rate( /// Returns [`PsychometricError::EventTimeRequired`] for any non-event clock, /// [`PsychometricError::NonPositiveInterval`] when `event_delta` is not /// strictly positive, and [`PsychometricError::InvalidNumericInput`] when the -/// log-rate is non-finite or the exponential overflows. +/// log-rate is non-finite or the exponential overflows or underflows to zero. +/// Binary64 `exp` of a large negative argument is `+0`, which is not a +/// discrete lag: the inverse `a = ln(φ) / Δt` requires `φ > 0`. pub fn recover_discrete_lag_from_log_rate( log_rate: f64, event_delta: f64, @@ -165,7 +167,11 @@ pub fn recover_discrete_lag_from_log_rate( if !log_rate.is_finite() { return Err(PsychometricError::InvalidNumericInput); } - require_finite((log_rate * event_delta).exp()) + let discrete_lag = (log_rate * event_delta).exp(); + if !discrete_lag.is_finite() || discrete_lag <= 0.0 { + return Err(PsychometricError::InvalidNumericInput); + } + Ok(discrete_lag) } /// Recover the exact scalar pair `(φ, a)` on event time. @@ -428,7 +434,7 @@ pub(crate) fn fit_scalar_log_rate(pairs: &[(f64, f64, f64)]) -> Result Result 0.0); + assert_eq!( + map_discrete_lag_across_event_intervals(source_lag, 1.0, 2000.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); assert_eq!( map_discrete_lag_across_event_intervals(0.5, 1.0, 2.0, LagClock::AssertionTime), Err(PsychometricError::EventTimeRequired) @@ -973,6 +994,10 @@ mod tests { fit_scalar_log_rate(&[(1e154, 1e154, 1.0)]), Err(PsychometricError::InvalidNumericInput) ); + assert_eq!( + fit_scalar_log_rate(&[(1.0, 1e-300, 1.0), (1.0, 1e-300, 2.0)]), + Err(PsychometricError::InvalidNumericInput) + ); } #[test] 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 c4f52908..0610df61 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -2,13 +2,13 @@ #![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_cluster_mean_within_between_slopes, - recover_discrete_lag_from_log_rate, recover_event_series_mean_log_rate, - recover_event_time_discrete_lag_and_log_rate, recover_irregular_centered_residual_log_rate, - recover_kish_weighted_slope, recover_within_residual_event_time_log_rate, - refuse_difference_quotient_as_local_rate, refuse_pooled_discrete_lag_across_unequal_intervals, + map_discrete_lag_across_event_intervals, ordinary_least_squares_slope, + recover_cluster_mean_within_between_slopes, recover_discrete_lag_from_log_rate, + recover_event_series_mean_log_rate, recover_event_time_discrete_lag_and_log_rate, + recover_irregular_centered_residual_log_rate, recover_kish_weighted_slope, + recover_within_residual_event_time_log_rate, refuse_difference_quotient_as_local_rate, + refuse_pooled_discrete_lag_across_unequal_intervals, ClusteredEventScore, ClusteredScore, + EventOccasion, IndicatorKind, LagClock, LaggedWithinResidual, PsychometricError, }; fn rmse(truth: &[f64], recovered: &[f64]) -> f64 { @@ -134,6 +134,21 @@ fn discrete_lag_remaps_across_unequal_event_intervals() { ); } +#[test] +fn forward_map_underflow_to_zero_fails_closed() { + assert_eq!( + recover_discrete_lag_from_log_rate(-800.0, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + let source_lag = + recover_discrete_lag_from_log_rate(-0.7, 1.0, LagClock::EventTime).expect("source φ"); + assert!(source_lag > 0.0); + assert_eq!( + map_discrete_lag_across_event_intervals(source_lag, 1.0, 2000.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); +} + #[test] fn within_residual_event_time_log_rate_beats_pooled_levels() { let true_drift = -0.3_f64; diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index 3df71991..88e5b5f5 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, and refuses both the difference quotient and pooling discrete lags from unequal intervals. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. 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 2e401415..5b7d8ff9 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -9,10 +9,11 @@ This slice stays inside `psychometric_core`. It does not add a second invariance 3. recover a Kish-weighted least-squares slope and report Kish ESS as the information diagnostic; 4. map a discrete lag-1 coefficient through the exact scalar exponential on **event time only**; 5. recover the exact scalar forward map `φ(Δt) = exp(a Δt)` and remap a discrete lag onto another event interval through that log-rate; -6. refuse pooling discrete lags from unequal event intervals as one coefficient; -7. refuse the difference quotient as a continuous-time rate; -8. apply the same event-time map to CWC residuals (still not DSEM); -9. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). +6. refuse a binary64 underflow of that forward map to `+0` (not a discrete lag); +7. refuse pooling discrete lags from unequal event intervals as one coefficient; +8. refuse the difference quotient as a continuous-time rate; +9. apply the same event-time map to CWC residuals (still not DSEM); +10. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). ## Claim boundary @@ -32,14 +33,14 @@ Driver, C. C., Oud, J. H. L., & Voelkle, M. C. (2017). Continuous time structura Kish, L. (1965). *Survey sampling*. John Wiley & Sons. -The Voelkle et al. (2012) ZORA accepted manuscript was opened 2026-08-17 (https://www.zora.uzh.ch/handle/20.500.14742/72792). Equation 7 is the exact discrete–continuous map; Equations 3–4 are the discouraged difference-quotient approximation; Appendix B sets Equation 6 equal to Equation 2 to obtain Equation 7. Meredith (1993) and Mislevy (1991) remain unread (Unpaywall/OpenAlex/Semantic Scholar/archive.org 2026-08-17: closed). +The Voelkle et al. (2012) ZORA accepted manuscript was opened 2026-08-17 and re-opened 2026-08-17T11:18Z (https://www.zora.uzh.ch/handle/20.500.14742/72792; bitstream 424f9082-0eeb-4a67-b687-9845a4ed892f). Equation 7 is `A*(Δt) = exp(A Δt)`; the accepted manuscript (p. 16) writes the discrete auto-effects as `e^{a_{yy} Δt}` and `e^{a_{xx} Δt}`. Those quantities are strictly positive for finite real drift and interval; the inverse `a = ln(φ) / Δt` therefore requires `φ > 0`. Binary64 `exp` of a large negative argument is `+0` and is refused. Equations 3–4 are the discouraged difference-quotient approximation; Appendix B sets Equation 6 equal to Equation 2 to obtain Equation 7. Meredith (1993) and Mislevy (1991) remain unread (Unpaywall/OpenAlex/Semantic Scholar/archive.org/ERIC 2026-08-17T11:18Z: closed; Cambridge Core PDF for Mislevy redirected to an HTML stub). ## Formula notes - **CWC.** For cluster \(i\) and occasion \(t\), \(x_{it}^{w} = x_{it}-\bar x_{i}\) and \(y_{it}^{w} = y_{it}-\bar y_{i}\). The within slope is OLS of \(y^{w}\) on \(x^{w}\). The between slope is OLS of the cluster means. A grand-mean pooled slope confounds the two. - **Contextual effect.** Enders and Tofighi (2007, Table 2, pp. 124–127): under CWC, the cluster-mean coefficient \(\gamma_{01}\) is the contextual effect (expected difference between two people with the same individual \(X\) from groups one unit apart on \(\bar X\)). Under CGM the same symbol is the between-cluster effect. The OLS identity is \(\gamma_{01}^{\mathrm{CWC}}=\beta_{\mathrm{between}}-\beta_{\mathrm{within}}\). Adding the CWC contextual coefficient to the within slope recovers the between-cluster slope. This crate reports the OLS analogue; it does not estimate their multilevel maximum-likelihood model. - **Kish ESS.** \(\mathrm{ESS}=(\sum w)^{2}/\sum w^{2}\) on non-negative finite weights. WLS uses the weights in the slope; ESS is not a second slope. -- **Exact scalar map.** Voelkle et al. (2012, Eq. 7) and Driver et al. (2017, Eq. 3): \(\varphi = A^{*}(\Delta t)=\exp(a\,\Delta t)\). The inverse is \(a=\ln\varphi/\Delta t\). The forward map is the same equation. The difference quotient \((x(t+\Delta t)-x(t))/\Delta t\) is refused. +- **Exact scalar map.** Voelkle et al. (2012, Eq. 7) and Driver et al. (2017, Eq. 3): \(\varphi = A^{*}(\Delta t)=\exp(a\,\Delta t)\). The inverse is \(a=\ln\varphi/\Delta t\). The forward map is the same equation. The real exponential is strictly positive; a binary64 underflow to `+0` is refused because the inverse logarithm does not exist at zero. The difference quotient \((x(t+\Delta t)-x(t))/\Delta t\) is refused. - **Unequal-interval remap.** Discrete \(\varphi(\Delta t_1)\) and \(\varphi(\Delta t_2)\) are not comparable when \(\Delta t_1\neq\Delta t_2\) (Voelkle et al., 2012, ZORA manuscript pp. 2, 16, 33). The licensed path is \(a=\ln\varphi_{\mathrm{src}}/\Delta t_{\mathrm{src}}\) then \(\varphi_{\mathrm{ref}}=\exp(a\,\Delta t_{\mathrm{ref}})\). Pooling those discrete lags fails closed. - **CWC-then-lag.** Sample cluster means are removed first. Consecutive within residuals are then fitted by least squares to \(r_{t+\Delta t}\approx\exp(a\Delta t)\,r_{t}\) on event time. Same-sign pair-wise logs initialize the scalar Newton step. Sign-flipping \(T=2\) CWC pairs have no real logarithm and fail closed. Curran and Bauer (2011, pp. 607–608) show that this person-mean subtraction on a raw autoregressive series does **not** isolate the lagged within-person effect; the helper therefore does not claim to recover the raw-process drift. - **Already-centered irregular residual.** The caller supplies lagged within residuals. The mean of \(a=\ln(r_{t+\Delta t}/r_t)/\Delta t\) is the exact scalar map. Intervals may be irregular. The helper does not center again. This is not DSEM. @@ -51,6 +52,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was opened 2026-08-17 (https: - Kish WLS recovers a known slope; - the exact scalar map recovers a known drift on event time and refuses every other clock plus the difference quotient; - the forward map inverts the log-rate, remaps \(\varphi(1)\) onto \(\varphi(2)\) at machine-scale RMSE, and that RMSE is smaller than treating \(\varphi(1)\) as \(\varphi(2)\); +- a binary64 underflow of \(\exp(a\Delta t)\) to `+0` (direct forward map and large-interval remap) fails closed; - pooling discrete lags from unequal intervals fails closed; - CWC-then-lag on a two-cluster decaying series has smaller computed RMSE than a level-pooled series when the latter is identified; - already-centered irregular residuals recover a known drift at machine-scale RMSE, and that RMSE is smaller than CWC of the corresponding raw autoregressive series (Curran & Bauer, 2011, pp. 607–608); diff --git a/docs/research/rubin-total-variance.md b/docs/research/rubin-total-variance.md index 49bf0a24..10cafc5d 100644 --- a/docs/research/rubin-total-variance.md +++ b/docs/research/rubin-total-variance.md @@ -6,7 +6,7 @@ Adds Rubin combining for complete-data OLS loadings across posterior indicator d ## Claim boundary -`T_m = \bar U_m + (1+1/m)B_m` is the complete-data combining rule. This slice does **not** implement Mislevy plausible values. The 1991 *Psychometrika* paper was not opened in this cycle (Unpaywall 2026-08-17: closed; Cambridge/Springer/ETS copies were HTML stubs or paywalled). Do not cite that paper as having been read. +`T_m = \bar U_m + (1+1/m)B_m` is the complete-data combining rule. This slice does **not** implement Mislevy plausible values. The 1991 *Psychometrika* paper was not opened in this cycle (Unpaywall/OpenAlex/Semantic Scholar/archive.org 2026-08-17T11:18Z: closed; Cambridge Core PDF redirected to an HTML stub; ETS landing page is HTML). Do not cite that paper as having been read. ## Authoritative sources diff --git a/docs/research/standards-and-literature.md b/docs/research/standards-and-literature.md index 350b60d3..3f4a8ce5 100644 --- a/docs/research/standards-and-literature.md +++ b/docs/research/standards-and-literature.md @@ -32,7 +32,7 @@ Meredith, W. (1993). Measurement invariance, factor analysis and factorial invar Holland, P. W. (1986). Statistics and causal inference. *Journal of the American Statistical Association, 81*(396), 945–960. https://doi.org/10.1080/01621459.1986.10478354 -TEPP applies these sources to construct definition, score interpretation, reliability, validity evidence, uncertainty, consequences, longitudinal invariance, ESEM cross-loadings, and DSEM. Topic outputs are treated as fallible indicators or components only after their construct role is evaluated. Reflective, formative, and network classes remain distinct (Bollen & Lennox, 1991). Complete-data OLS loadings across posterior indicator draws are combined with Rubin (1996) \(T_m\); the arithmetic-mean helper remains a point estimate. Mislevy (1991) is listed as a future plausible-value source and was not opened in this cycle (Unpaywall/OpenAlex/archive.org 2026-08-17: closed; Cambridge/ETS remain HTML stubs). Temporal precedence is not causal identification (Holland, 1986). Within/between OLS follows Enders and Tofighi (2007), Curran and Bauer (2011), and Hamaker et al. (2015). Enders and Tofighi (2007, Table 2, pp. 124–127; PDF opened 2026-08-17) show that the CWC cluster-mean coefficient is the contextual effect (`between − within`), not the between-cluster effect. Curran and Bauer (2011, pp. 607–608) reject person-mean subtraction on a raw autoregressive series as the lagged within-person residual; already-centered irregular residuals use the Voelkle et al. (2012, Eq. 7) / Driver et al. (2017, Eq. 3) scalar map. Discrete lags from unequal event intervals are remapped through that log-rate (Voelkle et al., 2012, ZORA accepted manuscript opened 2026-08-17, Eq. 7 and Appendix B) and are not pooled. Metric/weak invariance does not license latent-mean comparison. Meredith (1993) remains unread (Unpaywall/OpenAlex/archive.org 2026-08-17: closed). +TEPP applies these sources to construct definition, score interpretation, reliability, validity evidence, uncertainty, consequences, longitudinal invariance, ESEM cross-loadings, and DSEM. Topic outputs are treated as fallible indicators or components only after their construct role is evaluated. Reflective, formative, and network classes remain distinct (Bollen & Lennox, 1991). Complete-data OLS loadings across posterior indicator draws are combined with Rubin (1996) \(T_m\); the arithmetic-mean helper remains a point estimate. Mislevy (1991) is listed as a future plausible-value source and was not opened in this cycle (Unpaywall/OpenAlex/Semantic Scholar/archive.org 2026-08-17T11:18Z: closed; Cambridge Core PDF redirected to an HTML stub; ETS landing page is HTML). Temporal precedence is not causal identification (Holland, 1986). Within/between OLS follows Enders and Tofighi (2007), Curran and Bauer (2011), and Hamaker et al. (2015). Enders and Tofighi (2007, Table 2, pp. 124–127; PDF opened 2026-08-17) show that the CWC cluster-mean coefficient is the contextual effect (`between − within`), not the between-cluster effect. Curran and Bauer (2011, pp. 607–608) reject person-mean subtraction on a raw autoregressive series as the lagged within-person residual; already-centered irregular residuals use the Voelkle et al. (2012, Eq. 7) / Driver et al. (2017, Eq. 3) scalar map. Discrete lags from unequal event intervals are remapped through that log-rate (Voelkle et al., 2012, ZORA accepted manuscript opened 2026-08-17 and re-opened 2026-08-17T11:18Z, Eq. 7 and Appendix B) and are not pooled. A binary64 underflow of \(\exp(a\Delta t)\) to `+0` is refused because the accepted manuscript (p. 16) writes discrete auto-effects as \(e^{a\Delta t}\), which are strictly positive. Metric/weak invariance does not license latent-mean comparison. Meredith (1993) remains unread (Unpaywall/OpenAlex/Semantic Scholar/archive.org/ERIC 2026-08-17T11:18Z: closed). ## Structural, correlated, dynamic, relational, and multilingual topic models diff --git a/docs/research/strong-invariance-latent-means.md b/docs/research/strong-invariance-latent-means.md index dd79f346..2d182ea4 100644 --- a/docs/research/strong-invariance-latent-means.md +++ b/docs/research/strong-invariance-latent-means.md @@ -12,7 +12,7 @@ This slice does **not** import the unpublished `measurement_invariance` crate on - `#84` `scalar` is the strong/scalar status (equal loading and intercept). That status licenses latent means. - Strict (also equal residual variance) also licenses latent means. - This is two-group OLS, not MGCFA, not partial invariance, and not alignment optimization. -- Meredith (1993) names weak/strong/strict are used only as conventional labels. That PDF was not opened (Unpaywall 2026-08-17: closed; Springer remains an HTML stub). Do not cite Meredith equations as having been read. +- Meredith (1993) names weak/strong/strict are used only as conventional labels. That PDF was not opened (Unpaywall/OpenAlex/Semantic Scholar/archive.org/ERIC 2026-08-17T11:18Z: closed; Springer remains an HTML stub). Do not cite Meredith equations as having been read. ## Authoritative sources used for the mean gate From d247d7d9e8f7db7f1822a2f7f47373413d3e059b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 11:25:31 +0000 Subject: [PATCH 10/62] feat(psychometric): recover Voelkle Eq. 12 discrete constant-predictor effect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Voelkle et al. (2012, Eq. 12; ZORA accepted manuscript p. 16) map a constant predictor through b*_y.x(Δt) = (a_yx/a_xx)(exp(a_xx Δt)-1) for a_xx ≠ 0. Use expm1. a_xx = 0 fails closed. The first-order product a_yx Δt is not that discrete effect. Still not DSEM, not matrix expm. Meredith (1993) and Mislevy (1991) remain unread. --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 1 + CLAUDE.md | 2 +- crates/psychometric_core/src/event_time.rs | 138 +++++++++++++++++- crates/psychometric_core/src/lib.rs | 5 +- ...multilevel_event_time_recovery_contract.rs | 46 +++++- docs/adr/0005-posterior-esem-dsem.md | 4 +- .../multilevel-event-time-recovery.md | 11 +- docs/research/standards-and-literature.md | 2 +- 9 files changed, 188 insertions(+), 23 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 4a0c5d2f..dbd54767 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -61,7 +61,7 @@ boundaries above remain the target modular MSA architecture. | `tepp_simulation` | known-truth temporal/event data generation | | `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics | | `tepp_api` | versioned DTO, schema, and export contracts | -| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means | +| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means | No crate exposes placeholder production behavior in Task 1. This prevents an empty façade from becoming a de facto public API before its invariants and tests diff --git a/CHANGELOG.md b/CHANGELOG.md index 87f0ee95..b182c4af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang ### Added +- `psychometric_core` exact scalar discrete effect of a constant event-time predictor (Voelkle et al., 2012, Eq. 12): \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\) for \(a_{xx}\neq 0\). The first-order product \(a_{yx}\Delta t\) is not that discrete effect. Still not DSEM. - `psychometric_core` exact scalar forward map `φ(Δt) = exp(a Δt)` and interval remapping: a discrete lag at one event interval maps onto another through the Voelkle et al. (2012, Eq. 7) log-rate. Pooling discrete lags from unequal intervals fails closed. Still not a matrix `expm` and not DSEM. - Fail-closed CWC-lag coverage in `psychometric_core`: singleton clusters are skipped, all-singleton series and overflowing CWC residuals fail closed, later-only residual overflow is checked with bitwise `is_finite`, the scalar Newton step refuses a non-finite exponential, score, start-skip, or deriv-INF, and Pearson empty/mismatch/left-INF paths are recovered. Dead post-OLS `pred_ss` and midpoint `require_finite` guards were removed because those values are already finite after OLS. - `psychometric_core` multilevel/event-time recovery on the stacked psychometric PR: cluster-mean CWC within/between OLS, Kish ESS weighted slopes, event-time-only discrete lag-1 and exact scalar local log-rate, CWC-then-event-time residual lag, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, and two-group OLS strong/strict-gated latent-mean difference. Metric/weak is not a mean license. Not DSEM, not RI-CLPM, not MGCFA, not Mislevy PVs (ADR 0005; no new migration). diff --git a/CLAUDE.md b/CLAUDE.md index ccda6ef7..f5bfe07c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. -- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), not `a_yx Δt`. - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index d775c4ff..0a58509b 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -6,9 +6,10 @@ //! `A*(Δt) = exp(A Δt)`. The noiseless scalar inverse is //! `a = ln(φ) / Δt` with `φ = A*(Δt)`. The forward map is //! `φ(Δt) = exp(a Δt)`. Discrete lags from unequal event intervals are -//! not one coefficient; they map through `a` first. The difference -//! quotient `(x(t+Δt) − x(t)) / Δt` (their Eqs. 3–4) is refused. This is -//! not DSEM and not a matrix `expm`. +//! not one coefficient; they map through `a` first. The exact scalar +//! discrete effect of a constant predictor is Voelkle et al. (2012, +//! Eq. 12). The difference quotient `(x(t+Δt) − x(t)) / Δt` (their +//! Eqs. 3–4) is refused. This is not DSEM and not a matrix `expm`. use std::collections::BTreeMap; @@ -216,6 +217,42 @@ pub fn map_discrete_lag_across_event_intervals( recover_discrete_lag_from_log_rate(log_rate, reference_delta, clock) } +/// Exact scalar discrete effect of a constant event-time predictor. +/// +/// Voelkle et al. (2012, Eq. 12; ZORA accepted manuscript p. 16): +/// `b*_y.x(Δt) = (a_yx / a_xx) (exp(a_xx Δt) − 1)` for `a_xx ≠ 0`. +/// The increment uses `expm1` so `exp(z) − 1` does not cancel. This is +/// not DSEM and not a matrix `expm`. +/// +/// # Errors +/// +/// Returns [`PsychometricError::EventTimeRequired`] for any non-event clock, +/// [`PsychometricError::NonPositiveInterval`] when `event_delta` is not +/// strictly positive, and [`PsychometricError::InvalidNumericInput`] when +/// either rate is non-finite, the predictor auto-effect is zero, or the +/// mapped effect is non-finite. +pub fn recover_discrete_constant_predictor_effect( + outcome_on_predictor: f64, + predictor_log_rate: f64, + event_delta: f64, + clock: LagClock, +) -> Result { + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + if !event_delta.is_finite() || event_delta <= 0.0 { + return Err(PsychometricError::NonPositiveInterval); + } + if !outcome_on_predictor.is_finite() + || !predictor_log_rate.is_finite() + || predictor_log_rate == 0.0 + { + return Err(PsychometricError::InvalidNumericInput); + } + let increment = (predictor_log_rate * event_delta).exp_m1(); + require_finite((outcome_on_predictor / predictor_log_rate) * increment) +} + /// Refuse treating discrete lags from unequal event intervals as one coefficient. /// /// Always fails closed. Map each lag through @@ -462,10 +499,11 @@ pub(crate) fn fit_scalar_log_rate(pairs: &[(f64, f64, f64)]) -> Result 1e-3); + assert_eq!( + recover_discrete_constant_predictor_effect( + outcome_on_predictor, + predictor_log_rate, + delta, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_discrete_constant_predictor_effect( + outcome_on_predictor, + predictor_log_rate, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_constant_predictor_effect( + outcome_on_predictor, + predictor_log_rate, + -1.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_constant_predictor_effect( + outcome_on_predictor, + predictor_log_rate, + f64::NAN, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_constant_predictor_effect( + f64::NAN, + predictor_log_rate, + delta, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_constant_predictor_effect( + outcome_on_predictor, + 0.0, + delta, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_constant_predictor_effect( + outcome_on_predictor, + f64::NAN, + delta, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_constant_predictor_effect(1e300, 1e-300, 1e300, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + } + #[test] fn non_event_clocks_and_difference_quotient_fail_closed() { for clock in [ diff --git a/crates/psychometric_core/src/lib.rs b/crates/psychometric_core/src/lib.rs index 39a81be5..2660e4a9 100644 --- a/crates/psychometric_core/src/lib.rs +++ b/crates/psychometric_core/src/lib.rs @@ -11,7 +11,8 @@ //! decomposes cluster-mean within/between OLS and the CWC contextual effect, //! maps event-time discrete lags through the exact scalar exponential, maps //! already-centered irregular residuals without re-centering, remaps discrete -//! lags across unequal event intervals through that log-rate, and refuses +//! lags across unequal event intervals through that log-rate, recovers the +//! exact scalar discrete effect of a constant predictor, and refuses //! latent-mean comparison below strong invariance. mod causality; @@ -59,6 +60,8 @@ pub use event_time::LagClock; 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 discrete effect of a constant event-time predictor. +pub use event_time::recover_discrete_constant_predictor_effect; /// Exact scalar forward map `φ = exp(a Δt)`. pub use event_time::recover_discrete_lag_from_log_rate; /// Noiseless scalar discrete lag `later / earlier`. 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 0610df61..a4976b03 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -3,12 +3,13 @@ use psychometric_core::{ map_discrete_lag_across_event_intervals, ordinary_least_squares_slope, - recover_cluster_mean_within_between_slopes, recover_discrete_lag_from_log_rate, - recover_event_series_mean_log_rate, recover_event_time_discrete_lag_and_log_rate, - recover_irregular_centered_residual_log_rate, recover_kish_weighted_slope, - recover_within_residual_event_time_log_rate, refuse_difference_quotient_as_local_rate, - refuse_pooled_discrete_lag_across_unequal_intervals, ClusteredEventScore, ClusteredScore, - EventOccasion, IndicatorKind, LagClock, LaggedWithinResidual, PsychometricError, + recover_cluster_mean_within_between_slopes, recover_discrete_constant_predictor_effect, + recover_discrete_lag_from_log_rate, recover_event_series_mean_log_rate, + recover_event_time_discrete_lag_and_log_rate, recover_irregular_centered_residual_log_rate, + recover_kish_weighted_slope, recover_within_residual_event_time_log_rate, + refuse_difference_quotient_as_local_rate, refuse_pooled_discrete_lag_across_unequal_intervals, + ClusteredEventScore, ClusteredScore, EventOccasion, IndicatorKind, LagClock, + LaggedWithinResidual, PsychometricError, }; fn rmse(truth: &[f64], recovered: &[f64]) -> f64 { @@ -149,6 +150,39 @@ fn forward_map_underflow_to_zero_fails_closed() { ); } +#[test] +fn constant_predictor_discrete_effect_recovers_equation_twelve() { + let outcome_on_predictor = 0.2_f64; + let predictor_log_rate = -0.5_f64; + let delta = 2.0_f64; + let recovered = recover_discrete_constant_predictor_effect( + outcome_on_predictor, + predictor_log_rate, + delta, + LagClock::EventTime, + ) + .expect("eq 12"); + let expected = + (outcome_on_predictor / predictor_log_rate) * (predictor_log_rate * delta).exp_m1(); + let error = rmse(&[expected], &[recovered]); + assert!(error < 1e-15, "Eq. 12 RMSE {error}"); + let first_order = outcome_on_predictor * delta; + let first_order_error = rmse(&[expected], &[first_order]); + assert!( + first_order_error > error, + "Voelkle Eq. 12: first-order a_yx Δt RMSE {first_order_error} must exceed exact {error}" + ); + assert_eq!( + recover_discrete_constant_predictor_effect( + outcome_on_predictor, + 0.0, + delta, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); +} + #[test] fn within_residual_event_time_log_rate_beats_pooled_levels() { let true_drift = -0.3_f64; diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index 88e5b5f5..99d11d0c 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -1,7 +1,7 @@ # ADR 0005 — Posterior-aware ESEM/DSEM and structural interpretation **Decision status:** Accepted -**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target +**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target **Date:** 2026-08-05 **Supersedes:** None. ADR 0012 governs upstream topic measurement/network coordinates; this ADR governs higher-order psychometric structure and longitudinal interpretation. @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. 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 5b7d8ff9..fd33dc93 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -10,10 +10,11 @@ This slice stays inside `psychometric_core`. It does not add a second invariance 4. map a discrete lag-1 coefficient through the exact scalar exponential on **event time only**; 5. recover the exact scalar forward map `φ(Δt) = exp(a Δt)` and remap a discrete lag onto another event interval through that log-rate; 6. refuse a binary64 underflow of that forward map to `+0` (not a discrete lag); -7. refuse pooling discrete lags from unequal event intervals as one coefficient; -8. refuse the difference quotient as a continuous-time rate; -9. apply the same event-time map to CWC residuals (still not DSEM); -10. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). +7. recover the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12); +8. refuse pooling discrete lags from unequal event intervals as one coefficient; +9. refuse the difference quotient as a continuous-time rate; +10. apply the same event-time map to CWC residuals (still not DSEM); +11. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). ## Claim boundary @@ -42,6 +43,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was opened 2026-08-17 and re- - **Kish ESS.** \(\mathrm{ESS}=(\sum w)^{2}/\sum w^{2}\) on non-negative finite weights. WLS uses the weights in the slope; ESS is not a second slope. - **Exact scalar map.** Voelkle et al. (2012, Eq. 7) and Driver et al. (2017, Eq. 3): \(\varphi = A^{*}(\Delta t)=\exp(a\,\Delta t)\). The inverse is \(a=\ln\varphi/\Delta t\). The forward map is the same equation. The real exponential is strictly positive; a binary64 underflow to `+0` is refused because the inverse logarithm does not exist at zero. The difference quotient \((x(t+\Delta t)-x(t))/\Delta t\) is refused. - **Unequal-interval remap.** Discrete \(\varphi(\Delta t_1)\) and \(\varphi(\Delta t_2)\) are not comparable when \(\Delta t_1\neq\Delta t_2\) (Voelkle et al., 2012, ZORA manuscript pp. 2, 16, 33). The licensed path is \(a=\ln\varphi_{\mathrm{src}}/\Delta t_{\mathrm{src}}\) then \(\varphi_{\mathrm{ref}}=\exp(a\,\Delta t_{\mathrm{ref}})\). Pooling those discrete lags fails closed. +- **Constant-predictor discrete effect.** Voelkle et al. (2012, Eq. 12; ZORA accepted manuscript p. 16): for a constant predictor with \(a_{xx}\neq 0\), \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). The increment uses `expm1`. \(a_{xx}=0\) fails closed. The first-order product \(a_{yx}\Delta t\) is not that discrete effect. This is not DSEM. - **CWC-then-lag.** Sample cluster means are removed first. Consecutive within residuals are then fitted by least squares to \(r_{t+\Delta t}\approx\exp(a\Delta t)\,r_{t}\) on event time. Same-sign pair-wise logs initialize the scalar Newton step. Sign-flipping \(T=2\) CWC pairs have no real logarithm and fail closed. Curran and Bauer (2011, pp. 607–608) show that this person-mean subtraction on a raw autoregressive series does **not** isolate the lagged within-person effect; the helper therefore does not claim to recover the raw-process drift. - **Already-centered irregular residual.** The caller supplies lagged within residuals. The mean of \(a=\ln(r_{t+\Delta t}/r_t)/\Delta t\) is the exact scalar map. Intervals may be irregular. The helper does not center again. This is not DSEM. @@ -53,6 +55,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was opened 2026-08-17 and re- - the exact scalar map recovers a known drift on event time and refuses every other clock plus the difference quotient; - the forward map inverts the log-rate, remaps \(\varphi(1)\) onto \(\varphi(2)\) at machine-scale RMSE, and that RMSE is smaller than treating \(\varphi(1)\) as \(\varphi(2)\); - a binary64 underflow of \(\exp(a\Delta t)\) to `+0` (direct forward map and large-interval remap) fails closed; +- Voelkle et al. (2012, Eq. 12) recovers a known discrete constant-predictor effect at machine-scale RMSE, and that RMSE is smaller than the first-order product \(a_{yx}\Delta t\); \(a_{xx}=0\) fails closed; - pooling discrete lags from unequal intervals fails closed; - CWC-then-lag on a two-cluster decaying series has smaller computed RMSE than a level-pooled series when the latter is identified; - already-centered irregular residuals recover a known drift at machine-scale RMSE, and that RMSE is smaller than CWC of the corresponding raw autoregressive series (Curran & Bauer, 2011, pp. 607–608); diff --git a/docs/research/standards-and-literature.md b/docs/research/standards-and-literature.md index 3f4a8ce5..fb248df8 100644 --- a/docs/research/standards-and-literature.md +++ b/docs/research/standards-and-literature.md @@ -32,7 +32,7 @@ Meredith, W. (1993). Measurement invariance, factor analysis and factorial invar Holland, P. W. (1986). Statistics and causal inference. *Journal of the American Statistical Association, 81*(396), 945–960. https://doi.org/10.1080/01621459.1986.10478354 -TEPP applies these sources to construct definition, score interpretation, reliability, validity evidence, uncertainty, consequences, longitudinal invariance, ESEM cross-loadings, and DSEM. Topic outputs are treated as fallible indicators or components only after their construct role is evaluated. Reflective, formative, and network classes remain distinct (Bollen & Lennox, 1991). Complete-data OLS loadings across posterior indicator draws are combined with Rubin (1996) \(T_m\); the arithmetic-mean helper remains a point estimate. Mislevy (1991) is listed as a future plausible-value source and was not opened in this cycle (Unpaywall/OpenAlex/Semantic Scholar/archive.org 2026-08-17T11:18Z: closed; Cambridge Core PDF redirected to an HTML stub; ETS landing page is HTML). Temporal precedence is not causal identification (Holland, 1986). Within/between OLS follows Enders and Tofighi (2007), Curran and Bauer (2011), and Hamaker et al. (2015). Enders and Tofighi (2007, Table 2, pp. 124–127; PDF opened 2026-08-17) show that the CWC cluster-mean coefficient is the contextual effect (`between − within`), not the between-cluster effect. Curran and Bauer (2011, pp. 607–608) reject person-mean subtraction on a raw autoregressive series as the lagged within-person residual; already-centered irregular residuals use the Voelkle et al. (2012, Eq. 7) / Driver et al. (2017, Eq. 3) scalar map. Discrete lags from unequal event intervals are remapped through that log-rate (Voelkle et al., 2012, ZORA accepted manuscript opened 2026-08-17 and re-opened 2026-08-17T11:18Z, Eq. 7 and Appendix B) and are not pooled. A binary64 underflow of \(\exp(a\Delta t)\) to `+0` is refused because the accepted manuscript (p. 16) writes discrete auto-effects as \(e^{a\Delta t}\), which are strictly positive. Metric/weak invariance does not license latent-mean comparison. Meredith (1993) remains unread (Unpaywall/OpenAlex/Semantic Scholar/archive.org/ERIC 2026-08-17T11:18Z: closed). +TEPP applies these sources to construct definition, score interpretation, reliability, validity evidence, uncertainty, consequences, longitudinal invariance, ESEM cross-loadings, and DSEM. Topic outputs are treated as fallible indicators or components only after their construct role is evaluated. Reflective, formative, and network classes remain distinct (Bollen & Lennox, 1991). Complete-data OLS loadings across posterior indicator draws are combined with Rubin (1996) \(T_m\); the arithmetic-mean helper remains a point estimate. Mislevy (1991) is listed as a future plausible-value source and was not opened in this cycle (Unpaywall/OpenAlex/Semantic Scholar/archive.org 2026-08-17T11:18Z: closed; Cambridge Core PDF redirected to an HTML stub; ETS landing page is HTML). Temporal precedence is not causal identification (Holland, 1986). Within/between OLS follows Enders and Tofighi (2007), Curran and Bauer (2011), and Hamaker et al. (2015). Enders and Tofighi (2007, Table 2, pp. 124–127; PDF opened 2026-08-17) show that the CWC cluster-mean coefficient is the contextual effect (`between − within`), not the between-cluster effect. Curran and Bauer (2011, pp. 607–608) reject person-mean subtraction on a raw autoregressive series as the lagged within-person residual; already-centered irregular residuals use the Voelkle et al. (2012, Eq. 7) / Driver et al. (2017, Eq. 3) scalar map. Discrete lags from unequal event intervals are remapped through that log-rate (Voelkle et al., 2012, ZORA accepted manuscript opened 2026-08-17 and re-opened 2026-08-17T11:18Z, Eq. 7 and Appendix B) and are not pooled. A binary64 underflow of \(\exp(a\Delta t)\) to `+0` is refused because the accepted manuscript (p. 16) writes discrete auto-effects as \(e^{a\Delta t}\), which are strictly positive. The discrete effect of a constant predictor is Voelkle et al. (2012, Eq. 12), not the first-order product \(a_{yx}\Delta t\). Metric/weak invariance does not license latent-mean comparison. Meredith (1993) remains unread (Unpaywall/OpenAlex/Semantic Scholar/archive.org/ERIC 2026-08-17T11:18Z: closed). ## Structural, correlated, dynamic, relational, and multilingual topic models From 1fdcd2377e6721eaeba377778332ac76c60579eb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 12:07:55 +0000 Subject: [PATCH 11/62] feat(psychometric): evaluate Voelkle Eq. 12 without premature quotient overflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Evaluate b*_y.x(Δt) as a_yx Δt (expm1(z)/z) with z = a_xx Δt. That order is algebraically identical to (a_yx / a_xx)(exp(a_xx Δt) − 1). Binary64 underflow of z recovers the Eq. 12 limit a_yx Δt. The first-order product is not the general discrete effect. Driver, Oud, and Voelkle (2017, p. 4) restated the discrete intercept as a function of A and Δt. rustfmt import order is restored. Still not DSEM. Meredith (1993) and Mislevy (1991) remain unread. --- CHANGELOG.md | 1 + CLAUDE.md | 2 +- crates/psychometric_core/src/event_time.rs | 42 ++++++++++++++----- ...multilevel_event_time_recovery_contract.rs | 22 ++++++---- docs/adr/0005-posterior-esem-dsem.md | 2 +- .../multilevel-event-time-recovery.md | 6 +-- docs/research/standards-and-literature.md | 4 +- 7 files changed, 55 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b182c4af..ff34b27c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang ### Added +- `psychometric_core` evaluates Voelkle et al. (2012, Eq. 12) as \(a_{yx}\Delta t\,(\operatorname{expm1}(z)/z)\) with \(z=a_{xx}\Delta t\). That order is algebraically identical to \((a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). Binary64 underflow of \(z\) to `+0` recovers the Eq. 12 limit \(a_{yx}\Delta t\); the first-order product is still not the general discrete effect. Driver, Oud, and Voelkle (2017, p. 4) restated the discrete intercept as a function of \(A\) and \(\Delta t\) (PDF re-opened 2026-08-17T12:04Z). Still not DSEM. - `psychometric_core` exact scalar discrete effect of a constant event-time predictor (Voelkle et al., 2012, Eq. 12): \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\) for \(a_{xx}\neq 0\). The first-order product \(a_{yx}\Delta t\) is not that discrete effect. Still not DSEM. - `psychometric_core` exact scalar forward map `φ(Δt) = exp(a Δt)` and interval remapping: a discrete lag at one event interval maps onto another through the Voelkle et al. (2012, Eq. 7) log-rate. Pooling discrete lags from unequal intervals fails closed. Still not a matrix `expm` and not DSEM. - Fail-closed CWC-lag coverage in `psychometric_core`: singleton clusters are skipped, all-singleton series and overflowing CWC residuals fail closed, later-only residual overflow is checked with bitwise `is_finite`, the scalar Newton step refuses a non-finite exponential, score, start-skip, or deriv-INF, and Pearson empty/mismatch/left-INF paths are recovered. Dead post-OLS `pred_ss` and midpoint `require_finite` guards were removed because those values are already finite after OLS. diff --git a/CLAUDE.md b/CLAUDE.md index f5bfe07c..833e0619 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. -- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), not `a_yx Δt`. +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx Δt (expm1(z)/z)` with `z = a_xx Δt`. The first-order product is the underflow limit of that equation, not the general discrete effect. - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index 0a58509b..875548d4 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -221,7 +221,13 @@ pub fn map_discrete_lag_across_event_intervals( /// /// Voelkle et al. (2012, Eq. 12; ZORA accepted manuscript p. 16): /// `b*_y.x(Δt) = (a_yx / a_xx) (exp(a_xx Δt) − 1)` for `a_xx ≠ 0`. -/// The increment uses `expm1` so `exp(z) − 1` does not cancel. This is +/// Driver, Oud, and Voelkle (2017, p. 4, after Eq. 3) restate the same +/// discrete intercept as a function of `A` and `Δt`. The algebraically +/// identical evaluation is `a_yx Δt (expm1(z) / z)` with `z = a_xx Δt`. +/// That order is Eq. 12, not the first-order product. When binary64 `z` +/// underflows to `+0`, the mathematical limit of Eq. 12 is `a_yx Δt`. +/// Using that limit only at underflow is IEEE-754 evaluation of Eq. 12. +/// The first-order product is not the general discrete effect. This is /// not DSEM and not a matrix `expm`. /// /// # Errors @@ -249,8 +255,14 @@ pub fn recover_discrete_constant_predictor_effect( { return Err(PsychometricError::InvalidNumericInput); } - let increment = (predictor_log_rate * event_delta).exp_m1(); - require_finite((outcome_on_predictor / predictor_log_rate) * increment) + let increment_argument = predictor_log_rate * event_delta; + if increment_argument == 0.0 { + // Binary64 underflow of a_xx Δt. lim z→0 of Eq. 12 is a_yx Δt. + return require_finite(outcome_on_predictor * event_delta); + } + require_finite( + outcome_on_predictor * event_delta * (increment_argument.exp_m1() / increment_argument), + ) } /// Refuse treating discrete lags from unequal event intervals as one coefficient. @@ -498,14 +510,13 @@ pub(crate) fn fit_scalar_log_rate(pairs: &[(f64, f64, f64)]) -> Result f64 { @@ -181,6 +181,14 @@ fn constant_predictor_discrete_effect_recovers_equation_twelve() { ), Err(PsychometricError::InvalidNumericInput) ); + let underflowed = + recover_discrete_constant_predictor_effect(1e308, 1e-308, 1e-308, LagClock::EventTime) + .expect("eq 12 underflow limit"); + let underflow_error = rmse(&[1.0], &[underflowed]); + assert!( + underflow_error < 1e-15, + "Eq. 12 binary64 underflow limit RMSE {underflow_error}" + ); } #[test] diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index 99d11d0c..839917bd 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx Δt (expm1(z)/z)` with `z = a_xx Δt`, and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is the underflow limit of Eq. 12, not the general discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. 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 fd33dc93..684e3279 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -34,7 +34,7 @@ Driver, C. C., Oud, J. H. L., & Voelkle, M. C. (2017). Continuous time structura Kish, L. (1965). *Survey sampling*. John Wiley & Sons. -The Voelkle et al. (2012) ZORA accepted manuscript was opened 2026-08-17 and re-opened 2026-08-17T11:18Z (https://www.zora.uzh.ch/handle/20.500.14742/72792; bitstream 424f9082-0eeb-4a67-b687-9845a4ed892f). Equation 7 is `A*(Δt) = exp(A Δt)`; the accepted manuscript (p. 16) writes the discrete auto-effects as `e^{a_{yy} Δt}` and `e^{a_{xx} Δt}`. Those quantities are strictly positive for finite real drift and interval; the inverse `a = ln(φ) / Δt` therefore requires `φ > 0`. Binary64 `exp` of a large negative argument is `+0` and is refused. Equations 3–4 are the discouraged difference-quotient approximation; Appendix B sets Equation 6 equal to Equation 2 to obtain Equation 7. Meredith (1993) and Mislevy (1991) remain unread (Unpaywall/OpenAlex/Semantic Scholar/archive.org/ERIC 2026-08-17T11:18Z: closed; Cambridge Core PDF for Mislevy redirected to an HTML stub). +The Voelkle et al. (2012) ZORA accepted manuscript was opened 2026-08-17 (https://www.zora.uzh.ch/handle/20.500.14742/72792). This cycle the ZORA bitstream was Anubis-blocked (2026-08-17T12:03Z). Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4; JSS PDF re-opened 2026-08-17T12:04Z) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\) and restate the discrete intercept as a function of \(A\) and \(\Delta t\). Discrete auto-effects are strictly positive for finite real drift and interval; the inverse \(a=\ln\varphi/\Delta t\) therefore requires \(\varphi>0\). Binary64 `exp` of a large negative argument is `+0` and is refused. Equations 3–4 of Voelkle et al. (2012) are the discouraged difference-quotient approximation. Meredith (1993) remains unread (Unpaywall/OpenAlex/Semantic Scholar/Cambridge Core/Springer 2026-08-17T12:04Z: closed). Mislevy (1991) remains unread. The 1988 ETS RR-88-45 / DTIC ADA200179 technical report was opened 2026-08-17T12:04Z and is not the 1991 journal article. ## Formula notes @@ -43,7 +43,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was opened 2026-08-17 and re- - **Kish ESS.** \(\mathrm{ESS}=(\sum w)^{2}/\sum w^{2}\) on non-negative finite weights. WLS uses the weights in the slope; ESS is not a second slope. - **Exact scalar map.** Voelkle et al. (2012, Eq. 7) and Driver et al. (2017, Eq. 3): \(\varphi = A^{*}(\Delta t)=\exp(a\,\Delta t)\). The inverse is \(a=\ln\varphi/\Delta t\). The forward map is the same equation. The real exponential is strictly positive; a binary64 underflow to `+0` is refused because the inverse logarithm does not exist at zero. The difference quotient \((x(t+\Delta t)-x(t))/\Delta t\) is refused. - **Unequal-interval remap.** Discrete \(\varphi(\Delta t_1)\) and \(\varphi(\Delta t_2)\) are not comparable when \(\Delta t_1\neq\Delta t_2\) (Voelkle et al., 2012, ZORA manuscript pp. 2, 16, 33). The licensed path is \(a=\ln\varphi_{\mathrm{src}}/\Delta t_{\mathrm{src}}\) then \(\varphi_{\mathrm{ref}}=\exp(a\,\Delta t_{\mathrm{ref}})\). Pooling those discrete lags fails closed. -- **Constant-predictor discrete effect.** Voelkle et al. (2012, Eq. 12; ZORA accepted manuscript p. 16): for a constant predictor with \(a_{xx}\neq 0\), \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). The increment uses `expm1`. \(a_{xx}=0\) fails closed. The first-order product \(a_{yx}\Delta t\) is not that discrete effect. This is not DSEM. +- **Constant-predictor discrete effect.** Voelkle et al. (2012, Eq. 12; ZORA accepted manuscript p. 16): for a constant predictor with \(a_{xx}\neq 0\), \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). Driver, Oud, and Voelkle (2017, p. 4, after Eq. 3; PDF re-opened 2026-08-17T12:04Z) restate the discrete intercept as a function of \(A\) and \(\Delta t\). The algebraically identical evaluation is \(a_{yx}\Delta t\,(\operatorname{expm1}(z)/z)\) with \(z=a_{xx}\Delta t\). When binary64 \(z\) underflows to `+0`, the mathematical limit of Eq. 12 is \(a_{yx}\Delta t\). That limit is IEEE-754 evaluation of Eq. 12, not a substitution of the first-order product as the general discrete effect. \(a_{xx}=0\) fails closed. This is not DSEM. - **CWC-then-lag.** Sample cluster means are removed first. Consecutive within residuals are then fitted by least squares to \(r_{t+\Delta t}\approx\exp(a\Delta t)\,r_{t}\) on event time. Same-sign pair-wise logs initialize the scalar Newton step. Sign-flipping \(T=2\) CWC pairs have no real logarithm and fail closed. Curran and Bauer (2011, pp. 607–608) show that this person-mean subtraction on a raw autoregressive series does **not** isolate the lagged within-person effect; the helper therefore does not claim to recover the raw-process drift. - **Already-centered irregular residual.** The caller supplies lagged within residuals. The mean of \(a=\ln(r_{t+\Delta t}/r_t)/\Delta t\) is the exact scalar map. Intervals may be irregular. The helper does not center again. This is not DSEM. @@ -55,7 +55,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was opened 2026-08-17 and re- - the exact scalar map recovers a known drift on event time and refuses every other clock plus the difference quotient; - the forward map inverts the log-rate, remaps \(\varphi(1)\) onto \(\varphi(2)\) at machine-scale RMSE, and that RMSE is smaller than treating \(\varphi(1)\) as \(\varphi(2)\); - a binary64 underflow of \(\exp(a\Delta t)\) to `+0` (direct forward map and large-interval remap) fails closed; -- Voelkle et al. (2012, Eq. 12) recovers a known discrete constant-predictor effect at machine-scale RMSE, and that RMSE is smaller than the first-order product \(a_{yx}\Delta t\); \(a_{xx}=0\) fails closed; +- Voelkle et al. (2012, Eq. 12) recovers a known discrete constant-predictor effect at machine-scale RMSE, and that RMSE is smaller than the first-order product \(a_{yx}\Delta t\); \(a_{xx}=0\) fails closed; binary64 underflow of \(a_{xx}\Delta t\) to `+0` recovers the Eq. 12 limit \(a_{yx}\Delta t\); - pooling discrete lags from unequal intervals fails closed; - CWC-then-lag on a two-cluster decaying series has smaller computed RMSE than a level-pooled series when the latter is identified; - already-centered irregular residuals recover a known drift at machine-scale RMSE, and that RMSE is smaller than CWC of the corresponding raw autoregressive series (Curran & Bauer, 2011, pp. 607–608); diff --git a/docs/research/standards-and-literature.md b/docs/research/standards-and-literature.md index fb248df8..1ef169ba 100644 --- a/docs/research/standards-and-literature.md +++ b/docs/research/standards-and-literature.md @@ -26,13 +26,15 @@ Voelkle, M. C., Oud, J. H. L., Davidov, E., & Schmidt, P. (2012). An SEM approac Rubin, D. B. (1996). Multiple imputation after 18+ years. *Journal of the American Statistical Association, 91*(434), 473–489. https://doi.org/10.1080/01621459.1996.10476908 +Mislevy, R. J. (1988). *Randomization-based inferences about latent variables from complex samples* (ETS Research Report No. RR-88-45; DTIC ADA200179). Educational Testing Service. https://doi.org/10.1002/j.2330-8516.1988.tb00310.x + Mislevy, R. J. (1991). Randomization-based inference about latent variables from complex samples. *Psychometrika, 56*(2), 177–196. https://doi.org/10.1007/BF02294457 Meredith, W. (1993). Measurement invariance, factor analysis and factorial invariance. *Psychometrika, 58*(4), 525–543. https://doi.org/10.1007/BF02294825 Holland, P. W. (1986). Statistics and causal inference. *Journal of the American Statistical Association, 81*(396), 945–960. https://doi.org/10.1080/01621459.1986.10478354 -TEPP applies these sources to construct definition, score interpretation, reliability, validity evidence, uncertainty, consequences, longitudinal invariance, ESEM cross-loadings, and DSEM. Topic outputs are treated as fallible indicators or components only after their construct role is evaluated. Reflective, formative, and network classes remain distinct (Bollen & Lennox, 1991). Complete-data OLS loadings across posterior indicator draws are combined with Rubin (1996) \(T_m\); the arithmetic-mean helper remains a point estimate. Mislevy (1991) is listed as a future plausible-value source and was not opened in this cycle (Unpaywall/OpenAlex/Semantic Scholar/archive.org 2026-08-17T11:18Z: closed; Cambridge Core PDF redirected to an HTML stub; ETS landing page is HTML). Temporal precedence is not causal identification (Holland, 1986). Within/between OLS follows Enders and Tofighi (2007), Curran and Bauer (2011), and Hamaker et al. (2015). Enders and Tofighi (2007, Table 2, pp. 124–127; PDF opened 2026-08-17) show that the CWC cluster-mean coefficient is the contextual effect (`between − within`), not the between-cluster effect. Curran and Bauer (2011, pp. 607–608) reject person-mean subtraction on a raw autoregressive series as the lagged within-person residual; already-centered irregular residuals use the Voelkle et al. (2012, Eq. 7) / Driver et al. (2017, Eq. 3) scalar map. Discrete lags from unequal event intervals are remapped through that log-rate (Voelkle et al., 2012, ZORA accepted manuscript opened 2026-08-17 and re-opened 2026-08-17T11:18Z, Eq. 7 and Appendix B) and are not pooled. A binary64 underflow of \(\exp(a\Delta t)\) to `+0` is refused because the accepted manuscript (p. 16) writes discrete auto-effects as \(e^{a\Delta t}\), which are strictly positive. The discrete effect of a constant predictor is Voelkle et al. (2012, Eq. 12), not the first-order product \(a_{yx}\Delta t\). Metric/weak invariance does not license latent-mean comparison. Meredith (1993) remains unread (Unpaywall/OpenAlex/Semantic Scholar/archive.org/ERIC 2026-08-17T11:18Z: closed). +TEPP applies these sources to construct definition, score interpretation, reliability, validity evidence, uncertainty, consequences, longitudinal invariance, ESEM cross-loadings, and DSEM. Topic outputs are treated as fallible indicators or components only after their construct role is evaluated. Reflective, formative, and network classes remain distinct (Bollen & Lennox, 1991). Complete-data OLS loadings across posterior indicator draws are combined with Rubin (1996) \(T_m\); the arithmetic-mean helper remains a point estimate. Mislevy (1991) remains unread (Unpaywall/OpenAlex/Semantic Scholar/Cambridge Core/ERIC 2026-08-17T12:04Z: closed). The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title was opened 2026-08-17T12:04Z from archive.org; it is not the 1991 journal article and is not used as Mislevy plausible-value authority. Temporal precedence is not causal identification (Holland, 1986). Within/between OLS follows Enders and Tofighi (2007), Curran and Bauer (2011), and Hamaker et al. (2015). Enders and Tofighi (2007, Table 2, pp. 124–127; PDF opened 2026-08-17) show that the CWC cluster-mean coefficient is the contextual effect (`between − within`), not the between-cluster effect. Curran and Bauer (2011, pp. 607–608) reject person-mean subtraction on a raw autoregressive series as the lagged within-person residual; already-centered irregular residuals use the Voelkle et al. (2012, Eq. 7) / Driver et al. (2017, Eq. 3) scalar map. Discrete lags from unequal event intervals are remapped through that log-rate (Voelkle et al., 2012, ZORA accepted manuscript opened 2026-08-17; ZORA bitstream Anubis-blocked 2026-08-17T12:03Z this cycle) and are not pooled. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4; PDF re-opened 2026-08-17T12:04Z) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\) and restate the discrete intercept as a function of \(A\) and \(\Delta t\). A binary64 underflow of \(\exp(a\Delta t)\) to `+0` is refused because discrete auto-effects are strictly positive. The discrete effect of a constant predictor is Voelkle et al. (2012, Eq. 12), evaluated as \(a_{yx}\Delta t\,(\operatorname{expm1}(z)/z)\) with \(z=a_{xx}\Delta t\); the first-order product is the underflow limit of that equation, not the general discrete effect. Metric/weak invariance does not license latent-mean comparison. Meredith (1993) remains unread (Unpaywall/OpenAlex/Semantic Scholar/Cambridge Core/Springer 2026-08-17T12:04Z: closed). ## Structural, correlated, dynamic, relational, and multilingual topic models From 7c29e7c971d7940e1fb3def1ed3aae2d1bc8ad4a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 21:47:44 +0900 Subject: [PATCH 12/62] feat(api): adaptive orchestration router selects modes under budget (#47) * feat(api): adaptive orchestration router selects modes under budget ADR 0010 first executable slice: route direct/verify/committee/conductor/abstain from CPU f64 risk, ambiguity, evidence, and token budget. Documents cannot set policy, access, or credentials. Plans stay proposals under statistical gates. Comparable-budget ablation requires a direct baseline. Orchestrator bindings omit credentials. No migration while 0007 is in flight on #45. * test(api): expose orchestration provenance and ablation gaps * chore(ci): verify PR 47 orchestration security repair * fix(ci): move PR 47 security repair into a deterministic script * fix(ci): make PR 47 repair deterministic * fix(api): bind orchestration to verifiable evidence context * docs(orchestration): record ablation and manifest security invariants * test(api): bound orchestration compute and access resources * ci: verify PR 47 orchestration resource bounds * fix(api): bound orchestration compute authority * test(api): cover budget fallback boundary * fix(api): preserve blinded review semantics * test(api): use orchestration plan accessor * refactor(api): remove unreachable routing fallback arm * test(api): keep fallback branch coverage at function boundary * test(api): cover ablation rejection branches * test(api): use routed ablation evidence * test(api): exercise binding rejection branches * test(api): isolate routing branch contracts Split the oversized getter regression by behavior so Clippy accepts it. Exercise both concept-alignment threshold arms and both narrative OR paths in the unit-test binary that owns branch coverage. * test(api): cover compound routing predicates * test(api): cover finite out-of-range unit score * fix(api): make budget comparison coverage reachable * docs(changelog): drop shipped retention 0007 line from #47 Unreleased Migration 0007 already landed via #45; this PR only documents the adaptive orchestration router so OpenCode/CodeRabbit review stays scoped to the tip. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- CHANGELOG.md | 2 +- DOCUMENTATION.md | 1 + crates/tepp_api/src/lib.rs | 37 + crates/tepp_api/src/orchestration.rs | 1088 +++++++++++++++++ .../orchestration_resource_bounds_contract.rs | 68 ++ .../tests/orchestration_router_contract.rs | 463 +++++++ .../tests/orchestration_security_contract.rs | 156 +++ docs/API_CONTRACT.md | 6 +- docs/DOCUMENTATION_ASSESSMENT.md | 2 +- docs/LLM_ORCHESTRATION.md | 6 +- docs/TRACEABILITY.md | 10 +- docs/adr/0010-adaptive-llm-orchestration.md | 2 +- docs/adr/README.md | 4 +- ...extual-orchestrator-interpretation-port.md | 8 +- .../research/adaptive-orchestration-router.md | 47 + docs/research/standards-and-literature.md | 2 +- docs/validation/temporal-event-foundation.md | 3 +- 17 files changed, 1883 insertions(+), 22 deletions(-) create mode 100644 crates/tepp_api/src/orchestration.rs create mode 100644 crates/tepp_api/tests/orchestration_resource_bounds_contract.rs create mode 100644 crates/tepp_api/tests/orchestration_router_contract.rs create mode 100644 crates/tepp_api/tests/orchestration_security_contract.rs create mode 100644 docs/research/adaptive-orchestration-router.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fe6d08d..93891a27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,8 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang ### Added +- `tepp_api` adaptive orchestration router (ADR 0010): versioned `direct`/`verify`/`committee`/`conductor`/`abstain` selection from CPU `f64` risk, ambiguity, evidence, and token-budget inputs; recorded stages, recursion, decomposition, access lists, and role-specific reasoning effort; fail-closed document-controlled policy/access/credentials; LLM plans remain proposals under deterministic statistical authority; comparable-budget ablation requires a direct baseline; credential-free contextual-orchestrator binding. Live NIM HTTP remains accepted-target. - `tepp_api` purpose-bound provider-payload minimization: time-bounded `PurposeGrant` evaluation, fail-closed expired/not-yet-valid/inverted/cross-tenant/impossible-calendar denial, semantic UTC calendar validation, refusal to copy identity mappings into model-provider payloads or ordinary logs, preservation of opaque analytical identifiers and membership roles (no blanket PII mask), a separately authorized scientific re-identification path, and an internally bound FIPS 180-4 SHA-256 audit digest appended through `ReidentificationAuditSink` before disclosure. -- `persistence_postgres` retention/deletion/legal-hold (migration `0007`): policy rows, legal holds that block completed deletion, evidence tombstones without raw-source restore, analysis exclusion only for `logical_revocation`/`identity_tombstone` (not `cache_export_removal`), and deletion requests bound to the cited retention policy's tenant/class/purpose. - `persistence_postgres` backup/restore integrity: restored snapshots stay unusable until tenant, canonical `SHA-256`, knowledge-cutoff eligibility, temporal window order, and append-only triggers revalidate; SQL probes raise `restore integrity failed` (ADR 0013). - `persistence_postgres` concurrent document-write stress: atomic revise `DO` block that requires exactly one open `system_to` close, SQLSTATE mapping onto `ConcurrentWriteConflict` / `DuplicateDocumentRecord`, and live multi-session insert/revise/append-only proofs. No new migration number. - `tepp_api` naruon HTTP interchange: versioned `https` POST contracts for analysis-run create and modular export authorization that refuse table-access URLs, review/Copilot credential headers, reserved standard-header redefinition, principal-only export idempotency keys, and lexical inference claims (ADR 0011). diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index f64cf32b..3f094947 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -35,6 +35,7 @@ TEPP's approved PRD v0.4 and implementation plan are the primary product baselin | Actions fleet research doctoring | [`docs/research/actions-workflow-fleet.md`](docs/research/actions-workflow-fleet.md) | | Retention/deletion/legal-hold doctoring | [`docs/research/retention-deletion-legal-hold.md`](docs/research/retention-deletion-legal-hold.md) | | Provider-payload minimization doctoring | [`docs/research/provider-payload-minimization.md`](docs/research/provider-payload-minimization.md) | +| Adaptive orchestration router doctoring | [`docs/research/adaptive-orchestration-router.md`](docs/research/adaptive-orchestration-router.md) | | Hourly NIM OpenCode doctoring | [`docs/doctoring/hourly-nim-opencode-development.md`](docs/doctoring/hourly-nim-opencode-development.md) | | Change history | [`CHANGELOG.md`](CHANGELOG.md) | diff --git a/crates/tepp_api/src/lib.rs b/crates/tepp_api/src/lib.rs index 7be66c1b..411da83f 100644 --- a/crates/tepp_api/src/lib.rs +++ b/crates/tepp_api/src/lib.rs @@ -15,6 +15,7 @@ mod envelope; mod error; mod export; mod naruon_http; +mod orchestration; mod provider_payload; mod wire; @@ -67,6 +68,42 @@ pub use naruon_http::naruon_analysis_run_exchange_with_headers; pub use naruon_http::naruon_export_exchange; /// Refuse lexical heuristics as TEPP inference claims. pub use naruon_http::naruon_may_claim_tepp_inference; +/// Comparable-budget ablation record. +pub use orchestration::BudgetAblationRecord; +/// Credential-free contextual-orchestrator binding. +pub use orchestration::ContextualOrchestratorBinding; +/// Document attempt to override TEPP orchestration authority. +pub use orchestration::DocumentControlAttempt; +/// Bounded interpretation task kind. +pub use orchestration::InterpretationTaskKind; +/// Maximum access capabilities on one orchestration request. +pub use orchestration::MAX_ORCHESTRATION_ACCESS_ENTRIES; +/// Maximum UTF-8 bytes in one orchestration access token. +pub use orchestration::MAX_ORCHESTRATION_ACCESS_TOKEN_BYTES; +/// Maximum billable token budget on one orchestration request. +pub use orchestration::MAX_ORCHESTRATION_TOKEN_BUDGET; +/// Orchestration contract version for contextual-orchestrator bindings. +pub use orchestration::ORCHESTRATION_CONTRACT_VERSION; +/// Versioned TEPP orchestration policy identity. +pub use orchestration::ORCHESTRATION_POLICY_VERSION; +/// Versioned orchestration mode. +pub use orchestration::OrchestrationMode; +/// Governed orchestration plan. +pub use orchestration::OrchestrationPlan; +/// Orchestration router request. +pub use orchestration::OrchestrationRequest; +/// Orchestration role identity. +pub use orchestration::OrchestrationRole; +/// Role-specific reasoning effort. +pub use orchestration::ReasoningEffort; +/// Role plus recorded reasoning effort. +pub use orchestration::RoleAssignment; +/// Bind a plan for contextual-orchestrator execution. +pub use orchestration::bind_contextual_orchestrator; +/// Record a comparable-budget ablation against a direct baseline. +pub use orchestration::record_budget_ablation; +/// Route a task onto a versioned orchestration plan. +pub use orchestration::route_orchestration; /// Elevated re-identification result. pub use provider_payload::DisclosedIdentityMapping; /// Separately protected identity mapping. diff --git a/crates/tepp_api/src/orchestration.rs b/crates/tepp_api/src/orchestration.rs new file mode 100644 index 00000000..ff9de681 --- /dev/null +++ b/crates/tepp_api/src/orchestration.rs @@ -0,0 +1,1088 @@ +//! Deterministic ADR 0010 orchestration routing and orchestrator binding. + +use crate::ApiError; +use crate::wire::require_nonempty; +use std::fmt; + +/// Versioned TEPP orchestration policy identity. +pub const ORCHESTRATION_POLICY_VERSION: &str = "tepp.orchestration.v1"; + +/// Contract version for a contextual-orchestrator binding. +pub const ORCHESTRATION_CONTRACT_VERSION: u16 = 1; + +/// Maximum billable token budget accepted by one orchestration request. +pub const MAX_ORCHESTRATION_TOKEN_BUDGET: u64 = 1_000_000; +/// Maximum number of TEPP-owned access capabilities on one request. +pub const MAX_ORCHESTRATION_ACCESS_ENTRIES: usize = 64; +/// Maximum UTF-8 byte length of one access capability token. +pub const MAX_ORCHESTRATION_ACCESS_TOKEN_BYTES: usize = 128; + +const EVIDENCE_FLOOR: f64 = 0.35; +const LOW_COMPLEXITY: f64 = 0.35; +const HIGH_COMPLEXITY: f64 = 0.50; +const COMPARABLE_BUDGET_NUMERATOR: u64 = 10; + +/// Versioned orchestration mode selected by the governed router. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum OrchestrationMode { + /// One model call for low-ambiguity schema-constrained work. + Direct, + /// Producer plus an independent verifier. + Verify, + /// Blinded parallel raters plus adjudication. + Committee, + /// Adaptive roles and topology under an explicit budget. + Conductor, + /// No forced answer when evidence, budget, or gates are insufficient. + Abstain, +} + +impl OrchestrationMode { + /// Return the stable wire name for this mode. + #[must_use] + pub const fn wire_name(self) -> &'static str { + match self { + Self::Direct => "direct", + Self::Verify => "verify", + Self::Committee => "committee", + Self::Conductor => "conductor", + Self::Abstain => "abstain", + } + } + + /// Minimum token budget required to execute this mode. + #[must_use] + pub const fn minimum_token_budget(self) -> u64 { + match self { + Self::Direct => 4_000, + Self::Verify => 8_000, + Self::Committee => 16_000, + Self::Conductor => 24_000, + Self::Abstain => 0, + } + } + + /// Default workflow stage count recorded for this mode. + #[must_use] + pub const fn stage_count(self) -> u8 { + match self { + Self::Direct => 1, + Self::Verify => 2, + Self::Committee => 3, + Self::Conductor => 4, + Self::Abstain => 0, + } + } + + /// Default recursion depth recorded for this mode. + #[must_use] + pub const fn recursion_depth(self) -> u8 { + match self { + Self::Conductor => 2, + Self::Direct | Self::Verify | Self::Committee | Self::Abstain => 0, + } + } + + /// Cheaper bounded fallback when this mode cannot complete. + /// + /// Keeping this match at one function boundary prevents callers from + /// duplicating unreachable enum branches during coverage instrumentation. + #[must_use] + #[inline(never)] + pub const fn fallback_mode(self) -> Self { + match self { + Self::Conductor => Self::Committee, + Self::Committee => Self::Verify, + Self::Verify => Self::Direct, + Self::Direct | Self::Abstain => Self::Abstain, + } + } + + /// Decomposition code recorded on the plan. + #[must_use] + pub const fn decomposition_code(self) -> &'static str { + match self { + Self::Direct => "single_call", + Self::Verify => "producer_then_verifier", + Self::Committee => "blinded_parallel_then_adjudicate", + Self::Conductor => "adaptive_roles_under_budget", + Self::Abstain => "no_forced_answer", + } + } +} + +/// Role-specific reasoning effort recorded for ablation. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ReasoningEffort { + /// Schema conversion or formatting. + Minimal, + /// Low-ambiguity span classification. + Low, + /// Concept alignment or narrative synthesis. + Medium, + /// Verification, adjudication, or blinded review. + High, +} + +impl ReasoningEffort { + /// Return the stable wire name for this effort. + #[must_use] + pub const fn wire_name(self) -> &'static str { + match self { + Self::Minimal => "minimal", + Self::Low => "low", + Self::Medium => "medium", + Self::High => "high", + } + } +} + +/// Bounded interpretation task the router may schedule. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum InterpretationTaskKind { + /// Semantic span extraction or classification. + SpanClassification, + /// Concept merge or alignment review. + ConceptAlignment, + /// Blinded K/model-selection review after statistical gates. + BlindedModelReview, + /// Evidence-grounded narrative synthesis. + NarrativeSynthesis, + /// Adversarial unsupported-claim verification. + AdversarialVerification, + /// Routine schema conversion or formatting. + SchemaConversion, +} + +impl InterpretationTaskKind { + /// Return the stable wire name for this task kind. + #[must_use] + pub const fn wire_name(self) -> &'static str { + match self { + Self::SpanClassification => "span_classification", + Self::ConceptAlignment => "concept_alignment", + Self::BlindedModelReview => "blinded_model_review", + Self::NarrativeSynthesis => "narrative_synthesis", + Self::AdversarialVerification => "adversarial_verification", + Self::SchemaConversion => "schema_conversion", + } + } + + /// Default reasoning effort for a worker assigned this task. + #[must_use] + pub const fn default_effort(self) -> ReasoningEffort { + match self { + Self::SchemaConversion => ReasoningEffort::Minimal, + Self::SpanClassification => ReasoningEffort::Low, + Self::ConceptAlignment | Self::NarrativeSynthesis => ReasoningEffort::Medium, + Self::BlindedModelReview | Self::AdversarialVerification => ReasoningEffort::High, + } + } +} + +/// Orchestration role assigned under the selected mode. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum OrchestrationRole { + /// Lightweight coordinator or thinker. + Thinker, + /// Task worker that proposes an interpretation. + Worker, + /// Independent evidence-only verifier. + Verifier, + /// Committee adjudicator. + Adjudicator, + /// Adaptive conductor under an explicit budget. + Conductor, +} + +impl OrchestrationRole { + /// Return the stable wire name for this role. + #[must_use] + pub const fn wire_name(self) -> &'static str { + match self { + Self::Thinker => "thinker", + Self::Worker => "worker", + Self::Verifier => "verifier", + Self::Adjudicator => "adjudicator", + Self::Conductor => "conductor", + } + } +} + +/// One role plus its recorded reasoning effort. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct RoleAssignment { + role: OrchestrationRole, + effort: ReasoningEffort, +} + +impl RoleAssignment { + /// Assigned orchestration role. + #[must_use] + pub const fn role(self) -> OrchestrationRole { + self.role + } + + /// Recorded reasoning effort for this role. + #[must_use] + pub const fn effort(self) -> ReasoningEffort { + self.effort + } +} + +/// Document attempt to override TEPP orchestration authority. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum DocumentControlAttempt { + /// No document-supplied override. + None, + /// Document tried to set orchestration policy. + Policy, + /// Document tried to set the access list. + AccessList, + /// Document tried to supply provider credentials. + Credentials, +} + +/// Inputs to the governed orchestration router. +/// +/// Scores are CPU `f64` unit intervals. Documents cannot set policy, access +/// lists, or credentials; those attempts fail closed. +#[derive(Clone, Debug)] +pub struct OrchestrationRequest { + /// Must equal [`ORCHESTRATION_POLICY_VERSION`]. + pub policy_version: String, + /// Interpretation task being scheduled. + pub task_kind: InterpretationTaskKind, + /// Task risk in `[0, 1]`. + pub risk_score: f64, + /// Ambiguity in `[0, 1]`. + pub ambiguity_score: f64, + /// Evidence sufficiency in `[0, 1]`. + pub evidence_sufficiency: f64, + /// Explicit test-time token budget. + pub compute_budget_tokens: u64, + /// Document-supplied override attempt, if any. + pub document_control: DocumentControlAttempt, + /// Whether deterministic scientific gates already passed. + pub scientific_gate_passed: bool, + /// TEPP-owned access profile identifiers. + pub access_list: Vec, +} + +/// Recorded orchestration plan. Construct only via [`route_orchestration`]. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct OrchestrationPlan { + mode: OrchestrationMode, + task_kind: InterpretationTaskKind, + stages: u8, + recursion_depth: u8, + decomposition_code: &'static str, + access_list: Vec, + roles: Vec, + token_budget: u64, + fallback_mode: OrchestrationMode, + policy_version: String, + proposal_only: bool, + scientific_authority_code: &'static str, +} + +impl OrchestrationPlan { + /// Selected orchestration mode. + #[must_use] + pub const fn mode(&self) -> OrchestrationMode { + self.mode + } + + /// Interpretation task whose context this plan represents. + #[must_use] + pub const fn task_kind(&self) -> InterpretationTaskKind { + self.task_kind + } + + /// Recorded workflow stage count. + #[must_use] + pub const fn stage_count(&self) -> u8 { + self.stages + } + + /// Recorded recursion depth. + #[must_use] + pub const fn recursion_depth(&self) -> u8 { + self.recursion_depth + } + + /// Decomposition code used for ablation. + #[must_use] + pub const fn decomposition_code(&self) -> &'static str { + self.decomposition_code + } + + /// TEPP-owned access list copied onto the plan. + #[must_use] + pub fn access_list(&self) -> &[String] { + &self.access_list + } + + /// Role assignments with per-role reasoning effort. + #[must_use] + pub fn roles(&self) -> &[RoleAssignment] { + &self.roles + } + + /// Allocated test-time budget in tokens. + #[must_use] + pub const fn token_budget(&self) -> u64 { + self.token_budget + } + + /// Bounded fallback mode if this plan cannot complete. + #[must_use] + pub const fn fallback_mode(&self) -> OrchestrationMode { + self.fallback_mode + } + + /// Policy version bound into the plan. + #[must_use] + pub fn policy_version(&self) -> &str { + &self.policy_version + } + + /// LLM output remains a proposal, never scientific authority. + #[must_use] + pub const fn proposal_only(&self) -> bool { + self.proposal_only + } + + /// Stable code naming the authoritative scientific gate family. + #[must_use] + pub const fn scientific_authority_code(&self) -> &'static str { + self.scientific_authority_code + } +} + +impl fmt::Display for OrchestrationPlan { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + formatter, + "orchestration mode={} stages={} recursion={} proposal={}", + self.mode.wire_name(), + self.stages, + self.recursion_depth, + self.proposal_only + ) + } +} + +/// Comparable-budget ablation record. Direct is the required baseline. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct BudgetAblationRecord { + baseline_mode: OrchestrationMode, + compared_mode: OrchestrationMode, + baseline_budget: u64, + compared_budget: u64, + comparable: bool, +} + +impl BudgetAblationRecord { + /// Required direct baseline mode. + #[must_use] + pub const fn baseline_mode(self) -> OrchestrationMode { + self.baseline_mode + } + + /// Mode compared against the direct baseline. + #[must_use] + pub const fn compared_mode(self) -> OrchestrationMode { + self.compared_mode + } + + /// Baseline token budget. + #[must_use] + pub const fn baseline_budget(self) -> u64 { + self.baseline_budget + } + + /// Compared token budget. + #[must_use] + pub const fn compared_budget(self) -> u64 { + self.compared_budget + } + + /// Whether the two budgets are within a 10 percent relative band. + #[must_use] + pub const fn comparable(self) -> bool { + self.comparable + } +} + +/// Credential-free binding TEPP may hand to contextual-orchestrator. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ContextualOrchestratorBinding { + contract_version: u16, + mode: OrchestrationMode, + policy_version: String, + evidence_manifest_hash: String, + access_list: Vec, + roles: Vec, + token_budget: u64, + includes_credentials: bool, +} + +impl ContextualOrchestratorBinding { + /// Binding contract version. + #[must_use] + pub const fn contract_version(&self) -> u16 { + self.contract_version + } + + /// Mode the orchestrator may execute. + #[must_use] + pub const fn mode(&self) -> OrchestrationMode { + self.mode + } + + /// Policy version the orchestrator must not replace. + #[must_use] + pub fn policy_version(&self) -> &str { + &self.policy_version + } + + /// Evidence manifest digest; not raw source. + #[must_use] + pub fn evidence_manifest_hash(&self) -> &str { + &self.evidence_manifest_hash + } + + /// TEPP-owned access list. + #[must_use] + pub fn access_list(&self) -> &[String] { + &self.access_list + } + + /// Role assignments copied from the plan. + #[must_use] + pub fn roles(&self) -> &[RoleAssignment] { + &self.roles + } + + /// Allocated budget copied from the plan. + #[must_use] + pub const fn token_budget(&self) -> u64 { + self.token_budget + } + + /// Credentials are never included on this binding. + #[must_use] + pub const fn includes_credentials(&self) -> bool { + self.includes_credentials + } +} + +/// Route an interpretation task onto a versioned orchestration plan. +/// +/// The selected plan is a proposal. Deterministic statistical gates remain +/// authoritative. Documents cannot change policy, access lists, or credentials. +/// +/// # Errors +/// +/// Returns [`ApiError::UnsupportedContractVersion`] when the policy version is +/// not [`ORCHESTRATION_POLICY_VERSION`]. Returns +/// [`ApiError::AuthorizationDenied`] when a document supplied policy, access, +/// or credentials. Returns [`ApiError::InvalidWirePayload`] for non-unit +/// scores or empty access-list tokens. +pub fn route_orchestration(request: &OrchestrationRequest) -> Result { + validate_request(request)?; + let preferred = preferred_mode(request); + let minimum_mode = minimum_acceptable_mode(request.task_kind); + let mode = fit_mode(preferred, request.compute_budget_tokens, minimum_mode); + Ok(build_plan(request, mode)) +} + +/// Record a comparable-budget ablation against a required direct baseline. +/// +/// # Errors +/// +/// Returns [`ApiError::InvalidWirePayload`] when the baseline is not +/// [`OrchestrationMode::Direct`], or when the two plans disagree on task kind +/// or access list. Plans cannot disagree on policy version because the only +/// constructor, [`route_orchestration`], accepts the current version exactly. +pub fn record_budget_ablation( + baseline: &OrchestrationPlan, + compared: &OrchestrationPlan, +) -> Result { + if baseline.mode != OrchestrationMode::Direct + || baseline.task_kind != compared.task_kind + || baseline.access_list != compared.access_list + { + return Err(ApiError::InvalidWirePayload); + } + Ok(BudgetAblationRecord { + baseline_mode: baseline.mode, + compared_mode: compared.mode, + baseline_budget: baseline.token_budget, + compared_budget: compared.token_budget, + comparable: budgets_are_comparable(baseline.token_budget, compared.token_budget), + }) +} + +/// Bind a non-abstaining plan for contextual-orchestrator execution. +/// +/// The binding never includes credentials or raw source. TEPP retains +/// scientific authority; the orchestrator may only execute the recorded mode. +/// +/// # Errors +/// +/// Returns [`ApiError::AuthorizationDenied`] for [`OrchestrationMode::Abstain`]. +/// Returns [`ApiError::InvalidWirePayload`] unless the evidence manifest is a +/// canonical lowercase `sha256:` digest rather than raw source text. +pub fn bind_contextual_orchestrator( + plan: &OrchestrationPlan, + evidence_manifest_hash: &str, +) -> Result { + require_sha256_digest(evidence_manifest_hash)?; + if plan.mode == OrchestrationMode::Abstain { + return Err(ApiError::AuthorizationDenied); + } + Ok(ContextualOrchestratorBinding { + contract_version: ORCHESTRATION_CONTRACT_VERSION, + mode: plan.mode, + policy_version: plan.policy_version.clone(), + evidence_manifest_hash: evidence_manifest_hash.to_owned(), + access_list: plan.access_list.clone(), + roles: plan.roles.clone(), + token_budget: plan.token_budget, + includes_credentials: false, + }) +} + +fn require_sha256_digest(value: &str) -> Result<(), ApiError> { + let Some(digest) = value.strip_prefix("sha256:") else { + return Err(ApiError::InvalidWirePayload); + }; + if digest.len() == 64 + && digest + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) + { + Ok(()) + } else { + Err(ApiError::InvalidWirePayload) + } +} + +fn validate_request(request: &OrchestrationRequest) -> Result<(), ApiError> { + if request.policy_version != ORCHESTRATION_POLICY_VERSION { + return Err(ApiError::UnsupportedContractVersion); + } + if request.document_control != DocumentControlAttempt::None { + return Err(ApiError::AuthorizationDenied); + } + if request.compute_budget_tokens > MAX_ORCHESTRATION_TOKEN_BUDGET + || request.access_list.len() > MAX_ORCHESTRATION_ACCESS_ENTRIES + { + return Err(ApiError::LimitExceeded); + } + require_unit_score(request.risk_score)?; + require_unit_score(request.ambiguity_score)?; + require_unit_score(request.evidence_sufficiency)?; + for (index, token) in request.access_list.iter().enumerate() { + require_nonempty(token)?; + if token.len() > MAX_ORCHESTRATION_ACCESS_TOKEN_BYTES { + return Err(ApiError::LimitExceeded); + } + if request.access_list[..index].contains(token) { + return Err(ApiError::InvalidWirePayload); + } + } + Ok(()) +} + +fn require_unit_score(value: f64) -> Result<(), ApiError> { + if value.is_finite() && (0.0..=1.0).contains(&value) { + Ok(()) + } else { + Err(ApiError::InvalidWirePayload) + } +} + +fn preferred_mode(request: &OrchestrationRequest) -> OrchestrationMode { + if request.evidence_sufficiency < EVIDENCE_FLOOR { + return OrchestrationMode::Abstain; + } + if request.task_kind == InterpretationTaskKind::BlindedModelReview + && !request.scientific_gate_passed + { + return OrchestrationMode::Abstain; + } + match request.task_kind { + InterpretationTaskKind::SchemaConversion => OrchestrationMode::Direct, + InterpretationTaskKind::BlindedModelReview => OrchestrationMode::Committee, + InterpretationTaskKind::AdversarialVerification => OrchestrationMode::Verify, + InterpretationTaskKind::SpanClassification => { + if request.risk_score < LOW_COMPLEXITY && request.ambiguity_score < LOW_COMPLEXITY { + OrchestrationMode::Direct + } else { + OrchestrationMode::Verify + } + } + InterpretationTaskKind::ConceptAlignment => { + if request.ambiguity_score >= HIGH_COMPLEXITY { + OrchestrationMode::Committee + } else { + OrchestrationMode::Verify + } + } + InterpretationTaskKind::NarrativeSynthesis => { + if request.risk_score >= HIGH_COMPLEXITY || request.ambiguity_score >= HIGH_COMPLEXITY { + OrchestrationMode::Conductor + } else { + OrchestrationMode::Verify + } + } + } +} + +const fn minimum_acceptable_mode(task_kind: InterpretationTaskKind) -> OrchestrationMode { + match task_kind { + InterpretationTaskKind::BlindedModelReview => OrchestrationMode::Committee, + InterpretationTaskKind::SpanClassification + | InterpretationTaskKind::ConceptAlignment + | InterpretationTaskKind::NarrativeSynthesis + | InterpretationTaskKind::AdversarialVerification + | InterpretationTaskKind::SchemaConversion => OrchestrationMode::Direct, + } +} + +fn fit_mode( + preferred: OrchestrationMode, + budget: u64, + minimum_mode: OrchestrationMode, +) -> OrchestrationMode { + let mut mode = preferred; + loop { + if budget >= mode.minimum_token_budget() { + return mode; + } + if mode == minimum_mode { + return OrchestrationMode::Abstain; + } + mode = mode.fallback_mode(); + } +} + +fn build_plan(request: &OrchestrationRequest, mode: OrchestrationMode) -> OrchestrationPlan { + let token_budget = if mode == OrchestrationMode::Abstain { + 0 + } else { + request.compute_budget_tokens + }; + OrchestrationPlan { + mode, + task_kind: request.task_kind, + stages: mode.stage_count(), + recursion_depth: mode.recursion_depth(), + decomposition_code: mode.decomposition_code(), + access_list: request.access_list.clone(), + roles: assign_roles(mode, request.task_kind), + token_budget, + fallback_mode: mode.fallback_mode(), + policy_version: request.policy_version.clone(), + proposal_only: true, + scientific_authority_code: "deterministic_statistical_gates", + } +} + +fn assign_roles(mode: OrchestrationMode, task: InterpretationTaskKind) -> Vec { + let worker = RoleAssignment { + role: OrchestrationRole::Worker, + effort: task.default_effort(), + }; + match mode { + OrchestrationMode::Direct => vec![worker], + OrchestrationMode::Verify => vec![ + worker, + RoleAssignment { + role: OrchestrationRole::Verifier, + effort: ReasoningEffort::High, + }, + ], + OrchestrationMode::Committee => vec![ + worker, + worker, + RoleAssignment { + role: OrchestrationRole::Adjudicator, + effort: ReasoningEffort::High, + }, + ], + OrchestrationMode::Conductor => vec![ + RoleAssignment { + role: OrchestrationRole::Conductor, + effort: ReasoningEffort::High, + }, + RoleAssignment { + role: OrchestrationRole::Thinker, + effort: task.default_effort(), + }, + worker, + RoleAssignment { + role: OrchestrationRole::Verifier, + effort: ReasoningEffort::High, + }, + ], + OrchestrationMode::Abstain => Vec::new(), + } +} + +fn budgets_are_comparable(left: u64, right: u64) -> bool { + // A saturated product is zero exactly when either budget is zero. Expressing + // the guard this way keeps the helper total without an unreachable arm in + // the public Direct-baseline call path. + left.saturating_mul(right) > 0 + && left + .abs_diff(right) + .saturating_mul(COMPARABLE_BUDGET_NUMERATOR) + <= left.max(right) +} + +#[cfg(test)] +mod tests { + use super::{ + DocumentControlAttempt, InterpretationTaskKind, ORCHESTRATION_CONTRACT_VERSION, + ORCHESTRATION_POLICY_VERSION, OrchestrationMode, OrchestrationRequest, OrchestrationRole, + ReasoningEffort, bind_contextual_orchestrator, budgets_are_comparable, + record_budget_ablation, route_orchestration, + }; + use crate::ApiError; + + fn request( + task_kind: InterpretationTaskKind, + risk: f64, + ambiguity: f64, + evidence: f64, + budget: u64, + ) -> OrchestrationRequest { + OrchestrationRequest { + policy_version: ORCHESTRATION_POLICY_VERSION.into(), + task_kind, + risk_score: risk, + ambiguity_score: ambiguity, + evidence_sufficiency: evidence, + compute_budget_tokens: budget, + document_control: DocumentControlAttempt::None, + scientific_gate_passed: true, + access_list: Vec::new(), + } + } + + #[test] + fn thresholds_and_fallbacks_cover_remaining_arms() { + let boundary = route_orchestration(&request( + InterpretationTaskKind::SpanClassification, + 0.35, + 0.10, + 0.35, + 8_000, + )) + .expect("boundary"); + assert_eq!(boundary.mode(), OrchestrationMode::Verify); + assert_eq!(boundary.fallback_mode(), OrchestrationMode::Direct); + assert_eq!( + OrchestrationMode::Committee.fallback_mode(), + OrchestrationMode::Verify + ); + assert_eq!( + OrchestrationMode::Abstain.fallback_mode(), + OrchestrationMode::Abstain + ); + assert_eq!(OrchestrationMode::Committee.recursion_depth(), 0); + assert_eq!(OrchestrationMode::Conductor.stage_count(), 4); + assert_eq!(OrchestrationMode::Direct.minimum_token_budget(), 4_000); + assert_eq!(OrchestrationMode::Verify.minimum_token_budget(), 8_000); + assert_eq!(OrchestrationMode::Committee.minimum_token_budget(), 16_000); + assert_eq!(OrchestrationMode::Conductor.minimum_token_budget(), 24_000); + assert_eq!(OrchestrationMode::Abstain.minimum_token_budget(), 0); + + let narrative_boundary = route_orchestration(&request( + InterpretationTaskKind::NarrativeSynthesis, + 0.50, + 0.10, + 0.90, + 24_000, + )) + .expect("narrative boundary"); + assert_eq!(narrative_boundary.mode(), OrchestrationMode::Conductor); + assert_eq!(narrative_boundary.stage_count(), 4); + + let committee_budget = route_orchestration(&request( + InterpretationTaskKind::NarrativeSynthesis, + 0.70, + 0.70, + 0.90, + 16_000, + )) + .expect("committee fit"); + assert_eq!(committee_budget.mode(), OrchestrationMode::Committee); + assert_eq!(committee_budget.fallback_mode(), OrchestrationMode::Verify); + assert_eq!(committee_budget.roles().len(), 3); + + let underfunded_blinded_review = route_orchestration(&request( + InterpretationTaskKind::BlindedModelReview, + 0.40, + 0.40, + 0.80, + 8_000, + )) + .expect("underfunded blinded review"); + assert_eq!( + underfunded_blinded_review.mode(), + OrchestrationMode::Abstain + ); + } + + #[test] + fn ablation_getters_and_rejection_paths_are_observable() { + assert!(!budgets_are_comparable(8_000, 32_000)); + assert!(budgets_are_comparable(16_000, 16_000)); + assert!(!budgets_are_comparable(0, 16_000)); + assert!(!budgets_are_comparable(16_000, 0)); + + let direct = route_orchestration(&request( + InterpretationTaskKind::SpanClassification, + 0.1, + 0.1, + 1.0, + 8_000, + )) + .expect("direct"); + let verify = route_orchestration(&request( + InterpretationTaskKind::SpanClassification, + 0.8, + 0.1, + 1.0, + 32_000, + )) + .expect("verify"); + let record = record_budget_ablation(&direct, &verify).expect("wide band"); + assert!(!record.comparable()); + assert_eq!(record.baseline_mode(), OrchestrationMode::Direct); + assert_eq!(record.compared_mode(), OrchestrationMode::Verify); + assert_eq!(record.baseline_budget(), 8_000); + assert_eq!(record.compared_budget(), 32_000); + assert!( + record_budget_ablation(&direct, &direct) + .expect("same-budget direct comparison") + .comparable() + ); + + let committee = route_orchestration(&request( + InterpretationTaskKind::BlindedModelReview, + 0.8, + 0.8, + 1.0, + 32_000, + )) + .expect("committee"); + assert_eq!( + record_budget_ablation(&committee, &direct), + Err(ApiError::InvalidWirePayload), + ); + assert_eq!( + record_budget_ablation(&direct, &committee), + Err(ApiError::InvalidWirePayload), + ); + let mut different_access = request( + InterpretationTaskKind::SpanClassification, + 0.8, + 0.1, + 1.0, + 32_000, + ); + different_access.access_list.push("evidence_spans".into()); + let different_access = route_orchestration(&different_access).expect("different access"); + assert_eq!( + record_budget_ablation(&direct, &different_access), + Err(ApiError::InvalidWirePayload), + ); + } + + #[test] + fn binding_getters_and_rejection_paths_are_observable() { + let direct = route_orchestration(&request( + InterpretationTaskKind::SpanClassification, + 0.1, + 0.1, + 1.0, + 8_000, + )) + .expect("direct"); + let verify = route_orchestration(&request( + InterpretationTaskKind::SpanClassification, + 0.8, + 0.1, + 1.0, + 32_000, + )) + .expect("verify"); + + let binding = bind_contextual_orchestrator( + &verify, + "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + ) + .expect("bind"); + bind_contextual_orchestrator( + &verify, + "sha256:0000000000000000000000000000000000000000000000000000000000000000", + ) + .expect("digit-only canonical digest"); + assert_eq!(binding.contract_version(), ORCHESTRATION_CONTRACT_VERSION); + assert_eq!(binding.roles().len(), 2); + assert_eq!(binding.token_budget(), 32_000); + assert!(binding.access_list().is_empty()); + assert!(!binding.includes_credentials()); + assert_eq!(binding.mode(), OrchestrationMode::Verify); + assert_eq!(binding.policy_version(), ORCHESTRATION_POLICY_VERSION); + assert_eq!( + binding.evidence_manifest_hash(), + "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ); + assert_eq!( + bind_contextual_orchestrator(&verify, "raw evidence"), + Err(ApiError::InvalidWirePayload), + ); + assert_eq!( + bind_contextual_orchestrator(&verify, "sha256:a"), + Err(ApiError::InvalidWirePayload), + ); + assert_eq!( + bind_contextual_orchestrator( + &verify, + "sha256:gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg", + ), + Err(ApiError::InvalidWirePayload), + ); + let abstain = route_orchestration(&request( + InterpretationTaskKind::SpanClassification, + 0.1, + 0.1, + 0.1, + 8_000, + )) + .expect("abstain"); + assert_eq!( + bind_contextual_orchestrator( + &abstain, + "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + ), + Err(ApiError::AuthorizationDenied), + ); + + let conductor = route_orchestration(&request( + InterpretationTaskKind::NarrativeSynthesis, + 0.8, + 0.8, + 1.0, + 32_000, + )) + .expect("conductor"); + let thinker = conductor + .roles() + .iter() + .find(|assignment| assignment.role() == OrchestrationRole::Thinker) + .expect("routed thinker"); + assert_eq!(thinker.role().wire_name(), "thinker"); + assert_eq!(thinker.effort(), ReasoningEffort::Medium); + assert_eq!(thinker.effort().wire_name(), "medium"); + assert_eq!( + direct.to_string(), + "orchestration mode=direct stages=1 recursion=0 proposal=true" + ); + } + + #[test] + fn preferred_modes_cover_concept_and_narrative_boundaries() { + let cases = [ + ( + InterpretationTaskKind::SpanClassification, + 0.10, + 0.40, + OrchestrationMode::Verify, + ), + ( + InterpretationTaskKind::ConceptAlignment, + 0.1, + 0.50, + OrchestrationMode::Committee, + ), + ( + InterpretationTaskKind::ConceptAlignment, + 0.1, + 0.49, + OrchestrationMode::Verify, + ), + ( + InterpretationTaskKind::NarrativeSynthesis, + 0.1, + 0.50, + OrchestrationMode::Conductor, + ), + ( + InterpretationTaskKind::NarrativeSynthesis, + 0.1, + 0.49, + OrchestrationMode::Verify, + ), + ]; + + for (task_kind, risk_score, ambiguity_score, expected_mode) in cases { + let plan = route_orchestration(&request( + task_kind, + risk_score, + ambiguity_score, + 1.0, + 32_000, + )) + .expect("boundary route"); + assert_eq!(plan.mode(), expected_mode); + } + } + + #[test] + fn empty_policy_and_negative_infinity_fail_closed() { + let mut empty = request( + InterpretationTaskKind::SpanClassification, + 0.1, + 0.1, + 0.9, + 8_000, + ); + empty.policy_version.clear(); + assert_eq!( + route_orchestration(&empty), + Err(ApiError::UnsupportedContractVersion) + ); + assert_eq!( + route_orchestration(&request( + InterpretationTaskKind::SpanClassification, + f64::NEG_INFINITY, + 0.1, + 0.9, + 8_000, + )), + Err(ApiError::InvalidWirePayload) + ); + } + + #[test] + fn finite_out_of_range_score_fails_closed() { + assert_eq!( + route_orchestration(&request( + InterpretationTaskKind::SpanClassification, + 1.01, + 0.1, + 0.9, + 8_000, + )), + Err(ApiError::InvalidWirePayload) + ); + } +} diff --git a/crates/tepp_api/tests/orchestration_resource_bounds_contract.rs b/crates/tepp_api/tests/orchestration_resource_bounds_contract.rs new file mode 100644 index 00000000..19efac2a --- /dev/null +++ b/crates/tepp_api/tests/orchestration_resource_bounds_contract.rs @@ -0,0 +1,68 @@ +//! Orchestration routing must bound billable compute and access-list resources. + +use tepp_api::{ + ApiError, DocumentControlAttempt, InterpretationTaskKind, MAX_ORCHESTRATION_ACCESS_ENTRIES, + MAX_ORCHESTRATION_ACCESS_TOKEN_BYTES, MAX_ORCHESTRATION_TOKEN_BUDGET, + ORCHESTRATION_POLICY_VERSION, OrchestrationMode, OrchestrationRequest, route_orchestration, +}; + +fn request() -> OrchestrationRequest { + OrchestrationRequest { + policy_version: ORCHESTRATION_POLICY_VERSION.into(), + task_kind: InterpretationTaskKind::SpanClassification, + risk_score: 0.1, + ambiguity_score: 0.1, + evidence_sufficiency: 1.0, + compute_budget_tokens: 4_000, + document_control: DocumentControlAttempt::None, + scientific_gate_passed: true, + access_list: vec!["evidence.read".into()], + } +} + +#[test] +fn token_budget_is_bounded_before_any_plan_is_created() { + let mut oversized = request(); + oversized.compute_budget_tokens = MAX_ORCHESTRATION_TOKEN_BUDGET + 1; + assert_eq!( + route_orchestration(&oversized), + Err(ApiError::LimitExceeded) + ); + + let mut boundary = request(); + boundary.compute_budget_tokens = MAX_ORCHESTRATION_TOKEN_BUDGET; + assert!(route_orchestration(&boundary).is_ok()); +} + +#[test] +fn access_list_cardinality_and_token_bytes_are_bounded() { + let mut too_many = request(); + too_many.access_list = (0..=MAX_ORCHESTRATION_ACCESS_ENTRIES) + .map(|index| format!("scope.{index}")) + .collect(); + assert_eq!(route_orchestration(&too_many), Err(ApiError::LimitExceeded)); + + let mut too_long = request(); + too_long.access_list = vec!["x".repeat(MAX_ORCHESTRATION_ACCESS_TOKEN_BYTES + 1)]; + assert_eq!(route_orchestration(&too_long), Err(ApiError::LimitExceeded)); +} + +#[test] +fn duplicate_access_tokens_fail_closed_instead_of_amplifying_authority() { + let mut duplicate = request(); + duplicate.access_list = vec!["evidence.read".into(), "evidence.read".into()]; + assert_eq!( + route_orchestration(&duplicate), + Err(ApiError::InvalidWirePayload) + ); +} + +#[test] +fn verify_mode_falls_back_to_direct_at_the_direct_budget_boundary() { + let mut budgeted = request(); + budgeted.risk_score = 0.35; + budgeted.compute_budget_tokens = OrchestrationMode::Direct.minimum_token_budget(); + + let plan = route_orchestration(&budgeted).expect("bounded request"); + assert_eq!(plan.mode(), OrchestrationMode::Direct); +} diff --git a/crates/tepp_api/tests/orchestration_router_contract.rs b/crates/tepp_api/tests/orchestration_router_contract.rs new file mode 100644 index 00000000..4950aad6 --- /dev/null +++ b/crates/tepp_api/tests/orchestration_router_contract.rs @@ -0,0 +1,463 @@ +//! Adaptive orchestration routing refuses document-controlled policy. + +use tepp_api::{ + ApiError, DocumentControlAttempt, InterpretationTaskKind, ORCHESTRATION_CONTRACT_VERSION, + ORCHESTRATION_POLICY_VERSION, OrchestrationMode, OrchestrationRequest, OrchestrationRole, + ReasoningEffort, bind_contextual_orchestrator, record_budget_ablation, route_orchestration, +}; + +fn request( + task_kind: InterpretationTaskKind, + risk: f64, + ambiguity: f64, + evidence: f64, + budget: u64, +) -> OrchestrationRequest { + OrchestrationRequest { + policy_version: ORCHESTRATION_POLICY_VERSION.into(), + task_kind, + risk_score: risk, + ambiguity_score: ambiguity, + evidence_sufficiency: evidence, + compute_budget_tokens: budget, + document_control: DocumentControlAttempt::None, + scientific_gate_passed: true, + access_list: vec!["evidence_spans".into()], + } +} + +#[test] +fn low_risk_span_classification_routes_direct() { + let plan = route_orchestration(&request( + InterpretationTaskKind::SpanClassification, + 0.10, + 0.10, + 0.90, + 8_000, + )) + .expect("direct"); + assert_eq!(plan.mode(), OrchestrationMode::Direct); + assert_eq!(plan.mode().wire_name(), "direct"); + assert_eq!(plan.stage_count(), 1); + assert_eq!(plan.recursion_depth(), 0); + assert_eq!(plan.decomposition_code(), "single_call"); + assert_eq!(plan.fallback_mode(), OrchestrationMode::Abstain); + assert!(plan.proposal_only()); + assert_eq!( + plan.scientific_authority_code(), + "deterministic_statistical_gates" + ); + assert_eq!(plan.policy_version(), ORCHESTRATION_POLICY_VERSION); + assert_eq!(plan.access_list(), ["evidence_spans"]); + assert_eq!(plan.roles().len(), 1); + assert_eq!(plan.roles()[0].role(), OrchestrationRole::Worker); + assert_eq!(plan.roles()[0].effort(), ReasoningEffort::Low); + assert_eq!(plan.token_budget(), 8_000); + assert!(!plan.to_string().contains("token")); +} + +#[test] +fn schema_conversion_is_direct_with_minimal_effort() { + let plan = route_orchestration(&request( + InterpretationTaskKind::SchemaConversion, + 0.90, + 0.90, + 0.80, + 4_000, + )) + .expect("schema"); + assert_eq!(plan.mode(), OrchestrationMode::Direct); + assert_eq!(plan.roles()[0].effort(), ReasoningEffort::Minimal); + assert_eq!( + InterpretationTaskKind::SchemaConversion.default_effort(), + ReasoningEffort::Minimal + ); +} + +#[test] +fn material_risk_or_adversarial_work_routes_verify() { + let high_risk = route_orchestration(&request( + InterpretationTaskKind::SpanClassification, + 0.70, + 0.20, + 0.80, + 12_000, + )) + .expect("high risk"); + assert_eq!(high_risk.mode(), OrchestrationMode::Verify); + assert_eq!(high_risk.decomposition_code(), "producer_then_verifier"); + assert_eq!(high_risk.stage_count(), 2); + assert_eq!(high_risk.roles().len(), 2); + assert_eq!(high_risk.roles()[1].role(), OrchestrationRole::Verifier); + assert_eq!(high_risk.roles()[1].effort(), ReasoningEffort::High); + assert_eq!(high_risk.fallback_mode(), OrchestrationMode::Direct); + + let adversarial = route_orchestration(&request( + InterpretationTaskKind::AdversarialVerification, + 0.20, + 0.20, + 0.80, + 12_000, + )) + .expect("adversarial"); + assert_eq!(adversarial.mode(), OrchestrationMode::Verify); + assert_eq!( + InterpretationTaskKind::AdversarialVerification.default_effort(), + ReasoningEffort::High + ); +} + +#[test] +fn concept_alignment_and_low_narrative_use_verify_or_committee() { + let aligned = route_orchestration(&request( + InterpretationTaskKind::ConceptAlignment, + 0.20, + 0.20, + 0.80, + 16_000, + )) + .expect("concept low"); + assert_eq!(aligned.mode(), OrchestrationMode::Verify); + assert_eq!( + InterpretationTaskKind::ConceptAlignment.default_effort(), + ReasoningEffort::Medium + ); + + let ambiguous = route_orchestration(&request( + InterpretationTaskKind::ConceptAlignment, + 0.20, + 0.70, + 0.80, + 16_000, + )) + .expect("concept high"); + assert_eq!(ambiguous.mode(), OrchestrationMode::Committee); + assert_eq!( + ambiguous.decomposition_code(), + "blinded_parallel_then_adjudicate" + ); + assert_eq!(ambiguous.stage_count(), 3); + assert!( + ambiguous + .roles() + .iter() + .any(|role| role.role() == OrchestrationRole::Adjudicator) + ); + + let narrative_low = route_orchestration(&request( + InterpretationTaskKind::NarrativeSynthesis, + 0.20, + 0.20, + 0.80, + 16_000, + )) + .expect("narrative low"); + assert_eq!(narrative_low.mode(), OrchestrationMode::Verify); +} + +#[test] +fn blinded_review_requires_scientific_gate_and_uses_committee() { + let passed = route_orchestration(&request( + InterpretationTaskKind::BlindedModelReview, + 0.40, + 0.40, + 0.80, + 20_000, + )) + .expect("committee"); + assert_eq!(passed.mode(), OrchestrationMode::Committee); + assert_eq!( + InterpretationTaskKind::BlindedModelReview.default_effort(), + ReasoningEffort::High + ); + + let mut rejected = request( + InterpretationTaskKind::BlindedModelReview, + 0.40, + 0.40, + 0.80, + 20_000, + ); + rejected.scientific_gate_passed = false; + let abstain = route_orchestration(&rejected).expect("llm cannot rescue"); + assert_eq!(abstain.mode(), OrchestrationMode::Abstain); + assert_eq!(abstain.decomposition_code(), "no_forced_answer"); + assert!(abstain.roles().is_empty()); + assert_eq!(abstain.token_budget(), 0); +} + +#[test] +fn complex_synthesis_routes_conductor_when_budget_allows() { + let plan = route_orchestration(&request( + InterpretationTaskKind::NarrativeSynthesis, + 0.60, + 0.70, + 0.85, + 32_000, + )) + .expect("conductor"); + assert_eq!(plan.mode(), OrchestrationMode::Conductor); + assert_eq!(plan.mode().wire_name(), "conductor"); + assert_eq!(plan.decomposition_code(), "adaptive_roles_under_budget"); + assert_eq!(plan.recursion_depth(), 2); + assert!( + plan.roles() + .iter() + .any(|role| role.role() == OrchestrationRole::Conductor) + ); + assert_eq!(plan.fallback_mode(), OrchestrationMode::Committee); +} + +#[test] +fn insufficient_evidence_and_tiny_budget_abstain() { + let evidence = route_orchestration(&request( + InterpretationTaskKind::NarrativeSynthesis, + 0.80, + 0.80, + 0.20, + 32_000, + )) + .expect("evidence"); + assert_eq!(evidence.mode(), OrchestrationMode::Abstain); + assert_eq!(evidence.mode().wire_name(), "abstain"); + + let budget = route_orchestration(&request( + InterpretationTaskKind::SpanClassification, + 0.10, + 0.10, + 0.90, + 100, + )) + .expect("tiny budget"); + assert_eq!(budget.mode(), OrchestrationMode::Abstain); +} + +#[test] +fn budget_steps_down_without_changing_scientific_authority() { + let stepped = route_orchestration(&request( + InterpretationTaskKind::NarrativeSynthesis, + 0.70, + 0.70, + 0.90, + 10_000, + )) + .expect("step down"); + assert_eq!(stepped.mode(), OrchestrationMode::Verify); + assert_eq!( + stepped.scientific_authority_code(), + "deterministic_statistical_gates" + ); + assert!(stepped.proposal_only()); +} + +#[test] +fn document_controlled_policy_access_or_credentials_are_denied() { + for attempt in [ + DocumentControlAttempt::Policy, + DocumentControlAttempt::AccessList, + DocumentControlAttempt::Credentials, + ] { + let mut hostile = request( + InterpretationTaskKind::SpanClassification, + 0.10, + 0.10, + 0.90, + 8_000, + ); + hostile.document_control = attempt; + assert_eq!( + route_orchestration(&hostile), + Err(ApiError::AuthorizationDenied) + ); + } +} + +#[test] +fn invalid_scores_policy_version_and_access_tokens_fail_closed() { + let mut version = request( + InterpretationTaskKind::SpanClassification, + 0.10, + 0.10, + 0.90, + 8_000, + ); + version.policy_version = "tepp.orchestration.v0".into(); + assert_eq!( + route_orchestration(&version), + Err(ApiError::UnsupportedContractVersion) + ); + + for (risk, ambiguity, evidence) in [ + (f64::NAN, 0.1, 0.9), + (0.1, f64::INFINITY, 0.9), + (0.1, 0.1, -0.01), + (1.01, 0.1, 0.9), + ] { + assert_eq!( + route_orchestration(&request( + InterpretationTaskKind::SpanClassification, + risk, + ambiguity, + evidence, + 8_000, + )), + Err(ApiError::InvalidWirePayload) + ); + } + + let mut empty_token = request( + InterpretationTaskKind::SpanClassification, + 0.10, + 0.10, + 0.90, + 8_000, + ); + empty_token.access_list = vec![" ".into()]; + assert_eq!( + route_orchestration(&empty_token), + Err(ApiError::InvalidWirePayload) + ); +} + +#[test] +fn comparable_budget_ablation_requires_direct_baseline() { + let direct = route_orchestration(&request( + InterpretationTaskKind::SpanClassification, + 0.10, + 0.10, + 0.90, + 16_000, + )) + .expect("direct"); + let verify = route_orchestration(&request( + InterpretationTaskKind::SpanClassification, + 0.80, + 0.20, + 0.80, + 16_000, + )) + .expect("verify"); + let record = record_budget_ablation(&direct, &verify).expect("ablation"); + assert_eq!(record.baseline_mode(), OrchestrationMode::Direct); + assert_eq!(record.compared_mode(), OrchestrationMode::Verify); + assert_eq!(record.baseline_budget(), 16_000); + assert_eq!(record.compared_budget(), 16_000); + assert!(record.comparable()); + + let conductor = route_orchestration(&request( + InterpretationTaskKind::NarrativeSynthesis, + 0.70, + 0.70, + 0.90, + 32_000, + )) + .expect("conductor"); + assert_eq!( + record_budget_ablation(&conductor, &direct), + Err(ApiError::InvalidWirePayload) + ); + + let abstain = route_orchestration(&request( + InterpretationTaskKind::SpanClassification, + 0.10, + 0.10, + 0.10, + 32_000, + )) + .expect("abstain"); + let not_comparable = record_budget_ablation(&direct, &abstain).expect("report difference"); + assert!(!not_comparable.comparable()); +} + +#[test] +fn contextual_orchestrator_binding_never_carries_credentials() { + let plan = route_orchestration(&request( + InterpretationTaskKind::ConceptAlignment, + 0.20, + 0.20, + 0.80, + 16_000, + )) + .expect("verify"); + let binding = bind_contextual_orchestrator( + &plan, + "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + ) + .expect("bind"); + assert_eq!(binding.contract_version(), ORCHESTRATION_CONTRACT_VERSION); + assert_eq!(binding.mode(), OrchestrationMode::Verify); + assert_eq!( + binding.evidence_manifest_hash(), + "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ); + assert!(!binding.includes_credentials()); + assert_eq!(binding.access_list(), plan.access_list()); + assert_eq!(binding.policy_version(), ORCHESTRATION_POLICY_VERSION); + + let abstain = route_orchestration(&request( + InterpretationTaskKind::NarrativeSynthesis, + 0.80, + 0.80, + 0.10, + 8_000, + )) + .expect("abstain"); + assert_eq!( + bind_contextual_orchestrator( + &abstain, + "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ), + Err(ApiError::AuthorizationDenied) + ); + assert_eq!( + bind_contextual_orchestrator(&plan, ""), + Err(ApiError::InvalidWirePayload) + ); +} + +#[test] +fn wire_names_cover_every_public_variant() { + assert_eq!(OrchestrationMode::Verify.wire_name(), "verify"); + assert_eq!(OrchestrationMode::Committee.wire_name(), "committee"); + assert_eq!(ReasoningEffort::Low.wire_name(), "low"); + assert_eq!(ReasoningEffort::Medium.wire_name(), "medium"); + assert_eq!(ReasoningEffort::High.wire_name(), "high"); + assert_eq!(ReasoningEffort::Minimal.wire_name(), "minimal"); + assert_eq!( + InterpretationTaskKind::SpanClassification.wire_name(), + "span_classification" + ); + assert_eq!( + InterpretationTaskKind::ConceptAlignment.wire_name(), + "concept_alignment" + ); + assert_eq!( + InterpretationTaskKind::BlindedModelReview.wire_name(), + "blinded_model_review" + ); + assert_eq!( + InterpretationTaskKind::NarrativeSynthesis.wire_name(), + "narrative_synthesis" + ); + assert_eq!( + InterpretationTaskKind::AdversarialVerification.wire_name(), + "adversarial_verification" + ); + assert_eq!( + InterpretationTaskKind::SchemaConversion.wire_name(), + "schema_conversion" + ); + assert_eq!(OrchestrationRole::Thinker.wire_name(), "thinker"); + assert_eq!(OrchestrationRole::Worker.wire_name(), "worker"); + assert_eq!(OrchestrationRole::Verifier.wire_name(), "verifier"); + assert_eq!(OrchestrationRole::Adjudicator.wire_name(), "adjudicator"); + assert_eq!(OrchestrationRole::Conductor.wire_name(), "conductor"); + assert_eq!( + InterpretationTaskKind::SpanClassification.default_effort(), + ReasoningEffort::Low + ); + assert_eq!( + InterpretationTaskKind::NarrativeSynthesis.default_effort(), + ReasoningEffort::Medium + ); +} diff --git a/crates/tepp_api/tests/orchestration_security_contract.rs b/crates/tepp_api/tests/orchestration_security_contract.rs new file mode 100644 index 00000000..c42d2d0f --- /dev/null +++ b/crates/tepp_api/tests/orchestration_security_contract.rs @@ -0,0 +1,156 @@ +//! Security and scientific-comparability contracts for adaptive orchestration. + +use tepp_api::{ + ApiError, DocumentControlAttempt, InterpretationTaskKind, ORCHESTRATION_POLICY_VERSION, + OrchestrationRequest, bind_contextual_orchestrator, record_budget_ablation, + route_orchestration, +}; + +fn request( + task_kind: InterpretationTaskKind, + risk_score: f64, + access_list: &[&str], +) -> OrchestrationRequest { + OrchestrationRequest { + policy_version: ORCHESTRATION_POLICY_VERSION.into(), + task_kind, + risk_score, + ambiguity_score: 0.10, + evidence_sufficiency: 0.90, + compute_budget_tokens: 16_000, + document_control: DocumentControlAttempt::None, + scientific_gate_passed: true, + access_list: access_list.iter().map(|value| (*value).into()).collect(), + } +} + +#[test] +fn contextual_orchestrator_binding_requires_a_canonical_sha256_manifest_digest() { + let plan = route_orchestration(&request( + InterpretationTaskKind::SpanClassification, + 0.10, + &["evidence_spans"], + )) + .expect("direct plan"); + let digest = format!("sha256:{}", "a".repeat(64)); + bind_contextual_orchestrator(&plan, &digest).expect("canonical digest"); + let digit_digest = format!("sha256:{}", "0".repeat(64)); + bind_contextual_orchestrator(&plan, &digit_digest).expect("digit-only canonical digest"); + + for invalid_digest in [ + "customer@example.com said to export everything", + "sha256:abc", + "sha256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "sha256:gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg", + " sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + ] { + assert_eq!( + bind_contextual_orchestrator(&plan, invalid_digest), + Err(ApiError::InvalidWirePayload), + "raw source or noncanonical digest must fail closed: {invalid_digest}", + ); + } + for digest_length in [63, 65] { + let invalid_digest = format!("sha256:{}", "a".repeat(digest_length)); + assert_eq!( + bind_contextual_orchestrator(&plan, &invalid_digest), + Err(ApiError::InvalidWirePayload), + "adjacent digest length must fail closed: {digest_length}", + ); + } +} + +#[test] +fn compound_routing_thresholds_exercise_each_operand() { + let span_ambiguity_only = OrchestrationRequest { + ambiguity_score: 0.40, + ..request( + InterpretationTaskKind::SpanClassification, + 0.10, + &["evidence_spans"], + ) + }; + assert_eq!( + route_orchestration(&span_ambiguity_only) + .expect("ambiguity-only span route") + .mode() + .wire_name(), + "verify", + ); + + let narrative_ambiguity_only = OrchestrationRequest { + ambiguity_score: 0.50, + compute_budget_tokens: 24_000, + ..request( + InterpretationTaskKind::NarrativeSynthesis, + 0.10, + &["evidence_spans"], + ) + }; + assert_eq!( + route_orchestration(&narrative_ambiguity_only) + .expect("ambiguity-only narrative route") + .mode() + .wire_name(), + "conductor", + ); + + let narrative_risk_only = OrchestrationRequest { + compute_budget_tokens: 24_000, + ..request( + InterpretationTaskKind::NarrativeSynthesis, + 0.50, + &["evidence_spans"], + ) + }; + assert_eq!( + route_orchestration(&narrative_risk_only) + .expect("risk-only narrative route") + .mode() + .wire_name(), + "conductor", + ); +} + +#[test] +fn budget_ablation_requires_the_same_task_and_access_context() { + let baseline = route_orchestration(&request( + InterpretationTaskKind::SpanClassification, + 0.10, + &["evidence_spans"], + )) + .expect("direct baseline"); + assert_eq!( + baseline.task_kind(), + InterpretationTaskKind::SpanClassification + ); + let comparable = route_orchestration(&request( + InterpretationTaskKind::SpanClassification, + 0.80, + &["evidence_spans"], + )) + .expect("verify comparison"); + record_budget_ablation(&baseline, &comparable).expect("same-context ablation"); + + let different_task = route_orchestration(&request( + InterpretationTaskKind::AdversarialVerification, + 0.10, + &["evidence_spans"], + )) + .expect("different task"); + assert_eq!( + record_budget_ablation(&baseline, &different_task), + Err(ApiError::InvalidWirePayload), + ); + + let different_access = route_orchestration(&request( + InterpretationTaskKind::SpanClassification, + 0.80, + &["identity_mappings"], + )) + .expect("different access"); + assert_eq!( + record_budget_ablation(&baseline, &different_access), + Err(ApiError::InvalidWirePayload), + ); +} diff --git a/docs/API_CONTRACT.md b/docs/API_CONTRACT.md index 09b7998d..95a1b64f 100644 --- a/docs/API_CONTRACT.md +++ b/docs/API_CONTRACT.md @@ -1,7 +1,7 @@ # TEPP API and Modular Integration Contract **Status:** Accepted target contract; exact endpoints are introduced only with executable services. -**Last reviewed:** 2026-08-10 +**Last reviewed:** 2026-08-13 ## 1. Authority boundary @@ -18,7 +18,7 @@ Current protected main exposes Rust library/domain contracts, not a production H | interval relation/reasoner API | `temporal_core` | event/relation validation | active-PR #6 | | event/relation/membership API | future TEPP crates/services | naruon, analytics, UI | accepted-target | | semantic/topic measurement API | future TEPP measurement service | naruon, batch jobs, visual analytics | accepted-target | -| LLM interpretation provider port | TEPP interpretation gateway | contextual-orchestrator | accepted-target | +| LLM interpretation provider port | `tepp_api` orchestration router + future HTTP gateway | contextual-orchestrator | partial | | model/artifact/export API | `tepp_api` export envelopes + future HTTP service | standalone UI/CWL consumers | partial | | analysis-run request/accepted contracts | `tepp_api` v1 wire DTOs | naruon, orchestrator, UI | active-PR | @@ -118,7 +118,7 @@ Before any naruon, contextual-orchestrator, or NVIDIA NIM submission, callers mu ### contextual-orchestrator -TEPP may call a provider-neutral interpretation/orchestration port for semantic unitization, blinded model review, and evidence-bounded interpretation. The orchestrator does not own TEPP's statistical truth, source evidence, model registry, merge/release authority, or scientific acceptance. Detailed port boundary and credential separation are recorded in [`docs/connectors/contextual-orchestrator-interpretation-port.md`](connectors/contextual-orchestrator-interpretation-port.md). +TEPP may call a provider-neutral interpretation/orchestration port for semantic unitization, blinded model review, and evidence-bounded interpretation. Callers first obtain a plan from `tepp_api::route_orchestration` and may bind it with `tepp_api::bind_contextual_orchestrator` using an evidence-manifest digest. The orchestrator does not own TEPP's statistical truth, source evidence, model registry, merge/release authority, or scientific acceptance. Detailed port boundary and credential separation are recorded in [`docs/connectors/contextual-orchestrator-interpretation-port.md`](connectors/contextual-orchestrator-interpretation-port.md). ### organization `.github` diff --git a/docs/DOCUMENTATION_ASSESSMENT.md b/docs/DOCUMENTATION_ASSESSMENT.md index 2a14a651..ccbfb967 100644 --- a/docs/DOCUMENTATION_ASSESSMENT.md +++ b/docs/DOCUMENTATION_ASSESSMENT.md @@ -36,7 +36,7 @@ Decision status and implementation maturity are separate. ADR `Accepted` means t | Compliance/assurance readiness | PRESENT-CURRENT | `docs/COMPLIANCE_READINESS.md`; CSAP/SOC 2/ISO/NIST mappings without certification claims | | Test / scientific validation | PRESENT-CURRENT | `docs/TEST_STRATEGY.md`; true-parameter recovery, uncertainty, leakage, invariance, CPU/GPU parity and replacement-lineage evidence rules | | Operability / recovery / release | PRESENT-CURRENT | `docs/OPERABILITY.md`; ADR 0014 separates release authority from green CI | -| LLM orchestration / test-time compute | PRESENT-CURRENT | `docs/LLM_ORCHESTRATION.md` + ADR 0010; Fugu/Conductor/TRINITY motivate tested allocation, not authority | +| LLM orchestration / test-time compute | PRESENT-CURRENT | `docs/LLM_ORCHESTRATION.md` + ADR 0010 + `tepp_api` router doctoring; Fugu/Conductor/TRINITY motivate tested allocation, not authority | | Autonomous development/review/merge authority | PRESENT-CURRENT as design | ADR 0015 separates model proposal, deterministic verification, publication, independent review, and merge/release authority; implementation remains accepted-target | | Standards / APA 7 doctoring | PRESENT-CURRENT | research register covers psychometrics/topic/time/event/Unicode/security/AI-governance/orchestration foundations | | Traceability | PRESENT-CURRENT | `docs/TRACEABILITY.md` maps requirements, owning ADRs, canonical replacement lineage, and maturity | diff --git a/docs/LLM_ORCHESTRATION.md b/docs/LLM_ORCHESTRATION.md index 42af070c..98102aa5 100644 --- a/docs/LLM_ORCHESTRATION.md +++ b/docs/LLM_ORCHESTRATION.md @@ -1,7 +1,7 @@ # TEPP LLM Orchestration and Test-Time Compute Contract -**Status:** Accepted target orchestration baseline; production implementation is not yet shipped. -**Last reviewed:** 2026-08-10 +**Status:** Partial — `tepp_api::route_orchestration` is the governed selector; live provider execution is not yet shipped. +**Last reviewed:** 2026-08-13 ## 1. Purpose @@ -29,7 +29,7 @@ These results motivate experiments; they do not prove that deeper orchestration | conductor | complex evidence synthesis or multi-stage semantic reasoning | adaptive roles/topology under explicit budget | | abstain | provider/evidence/validation insufficient | no forced answer | -A router chooses the cheapest mode expected to satisfy the quality/risk profile, but latency is not the primary objective. Quality, evidence support, calibration, disagreement, controllability, and reproducibility dominate. +`tepp_api::route_orchestration` is the governed selector. It chooses the cheapest mode expected to satisfy the quality/risk profile, but latency is not the primary objective. Quality, evidence support, calibration, disagreement, controllability, and reproducibility dominate. The returned plan is a proposal: `scientific_authority_code` remains `deterministic_statistical_gates`. ## 4. Explicit experimental variables diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index dfcdd9e8..f6739641 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -31,16 +31,16 @@ The full APA 7th standards/literature register remains `docs/research/standards- | posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | future `psychometric_core` | accepted-target | | CPU bounded multithreading + GPU/VRAM streaming/parity | ADR 0001/0006 | future `compute_backend` | accepted-target | | TDT detection/tracking vs CHRONOS schema/prediction/temporal consistency | ADR 0016; PRD/research | future `event_intelligence` | accepted-target | -| evidence-bounded LLM interpretation | ADR 0010/0012; PRD | future `interpretation_gateway` | accepted-target | -| adaptive direct/verify/committee/conductor test-time compute | ADR 0010; `docs/LLM_ORCHESTRATION.md` | future contextual-orchestrator integration + ablation evidence | accepted-target | -| purpose-bound PII handling without blanket masking | ADR 0009; `docs/PRIVACY_DATA_GOVERNANCE.md` | `tepp_api` export authorization plus provider-payload minimization / elevated re-identification on the active PR; persistence retention/deletion remaining | partial | -| tenant/purpose/role/lifetime access and identity separation | ADR 0009; Threat Model | `tepp_api` time-bounded `PurposeGrant` + cross-tenant denial on the active PR; persistent `access_grant` storage remaining | partial | +| evidence-bounded LLM interpretation | ADR 0010/0012; PRD | `tepp_api` router plus future `interpretation_gateway` | partial | +| adaptive direct/verify/committee/conductor test-time compute | ADR 0010; `docs/LLM_ORCHESTRATION.md` | `tepp_api::route_orchestration` + ablation record on the active PR; live contextual-orchestrator execution remaining | partial | +| purpose-bound PII handling without blanket masking | ADR 0009; `docs/PRIVACY_DATA_GOVERNANCE.md` | `tepp_api` export authorization plus provider-payload minimization / elevated re-identification implemented-main; persistence retention/deletion remaining | partial | +| tenant/purpose/role/lifetime access and identity separation | ADR 0009; Threat Model | `tepp_api` time-bounded `PurposeGrant` + cross-tenant denial implemented-main; persistent `access_grant` storage remaining | partial | | standalone + modular CWL MSA / no cross-service DB coupling | ADR 0011; `docs/API_CONTRACT.md` | current standalone crates; future service ports | partial | | naruon modular artifact consumer boundary | ADR 0011/0012; API contract | `docs/connectors/naruon-artifact-consumer.md` + PR #22 versioned consumer contract on protected main; `tepp_api` HTTP interchange (active PR); live HTTP service remaining | partial | | contextual-orchestrator interpretation port boundary | ADR 0010/0011; LLM orchestration | `docs/connectors/contextual-orchestrator-interpretation-port.md`; live port remaining | partial | | Actions registry identities bound to protected-main tree (orphan disable) | Operability; GitHub Actions REST | `scripts/actions_workflow_fleet.py` + issue #20 tests/doctoring; live disable remains operator-authorized | active-PR | | autonomous model proposal separated from verification/publication/review/merge | ADR 0015 | future safe OpenCode/NVIDIA autonomous-development workflow | accepted-target | -| contextual-orchestrator execution boundary | ADR 0010/0011 | provider-neutral orchestration port; TEPP retains scientific authority | accepted-target | +| contextual-orchestrator execution boundary | ADR 0010/0011 | credential-free `bind_contextual_orchestrator` on the active PR; live HTTP remaining | partial | | foundation validation / release-readiness ledger | ADR 0014; Test Strategy | PR #24 `docs/validation/temporal-event-foundation.md` on protected main | implemented-main | | scientific claim promotion separated from design/implementation/release | ADR 0014; ADR policy | documentation/CI/domain validation/release evidence | partial | | CSAP/SOC 2/ISO/NIST assurance readiness | `docs/COMPLIANCE_READINESS.md`; research register | repository controls + future deployment evidence | accepted-target / deployment-owned | diff --git a/docs/adr/0010-adaptive-llm-orchestration.md b/docs/adr/0010-adaptive-llm-orchestration.md index 20ae9ae0..d04bd6bc 100644 --- a/docs/adr/0010-adaptive-llm-orchestration.md +++ b/docs/adr/0010-adaptive-llm-orchestration.md @@ -1,7 +1,7 @@ # ADR 0010 — Adaptive LLM orchestration and test-time compute **Decision status:** Accepted -**Implementation maturity:** accepted-target +**Implementation maturity:** partial — `tepp_api` governed router, comparable-budget ablation record, and credential-free contextual-orchestrator binding are implemented on the active PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; live NIM execution, learned conductor calibration, and production ablation evidence remain accepted-target **Date:** 2026-08-10 **Supersedes:** The LLM orchestration-selection/ablation clauses previously co-located in ADR 0006. ADR 0006 remains authoritative for GPU/VRAM and model-credential separation; ADR 0015 governs autonomous repository-write/review/merge authority. diff --git a/docs/adr/README.md b/docs/adr/README.md index f16c2345..258eb7f3 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -14,8 +14,8 @@ Read [`ADR_POLICY.md`](ADR_POLICY.md) first. **Decision status and implementatio | [0006](0006-vram-gpu-nvidia-orchestration.md) | VRAM-adaptive GPU compute and model-credential boundary | Accepted | accepted-target | LLM orchestration policy superseded by ADR 0010; autonomous development authority governed by ADR 0015. | | [0007](0007-rust-workspace-quality-gates.md) | Explicit Rust workspace, pinned toolchains, and exact quality gates | Accepted | implemented-main | ADR 0014 governs scientific/product claim promotion beyond repository-quality tooling. | | [0008](0008-immutable-evidence-identities-digests-and-spans.md) | Immutable evidence identities, `SHA-256` digests, exact spans, and strict wire reconstruction | Accepted | implemented-main | ADR 0013 governs future persistence/reproducibility/split authority. | -| [0009](0009-purpose-bound-pii-governance.md) | Purpose-bound PII governance without blanket masking | Accepted | partial | Persistence retention/deletion/legal-hold (`0007`) implemented-main; provider-payload minimization and elevated re-identification are on the active PR; deployment evidence remains accepted-target. | -| [0010](0010-adaptive-llm-orchestration.md) | Adaptive LLM orchestration and test-time compute | Accepted | accepted-target | Owns direct/verify/committee/conductor selection, budget, role/topology, and ablation policy. | +| [0009](0009-purpose-bound-pii-governance.md) | Purpose-bound PII governance without blanket masking | Accepted | partial | Persistence retention/deletion/legal-hold (`0007`) and provider-payload minimization implemented-main; deployment evidence remains accepted-target. | +| [0010](0010-adaptive-llm-orchestration.md) | Adaptive LLM orchestration and test-time compute | Accepted | partial | `tepp_api` router/ablation/orchestrator binding on the active PR; live NIM execution and production ablation evidence remain accepted-target. | | [0011](0011-standalone-modular-msa-boundary.md) | Standalone operation and modular CWL MSA boundary | Accepted | partial | Owns cross-service persistence/credential/API authority; no direct cross-service application-table coupling. | | [0012](0012-temporal-relational-shared-latent-topic-measurement.md) | Temporal Relational Shared-Latent Topic Measurement (TRSL-TM) | Accepted | accepted-target | Owns topic backend compatibility, global topic identity, method effects, K/model-selection prerequisites, and compositional topic coordinates. | | [0013](0013-bitemporal-persistence-reproducibility-and-split-authority.md) | Bitemporal persistence, reproducibility manifests, and relation-aware split authority | Accepted | partial | Owns PostgreSQL adapter semantics, immutable run/split manifests, leakage-safe partitions, and recovery identity; optional `live-sqlx` `PgPool`, live PG CI, tenant RLS, and `0006` membership implemented-main; `0007` retention/deletion/legal-hold on the active PR; remaining physical ERD/backup accepted-target. | diff --git a/docs/connectors/contextual-orchestrator-interpretation-port.md b/docs/connectors/contextual-orchestrator-interpretation-port.md index 62889fc2..377c91c3 100644 --- a/docs/connectors/contextual-orchestrator-interpretation-port.md +++ b/docs/connectors/contextual-orchestrator-interpretation-port.md @@ -1,7 +1,7 @@ # contextual-orchestrator interpretation port for TEPP -**Status:** Accepted-target modular integration contract -**Last reviewed:** 2026-08-12 +**Status:** Partial modular integration contract — `tepp_api::bind_contextual_orchestrator` is the credential-free binding; live HTTP remains accepted-target. +**Last reviewed:** 2026-08-13 ## Boundary @@ -16,12 +16,12 @@ LLM/provider settings are execution policy only. Deterministic scientific gates ## Allowed orchestration modes -TEPP may allocate test-time computation between: +`tepp_api::route_orchestration` selects `direct`, `verify`, `committee`, `conductor`, or `abstain` from TEPP-owned risk, ambiguity, evidence-sufficiency, and token-budget scores. `tepp_api::bind_contextual_orchestrator` may be called only for a non-abstaining plan and never includes credentials or raw source. TEPP may allocate test-time computation between: 1. direct model routing with bounded reasoning effort; 2. deeper multi-agent workflows with recorded workflow depth, decomposition, access lists, recursion, role-specific reasoning effort, verification/adjudication, and comparable-budget ablations. -These allocations are guided by Fugu, Conductor, and TRINITY research cited in `docs/research/standards-and-literature.md` and `docs/LLM_ORCHESTRATION.md`. +These allocations are guided by Fugu, Conductor, and TRINITY research cited in `docs/research/standards-and-literature.md` and `docs/LLM_ORCHESTRATION.md`. Documents cannot change policy, access lists, or credentials. ## Credential separation diff --git a/docs/research/adaptive-orchestration-router.md b/docs/research/adaptive-orchestration-router.md new file mode 100644 index 00000000..58c4a54e --- /dev/null +++ b/docs/research/adaptive-orchestration-router.md @@ -0,0 +1,47 @@ +# Adaptive orchestration router and comparable-budget ablation + +## Scope + +This note documents the `tepp_api` governed router that implements the first executable slice of ADR 0010 without a database migration and without live model I/O: + +1. `route_orchestration` selects versioned modes `direct`, `verify`, `committee`, `conductor`, or `abstain` from CPU `f64` unit-interval risk, ambiguity, and evidence-sufficiency scores plus an explicit token budget; +2. the plan records workflow stage count, recursion depth, decomposition, TEPP-owned access lists, and role-specific reasoning effort; +3. documents cannot change policy, access lists, or credentials (`DocumentControlAttempt`); +4. blinded model review that failed a deterministic scientific gate abstains — LLM preference cannot rescue a statistically rejected candidate; +5. `record_budget_ablation` requires a `direct` baseline and the same task kind, policy version, and access list before it reports whether the compared budget is within a 10 percent relative band; +6. `bind_contextual_orchestrator` emits a credential-free execution binding, accepts only a canonical lowercase `sha256:` evidence-manifest digest, and refuses abstention. + +Live NVIDIA NIM HTTP, learned conductor calibration, and production-quality claims remain accepted-target. + +## Authoritative sources + +Xu, J., Sun, Q., Schwendeman, P., Nielsen, S., Cetin, E., & Tang, Y. (2026). *TRINITY: An evolved LLM coordinator* [Preprint]. arXiv. https://arxiv.org/abs/2512.04695 + +Nielsen, S., Cetin, E., Schwendeman, P., Sun, Q., Xu, J., & Tang, Y. (2026). *Learning to orchestrate agents in natural language with the Conductor* [Preprint]. arXiv. https://arxiv.org/abs/2512.04388 + +Tang, Y., Cetin, E., Xu, J., Sun, Q., Nielsen, S., Richard, V., Goda, H., Tymchenko, I., Nguyen, N., Lee, H., Ashiga, M., Kotyan, S., Kuroki, S., & Clanuwat, T. (2026). *Sakana Fugu technical report* [Preprint]. arXiv. https://arxiv.org/abs/2606.21228 + +## Application + +TRINITY motivates lightweight model/role delegation rather than a fixed deep graph (Xu et al., 2026). Conductor motivates recording topology, instructions, and recursive test-time scaling as explicit variables (Nielsen et al., 2026). Fugu frames production orchestration as a query-adaptive scaffold behind a model-compatible boundary (Tang et al., 2026). TEPP therefore treats the router as a deterministic policy object: deeper modes must justify themselves against a direct baseline at a comparable budget, and LLM output remains an untrusted proposal under deterministic statistical gates. These citations are experimental motivation, not authority to replace TEPP estimands. + +## Verification + +- low-risk span classification and schema conversion route `direct`; +- material risk, adversarial verification, and low-ambiguity concept/narrative work route `verify`; +- high-ambiguity concept alignment routes `committee`; gated blinded review routes `committee` only with its full minimum budget and otherwise abstains; +- high-complexity narrative synthesis routes `conductor` when the budget allows and steps down otherwise; +- insufficient evidence, failed scientific gates, and sub-minimum budgets abstain; +- document-controlled policy, access, or credentials are denied; +- non-unit scores and unknown policy versions fail closed; +- ablation rejects a non-direct baseline or a comparison that changes task kind, policy version, or access list, and reports incomparable zero or wide-band budgets; +- orchestrator bindings omit credentials, reject raw source and malformed evidence-manifest values, and refuse abstention. + +## Resource-authority boundary + +A request may allocate at most 1,000,000 tokens, 64 TEPP-owned access +capabilities, and 128 UTF-8 bytes per capability token. Duplicate +capabilities fail closed instead of amplifying or obscuring authority. +These are admission limits, not a promise that the full budget will be +consumed; downstream providers remain subject to stricter policy and cost +controls. diff --git a/docs/research/standards-and-literature.md b/docs/research/standards-and-literature.md index 6e0438fe..bfda7a79 100644 --- a/docs/research/standards-and-literature.md +++ b/docs/research/standards-and-literature.md @@ -160,4 +160,4 @@ Nielsen, S., Cetin, E., Schwendeman, P., Sun, Q., Xu, J., & Tang, Y. (2026). Lea Tang, Y., Cetin, E., Xu, J., Sun, Q., Nielsen, S., Richard, V., Goda, H., Tymchenko, I., Nguyen, N., Lee, H., Ashiga, M., Kotyan, S., Kuroki, S., & Clanuwat, T. (2026). *Sakana Fugu technical report* [Preprint]. arXiv. https://arxiv.org/abs/2606.21228 -TRINITY motivates lightweight learned model/role delegation over multiple turns; Conductor motivates query-adaptive natural-language workflow/topology/instruction generation and recursive test-time scaling; Fugu demonstrates a production-oriented family of query-adaptive agentic scaffolds building on these research lines (Xu et al., 2026; Nielsen et al., 2026; Tang et al., 2026). TEPP therefore treats direct routing, verification, fixed multi-agent workflows, adaptive orchestration, stage count, decomposition, recursion, access lists, role-specific reasoning effort, and total test-time budget as explicit experimental variables. Deeper/more-agent orchestration is never assumed better by default. Comparable-budget ablation, evidence support, calibration, disagreement, safety, cost, and failure behavior are required before a production claim. +TRINITY motivates lightweight learned model/role delegation over multiple turns; Conductor motivates query-adaptive natural-language workflow/topology/instruction generation and recursive test-time scaling; Fugu demonstrates a production-oriented family of query-adaptive agentic scaffolds building on these research lines (Xu et al., 2026; Nielsen et al., 2026; Tang et al., 2026). TEPP therefore treats direct routing, verification, fixed multi-agent workflows, adaptive orchestration, stage count, decomposition, recursion, access lists, role-specific reasoning effort, and total test-time budget as explicit experimental variables. `tepp_api::route_orchestration` is the deterministic selector for those variables; live provider execution and production-quality claims remain later work. Deeper/more-agent orchestration is never assumed better by default. Comparable-budget ablation, evidence support, calibration, disagreement, safety, cost, and failure behavior are required before a production claim. See `docs/research/adaptive-orchestration-router.md`. diff --git a/docs/validation/temporal-event-foundation.md b/docs/validation/temporal-event-foundation.md index 7c50db23..aae1a06e 100644 --- a/docs/validation/temporal-event-foundation.md +++ b/docs/validation/temporal-event-foundation.md @@ -23,7 +23,8 @@ This report tracks exact-head scientific and engineering evidence required befor | Truth corpora / manifests | `tepp_simulation` | implemented-main | — | deterministic generator tests | Task 10 / PR #18 | | Recovery metrics | `validation_core` | implemented-main | — | RMSE/bias/coverage/MC gates | Task 11 / PR #19 | | Versioned API/export contracts | `tepp_api` | implemented-main | naruon HTTP interchange | unknown-field/version/limit + naruon HTTPS interchange tests | Task 12 / PR #21; live HTTP service remaining | -| Purpose-bound provider payloads | `tepp_api` | active-PR | provider-payload minimization | expired/not-yet-valid/inverted/cross-tenant/impossible-calendar grant, mapping refusal, audited elevated re-id replay | ADR 0009; `docs/research/provider-payload-minimization.md` | +| Purpose-bound provider payloads | `tepp_api` | implemented-main | provider-payload minimization | expired/not-yet-valid/inverted/cross-tenant/impossible-calendar grant, mapping refusal, audited elevated re-id replay | ADR 0009; `docs/research/provider-payload-minimization.md` | +| Adaptive orchestration router | `tepp_api` | accepted-target | active PR | mode selection, document-control denial, ablation, credential-free bind | ADR 0010; `docs/research/adaptive-orchestration-router.md` | | CWL modular connectors | `docs/connectors/*` | implemented-main | — | contract docs + examples | PR #22; live HTTP ports remaining | | Release SBOM/provenance generator | `scripts/release_evidence.py` | partial | — | generate+validate in CI | Task 13 partial / PR #28 | From 7ffb65b2b4d04b85b0f2a43ed5fb23c9828adcf1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 13:16:02 +0000 Subject: [PATCH 13/62] feat(psychometric): evaluate Voelkle Eq. 12 without premature product overflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Evaluate b*_y.x(Δt) as a_yx (expm1(z)/z · Δt) so a finite Eq. 12 result is not lost when a_yx Δt overflows (CodeRabbit on 1fdcd23; a_yx=1e308, a_xx=-100, Δt=10). Algebraically identical to (a_yx / a_xx)(exp(a_xx Δt) − 1). ZORA accepted manuscript re-opened 2026-08-17T13:13Z, p. 16. The first-order product is still not the general discrete effect. Still not DSEM. Meredith (1993) and Mislevy (1991, DOI 10.1007/bf02294457) remain unread (Unpaywall/OpenAlex 2026-08-17T13:13Z: closed). --- CHANGELOG.md | 1 + CLAUDE.md | 2 +- crates/psychometric_core/src/event_time.rs | 20 ++++++++++++++----- ...multilevel_event_time_recovery_contract.rs | 11 ++++++++++ docs/adr/0005-posterior-esem-dsem.md | 2 +- .../multilevel-event-time-recovery.md | 6 +++--- docs/research/standards-and-literature.md | 2 +- 7 files changed, 33 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff34b27c..fdfeea3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang ### Added +- `psychometric_core` evaluates Voelkle et al. (2012, Eq. 12) as \(a_{yx}(\operatorname{expm1}(z)/z\cdot\Delta t)\) with \(z=a_{xx}\Delta t\). That order is algebraically identical to \((a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). Multiplying the unitless scale by \(\Delta t\) before \(a_{yx}\) keeps a finite Eq. 12 result when \(a_{yx}\Delta t\) overflows (ZORA accepted manuscript re-opened 2026-08-17T13:13Z, p. 16). Binary64 underflow of \(z\) to `+0` recovers the Eq. 12 limit \(a_{yx}\Delta t\); the first-order product is still not the general discrete effect. Driver, Oud, and Voelkle (2017, p. 4) restated the discrete intercept as a function of \(A\) and \(\Delta t\). Still not DSEM. - `psychometric_core` evaluates Voelkle et al. (2012, Eq. 12) as \(a_{yx}\Delta t\,(\operatorname{expm1}(z)/z)\) with \(z=a_{xx}\Delta t\). That order is algebraically identical to \((a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). Binary64 underflow of \(z\) to `+0` recovers the Eq. 12 limit \(a_{yx}\Delta t\); the first-order product is still not the general discrete effect. Driver, Oud, and Voelkle (2017, p. 4) restated the discrete intercept as a function of \(A\) and \(\Delta t\) (PDF re-opened 2026-08-17T12:04Z). Still not DSEM. - `psychometric_core` exact scalar discrete effect of a constant event-time predictor (Voelkle et al., 2012, Eq. 12): \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\) for \(a_{xx}\neq 0\). The first-order product \(a_{yx}\Delta t\) is not that discrete effect. Still not DSEM. - `psychometric_core` exact scalar forward map `φ(Δt) = exp(a Δt)` and interval remapping: a discrete lag at one event interval maps onto another through the Voelkle et al. (2012, Eq. 7) log-rate. Pooling discrete lags from unequal intervals fails closed. Still not a matrix `expm` and not DSEM. diff --git a/CLAUDE.md b/CLAUDE.md index 833e0619..1acf4476 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. -- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx Δt (expm1(z)/z)` with `z = a_xx Δt`. The first-order product is the underflow limit of that equation, not the general discrete effect. +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z)/z * Δt)` with `z = a_xx Δt` so a finite result is not lost when `a_yx Δt` overflows. The first-order product is the underflow limit of that equation, not the general discrete effect. - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index 875548d4..50e8d06c 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -223,8 +223,9 @@ pub fn map_discrete_lag_across_event_intervals( /// `b*_y.x(Δt) = (a_yx / a_xx) (exp(a_xx Δt) − 1)` for `a_xx ≠ 0`. /// Driver, Oud, and Voelkle (2017, p. 4, after Eq. 3) restate the same /// discrete intercept as a function of `A` and `Δt`. The algebraically -/// identical evaluation is `a_yx Δt (expm1(z) / z)` with `z = a_xx Δt`. -/// That order is Eq. 12, not the first-order product. When binary64 `z` +/// identical evaluation is `a_yx (expm1(z) / z * Δt)` with `z = a_xx Δt`. +/// The unitless scale multiplies `Δt` before `a_yx` so a finite Eq. 12 +/// result is not lost when `a_yx Δt` overflows. When binary64 `z` /// underflows to `+0`, the mathematical limit of Eq. 12 is `a_yx Δt`. /// Using that limit only at underflow is IEEE-754 evaluation of Eq. 12. /// The first-order product is not the general discrete effect. This is @@ -260,9 +261,10 @@ pub fn recover_discrete_constant_predictor_effect( // Binary64 underflow of a_xx Δt. lim z→0 of Eq. 12 is a_yx Δt. return require_finite(outcome_on_predictor * event_delta); } - require_finite( - outcome_on_predictor * event_delta * (increment_argument.exp_m1() / increment_argument), - ) + // Multiply expm1(z)/z by Δt first. a_yx * Δt can overflow a finite + // Eq. 12 result (a_yx = 1e308, a_xx = -100, Δt = 10 → ≈ 1e306). + let scaled_interval = increment_argument.exp_m1() / increment_argument * event_delta; + require_finite(outcome_on_predictor * scaled_interval) } /// Refuse treating discrete lags from unequal event intervals as one coefficient. @@ -727,6 +729,14 @@ mod tests { .expect("eq 12 scaled"); assert!(tiny_nonzero.is_finite()); assert!((tiny_nonzero - 1e154).abs() / 1e154 < 1e-12); + // a_yx Δt overflows; Eq. 12 remains finite (Voelkle 2012, Eq. 12). + let product_overflow = + recover_discrete_constant_predictor_effect(1e308, -100.0, 10.0, LagClock::EventTime) + .expect("eq 12 finite after a_yx Δt overflow"); + let product_overflow_expected = (1e308 / -100.0) * (-100.0_f64 * 10.0).exp_m1(); + assert!((product_overflow - product_overflow_expected).abs() / 1e306 < 1e-12); + assert!(product_overflow.is_finite()); + assert!(!(1e308_f64 * 10.0).is_finite()); } #[test] 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 2636d97a..6f925480 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -189,6 +189,17 @@ fn constant_predictor_discrete_effect_recovers_equation_twelve() { underflow_error < 1e-15, "Eq. 12 binary64 underflow limit RMSE {underflow_error}" ); + // a_yx Δt overflows; Eq. 12 remains finite. + let product_overflow = + recover_discrete_constant_predictor_effect(1e308, -100.0, 10.0, LagClock::EventTime) + .expect("eq 12 finite after a_yx Δt overflow"); + let product_overflow_truth = (1e308 / -100.0) * (-100.0_f64 * 10.0).exp_m1(); + let product_overflow_error = rmse(&[product_overflow_truth], &[product_overflow]); + assert!( + product_overflow_error / 1e306 < 1e-12, + "Eq. 12 a_yx Δt overflow RMSE {product_overflow_error}" + ); + assert!(!(1e308_f64 * 10.0).is_finite()); } #[test] diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index 839917bd..5e0e2c70 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx Δt (expm1(z)/z)` with `z = a_xx Δt`, and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is the underflow limit of Eq. 12, not the general discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z)/z * Δt)` with `z = a_xx Δt` so a finite result is not lost when `a_yx Δt` overflows, and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is the underflow limit of Eq. 12, not the general discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. 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 684e3279..8a3962df 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -34,7 +34,7 @@ Driver, C. C., Oud, J. H. L., & Voelkle, M. C. (2017). Continuous time structura Kish, L. (1965). *Survey sampling*. John Wiley & Sons. -The Voelkle et al. (2012) ZORA accepted manuscript was opened 2026-08-17 (https://www.zora.uzh.ch/handle/20.500.14742/72792). This cycle the ZORA bitstream was Anubis-blocked (2026-08-17T12:03Z). Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4; JSS PDF re-opened 2026-08-17T12:04Z) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\) and restate the discrete intercept as a function of \(A\) and \(\Delta t\). Discrete auto-effects are strictly positive for finite real drift and interval; the inverse \(a=\ln\varphi/\Delta t\) therefore requires \(\varphi>0\). Binary64 `exp` of a large negative argument is `+0` and is refused. Equations 3–4 of Voelkle et al. (2012) are the discouraged difference-quotient approximation. Meredith (1993) remains unread (Unpaywall/OpenAlex/Semantic Scholar/Cambridge Core/Springer 2026-08-17T12:04Z: closed). Mislevy (1991) remains unread. The 1988 ETS RR-88-45 / DTIC ADA200179 technical report was opened 2026-08-17T12:04Z and is not the 1991 journal article. +The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-17T13:13Z (https://www.zora.uzh.ch/handle/20.500.14742/72792; bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`). Page 16 writes discrete auto-effects as \(A^{*}(\Delta t)=\exp(A\Delta t)\) (Eq. 7) and the constant-predictor discrete effect as \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\) (Eq. 12). Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\) and restate the discrete intercept as a function of \(A\) and \(\Delta t\). Discrete auto-effects are strictly positive for finite real drift and interval; the inverse \(a=\ln\varphi/\Delta t\) therefore requires \(\varphi>0\). Binary64 `exp` of a large negative argument is `+0` and is refused. Equations 3–4 of Voelkle et al. (2012) are the discouraged difference-quotient approximation. Meredith (1993) remains unread (Unpaywall/OpenAlex 2026-08-17T13:13Z: closed; Cambridge Core redirected to an HTML product page; Springer content/pdf was an HTML stub). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex 2026-08-17T13:13Z: closed). The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title is not the 1991 journal article. ## Formula notes @@ -43,7 +43,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was opened 2026-08-17 (https: - **Kish ESS.** \(\mathrm{ESS}=(\sum w)^{2}/\sum w^{2}\) on non-negative finite weights. WLS uses the weights in the slope; ESS is not a second slope. - **Exact scalar map.** Voelkle et al. (2012, Eq. 7) and Driver et al. (2017, Eq. 3): \(\varphi = A^{*}(\Delta t)=\exp(a\,\Delta t)\). The inverse is \(a=\ln\varphi/\Delta t\). The forward map is the same equation. The real exponential is strictly positive; a binary64 underflow to `+0` is refused because the inverse logarithm does not exist at zero. The difference quotient \((x(t+\Delta t)-x(t))/\Delta t\) is refused. - **Unequal-interval remap.** Discrete \(\varphi(\Delta t_1)\) and \(\varphi(\Delta t_2)\) are not comparable when \(\Delta t_1\neq\Delta t_2\) (Voelkle et al., 2012, ZORA manuscript pp. 2, 16, 33). The licensed path is \(a=\ln\varphi_{\mathrm{src}}/\Delta t_{\mathrm{src}}\) then \(\varphi_{\mathrm{ref}}=\exp(a\,\Delta t_{\mathrm{ref}})\). Pooling those discrete lags fails closed. -- **Constant-predictor discrete effect.** Voelkle et al. (2012, Eq. 12; ZORA accepted manuscript p. 16): for a constant predictor with \(a_{xx}\neq 0\), \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). Driver, Oud, and Voelkle (2017, p. 4, after Eq. 3; PDF re-opened 2026-08-17T12:04Z) restate the discrete intercept as a function of \(A\) and \(\Delta t\). The algebraically identical evaluation is \(a_{yx}\Delta t\,(\operatorname{expm1}(z)/z)\) with \(z=a_{xx}\Delta t\). When binary64 \(z\) underflows to `+0`, the mathematical limit of Eq. 12 is \(a_{yx}\Delta t\). That limit is IEEE-754 evaluation of Eq. 12, not a substitution of the first-order product as the general discrete effect. \(a_{xx}=0\) fails closed. This is not DSEM. +- **Constant-predictor discrete effect.** Voelkle et al. (2012, Eq. 12; ZORA accepted manuscript re-opened 2026-08-17T13:13Z, p. 16): for a constant predictor with \(a_{xx}\neq 0\), \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). Driver, Oud, and Voelkle (2017, p. 4, after Eq. 3) restate the discrete intercept as a function of \(A\) and \(\Delta t\). The algebraically identical evaluation is \(a_{yx}(\operatorname{expm1}(z)/z\cdot\Delta t)\) with \(z=a_{xx}\Delta t\). The unitless scale multiplies \(\Delta t\) before \(a_{yx}\) so a finite Eq. 12 result is not lost when \(a_{yx}\Delta t\) overflows. When binary64 \(z\) underflows to `+0`, the mathematical limit of Eq. 12 is \(a_{yx}\Delta t\). That limit is IEEE-754 evaluation of Eq. 12, not a substitution of the first-order product as the general discrete effect. \(a_{xx}=0\) fails closed. This is not DSEM. - **CWC-then-lag.** Sample cluster means are removed first. Consecutive within residuals are then fitted by least squares to \(r_{t+\Delta t}\approx\exp(a\Delta t)\,r_{t}\) on event time. Same-sign pair-wise logs initialize the scalar Newton step. Sign-flipping \(T=2\) CWC pairs have no real logarithm and fail closed. Curran and Bauer (2011, pp. 607–608) show that this person-mean subtraction on a raw autoregressive series does **not** isolate the lagged within-person effect; the helper therefore does not claim to recover the raw-process drift. - **Already-centered irregular residual.** The caller supplies lagged within residuals. The mean of \(a=\ln(r_{t+\Delta t}/r_t)/\Delta t\) is the exact scalar map. Intervals may be irregular. The helper does not center again. This is not DSEM. @@ -55,7 +55,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was opened 2026-08-17 (https: - the exact scalar map recovers a known drift on event time and refuses every other clock plus the difference quotient; - the forward map inverts the log-rate, remaps \(\varphi(1)\) onto \(\varphi(2)\) at machine-scale RMSE, and that RMSE is smaller than treating \(\varphi(1)\) as \(\varphi(2)\); - a binary64 underflow of \(\exp(a\Delta t)\) to `+0` (direct forward map and large-interval remap) fails closed; -- Voelkle et al. (2012, Eq. 12) recovers a known discrete constant-predictor effect at machine-scale RMSE, and that RMSE is smaller than the first-order product \(a_{yx}\Delta t\); \(a_{xx}=0\) fails closed; binary64 underflow of \(a_{xx}\Delta t\) to `+0` recovers the Eq. 12 limit \(a_{yx}\Delta t\); +- Voelkle et al. (2012, Eq. 12) recovers a known discrete constant-predictor effect at machine-scale RMSE, and that RMSE is smaller than the first-order product \(a_{yx}\Delta t\); \(a_{xx}=0\) fails closed; binary64 underflow of \(a_{xx}\Delta t\) to `+0` recovers the Eq. 12 limit \(a_{yx}\Delta t\); a finite Eq. 12 result whose first factor \(a_{yx}\Delta t\) overflows is recovered; - pooling discrete lags from unequal intervals fails closed; - CWC-then-lag on a two-cluster decaying series has smaller computed RMSE than a level-pooled series when the latter is identified; - already-centered irregular residuals recover a known drift at machine-scale RMSE, and that RMSE is smaller than CWC of the corresponding raw autoregressive series (Curran & Bauer, 2011, pp. 607–608); diff --git a/docs/research/standards-and-literature.md b/docs/research/standards-and-literature.md index 1ef169ba..325f807d 100644 --- a/docs/research/standards-and-literature.md +++ b/docs/research/standards-and-literature.md @@ -34,7 +34,7 @@ Meredith, W. (1993). Measurement invariance, factor analysis and factorial invar Holland, P. W. (1986). Statistics and causal inference. *Journal of the American Statistical Association, 81*(396), 945–960. https://doi.org/10.1080/01621459.1986.10478354 -TEPP applies these sources to construct definition, score interpretation, reliability, validity evidence, uncertainty, consequences, longitudinal invariance, ESEM cross-loadings, and DSEM. Topic outputs are treated as fallible indicators or components only after their construct role is evaluated. Reflective, formative, and network classes remain distinct (Bollen & Lennox, 1991). Complete-data OLS loadings across posterior indicator draws are combined with Rubin (1996) \(T_m\); the arithmetic-mean helper remains a point estimate. Mislevy (1991) remains unread (Unpaywall/OpenAlex/Semantic Scholar/Cambridge Core/ERIC 2026-08-17T12:04Z: closed). The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title was opened 2026-08-17T12:04Z from archive.org; it is not the 1991 journal article and is not used as Mislevy plausible-value authority. Temporal precedence is not causal identification (Holland, 1986). Within/between OLS follows Enders and Tofighi (2007), Curran and Bauer (2011), and Hamaker et al. (2015). Enders and Tofighi (2007, Table 2, pp. 124–127; PDF opened 2026-08-17) show that the CWC cluster-mean coefficient is the contextual effect (`between − within`), not the between-cluster effect. Curran and Bauer (2011, pp. 607–608) reject person-mean subtraction on a raw autoregressive series as the lagged within-person residual; already-centered irregular residuals use the Voelkle et al. (2012, Eq. 7) / Driver et al. (2017, Eq. 3) scalar map. Discrete lags from unequal event intervals are remapped through that log-rate (Voelkle et al., 2012, ZORA accepted manuscript opened 2026-08-17; ZORA bitstream Anubis-blocked 2026-08-17T12:03Z this cycle) and are not pooled. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4; PDF re-opened 2026-08-17T12:04Z) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\) and restate the discrete intercept as a function of \(A\) and \(\Delta t\). A binary64 underflow of \(\exp(a\Delta t)\) to `+0` is refused because discrete auto-effects are strictly positive. The discrete effect of a constant predictor is Voelkle et al. (2012, Eq. 12), evaluated as \(a_{yx}\Delta t\,(\operatorname{expm1}(z)/z)\) with \(z=a_{xx}\Delta t\); the first-order product is the underflow limit of that equation, not the general discrete effect. Metric/weak invariance does not license latent-mean comparison. Meredith (1993) remains unread (Unpaywall/OpenAlex/Semantic Scholar/Cambridge Core/Springer 2026-08-17T12:04Z: closed). +TEPP applies these sources to construct definition, score interpretation, reliability, validity evidence, uncertainty, consequences, longitudinal invariance, ESEM cross-loadings, and DSEM. Topic outputs are treated as fallible indicators or components only after their construct role is evaluated. Reflective, formative, and network classes remain distinct (Bollen & Lennox, 1991). Complete-data OLS loadings across posterior indicator draws are combined with Rubin (1996) \(T_m\); the arithmetic-mean helper remains a point estimate. Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex 2026-08-17T13:13Z: closed). The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title was opened 2026-08-17T12:04Z from archive.org; it is not the 1991 journal article and is not used as Mislevy plausible-value authority. Temporal precedence is not causal identification (Holland, 1986). Within/between OLS follows Enders and Tofighi (2007), Curran and Bauer (2011), and Hamaker et al. (2015). Enders and Tofighi (2007, Table 2, pp. 124–127; PDF opened 2026-08-17) show that the CWC cluster-mean coefficient is the contextual effect (`between − within`), not the between-cluster effect. Curran and Bauer (2011, pp. 607–608) reject person-mean subtraction on a raw autoregressive series as the lagged within-person residual; already-centered irregular residuals use the Voelkle et al. (2012, Eq. 7) / Driver et al. (2017, Eq. 3) scalar map. Discrete lags from unequal event intervals are remapped through that log-rate (Voelkle et al., 2012, ZORA accepted manuscript re-opened 2026-08-17T13:13Z) and are not pooled. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\) and restate the discrete intercept as a function of \(A\) and \(\Delta t\). A binary64 underflow of \(\exp(a\Delta t)\) to `+0` is refused because discrete auto-effects are strictly positive. The discrete effect of a constant predictor is Voelkle et al. (2012, Eq. 12), evaluated as \(a_{yx}(\operatorname{expm1}(z)/z\cdot\Delta t)\) with \(z=a_{xx}\Delta t\) so a finite result is not lost when \(a_{yx}\Delta t\) overflows; the first-order product is the underflow limit of that equation, not the general discrete effect. Metric/weak invariance does not license latent-mean comparison. Meredith (1993) remains unread (Unpaywall/OpenAlex 2026-08-17T13:13Z: closed; Cambridge Core redirected to an HTML product page; Springer content/pdf was an HTML stub). ## Structural, correlated, dynamic, relational, and multilingual topic models From 0d8ae63b4f57c915e210ad172a999870893e35f4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 13:22:38 +0000 Subject: [PATCH 14/62] feat(psychometric): evaluate Voelkle Eq. 12 without negative increment overflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When z = a_xx Δt overflows to -∞, expm1(z)/z collapses to +0 and loses the finite Eq. 12 equilibrium increment -a_yx/a_xx. Evaluate as a_yx (expm1(z)/a_xx). Still not DSEM. --- CHANGELOG.md | 1 + CLAUDE.md | 2 +- crates/psychometric_core/src/event_time.rs | 48 +++++++++++++------ ...multilevel_event_time_recovery_contract.rs | 17 +++++++ docs/adr/0005-posterior-esem-dsem.md | 2 +- .../multilevel-event-time-recovery.md | 6 +-- docs/research/standards-and-literature.md | 2 +- 7 files changed, 57 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fdfeea3d..268b010d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang ### Added +- `psychometric_core` evaluates Voelkle et al. (2012, Eq. 12) as \(a_{yx}(\operatorname{expm1}(z)/a_{xx})\) with \(z=a_{xx}\Delta t\). That order is algebraically identical to \((a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). Dividing the increment by the finite auto-effect keeps the equilibrium increment \(-a_{yx}/a_{xx}\) when \(z\) overflows to \(-\infty\) (ZORA accepted manuscript, Introducing Intercepts: the exponential vanishes as \(\Delta t\) grows; CodeRabbit finding on `7ffb65b`). The prior \(a_{yx}\Delta t\) overflow case remains finite. Binary64 underflow of \(z\) to `+0` recovers the Eq. 12 limit \(a_{yx}\Delta t\); the first-order product is still not the general discrete effect. Still not DSEM. - `psychometric_core` evaluates Voelkle et al. (2012, Eq. 12) as \(a_{yx}(\operatorname{expm1}(z)/z\cdot\Delta t)\) with \(z=a_{xx}\Delta t\). That order is algebraically identical to \((a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). Multiplying the unitless scale by \(\Delta t\) before \(a_{yx}\) keeps a finite Eq. 12 result when \(a_{yx}\Delta t\) overflows (ZORA accepted manuscript re-opened 2026-08-17T13:13Z, p. 16). Binary64 underflow of \(z\) to `+0` recovers the Eq. 12 limit \(a_{yx}\Delta t\); the first-order product is still not the general discrete effect. Driver, Oud, and Voelkle (2017, p. 4) restated the discrete intercept as a function of \(A\) and \(\Delta t\). Still not DSEM. - `psychometric_core` evaluates Voelkle et al. (2012, Eq. 12) as \(a_{yx}\Delta t\,(\operatorname{expm1}(z)/z)\) with \(z=a_{xx}\Delta t\). That order is algebraically identical to \((a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). Binary64 underflow of \(z\) to `+0` recovers the Eq. 12 limit \(a_{yx}\Delta t\); the first-order product is still not the general discrete effect. Driver, Oud, and Voelkle (2017, p. 4) restated the discrete intercept as a function of \(A\) and \(\Delta t\) (PDF re-opened 2026-08-17T12:04Z). Still not DSEM. - `psychometric_core` exact scalar discrete effect of a constant event-time predictor (Voelkle et al., 2012, Eq. 12): \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\) for \(a_{xx}\neq 0\). The first-order product \(a_{yx}\Delta t\) is not that discrete effect. Still not DSEM. diff --git a/CLAUDE.md b/CLAUDE.md index 1acf4476..8e0604c3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. -- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z)/z * Δt)` with `z = a_xx Δt` so a finite result is not lost when `a_yx Δt` overflows. The first-order product is the underflow limit of that equation, not the general discrete effect. +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. The first-order product is the underflow limit of that equation, not the general discrete effect. - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index 50e8d06c..7f6b3c72 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -219,17 +219,18 @@ pub fn map_discrete_lag_across_event_intervals( /// Exact scalar discrete effect of a constant event-time predictor. /// -/// Voelkle et al. (2012, Eq. 12; ZORA accepted manuscript p. 16): -/// `b*_y.x(Δt) = (a_yx / a_xx) (exp(a_xx Δt) − 1)` for `a_xx ≠ 0`. -/// Driver, Oud, and Voelkle (2017, p. 4, after Eq. 3) restate the same -/// discrete intercept as a function of `A` and `Δt`. The algebraically -/// identical evaluation is `a_yx (expm1(z) / z * Δt)` with `z = a_xx Δt`. -/// The unitless scale multiplies `Δt` before `a_yx` so a finite Eq. 12 -/// result is not lost when `a_yx Δt` overflows. When binary64 `z` -/// underflows to `+0`, the mathematical limit of Eq. 12 is `a_yx Δt`. -/// Using that limit only at underflow is IEEE-754 evaluation of Eq. 12. -/// The first-order product is not the general discrete effect. This is -/// not DSEM and not a matrix `expm`. +/// Voelkle et al. (2012, Eq. 12; ZORA accepted manuscript, Introducing +/// Intercepts): `b*_y.x(Δt) = (a_yx / a_xx) (exp(a_xx Δt) − 1)` for +/// `a_xx ≠ 0`. Driver, Oud, and Voelkle (2017, p. 4, after Eq. 3) +/// restate the same discrete intercept as a function of `A` and `Δt`. +/// The algebraically identical evaluation is `a_yx (expm1(z) / a_xx)` +/// with `z = a_xx Δt`. Dividing the increment by the finite auto-effect +/// keeps a finite Eq. 12 result when `z` overflows to `-∞` +/// (`exp(z) → 0`, so Eq. 12 → `-a_yx / a_xx`) and when `a_yx Δt` +/// overflows. When binary64 `z` underflows to `+0`, the mathematical +/// limit of Eq. 12 is `a_yx Δt`. Using that limit only at underflow is +/// IEEE-754 evaluation of Eq. 12. The first-order product is not the +/// general discrete effect. This is not DSEM and not a matrix `expm`. /// /// # Errors /// @@ -261,10 +262,10 @@ pub fn recover_discrete_constant_predictor_effect( // Binary64 underflow of a_xx Δt. lim z→0 of Eq. 12 is a_yx Δt. return require_finite(outcome_on_predictor * event_delta); } - // Multiply expm1(z)/z by Δt first. a_yx * Δt can overflow a finite - // Eq. 12 result (a_yx = 1e308, a_xx = -100, Δt = 10 → ≈ 1e306). - let scaled_interval = increment_argument.exp_m1() / increment_argument * event_delta; - require_finite(outcome_on_predictor * scaled_interval) + // Divide expm1(z) by the finite a_xx, not by z. expm1(-∞)/-∞ is +0 + // and loses the equilibrium increment -a_yx/a_xx (Voelkle 2012, + // Introducing Intercepts: the exponential vanishes as Δt grows). + require_finite(outcome_on_predictor * (increment_argument.exp_m1() / predictor_log_rate)) } /// Refuse treating discrete lags from unequal event intervals as one coefficient. @@ -739,6 +740,23 @@ mod tests { assert!(!(1e308_f64 * 10.0).is_finite()); } + #[test] + fn constant_predictor_negative_overflow_recovers_equilibrium_increment() { + // z → -∞: expm1(z)/z * Δt is +0; Eq. 12 → -a_yx/a_xx (Voelkle + // 2012, Introducing Intercepts equilibrium increment). + let increment_argument = -1e308_f64 * 2.0; + assert!(increment_argument.is_infinite() && increment_argument.is_sign_negative()); + let lost_scale = increment_argument.exp_m1() / increment_argument * 2.0; + assert_eq!(lost_scale.to_bits(), 0.0_f64.to_bits()); + let negative_overflow = + recover_discrete_constant_predictor_effect(1.0, -1e308, 2.0, LagClock::EventTime) + .expect("eq 12 equilibrium increment"); + let negative_overflow_expected = -(1.0 / -1e308); + assert!((negative_overflow - negative_overflow_expected).abs() / 1e-308 < 1e-12); + assert!(negative_overflow > 0.0); + assert!(negative_overflow.is_finite()); + } + #[test] fn non_event_clocks_and_difference_quotient_fail_closed() { for clock in [ 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 6f925480..0773b646 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -200,6 +200,23 @@ fn constant_predictor_discrete_effect_recovers_equation_twelve() { "Eq. 12 a_yx Δt overflow RMSE {product_overflow_error}" ); assert!(!(1e308_f64 * 10.0).is_finite()); + // z → -∞: expm1(z)/z * Δt is +0; Eq. 12 → -a_yx/a_xx. + let increment_argument = -1e308_f64 * 2.0; + assert!(increment_argument.is_infinite() && increment_argument.is_sign_negative()); + assert_eq!( + (increment_argument.exp_m1() / increment_argument * 2.0).to_bits(), + 0.0_f64.to_bits() + ); + let negative_overflow = + recover_discrete_constant_predictor_effect(1.0, -1e308, 2.0, LagClock::EventTime) + .expect("eq 12 equilibrium increment"); + let negative_overflow_truth = -(1.0 / -1e308); + let negative_overflow_error = rmse(&[negative_overflow_truth], &[negative_overflow]); + assert!( + negative_overflow_error / 1e-308 < 1e-12, + "Eq. 12 z→-∞ equilibrium RMSE {negative_overflow_error}" + ); + assert!(negative_overflow > 0.0); } #[test] diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index 5e0e2c70..d6657e7f 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z)/z * Δt)` with `z = a_xx Δt` so a finite result is not lost when `a_yx Δt` overflows, and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is the underflow limit of Eq. 12, not the general discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is the underflow limit of Eq. 12, not the general discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. 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 8a3962df..20e4d77b 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -34,7 +34,7 @@ Driver, C. C., Oud, J. H. L., & Voelkle, M. C. (2017). Continuous time structura Kish, L. (1965). *Survey sampling*. John Wiley & Sons. -The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-17T13:13Z (https://www.zora.uzh.ch/handle/20.500.14742/72792; bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`). Page 16 writes discrete auto-effects as \(A^{*}(\Delta t)=\exp(A\Delta t)\) (Eq. 7) and the constant-predictor discrete effect as \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\) (Eq. 12). Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\) and restate the discrete intercept as a function of \(A\) and \(\Delta t\). Discrete auto-effects are strictly positive for finite real drift and interval; the inverse \(a=\ln\varphi/\Delta t\) therefore requires \(\varphi>0\). Binary64 `exp` of a large negative argument is `+0` and is refused. Equations 3–4 of Voelkle et al. (2012) are the discouraged difference-quotient approximation. Meredith (1993) remains unread (Unpaywall/OpenAlex 2026-08-17T13:13Z: closed; Cambridge Core redirected to an HTML product page; Springer content/pdf was an HTML stub). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex 2026-08-17T13:13Z: closed). The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title is not the 1991 journal article. +The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-17T13:13Z (https://www.zora.uzh.ch/handle/20.500.14742/72792; bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`). Page 16 writes discrete auto-effects as \(A^{*}(\Delta t)=\exp(A\Delta t)\) (Eq. 7) and the constant-predictor discrete effect as \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\) (Eq. 12). Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\) and restate the discrete intercept as a function of \(A\) and \(\Delta t\). Discrete auto-effects are strictly positive for finite real drift and interval; the inverse \(a=\ln\varphi/\Delta t\) therefore requires \(\varphi>0\). Binary64 `exp` of a large negative argument is `+0` and is refused. Equations 3–4 of Voelkle et al. (2012) are the discouraged difference-quotient approximation. Meredith (1993) remains unread (Unpaywall/OpenAlex 2026-08-17T13:23Z: closed; Cambridge Core redirected to an HTML product page; Springer content/pdf was an HTML stub). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex 2026-08-17T13:23Z: closed). The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title is not the 1991 journal article. ## Formula notes @@ -43,7 +43,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-17T13:1 - **Kish ESS.** \(\mathrm{ESS}=(\sum w)^{2}/\sum w^{2}\) on non-negative finite weights. WLS uses the weights in the slope; ESS is not a second slope. - **Exact scalar map.** Voelkle et al. (2012, Eq. 7) and Driver et al. (2017, Eq. 3): \(\varphi = A^{*}(\Delta t)=\exp(a\,\Delta t)\). The inverse is \(a=\ln\varphi/\Delta t\). The forward map is the same equation. The real exponential is strictly positive; a binary64 underflow to `+0` is refused because the inverse logarithm does not exist at zero. The difference quotient \((x(t+\Delta t)-x(t))/\Delta t\) is refused. - **Unequal-interval remap.** Discrete \(\varphi(\Delta t_1)\) and \(\varphi(\Delta t_2)\) are not comparable when \(\Delta t_1\neq\Delta t_2\) (Voelkle et al., 2012, ZORA manuscript pp. 2, 16, 33). The licensed path is \(a=\ln\varphi_{\mathrm{src}}/\Delta t_{\mathrm{src}}\) then \(\varphi_{\mathrm{ref}}=\exp(a\,\Delta t_{\mathrm{ref}})\). Pooling those discrete lags fails closed. -- **Constant-predictor discrete effect.** Voelkle et al. (2012, Eq. 12; ZORA accepted manuscript re-opened 2026-08-17T13:13Z, p. 16): for a constant predictor with \(a_{xx}\neq 0\), \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). Driver, Oud, and Voelkle (2017, p. 4, after Eq. 3) restate the discrete intercept as a function of \(A\) and \(\Delta t\). The algebraically identical evaluation is \(a_{yx}(\operatorname{expm1}(z)/z\cdot\Delta t)\) with \(z=a_{xx}\Delta t\). The unitless scale multiplies \(\Delta t\) before \(a_{yx}\) so a finite Eq. 12 result is not lost when \(a_{yx}\Delta t\) overflows. When binary64 \(z\) underflows to `+0`, the mathematical limit of Eq. 12 is \(a_{yx}\Delta t\). That limit is IEEE-754 evaluation of Eq. 12, not a substitution of the first-order product as the general discrete effect. \(a_{xx}=0\) fails closed. This is not DSEM. +- **Constant-predictor discrete effect.** Voelkle et al. (2012, Eq. 12; ZORA accepted manuscript re-opened 2026-08-17T13:23Z, Introducing Intercepts): for a constant predictor with \(a_{xx}\neq 0\), \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). Driver, Oud, and Voelkle (2017, p. 4, after Eq. 3) restate the discrete intercept as a function of \(A\) and \(\Delta t\). The algebraically identical evaluation is \(a_{yx}(\operatorname{expm1}(z)/a_{xx})\) with \(z=a_{xx}\Delta t\). Dividing the increment by the finite auto-effect keeps the equilibrium increment \(-a_{yx}/a_{xx}\) when \(z\) overflows to \(-\infty\) (the manuscript notes that the exponential vanishes as \(\Delta t\) grows) and keeps a finite result when \(a_{yx}\Delta t\) overflows. When binary64 \(z\) underflows to `+0`, the mathematical limit of Eq. 12 is \(a_{yx}\Delta t\). That limit is IEEE-754 evaluation of Eq. 12, not a substitution of the first-order product as the general discrete effect. \(a_{xx}=0\) fails closed. This is not DSEM. - **CWC-then-lag.** Sample cluster means are removed first. Consecutive within residuals are then fitted by least squares to \(r_{t+\Delta t}\approx\exp(a\Delta t)\,r_{t}\) on event time. Same-sign pair-wise logs initialize the scalar Newton step. Sign-flipping \(T=2\) CWC pairs have no real logarithm and fail closed. Curran and Bauer (2011, pp. 607–608) show that this person-mean subtraction on a raw autoregressive series does **not** isolate the lagged within-person effect; the helper therefore does not claim to recover the raw-process drift. - **Already-centered irregular residual.** The caller supplies lagged within residuals. The mean of \(a=\ln(r_{t+\Delta t}/r_t)/\Delta t\) is the exact scalar map. Intervals may be irregular. The helper does not center again. This is not DSEM. @@ -55,7 +55,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-17T13:1 - the exact scalar map recovers a known drift on event time and refuses every other clock plus the difference quotient; - the forward map inverts the log-rate, remaps \(\varphi(1)\) onto \(\varphi(2)\) at machine-scale RMSE, and that RMSE is smaller than treating \(\varphi(1)\) as \(\varphi(2)\); - a binary64 underflow of \(\exp(a\Delta t)\) to `+0` (direct forward map and large-interval remap) fails closed; -- Voelkle et al. (2012, Eq. 12) recovers a known discrete constant-predictor effect at machine-scale RMSE, and that RMSE is smaller than the first-order product \(a_{yx}\Delta t\); \(a_{xx}=0\) fails closed; binary64 underflow of \(a_{xx}\Delta t\) to `+0` recovers the Eq. 12 limit \(a_{yx}\Delta t\); a finite Eq. 12 result whose first factor \(a_{yx}\Delta t\) overflows is recovered; +- Voelkle et al. (2012, Eq. 12) recovers a known discrete constant-predictor effect at machine-scale RMSE, and that RMSE is smaller than the first-order product \(a_{yx}\Delta t\); \(a_{xx}=0\) fails closed; binary64 underflow of \(a_{xx}\Delta t\) to `+0` recovers the Eq. 12 limit \(a_{yx}\Delta t\); a finite Eq. 12 result whose first factor \(a_{yx}\Delta t\) overflows is recovered; a finite Eq. 12 equilibrium increment whose \(z=a_{xx}\Delta t\) overflows to \(-\infty\) is recovered as \(-a_{yx}/a_{xx}\); - pooling discrete lags from unequal intervals fails closed; - CWC-then-lag on a two-cluster decaying series has smaller computed RMSE than a level-pooled series when the latter is identified; - already-centered irregular residuals recover a known drift at machine-scale RMSE, and that RMSE is smaller than CWC of the corresponding raw autoregressive series (Curran & Bauer, 2011, pp. 607–608); diff --git a/docs/research/standards-and-literature.md b/docs/research/standards-and-literature.md index 325f807d..07b47e2e 100644 --- a/docs/research/standards-and-literature.md +++ b/docs/research/standards-and-literature.md @@ -34,7 +34,7 @@ Meredith, W. (1993). Measurement invariance, factor analysis and factorial invar Holland, P. W. (1986). Statistics and causal inference. *Journal of the American Statistical Association, 81*(396), 945–960. https://doi.org/10.1080/01621459.1986.10478354 -TEPP applies these sources to construct definition, score interpretation, reliability, validity evidence, uncertainty, consequences, longitudinal invariance, ESEM cross-loadings, and DSEM. Topic outputs are treated as fallible indicators or components only after their construct role is evaluated. Reflective, formative, and network classes remain distinct (Bollen & Lennox, 1991). Complete-data OLS loadings across posterior indicator draws are combined with Rubin (1996) \(T_m\); the arithmetic-mean helper remains a point estimate. Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex 2026-08-17T13:13Z: closed). The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title was opened 2026-08-17T12:04Z from archive.org; it is not the 1991 journal article and is not used as Mislevy plausible-value authority. Temporal precedence is not causal identification (Holland, 1986). Within/between OLS follows Enders and Tofighi (2007), Curran and Bauer (2011), and Hamaker et al. (2015). Enders and Tofighi (2007, Table 2, pp. 124–127; PDF opened 2026-08-17) show that the CWC cluster-mean coefficient is the contextual effect (`between − within`), not the between-cluster effect. Curran and Bauer (2011, pp. 607–608) reject person-mean subtraction on a raw autoregressive series as the lagged within-person residual; already-centered irregular residuals use the Voelkle et al. (2012, Eq. 7) / Driver et al. (2017, Eq. 3) scalar map. Discrete lags from unequal event intervals are remapped through that log-rate (Voelkle et al., 2012, ZORA accepted manuscript re-opened 2026-08-17T13:13Z) and are not pooled. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\) and restate the discrete intercept as a function of \(A\) and \(\Delta t\). A binary64 underflow of \(\exp(a\Delta t)\) to `+0` is refused because discrete auto-effects are strictly positive. The discrete effect of a constant predictor is Voelkle et al. (2012, Eq. 12), evaluated as \(a_{yx}(\operatorname{expm1}(z)/z\cdot\Delta t)\) with \(z=a_{xx}\Delta t\) so a finite result is not lost when \(a_{yx}\Delta t\) overflows; the first-order product is the underflow limit of that equation, not the general discrete effect. Metric/weak invariance does not license latent-mean comparison. Meredith (1993) remains unread (Unpaywall/OpenAlex 2026-08-17T13:13Z: closed; Cambridge Core redirected to an HTML product page; Springer content/pdf was an HTML stub). +TEPP applies these sources to construct definition, score interpretation, reliability, validity evidence, uncertainty, consequences, longitudinal invariance, ESEM cross-loadings, and DSEM. Topic outputs are treated as fallible indicators or components only after their construct role is evaluated. Reflective, formative, and network classes remain distinct (Bollen & Lennox, 1991). Complete-data OLS loadings across posterior indicator draws are combined with Rubin (1996) \(T_m\); the arithmetic-mean helper remains a point estimate. Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex 2026-08-17T13:23Z: closed). The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title was opened 2026-08-17T12:04Z from archive.org; it is not the 1991 journal article and is not used as Mislevy plausible-value authority. Temporal precedence is not causal identification (Holland, 1986). Within/between OLS follows Enders and Tofighi (2007), Curran and Bauer (2011), and Hamaker et al. (2015). Enders and Tofighi (2007, Table 2, pp. 124–127; PDF opened 2026-08-17) show that the CWC cluster-mean coefficient is the contextual effect (`between − within`), not the between-cluster effect. Curran and Bauer (2011, pp. 607–608) reject person-mean subtraction on a raw autoregressive series as the lagged within-person residual; already-centered irregular residuals use the Voelkle et al. (2012, Eq. 7) / Driver et al. (2017, Eq. 3) scalar map. Discrete lags from unequal event intervals are remapped through that log-rate (Voelkle et al., 2012, ZORA accepted manuscript re-opened 2026-08-17T13:13Z) and are not pooled. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\) and restate the discrete intercept as a function of \(A\) and \(\Delta t\). A binary64 underflow of \(\exp(a\Delta t)\) to `+0` is refused because discrete auto-effects are strictly positive. The discrete effect of a constant predictor is Voelkle et al. (2012, Eq. 12), evaluated as \(a_{yx}(\operatorname{expm1}(z)/a_{xx})\) with \(z=a_{xx}\Delta t\) so a finite result is not lost when \(z\) overflows to \(-\infty\) or when \(a_{yx}\Delta t\) overflows; the first-order product is the underflow limit of that equation, not the general discrete effect. Metric/weak invariance does not license latent-mean comparison. Meredith (1993) remains unread (Unpaywall/OpenAlex 2026-08-17T13:23Z: closed; Cambridge Core redirected to an HTML product page; Springer content/pdf was an HTML stub). ## Structural, correlated, dynamic, relational, and multilingual topic models From b6963877c6c464d12a496ad7cf3f7826467dba8d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 13:31:27 +0000 Subject: [PATCH 15/62] feat(psychometric): evaluate Voelkle Eq. 12 without expm1 overflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When expm1(z) overflows to +∞ at a finite z, rewrite as sign(a_yx/a_xx) exp(ln|a_yx|+z-ln|a_xx|) - a_yx/a_xx so a finite Eq. 12 result is not lost. A zero continuous effect is exactly zero even if expm1 overflows. z → +∞ remains fail-closed. ZORA accepted manuscript re-opened 2026-08-17T13:26Z, Introducing Intercepts (manuscript p. 20). Still not DSEM. --- CHANGELOG.md | 1 + CLAUDE.md | 2 +- crates/psychometric_core/src/event_time.rs | 96 ++++++++++++++++--- ...multilevel_event_time_recovery_contract.rs | 19 ++++ docs/adr/0005-posterior-esem-dsem.md | 2 +- .../multilevel-event-time-recovery.md | 6 +- docs/research/standards-and-literature.md | 2 +- 7 files changed, 107 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 268b010d..169cc366 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang ### Added +- `psychometric_core` recovers Voelkle et al. (2012, Eq. 12) when `expm1(z)` overflows to `+∞` at a finite `z`. The rewrite is `sign(a_{yx}/a_{xx})\exp(\ln|a_{yx}|+z-\ln|a_{xx}|)-a_{yx}/a_{xx}` (algebraically identical to `(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)`). A zero continuous effect is exactly zero even when `expm1` overflows (`0\cdot+\infty` is `NaN`). `z\to+\infty` remains fail-closed. ZORA accepted manuscript re-opened 2026-08-17T13:26Z, Introducing Intercepts, manuscript p. 20; Driver, Oud, and Voelkle (2017, Eq. 3) restated `A^{-1}[e^{A\Delta t}-I]\xi`. Still not DSEM. - `psychometric_core` evaluates Voelkle et al. (2012, Eq. 12) as \(a_{yx}(\operatorname{expm1}(z)/a_{xx})\) with \(z=a_{xx}\Delta t\). That order is algebraically identical to \((a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). Dividing the increment by the finite auto-effect keeps the equilibrium increment \(-a_{yx}/a_{xx}\) when \(z\) overflows to \(-\infty\) (ZORA accepted manuscript, Introducing Intercepts: the exponential vanishes as \(\Delta t\) grows; CodeRabbit finding on `7ffb65b`). The prior \(a_{yx}\Delta t\) overflow case remains finite. Binary64 underflow of \(z\) to `+0` recovers the Eq. 12 limit \(a_{yx}\Delta t\); the first-order product is still not the general discrete effect. Still not DSEM. - `psychometric_core` evaluates Voelkle et al. (2012, Eq. 12) as \(a_{yx}(\operatorname{expm1}(z)/z\cdot\Delta t)\) with \(z=a_{xx}\Delta t\). That order is algebraically identical to \((a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). Multiplying the unitless scale by \(\Delta t\) before \(a_{yx}\) keeps a finite Eq. 12 result when \(a_{yx}\Delta t\) overflows (ZORA accepted manuscript re-opened 2026-08-17T13:13Z, p. 16). Binary64 underflow of \(z\) to `+0` recovers the Eq. 12 limit \(a_{yx}\Delta t\); the first-order product is still not the general discrete effect. Driver, Oud, and Voelkle (2017, p. 4) restated the discrete intercept as a function of \(A\) and \(\Delta t\). Still not DSEM. - `psychometric_core` evaluates Voelkle et al. (2012, Eq. 12) as \(a_{yx}\Delta t\,(\operatorname{expm1}(z)/z)\) with \(z=a_{xx}\Delta t\). That order is algebraically identical to \((a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). Binary64 underflow of \(z\) to `+0` recovers the Eq. 12 limit \(a_{yx}\Delta t\); the first-order product is still not the general discrete effect. Driver, Oud, and Voelkle (2017, p. 4) restated the discrete intercept as a function of \(A\) and \(\Delta t\) (PDF re-opened 2026-08-17T12:04Z). Still not DSEM. diff --git a/CLAUDE.md b/CLAUDE.md index 8e0604c3..6df636ac 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. -- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. The first-order product is the underflow limit of that equation, not the general discrete effect. +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero. The first-order product is the underflow limit of that equation, not the general discrete effect. - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index 7f6b3c72..87e9a61f 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -220,17 +220,24 @@ pub fn map_discrete_lag_across_event_intervals( /// Exact scalar discrete effect of a constant event-time predictor. /// /// Voelkle et al. (2012, Eq. 12; ZORA accepted manuscript, Introducing -/// Intercepts): `b*_y.x(Δt) = (a_yx / a_xx) (exp(a_xx Δt) − 1)` for -/// `a_xx ≠ 0`. Driver, Oud, and Voelkle (2017, p. 4, after Eq. 3) -/// restate the same discrete intercept as a function of `A` and `Δt`. -/// The algebraically identical evaluation is `a_yx (expm1(z) / a_xx)` -/// with `z = a_xx Δt`. Dividing the increment by the finite auto-effect -/// keeps a finite Eq. 12 result when `z` overflows to `-∞` -/// (`exp(z) → 0`, so Eq. 12 → `-a_yx / a_xx`) and when `a_yx Δt` -/// overflows. When binary64 `z` underflows to `+0`, the mathematical -/// limit of Eq. 12 is `a_yx Δt`. Using that limit only at underflow is -/// IEEE-754 evaluation of Eq. 12. The first-order product is not the -/// general discrete effect. This is not DSEM and not a matrix `expm`. +/// Intercepts, manuscript p. 20): adding a continuous-time intercept +/// `b` yields the discrete increment `A^{-1}(exp(A Δt) − I) b`. Driver, +/// Oud, and Voelkle (2017, Eq. 3) write the same term as +/// `A^{-1}[e^{A Δt} − I] ξ`. The scalar case is +/// `b*_y.x(Δt) = (a_yx / a_xx) (exp(a_xx Δt) − 1)` for `a_xx ≠ 0`. +/// The algebraically identical finite-`expm1` evaluation is +/// `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt`. Dividing the increment +/// by the finite auto-effect keeps a finite Eq. 12 result when `z` +/// overflows to `-∞` (`exp(z) → 0`, so Eq. 12 → `-a_yx / a_xx`) and +/// when `a_yx Δt` overflows. When binary64 `z` underflows to `+0`, the +/// mathematical limit of Eq. 12 is `a_yx Δt`. When `a_yx = 0`, Eq. 12 +/// is exactly `0` even if `expm1(z)` overflows (`0 * +∞` is `NaN`). +/// When `expm1(z)` overflows to `+∞` at a finite `z`, rewrite as +/// `sign(a_yx / a_xx) exp(ln|a_yx| + z − ln|a_xx|) − a_yx / a_xx` so a +/// finite Eq. 12 result is not lost. `z → +∞` is an unstable process +/// and fails closed unless `a_yx = 0`. The first-order product is not +/// the general discrete effect. This is not DSEM and not a matrix +/// `expm`. /// /// # Errors /// @@ -257,15 +264,41 @@ pub fn recover_discrete_constant_predictor_effect( { return Err(PsychometricError::InvalidNumericInput); } + // Voelkle Eq. 12 / Driver Eq. 3: (0 / a)(exp(a Δt) − 1) = 0. + // Direct 0 * (expm1(z) / a) is NaN when expm1 overflows. + if outcome_on_predictor == 0.0 { + return Ok(0.0); + } let increment_argument = predictor_log_rate * event_delta; if increment_argument == 0.0 { // Binary64 underflow of a_xx Δt. lim z→0 of Eq. 12 is a_yx Δt. return require_finite(outcome_on_predictor * event_delta); } - // Divide expm1(z) by the finite a_xx, not by z. expm1(-∞)/-∞ is +0 - // and loses the equilibrium increment -a_yx/a_xx (Voelkle 2012, - // Introducing Intercepts: the exponential vanishes as Δt grows). - require_finite(outcome_on_predictor * (increment_argument.exp_m1() / predictor_log_rate)) + let increment = increment_argument.exp_m1(); + if increment.is_finite() { + // Divide expm1(z) by the finite a_xx, not by z. expm1(-∞)/-∞ + // is +0 and loses the equilibrium increment -a_yx/a_xx (Voelkle + // 2012, Introducing Intercepts: the exponential vanishes as Δt + // grows). + return require_finite(outcome_on_predictor * (increment / predictor_log_rate)); + } + // expm1 overflowed. z → +∞ diverges (unstable auto-effect). + if !increment_argument.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + // Finite z, overflowed expm1. (a_yx/a_xx)(exp(z) − 1) = + // sign(a_yx/a_xx) exp(ln|a_yx| + z − ln|a_xx|) − a_yx/a_xx. + let log_abs_dominant = + outcome_on_predictor.abs().ln() + increment_argument - predictor_log_rate.abs().ln(); + let dominant = require_finite( + outcome_on_predictor.signum() * predictor_log_rate.signum() * log_abs_dominant.exp(), + )?; + let scale = outcome_on_predictor / predictor_log_rate; + if scale.is_finite() { + require_finite(dominant - scale) + } else { + Ok(dominant) + } } /// Refuse treating discrete lags from unequal event intervals as one coefficient. @@ -757,6 +790,39 @@ mod tests { assert!(negative_overflow.is_finite()); } + #[test] + fn constant_predictor_expm1_overflow_recovers_finite_equation_twelve() { + // expm1(800) is +∞; (1e-308/800)(exp(800)−1) is finite. + assert!(!800.0_f64.exp_m1().is_finite()); + assert!(!(1e-308_f64 * (800.0_f64.exp_m1() / 800.0)).is_finite()); + let recovered = + recover_discrete_constant_predictor_effect(1e-308, 800.0, 1.0, LagClock::EventTime) + .expect("eq 12 log-space"); + let expected = (1e-308_f64.ln() + 800.0 - 800.0_f64.ln()).exp() - 1e-308 / 800.0; + assert!((recovered - expected).abs() / expected < 1e-12); + assert!(recovered.is_finite() && recovered > 0.0); + let negative = + recover_discrete_constant_predictor_effect(-1e-308, 800.0, 1.0, LagClock::EventTime) + .expect("eq 12 signed log-space"); + assert!((negative + expected).abs() / expected < 1e-12); + assert_eq!( + recover_discrete_constant_predictor_effect(0.0, 800.0, 1.0, LagClock::EventTime), + Ok(0.0) + ); + assert_eq!( + recover_discrete_constant_predictor_effect(0.0, 1e308, 2.0, LagClock::EventTime), + Ok(0.0) + ); + assert_eq!( + recover_discrete_constant_predictor_effect(1.0, 800.0, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_constant_predictor_effect(1.0, 1e308, 2.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + } + #[test] fn non_event_clocks_and_difference_quotient_fail_closed() { for clock in [ 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 0773b646..170d2955 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -217,6 +217,25 @@ fn constant_predictor_discrete_effect_recovers_equation_twelve() { "Eq. 12 z→-∞ equilibrium RMSE {negative_overflow_error}" ); assert!(negative_overflow > 0.0); + // expm1(800) is +∞; (1e-308/800)(exp(800)−1) remains finite. + assert!(!800.0_f64.exp_m1().is_finite()); + let overflowed = + recover_discrete_constant_predictor_effect(1e-308, 800.0, 1.0, LagClock::EventTime) + .expect("eq 12 expm1 overflow"); + let overflowed_truth = (1e-308_f64.ln() + 800.0 - 800.0_f64.ln()).exp() - 1e-308 / 800.0; + let overflowed_error = rmse(&[overflowed_truth], &[overflowed]); + assert!( + overflowed_error / overflowed_truth < 1e-12, + "Eq. 12 expm1-overflow RMSE {overflowed_error}" + ); + assert_eq!( + recover_discrete_constant_predictor_effect(0.0, 800.0, 1.0, LagClock::EventTime), + Ok(0.0) + ); + assert_eq!( + recover_discrete_constant_predictor_effect(0.0, 1e308, 2.0, LagClock::EventTime), + Ok(0.0) + ); } #[test] diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index d6657e7f..82b2efe1 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is the underflow limit of Eq. 12, not the general discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is the underflow limit of Eq. 12, not the general discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. 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 20e4d77b..3122dc92 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -34,7 +34,7 @@ Driver, C. C., Oud, J. H. L., & Voelkle, M. C. (2017). Continuous time structura Kish, L. (1965). *Survey sampling*. John Wiley & Sons. -The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-17T13:13Z (https://www.zora.uzh.ch/handle/20.500.14742/72792; bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`). Page 16 writes discrete auto-effects as \(A^{*}(\Delta t)=\exp(A\Delta t)\) (Eq. 7) and the constant-predictor discrete effect as \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\) (Eq. 12). Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\) and restate the discrete intercept as a function of \(A\) and \(\Delta t\). Discrete auto-effects are strictly positive for finite real drift and interval; the inverse \(a=\ln\varphi/\Delta t\) therefore requires \(\varphi>0\). Binary64 `exp` of a large negative argument is `+0` and is refused. Equations 3–4 of Voelkle et al. (2012) are the discouraged difference-quotient approximation. Meredith (1993) remains unread (Unpaywall/OpenAlex 2026-08-17T13:23Z: closed; Cambridge Core redirected to an HTML product page; Springer content/pdf was an HTML stub). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex 2026-08-17T13:23Z: closed). The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title is not the 1991 journal article. +The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-17T13:26Z (https://www.zora.uzh.ch/handle/20.500.14742/72792; bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`). Page 16 writes discrete auto-effects as \(A^{*}(\Delta t)=\exp(A\Delta t)\) (Eq. 7). Introducing Intercepts (manuscript p. 20, Eq. 12) adds a continuous-time intercept \(b\) and writes the expected-value solution whose discrete increment is \(A^{-1}(\exp(A\Delta t)-I)b\); the scalar constant-predictor effect is \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\) and restate the discrete intercept as a function of \(A\) and \(\Delta t\). Discrete auto-effects are strictly positive for finite real drift and interval; the inverse \(a=\ln\varphi/\Delta t\) therefore requires \(\varphi>0\). Binary64 `exp` of a large negative argument is `+0` and is refused. Equations 3–4 of Voelkle et al. (2012) are the discouraged difference-quotient approximation. Meredith (1993) remains unread (Unpaywall/OpenAlex/Semantic Scholar/archive.org 2026-08-17T13:26Z: closed; Springer content/pdf was HTML; Cambridge Core redirected to an HTML product page). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T13:26Z: closed; Cambridge Core redirected to an HTML stub). The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title is not the 1991 journal article. ## Formula notes @@ -43,7 +43,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-17T13:1 - **Kish ESS.** \(\mathrm{ESS}=(\sum w)^{2}/\sum w^{2}\) on non-negative finite weights. WLS uses the weights in the slope; ESS is not a second slope. - **Exact scalar map.** Voelkle et al. (2012, Eq. 7) and Driver et al. (2017, Eq. 3): \(\varphi = A^{*}(\Delta t)=\exp(a\,\Delta t)\). The inverse is \(a=\ln\varphi/\Delta t\). The forward map is the same equation. The real exponential is strictly positive; a binary64 underflow to `+0` is refused because the inverse logarithm does not exist at zero. The difference quotient \((x(t+\Delta t)-x(t))/\Delta t\) is refused. - **Unequal-interval remap.** Discrete \(\varphi(\Delta t_1)\) and \(\varphi(\Delta t_2)\) are not comparable when \(\Delta t_1\neq\Delta t_2\) (Voelkle et al., 2012, ZORA manuscript pp. 2, 16, 33). The licensed path is \(a=\ln\varphi_{\mathrm{src}}/\Delta t_{\mathrm{src}}\) then \(\varphi_{\mathrm{ref}}=\exp(a\,\Delta t_{\mathrm{ref}})\). Pooling those discrete lags fails closed. -- **Constant-predictor discrete effect.** Voelkle et al. (2012, Eq. 12; ZORA accepted manuscript re-opened 2026-08-17T13:23Z, Introducing Intercepts): for a constant predictor with \(a_{xx}\neq 0\), \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). Driver, Oud, and Voelkle (2017, p. 4, after Eq. 3) restate the discrete intercept as a function of \(A\) and \(\Delta t\). The algebraically identical evaluation is \(a_{yx}(\operatorname{expm1}(z)/a_{xx})\) with \(z=a_{xx}\Delta t\). Dividing the increment by the finite auto-effect keeps the equilibrium increment \(-a_{yx}/a_{xx}\) when \(z\) overflows to \(-\infty\) (the manuscript notes that the exponential vanishes as \(\Delta t\) grows) and keeps a finite result when \(a_{yx}\Delta t\) overflows. When binary64 \(z\) underflows to `+0`, the mathematical limit of Eq. 12 is \(a_{yx}\Delta t\). That limit is IEEE-754 evaluation of Eq. 12, not a substitution of the first-order product as the general discrete effect. \(a_{xx}=0\) fails closed. This is not DSEM. +- **Constant-predictor discrete effect.** Voelkle et al. (2012, Eq. 12; ZORA accepted manuscript re-opened 2026-08-17T13:23Z, Introducing Intercepts): for a constant predictor with \(a_{xx}\neq 0\), \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). Driver, Oud, and Voelkle (2017, p. 4, after Eq. 3) restate the discrete intercept as a function of \(A\) and \(\Delta t\). The algebraically identical finite-`expm1` evaluation is \(a_{yx}(\operatorname{expm1}(z)/a_{xx})\) with \(z=a_{xx}\Delta t\). Dividing the increment by the finite auto-effect keeps the equilibrium increment \(-a_{yx}/a_{xx}\) when \(z\) overflows to \(-\infty\) (the manuscript notes that the exponential vanishes as \(\Delta t\) grows) and keeps a finite result when \(a_{yx}\Delta t\) overflows. When `expm1(z)` overflows to `+\infty` at a finite \(z\), rewrite as \(\operatorname{sign}(a_{yx}/a_{xx})\exp(\ln|a_{yx}|+z-\ln|a_{xx}|)-a_{yx}/a_{xx}\). A zero continuous effect is exactly zero even if `expm1` overflows. \(z\to+\infty\) fails closed unless \(a_{yx}=0\). When binary64 \(z\) underflows to `+0`, the mathematical limit of Eq. 12 is \(a_{yx}\Delta t\). That limit is IEEE-754 evaluation of Eq. 12, not a substitution of the first-order product as the general discrete effect. \(a_{xx}=0\) fails closed. This is not DSEM. - **CWC-then-lag.** Sample cluster means are removed first. Consecutive within residuals are then fitted by least squares to \(r_{t+\Delta t}\approx\exp(a\Delta t)\,r_{t}\) on event time. Same-sign pair-wise logs initialize the scalar Newton step. Sign-flipping \(T=2\) CWC pairs have no real logarithm and fail closed. Curran and Bauer (2011, pp. 607–608) show that this person-mean subtraction on a raw autoregressive series does **not** isolate the lagged within-person effect; the helper therefore does not claim to recover the raw-process drift. - **Already-centered irregular residual.** The caller supplies lagged within residuals. The mean of \(a=\ln(r_{t+\Delta t}/r_t)/\Delta t\) is the exact scalar map. Intervals may be irregular. The helper does not center again. This is not DSEM. @@ -55,7 +55,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-17T13:1 - the exact scalar map recovers a known drift on event time and refuses every other clock plus the difference quotient; - the forward map inverts the log-rate, remaps \(\varphi(1)\) onto \(\varphi(2)\) at machine-scale RMSE, and that RMSE is smaller than treating \(\varphi(1)\) as \(\varphi(2)\); - a binary64 underflow of \(\exp(a\Delta t)\) to `+0` (direct forward map and large-interval remap) fails closed; -- Voelkle et al. (2012, Eq. 12) recovers a known discrete constant-predictor effect at machine-scale RMSE, and that RMSE is smaller than the first-order product \(a_{yx}\Delta t\); \(a_{xx}=0\) fails closed; binary64 underflow of \(a_{xx}\Delta t\) to `+0` recovers the Eq. 12 limit \(a_{yx}\Delta t\); a finite Eq. 12 result whose first factor \(a_{yx}\Delta t\) overflows is recovered; a finite Eq. 12 equilibrium increment whose \(z=a_{xx}\Delta t\) overflows to \(-\infty\) is recovered as \(-a_{yx}/a_{xx}\); +- Voelkle et al. (2012, Eq. 12) recovers a known discrete constant-predictor effect at machine-scale RMSE, and that RMSE is smaller than the first-order product \(a_{yx}\Delta t\); \(a_{xx}=0\) fails closed; binary64 underflow of \(a_{xx}\Delta t\) to `+0` recovers the Eq. 12 limit \(a_{yx}\Delta t\); a finite Eq. 12 result whose first factor \(a_{yx}\Delta t\) overflows is recovered; a finite Eq. 12 equilibrium increment whose \(z=a_{xx}\Delta t\) overflows to \(-\infty\) is recovered as \(-a_{yx}/a_{xx}\); a finite Eq. 12 result whose `expm1(z)` overflows at a finite \(z\) is recovered in log space; a zero continuous effect is exactly zero; - pooling discrete lags from unequal intervals fails closed; - CWC-then-lag on a two-cluster decaying series has smaller computed RMSE than a level-pooled series when the latter is identified; - already-centered irregular residuals recover a known drift at machine-scale RMSE, and that RMSE is smaller than CWC of the corresponding raw autoregressive series (Curran & Bauer, 2011, pp. 607–608); diff --git a/docs/research/standards-and-literature.md b/docs/research/standards-and-literature.md index 07b47e2e..8f299096 100644 --- a/docs/research/standards-and-literature.md +++ b/docs/research/standards-and-literature.md @@ -34,7 +34,7 @@ Meredith, W. (1993). Measurement invariance, factor analysis and factorial invar Holland, P. W. (1986). Statistics and causal inference. *Journal of the American Statistical Association, 81*(396), 945–960. https://doi.org/10.1080/01621459.1986.10478354 -TEPP applies these sources to construct definition, score interpretation, reliability, validity evidence, uncertainty, consequences, longitudinal invariance, ESEM cross-loadings, and DSEM. Topic outputs are treated as fallible indicators or components only after their construct role is evaluated. Reflective, formative, and network classes remain distinct (Bollen & Lennox, 1991). Complete-data OLS loadings across posterior indicator draws are combined with Rubin (1996) \(T_m\); the arithmetic-mean helper remains a point estimate. Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex 2026-08-17T13:23Z: closed). The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title was opened 2026-08-17T12:04Z from archive.org; it is not the 1991 journal article and is not used as Mislevy plausible-value authority. Temporal precedence is not causal identification (Holland, 1986). Within/between OLS follows Enders and Tofighi (2007), Curran and Bauer (2011), and Hamaker et al. (2015). Enders and Tofighi (2007, Table 2, pp. 124–127; PDF opened 2026-08-17) show that the CWC cluster-mean coefficient is the contextual effect (`between − within`), not the between-cluster effect. Curran and Bauer (2011, pp. 607–608) reject person-mean subtraction on a raw autoregressive series as the lagged within-person residual; already-centered irregular residuals use the Voelkle et al. (2012, Eq. 7) / Driver et al. (2017, Eq. 3) scalar map. Discrete lags from unequal event intervals are remapped through that log-rate (Voelkle et al., 2012, ZORA accepted manuscript re-opened 2026-08-17T13:13Z) and are not pooled. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\) and restate the discrete intercept as a function of \(A\) and \(\Delta t\). A binary64 underflow of \(\exp(a\Delta t)\) to `+0` is refused because discrete auto-effects are strictly positive. The discrete effect of a constant predictor is Voelkle et al. (2012, Eq. 12), evaluated as \(a_{yx}(\operatorname{expm1}(z)/a_{xx})\) with \(z=a_{xx}\Delta t\) so a finite result is not lost when \(z\) overflows to \(-\infty\) or when \(a_{yx}\Delta t\) overflows; the first-order product is the underflow limit of that equation, not the general discrete effect. Metric/weak invariance does not license latent-mean comparison. Meredith (1993) remains unread (Unpaywall/OpenAlex 2026-08-17T13:23Z: closed; Cambridge Core redirected to an HTML product page; Springer content/pdf was an HTML stub). +TEPP applies these sources to construct definition, score interpretation, reliability, validity evidence, uncertainty, consequences, longitudinal invariance, ESEM cross-loadings, and DSEM. Topic outputs are treated as fallible indicators or components only after their construct role is evaluated. Reflective, formative, and network classes remain distinct (Bollen & Lennox, 1991). Complete-data OLS loadings across posterior indicator draws are combined with Rubin (1996) \(T_m\); the arithmetic-mean helper remains a point estimate. Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T13:26Z: closed). The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title was opened 2026-08-17T12:04Z from archive.org; it is not the 1991 journal article and is not used as Mislevy plausible-value authority. Temporal precedence is not causal identification (Holland, 1986). Within/between OLS follows Enders and Tofighi (2007), Curran and Bauer (2011), and Hamaker et al. (2015). Enders and Tofighi (2007, Table 2, pp. 124–127; PDF opened 2026-08-17) show that the CWC cluster-mean coefficient is the contextual effect (`between − within`), not the between-cluster effect. Curran and Bauer (2011, pp. 607–608) reject person-mean subtraction on a raw autoregressive series as the lagged within-person residual; already-centered irregular residuals use the Voelkle et al. (2012, Eq. 7) / Driver et al. (2017, Eq. 3) scalar map. Discrete lags from unequal event intervals are remapped through that log-rate (Voelkle et al., 2012, ZORA accepted manuscript re-opened 2026-08-17T13:13Z) and are not pooled. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\) and restate the discrete intercept as a function of \(A\) and \(\Delta t\). A binary64 underflow of \(\exp(a\Delta t)\) to `+0` is refused because discrete auto-effects are strictly positive. The discrete effect of a constant predictor is Voelkle et al. (2012, Eq. 12; ZORA accepted manuscript re-opened 2026-08-17T13:26Z, Introducing Intercepts, manuscript p. 20), evaluated as \(a_{yx}(\operatorname{expm1}(z)/a_{xx})\) with \(z=a_{xx}\Delta t\) so a finite result is not lost when \(z\) overflows to \(-\infty\) or when \(a_{yx}\Delta t\) overflows, and in log space when `expm1(z)` overflows at a finite \(z\); a zero continuous effect is exactly zero; the first-order product is the underflow limit of that equation, not the general discrete effect. Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-17T13:26Z) write the same discrete intercept as \(A^{-1}[e^{A\Delta t}-I]\xi\). Metric/weak invariance does not license latent-mean comparison. Meredith (1993) remains unread (Unpaywall/OpenAlex/Semantic Scholar/archive.org 2026-08-17T13:26Z: closed; Cambridge Core redirected to an HTML product page; Springer content/pdf was an HTML stub). ## Structural, correlated, dynamic, relational, and multilingual topic models From 8a4120349348ee0f3ca056feb26fdaae2c640ff7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 14:25:49 +0000 Subject: [PATCH 16/62] feat(psychometric): recover Voelkle Eq. 14 time-varying predictor effect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ZORA accepted manuscript re-opened 2026-08-17T14:20Z (Introducing Intercepts, p. 21). When sampling equals constancy, the discrete effect is a_yx Δt. That product is not Eq. 12. Unmatched intervals fail closed because Oud and Jansen (2000) is unread. An overflowing Eq. 12 rewrite scale a_yx/a_xx also fails closed. Still not DSEM. --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 1 + CLAUDE.md | 2 +- crates/psychometric_core/Cargo.toml | 2 +- crates/psychometric_core/src/error.rs | 10 + crates/psychometric_core/src/event_time.rs | 240 +++++++++++++++++- crates/psychometric_core/src/latent_mean.rs | 2 + crates/psychometric_core/src/lib.rs | 8 +- ...multilevel_event_time_recovery_contract.rs | 52 +++- .../scientific_claim_boundary_contract.rs | 29 ++- docs/TRACEABILITY.md | 2 +- docs/adr/0005-posterior-esem-dsem.md | 4 +- docs/adr/README.md | 2 +- .../multilevel-event-time-recovery.md | 20 +- docs/research/standards-and-literature.md | 2 +- docs/validation/temporal-event-foundation.md | 2 +- 16 files changed, 348 insertions(+), 32 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index dbd54767..7d7877b8 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -61,7 +61,7 @@ boundaries above remain the target modular MSA architecture. | `tepp_simulation` | known-truth temporal/event data generation | | `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics | | `tepp_api` | versioned DTO, schema, and export contracts | -| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means | +| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means | No crate exposes placeholder production behavior in Task 1. This prevents an empty façade from becoming a de facto public API before its invariants and tests diff --git a/CHANGELOG.md b/CHANGELOG.md index 169cc366..f362f8e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang ### Added +- `psychometric_core` recovers Voelkle et al. (2012, Eq. 14; ZORA accepted manuscript re-opened 2026-08-17T14:20Z, Introducing Intercepts, manuscript p. 21): the discrete effect of a time-varying predictor whose sampling interval equals its constancy interval is \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). It does not depend on the predictor auto-effect. That product is not Eq. 12. Unmatched sampling and constancy intervals fail closed (Oud & Jansen, 2000, unread). An overflowing Eq. 12 rewrite scale \(a_{yx}/a_{xx}\) also fails closed. Still not DSEM. - `psychometric_core` recovers Voelkle et al. (2012, Eq. 12) when `expm1(z)` overflows to `+∞` at a finite `z`. The rewrite is `sign(a_{yx}/a_{xx})\exp(\ln|a_{yx}|+z-\ln|a_{xx}|)-a_{yx}/a_{xx}` (algebraically identical to `(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)`). A zero continuous effect is exactly zero even when `expm1` overflows (`0\cdot+\infty` is `NaN`). `z\to+\infty` remains fail-closed. ZORA accepted manuscript re-opened 2026-08-17T13:26Z, Introducing Intercepts, manuscript p. 20; Driver, Oud, and Voelkle (2017, Eq. 3) restated `A^{-1}[e^{A\Delta t}-I]\xi`. Still not DSEM. - `psychometric_core` evaluates Voelkle et al. (2012, Eq. 12) as \(a_{yx}(\operatorname{expm1}(z)/a_{xx})\) with \(z=a_{xx}\Delta t\). That order is algebraically identical to \((a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). Dividing the increment by the finite auto-effect keeps the equilibrium increment \(-a_{yx}/a_{xx}\) when \(z\) overflows to \(-\infty\) (ZORA accepted manuscript, Introducing Intercepts: the exponential vanishes as \(\Delta t\) grows; CodeRabbit finding on `7ffb65b`). The prior \(a_{yx}\Delta t\) overflow case remains finite. Binary64 underflow of \(z\) to `+0` recovers the Eq. 12 limit \(a_{yx}\Delta t\); the first-order product is still not the general discrete effect. Still not DSEM. - `psychometric_core` evaluates Voelkle et al. (2012, Eq. 12) as \(a_{yx}(\operatorname{expm1}(z)/z\cdot\Delta t)\) with \(z=a_{xx}\Delta t\). That order is algebraically identical to \((a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). Multiplying the unitless scale by \(\Delta t\) before \(a_{yx}\) keeps a finite Eq. 12 result when \(a_{yx}\Delta t\) overflows (ZORA accepted manuscript re-opened 2026-08-17T13:13Z, p. 16). Binary64 underflow of \(z\) to `+0` recovers the Eq. 12 limit \(a_{yx}\Delta t\); the first-order product is still not the general discrete effect. Driver, Oud, and Voelkle (2017, p. 4) restated the discrete intercept as a function of \(A\) and \(\Delta t\). Still not DSEM. diff --git a/CLAUDE.md b/CLAUDE.md index 6df636ac..dab7426a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. -- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero. The first-order product is the underflow limit of that equation, not the general discrete effect. +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. diff --git a/crates/psychometric_core/Cargo.toml b/crates/psychometric_core/Cargo.toml index 97b05cf6..03915bab 100644 --- a/crates/psychometric_core/Cargo.toml +++ b/crates/psychometric_core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "psychometric_core" -description = "Posterior-aware ESEM/DSEM input gates, multilevel/event-time recovery, CWC contextual effect, Rubin T, and strong-invariance latent means." +description = "Posterior-aware ESEM/DSEM input gates, multilevel/event-time recovery, CWC contextual effect, Voelkle Eqs. 12 and 14, Rubin T, and strong-invariance latent means." version.workspace = true edition.workspace = true rust-version.workspace = true diff --git a/crates/psychometric_core/src/error.rs b/crates/psychometric_core/src/error.rs index fba15858..7c3bfbfc 100644 --- a/crates/psychometric_core/src/error.rs +++ b/crates/psychometric_core/src/error.rs @@ -31,6 +31,9 @@ pub enum PsychometricError { DifferenceQuotientForbidden, /// Discrete lags from unequal event intervals were treated as one coefficient. UnequalIntervalPoolingForbidden, + /// A time-varying predictor was mapped with unmatched sampling and + /// constancy intervals. Oud and Jansen (2000) is unread. + UnmatchedTimeVaryingInterval, /// Fewer than two clusters were supplied for a within/between decomposition. InsufficientClusters, /// A membership or survey weight is empty, negative, or non-finite. @@ -66,6 +69,9 @@ impl fmt::Display for PsychometricError { Self::UnequalIntervalPoolingForbidden => { "discrete lags from unequal event intervals are not one coefficient" } + Self::UnmatchedTimeVaryingInterval => { + "time-varying predictor discrete effect requires matching sampling and constancy intervals" + } Self::InsufficientClusters => "within/between recovery requires at least two clusters", Self::InvalidWeight => "invalid non-negative finite psychometric weight", Self::NonPositiveInterval => "event-time interval must be strictly positive", @@ -128,6 +134,10 @@ mod tests { PsychometricError::UnequalIntervalPoolingForbidden.to_string(), "discrete lags from unequal event intervals are not one coefficient" ); + assert_eq!( + PsychometricError::UnmatchedTimeVaryingInterval.to_string(), + "time-varying predictor discrete effect requires matching sampling and constancy intervals" + ); assert_eq!( PsychometricError::InsufficientClusters.to_string(), "within/between recovery requires at least two clusters" diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index 87e9a61f..83a1816c 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -8,7 +8,9 @@ //! `φ(Δt) = exp(a Δt)`. Discrete lags from unequal event intervals are //! not one coefficient; they map through `a` first. The exact scalar //! discrete effect of a constant predictor is Voelkle et al. (2012, -//! Eq. 12). The difference quotient `(x(t+Δt) − x(t)) / Δt` (their +//! Eq. 12). The discrete effect of a time-varying predictor whose +//! sampling interval equals its constancy interval is their Eq. 14. +//! The difference quotient `(x(t+Δt) − x(t)) / Δt` (their //! Eqs. 3–4) is refused. This is not DSEM and not a matrix `expm`. use std::collections::BTreeMap; @@ -288,17 +290,20 @@ pub fn recover_discrete_constant_predictor_effect( } // Finite z, overflowed expm1. (a_yx/a_xx)(exp(z) − 1) = // sign(a_yx/a_xx) exp(ln|a_yx| + z − ln|a_xx|) − a_yx/a_xx. + // The subtracted scale must itself be finite: if a_yx/a_xx overflows, + // the rewrite term is not a binary64 number. That path is dead if + // dominant is required first (dominant is then also infinite), so + // refuse the scale before forming the exponential. + let scale = outcome_on_predictor / predictor_log_rate; + if !scale.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } let log_abs_dominant = outcome_on_predictor.abs().ln() + increment_argument - predictor_log_rate.abs().ln(); let dominant = require_finite( outcome_on_predictor.signum() * predictor_log_rate.signum() * log_abs_dominant.exp(), )?; - let scale = outcome_on_predictor / predictor_log_rate; - if scale.is_finite() { - require_finite(dominant - scale) - } else { - Ok(dominant) - } + require_finite(dominant - scale) } /// Refuse treating discrete lags from unequal event intervals as one coefficient. @@ -317,6 +322,74 @@ pub fn refuse_pooled_discrete_lag_across_unequal_intervals( Err(PsychometricError::UnequalIntervalPoolingForbidden) } +/// Exact scalar discrete effect of a time-varying event-time predictor. +/// +/// Voelkle et al. (2012, Eq. 14; ZORA accepted manuscript, Introducing +/// Intercepts, manuscript p. 21): when the predictor can take a new value +/// at each occasion **and** the sampling interval equals the interval +/// during which that predictor is assumed constant, the discrete effect +/// is `b*_y.x(Δt) = a_yx Δt`. It does not depend on the predictor +/// auto-effect. The manuscript calls this a first-order approximation +/// that deteriorates as `Δt` grows. It is not Eq. 12. The general case +/// (sampling interval ≠ constancy interval) cites Oud and Jansen (2000), +/// which is unread, and fails closed. This is not DSEM. +/// +/// # Errors +/// +/// Returns [`PsychometricError::EventTimeRequired`] for any non-event clock, +/// [`PsychometricError::NonPositiveInterval`] when any interval is not +/// strictly positive, [`PsychometricError::UnmatchedTimeVaryingInterval`] +/// when the event, sampling, and constancy intervals are not the same +/// finite value, and [`PsychometricError::InvalidNumericInput`] when the +/// continuous effect is non-finite or the product overflows. +pub fn recover_discrete_time_varying_predictor_effect( + outcome_on_predictor: f64, + event_delta: f64, + sampling_interval: f64, + constancy_interval: f64, + clock: LagClock, +) -> Result { + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + if !event_delta.is_finite() + || event_delta <= 0.0 + || !sampling_interval.is_finite() + || sampling_interval <= 0.0 + || !constancy_interval.is_finite() + || constancy_interval <= 0.0 + { + return Err(PsychometricError::NonPositiveInterval); + } + if event_delta.to_bits() != sampling_interval.to_bits() + || sampling_interval.to_bits() != constancy_interval.to_bits() + { + return Err(PsychometricError::UnmatchedTimeVaryingInterval); + } + if !outcome_on_predictor.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + require_finite(outcome_on_predictor * event_delta) +} + +/// Refuse mapping a time-varying predictor when sampling ≠ constancy. +/// +/// Always fails closed. Oud and Jansen (2000) is unread. Use +/// [`recover_discrete_time_varying_predictor_effect`] only when the +/// intervals already match, or [`recover_discrete_constant_predictor_effect`] +/// for a constant predictor (Eq. 12). +/// +/// # Errors +/// +/// Always returns [`PsychometricError::UnmatchedTimeVaryingInterval`]. +pub fn refuse_unmatched_time_varying_predictor_interval( + sampling_interval: f64, + constancy_interval: f64, +) -> Result { + let _ = (sampling_interval, constancy_interval); + Err(PsychometricError::UnmatchedTimeVaryingInterval) +} + /// Refuse the difference quotient as a continuous-time rate. /// /// Voelkle et al. (2012) discourage `(x(t+Δt) − x(t)) / Δt` as the drift. @@ -549,10 +622,12 @@ mod tests { ClusteredEventScore, EventOccasion, LagClock, LaggedWithinResidual, fit_scalar_log_rate, map_discrete_lag_across_event_intervals, recover_discrete_constant_predictor_effect, recover_discrete_lag_from_log_rate, recover_discrete_lag_one, - recover_event_series_mean_log_rate, recover_event_time_discrete_lag_and_log_rate, - recover_irregular_centered_residual_log_rate, recover_local_log_rate, - recover_within_residual_event_time_log_rate, refuse_difference_quotient_as_local_rate, + recover_discrete_time_varying_predictor_effect, recover_event_series_mean_log_rate, + recover_event_time_discrete_lag_and_log_rate, recover_irregular_centered_residual_log_rate, + recover_local_log_rate, recover_within_residual_event_time_log_rate, + refuse_difference_quotient_as_local_rate, refuse_pooled_discrete_lag_across_unequal_intervals, + refuse_unmatched_time_varying_predictor_interval, }; use crate::error::PsychometricError; @@ -821,6 +896,151 @@ mod tests { recover_discrete_constant_predictor_effect(1.0, 1e308, 2.0, LagClock::EventTime), Err(PsychometricError::InvalidNumericInput) ); + // a_yx/a_xx overflows; the Eq. 12 rewrite term is not a binary64 number. + assert!(!800.0_f64.exp_m1().is_finite()); + assert!(!(1e308_f64 / 1e-10).is_finite()); + assert_eq!( + recover_discrete_constant_predictor_effect(1e308, 1e-10, 8e12, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + } + + #[test] + fn time_varying_predictor_discrete_effect_recovers_equation_fourteen() { + let outcome_on_predictor = 0.2_f64; + let delta = 2.0_f64; + let recovered = recover_discrete_time_varying_predictor_effect( + outcome_on_predictor, + delta, + delta, + delta, + LagClock::EventTime, + ) + .expect("eq 14"); + assert!((recovered - outcome_on_predictor * delta).abs() < 1e-15); + let constant = recover_discrete_constant_predictor_effect( + outcome_on_predictor, + -0.5, + delta, + LagClock::EventTime, + ) + .expect("eq 12"); + // Voelkle 2012, p. 21: Eq. 14 is not Eq. 12. + assert!((recovered - constant).abs() > 1e-3); + assert_eq!( + recover_discrete_time_varying_predictor_effect( + 0.0, + delta, + delta, + delta, + LagClock::EventTime + ), + Ok(0.0) + ); + } + + #[test] + fn time_varying_predictor_unmatched_and_invalid_inputs_fail_closed() { + let outcome_on_predictor = 0.2_f64; + let delta = 2.0_f64; + assert_eq!( + recover_discrete_time_varying_predictor_effect( + outcome_on_predictor, + delta, + delta, + delta, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_discrete_time_varying_predictor_effect( + outcome_on_predictor, + 0.0, + 0.0, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_time_varying_predictor_effect( + outcome_on_predictor, + -1.0, + 1.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_time_varying_predictor_effect( + outcome_on_predictor, + 1.0, + f64::NAN, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_time_varying_predictor_effect( + outcome_on_predictor, + 1.0, + 1.0, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_time_varying_predictor_effect( + outcome_on_predictor, + 1.0, + 2.0, + 2.0, + LagClock::EventTime + ), + Err(PsychometricError::UnmatchedTimeVaryingInterval) + ); + assert_eq!( + recover_discrete_time_varying_predictor_effect( + outcome_on_predictor, + 2.0, + 2.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::UnmatchedTimeVaryingInterval) + ); + assert_eq!( + recover_discrete_time_varying_predictor_effect( + f64::NAN, + delta, + delta, + delta, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_time_varying_predictor_effect( + 1e308, + 10.0, + 10.0, + 10.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + refuse_unmatched_time_varying_predictor_interval(1.0, 2.0), + Err(PsychometricError::UnmatchedTimeVaryingInterval) + ); + assert_eq!( + refuse_unmatched_time_varying_predictor_interval(1.0, 1.0), + Err(PsychometricError::UnmatchedTimeVaryingInterval) + ); } #[test] diff --git a/crates/psychometric_core/src/latent_mean.rs b/crates/psychometric_core/src/latent_mean.rs index e5234dfa..933edb08 100644 --- a/crates/psychometric_core/src/latent_mean.rs +++ b/crates/psychometric_core/src/latent_mean.rs @@ -221,6 +221,8 @@ mod tests { assert!(MeanInvarianceStatus::Metric.licenses_shared_metric_meaning()); assert!(!MeanInvarianceStatus::Metric.licenses_latent_mean_comparison()); assert_eq!(MeanInvarianceStatus::Metric.as_str(), "metric"); + assert_eq!(MeanInvarianceStatus::Configural.as_str(), "configural"); + assert_eq!(MeanInvarianceStatus::Strict.as_str(), "strict"); } #[test] diff --git a/crates/psychometric_core/src/lib.rs b/crates/psychometric_core/src/lib.rs index 2660e4a9..34af58b4 100644 --- a/crates/psychometric_core/src/lib.rs +++ b/crates/psychometric_core/src/lib.rs @@ -12,7 +12,9 @@ //! maps event-time discrete lags through the exact scalar exponential, maps //! already-centered irregular residuals without re-centering, remaps discrete //! lags across unequal event intervals through that log-rate, recovers the -//! exact scalar discrete effect of a constant predictor, and refuses +//! exact scalar discrete effect of a constant predictor, recovers the +//! first-order discrete effect of a time-varying predictor with matched +//! sampling and constancy intervals, and refuses //! latent-mean comparison below strong invariance. mod causality; @@ -66,6 +68,8 @@ pub use event_time::recover_discrete_constant_predictor_effect; pub use event_time::recover_discrete_lag_from_log_rate; /// Noiseless scalar discrete lag `later / earlier`. pub use event_time::recover_discrete_lag_one; +/// First-order discrete effect of a time-varying event-time predictor. +pub use event_time::recover_discrete_time_varying_predictor_effect; /// Mean local log-rate on a sorted event-time series. pub use event_time::recover_event_series_mean_log_rate; /// Exact scalar pair `(φ, a)` on event time. @@ -80,6 +84,8 @@ pub use event_time::recover_within_residual_event_time_log_rate; pub use event_time::refuse_difference_quotient_as_local_rate; /// Refuse pooling discrete lags from unequal event intervals. pub use event_time::refuse_pooled_discrete_lag_across_unequal_intervals; +/// Refuse a time-varying predictor whose sampling and constancy intervals differ. +pub use event_time::refuse_unmatched_time_varying_predictor_interval; /// Indicator coordinate kind. pub use indicator::IndicatorKind; /// Pearson correlation on valid coordinates. 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 170d2955..614eb793 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -6,10 +6,11 @@ use psychometric_core::{ LaggedWithinResidual, PsychometricError, map_discrete_lag_across_event_intervals, ordinary_least_squares_slope, recover_cluster_mean_within_between_slopes, recover_discrete_constant_predictor_effect, recover_discrete_lag_from_log_rate, - recover_event_series_mean_log_rate, recover_event_time_discrete_lag_and_log_rate, - recover_irregular_centered_residual_log_rate, recover_kish_weighted_slope, - recover_within_residual_event_time_log_rate, refuse_difference_quotient_as_local_rate, - refuse_pooled_discrete_lag_across_unequal_intervals, + recover_discrete_time_varying_predictor_effect, recover_event_series_mean_log_rate, + recover_event_time_discrete_lag_and_log_rate, recover_irregular_centered_residual_log_rate, + recover_kish_weighted_slope, recover_within_residual_event_time_log_rate, + refuse_difference_quotient_as_local_rate, refuse_pooled_discrete_lag_across_unequal_intervals, + refuse_unmatched_time_varying_predictor_interval, }; fn rmse(truth: &[f64], recovered: &[f64]) -> f64 { @@ -238,6 +239,49 @@ fn constant_predictor_discrete_effect_recovers_equation_twelve() { ); } +#[test] +fn time_varying_predictor_discrete_effect_recovers_equation_fourteen() { + let outcome_on_predictor = 0.2_f64; + let delta = 2.0_f64; + let recovered = recover_discrete_time_varying_predictor_effect( + outcome_on_predictor, + delta, + delta, + delta, + LagClock::EventTime, + ) + .expect("eq 14"); + let expected = outcome_on_predictor * delta; + let error = rmse(&[expected], &[recovered]); + assert!(error < 1e-15, "Eq. 14 RMSE {error}"); + let constant = recover_discrete_constant_predictor_effect( + outcome_on_predictor, + -0.5, + delta, + LagClock::EventTime, + ) + .expect("eq 12"); + let crossed_error = rmse(&[constant], &[recovered]); + assert!( + crossed_error > error, + "Voelkle Eq. 14 is not Eq. 12: crossed RMSE {crossed_error} must exceed {error}" + ); + assert_eq!( + recover_discrete_time_varying_predictor_effect( + outcome_on_predictor, + 1.0, + 2.0, + 2.0, + LagClock::EventTime + ), + Err(PsychometricError::UnmatchedTimeVaryingInterval) + ); + assert_eq!( + refuse_unmatched_time_varying_predictor_interval(1.0, 2.0), + Err(PsychometricError::UnmatchedTimeVaryingInterval) + ); +} + #[test] fn within_residual_event_time_log_rate_beats_pooled_levels() { let true_drift = -0.3_f64; diff --git a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs index 6bf87138..53d2dd50 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -3,7 +3,8 @@ use psychometric_core::{ ClusteredEventScore, ClusteredScore, IndicatorKind, LagClock, LaggedWithinResidual, ordinary_least_squares_slope, posterior_draw_point_estimate_mean, - recover_cluster_mean_within_between_slopes, recover_irregular_centered_residual_log_rate, + recover_cluster_mean_within_between_slopes, recover_discrete_constant_predictor_effect, + recover_discrete_time_varying_predictor_effect, recover_irregular_centered_residual_log_rate, recover_loading_point_estimate_mean, recover_within_residual_event_time_log_rate, }; @@ -131,3 +132,29 @@ fn cwc_cluster_mean_coefficient_is_not_the_between_cluster_effect() { "adding CWC γ01 to γ10 must recover the between-cluster slope" ); } + +#[test] +fn time_varying_equation_fourteen_is_not_constant_equation_twelve() { + let outcome_on_predictor = 0.35_f64; + let delta = 1.5_f64; + let time_varying = recover_discrete_time_varying_predictor_effect( + outcome_on_predictor, + delta, + delta, + delta, + LagClock::EventTime, + ) + .expect("eq 14"); + let constant = recover_discrete_constant_predictor_effect( + outcome_on_predictor, + -0.4, + delta, + LagClock::EventTime, + ) + .expect("eq 12"); + assert!( + (time_varying - constant).abs() > 1e-3, + "Voelkle et al. (2012, manuscript p. 21): Eq. 14 a_yx Δt must not equal Eq. 12" + ); + assert!((time_varying - outcome_on_predictor * delta).abs() < 1e-15); +} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index b94878ba..aeef37a7 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -28,7 +28,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | report template/section/copied/style/modality method effects | ADR 0004/0012; PRD/TRD | simulation truth factors implemented; estimator-side method model remains future | partial | | candidate K statistical/Pareto gates + blinded LLM review | ADR 0012; research | future `model_selection` | accepted-target | | compositional topic correlation / stable clustering | ADR 0005/0012; research | future `network_analysis` | accepted-target | -| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR; full ESEM/DSEM remaining | partial | +| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR; full ESEM/DSEM remaining | partial | | CPU bounded multithreading + GPU/VRAM streaming/parity | ADR 0001/0006 | future `compute_backend` | accepted-target | | TDT detection/tracking vs CHRONOS schema/prediction/temporal consistency | ADR 0016; PRD/research | future `event_intelligence` | accepted-target | | evidence-bounded LLM interpretation | ADR 0010/0012; PRD | future `interpretation_gateway` | accepted-target | diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index 82b2efe1..3e3353a9 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -1,7 +1,7 @@ # ADR 0005 — Posterior-aware ESEM/DSEM and structural interpretation **Decision status:** Accepted -**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target +**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target **Date:** 2026-08-05 **Supersedes:** None. ADR 0012 governs upstream topic measurement/network coordinates; this ADR governs higher-order psychometric structure and longitudinal interpretation. @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is the underflow limit of Eq. 12, not the general discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. 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/adr/README.md b/docs/adr/README.md index b277ad91..0477eb78 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -10,7 +10,7 @@ Read [`ADR_POLICY.md`](ADR_POLICY.md) first. **Decision status and implementatio | [0002](0002-six-clock-temporal-semantics.md) | Six-clock temporal semantics and fail-closed historical leakage prevention | Accepted | active-PR | Unmerged PR #8 is the canonical Task 3 replacement implementing typed clocks/intervals against the current protected-main lineage; conflicted PR #5 is superseded lineage. Later graph/split enforcement remains target work. | | [0003](0003-relational-event-multiple-membership.md) | Relational event ontology and time-varying cross-classified multiple membership | Accepted | partial | Weighted time-varying membership network/roles are active-PR (PR #12); full multilevel estimators, graph ontology, and persistence remain accepted-target. ADR 0016 owns event-intelligence tasks. | | [0004](0004-shared-multilingual-latent-space.md) | One shared multilingual latent space with explicit invariance status | Accepted | accepted-target | ADR 0012 owns the full topic-estimator/backend/global-topic contract. | -| [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | partial | Input gates, posterior-draw point estimates, Rubin `T` on OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, irregular already-centered residual lag, and strong-gated latent means are on the stacked psychometric PR; full ESEM/DSEM estimator remains accepted-target. | +| [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | partial | Input gates, posterior-draw point estimates, Rubin `T` on OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects, irregular already-centered residual lag, and strong-gated latent means are on the stacked psychometric PR; full ESEM/DSEM estimator remains accepted-target. | | [0006](0006-vram-gpu-nvidia-orchestration.md) | VRAM-adaptive GPU compute and model-credential boundary | Accepted | accepted-target | LLM orchestration policy superseded by ADR 0010; autonomous development authority governed by ADR 0015. | | [0007](0007-rust-workspace-quality-gates.md) | Explicit Rust workspace, pinned toolchains, and exact quality gates | Accepted | implemented-main | ADR 0014 governs scientific/product claim promotion beyond repository-quality tooling. | | [0008](0008-immutable-evidence-identities-digests-and-spans.md) | Immutable evidence identities, `SHA-256` digests, exact spans, and strict wire reconstruction | Accepted | implemented-main | ADR 0013 governs future persistence/reproducibility/split authority. | diff --git a/docs/research/multilevel-event-time-recovery.md b/docs/research/multilevel-event-time-recovery.md index 3122dc92..feefebc4 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -11,14 +11,16 @@ This slice stays inside `psychometric_core`. It does not add a second invariance 5. recover the exact scalar forward map `φ(Δt) = exp(a Δt)` and remap a discrete lag onto another event interval through that log-rate; 6. refuse a binary64 underflow of that forward map to `+0` (not a discrete lag); 7. recover the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12); -8. refuse pooling discrete lags from unequal event intervals as one coefficient; -9. refuse the difference quotient as a continuous-time rate; -10. apply the same event-time map to CWC residuals (still not DSEM); -11. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). +8. recover the first-order discrete effect of a time-varying predictor whose sampling interval equals its constancy interval (Voelkle et al., 2012, Eq. 14); +9. refuse pooling discrete lags from unequal event intervals as one coefficient; +10. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); +11. refuse the difference quotient as a continuous-time rate; +12. apply the same event-time map to CWC residuals (still not DSEM); +13. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). ## Claim boundary -This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. +This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. ## Authoritative sources @@ -34,7 +36,9 @@ Driver, C. C., Oud, J. H. L., & Voelkle, M. C. (2017). Continuous time structura Kish, L. (1965). *Survey sampling*. John Wiley & Sons. -The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-17T13:26Z (https://www.zora.uzh.ch/handle/20.500.14742/72792; bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`). Page 16 writes discrete auto-effects as \(A^{*}(\Delta t)=\exp(A\Delta t)\) (Eq. 7). Introducing Intercepts (manuscript p. 20, Eq. 12) adds a continuous-time intercept \(b\) and writes the expected-value solution whose discrete increment is \(A^{-1}(\exp(A\Delta t)-I)b\); the scalar constant-predictor effect is \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\) and restate the discrete intercept as a function of \(A\) and \(\Delta t\). Discrete auto-effects are strictly positive for finite real drift and interval; the inverse \(a=\ln\varphi/\Delta t\) therefore requires \(\varphi>0\). Binary64 `exp` of a large negative argument is `+0` and is refused. Equations 3–4 of Voelkle et al. (2012) are the discouraged difference-quotient approximation. Meredith (1993) remains unread (Unpaywall/OpenAlex/Semantic Scholar/archive.org 2026-08-17T13:26Z: closed; Springer content/pdf was HTML; Cambridge Core redirected to an HTML product page). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T13:26Z: closed; Cambridge Core redirected to an HTML stub). The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title is not the 1991 journal article. +Oud, J. H. L., & Jansen, R. A. R. G. (2000). Continuous time state space modeling of panel data by means of SEM. *Psychometrika, 65*(2), 199–215. https://doi.org/10.1007/BF02294374 (cited by Voelkle et al., 2012, Eq. 14 discussion; PDF not opened). + +The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-17T14:20Z (https://www.zora.uzh.ch/handle/20.500.14742/72792; bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`). Page 16 writes discrete auto-effects as \(A^{*}(\Delta t)=\exp(A\Delta t)\) (Eq. 7). Introducing Intercepts (manuscript p. 20, Eq. 12) adds a continuous-time intercept \(b\) and writes the expected-value solution whose discrete increment is \(A^{-1}(\exp(A\Delta t)-I)b\); the scalar constant-predictor effect is \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). The next paragraph (manuscript p. 21, Eq. 14) writes the discrete effect of a **time-varying** predictor, when the sampling interval equals the constancy interval, as \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product does not depend on the predictor auto-effect. The manuscript calls Eq. 14 a first-order approximation that deteriorates as \(\Delta t\) grows, and defers unmatched sampling/constancy intervals to Oud and Jansen (2000), which is unread. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\) and restate the discrete intercept as a function of \(A\) and \(\Delta t\). Discrete auto-effects are strictly positive for finite real drift and interval; the inverse \(a=\ln\varphi/\Delta t\) therefore requires \(\varphi>0\). Binary64 `exp` of a large negative argument is `+0` and is refused. Equations 3–4 of Voelkle et al. (2012) are the discouraged difference-quotient approximation. Meredith (1993) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T14:20Z: closed; Springer content/pdf was HTML). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T14:20Z: closed). The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title is not the 1991 journal article. ## Formula notes @@ -44,6 +48,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-17T13:2 - **Exact scalar map.** Voelkle et al. (2012, Eq. 7) and Driver et al. (2017, Eq. 3): \(\varphi = A^{*}(\Delta t)=\exp(a\,\Delta t)\). The inverse is \(a=\ln\varphi/\Delta t\). The forward map is the same equation. The real exponential is strictly positive; a binary64 underflow to `+0` is refused because the inverse logarithm does not exist at zero. The difference quotient \((x(t+\Delta t)-x(t))/\Delta t\) is refused. - **Unequal-interval remap.** Discrete \(\varphi(\Delta t_1)\) and \(\varphi(\Delta t_2)\) are not comparable when \(\Delta t_1\neq\Delta t_2\) (Voelkle et al., 2012, ZORA manuscript pp. 2, 16, 33). The licensed path is \(a=\ln\varphi_{\mathrm{src}}/\Delta t_{\mathrm{src}}\) then \(\varphi_{\mathrm{ref}}=\exp(a\,\Delta t_{\mathrm{ref}})\). Pooling those discrete lags fails closed. - **Constant-predictor discrete effect.** Voelkle et al. (2012, Eq. 12; ZORA accepted manuscript re-opened 2026-08-17T13:23Z, Introducing Intercepts): for a constant predictor with \(a_{xx}\neq 0\), \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). Driver, Oud, and Voelkle (2017, p. 4, after Eq. 3) restate the discrete intercept as a function of \(A\) and \(\Delta t\). The algebraically identical finite-`expm1` evaluation is \(a_{yx}(\operatorname{expm1}(z)/a_{xx})\) with \(z=a_{xx}\Delta t\). Dividing the increment by the finite auto-effect keeps the equilibrium increment \(-a_{yx}/a_{xx}\) when \(z\) overflows to \(-\infty\) (the manuscript notes that the exponential vanishes as \(\Delta t\) grows) and keeps a finite result when \(a_{yx}\Delta t\) overflows. When `expm1(z)` overflows to `+\infty` at a finite \(z\), rewrite as \(\operatorname{sign}(a_{yx}/a_{xx})\exp(\ln|a_{yx}|+z-\ln|a_{xx}|)-a_{yx}/a_{xx}\). A zero continuous effect is exactly zero even if `expm1` overflows. \(z\to+\infty\) fails closed unless \(a_{yx}=0\). When binary64 \(z\) underflows to `+0`, the mathematical limit of Eq. 12 is \(a_{yx}\Delta t\). That limit is IEEE-754 evaluation of Eq. 12, not a substitution of the first-order product as the general discrete effect. \(a_{xx}=0\) fails closed. This is not DSEM. +- **Time-varying-predictor discrete effect.** Voelkle et al. (2012, Eq. 14; ZORA accepted manuscript re-opened 2026-08-17T14:20Z, Introducing Intercepts, manuscript p. 21): when the predictor can take a new value at each occasion and the sampling interval equals the constancy interval, \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). The effect does not depend on \(a_{xx}\). The manuscript states that this is a first-order approximation that deteriorates as \(\Delta t\) grows. It is not Eq. 12. Unmatched sampling and constancy intervals cite Oud and Jansen (2000), which is unread, and fail closed. - **CWC-then-lag.** Sample cluster means are removed first. Consecutive within residuals are then fitted by least squares to \(r_{t+\Delta t}\approx\exp(a\Delta t)\,r_{t}\) on event time. Same-sign pair-wise logs initialize the scalar Newton step. Sign-flipping \(T=2\) CWC pairs have no real logarithm and fail closed. Curran and Bauer (2011, pp. 607–608) show that this person-mean subtraction on a raw autoregressive series does **not** isolate the lagged within-person effect; the helper therefore does not claim to recover the raw-process drift. - **Already-centered irregular residual.** The caller supplies lagged within residuals. The mean of \(a=\ln(r_{t+\Delta t}/r_t)/\Delta t\) is the exact scalar map. Intervals may be irregular. The helper does not center again. This is not DSEM. @@ -55,7 +60,8 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-17T13:2 - the exact scalar map recovers a known drift on event time and refuses every other clock plus the difference quotient; - the forward map inverts the log-rate, remaps \(\varphi(1)\) onto \(\varphi(2)\) at machine-scale RMSE, and that RMSE is smaller than treating \(\varphi(1)\) as \(\varphi(2)\); - a binary64 underflow of \(\exp(a\Delta t)\) to `+0` (direct forward map and large-interval remap) fails closed; -- Voelkle et al. (2012, Eq. 12) recovers a known discrete constant-predictor effect at machine-scale RMSE, and that RMSE is smaller than the first-order product \(a_{yx}\Delta t\); \(a_{xx}=0\) fails closed; binary64 underflow of \(a_{xx}\Delta t\) to `+0` recovers the Eq. 12 limit \(a_{yx}\Delta t\); a finite Eq. 12 result whose first factor \(a_{yx}\Delta t\) overflows is recovered; a finite Eq. 12 equilibrium increment whose \(z=a_{xx}\Delta t\) overflows to \(-\infty\) is recovered as \(-a_{yx}/a_{xx}\); a finite Eq. 12 result whose `expm1(z)` overflows at a finite \(z\) is recovered in log space; a zero continuous effect is exactly zero; +- Voelkle et al. (2012, Eq. 12) recovers a known discrete constant-predictor effect at machine-scale RMSE, and that RMSE is smaller than the first-order product \(a_{yx}\Delta t\); \(a_{xx}=0\) fails closed; binary64 underflow of \(a_{xx}\Delta t\) to `+0` recovers the Eq. 12 limit \(a_{yx}\Delta t\); a finite Eq. 12 result whose first factor \(a_{yx}\Delta t\) overflows is recovered; a finite Eq. 12 equilibrium increment whose \(z=a_{xx}\Delta t\) overflows to \(-\infty\) is recovered as \(-a_{yx}/a_{xx}\); a finite Eq. 12 result whose `expm1(z)` overflows at a finite \(z\) is recovered in log space; a zero continuous effect is exactly zero; an overflowing \(a_{yx}/a_{xx}\) rewrite term fails closed; +- Voelkle et al. (2012, Eq. 14) recovers \(a_{yx}\Delta t\) at machine-scale RMSE when sampling, constancy, and event intervals match, and that value is not the Eq. 12 constant-predictor effect; unmatched intervals fail closed; - pooling discrete lags from unequal intervals fails closed; - CWC-then-lag on a two-cluster decaying series has smaller computed RMSE than a level-pooled series when the latter is identified; - already-centered irregular residuals recover a known drift at machine-scale RMSE, and that RMSE is smaller than CWC of the corresponding raw autoregressive series (Curran & Bauer, 2011, pp. 607–608); diff --git a/docs/research/standards-and-literature.md b/docs/research/standards-and-literature.md index 8f299096..6bf59750 100644 --- a/docs/research/standards-and-literature.md +++ b/docs/research/standards-and-literature.md @@ -34,7 +34,7 @@ Meredith, W. (1993). Measurement invariance, factor analysis and factorial invar Holland, P. W. (1986). Statistics and causal inference. *Journal of the American Statistical Association, 81*(396), 945–960. https://doi.org/10.1080/01621459.1986.10478354 -TEPP applies these sources to construct definition, score interpretation, reliability, validity evidence, uncertainty, consequences, longitudinal invariance, ESEM cross-loadings, and DSEM. Topic outputs are treated as fallible indicators or components only after their construct role is evaluated. Reflective, formative, and network classes remain distinct (Bollen & Lennox, 1991). Complete-data OLS loadings across posterior indicator draws are combined with Rubin (1996) \(T_m\); the arithmetic-mean helper remains a point estimate. Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T13:26Z: closed). The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title was opened 2026-08-17T12:04Z from archive.org; it is not the 1991 journal article and is not used as Mislevy plausible-value authority. Temporal precedence is not causal identification (Holland, 1986). Within/between OLS follows Enders and Tofighi (2007), Curran and Bauer (2011), and Hamaker et al. (2015). Enders and Tofighi (2007, Table 2, pp. 124–127; PDF opened 2026-08-17) show that the CWC cluster-mean coefficient is the contextual effect (`between − within`), not the between-cluster effect. Curran and Bauer (2011, pp. 607–608) reject person-mean subtraction on a raw autoregressive series as the lagged within-person residual; already-centered irregular residuals use the Voelkle et al. (2012, Eq. 7) / Driver et al. (2017, Eq. 3) scalar map. Discrete lags from unequal event intervals are remapped through that log-rate (Voelkle et al., 2012, ZORA accepted manuscript re-opened 2026-08-17T13:13Z) and are not pooled. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\) and restate the discrete intercept as a function of \(A\) and \(\Delta t\). A binary64 underflow of \(\exp(a\Delta t)\) to `+0` is refused because discrete auto-effects are strictly positive. The discrete effect of a constant predictor is Voelkle et al. (2012, Eq. 12; ZORA accepted manuscript re-opened 2026-08-17T13:26Z, Introducing Intercepts, manuscript p. 20), evaluated as \(a_{yx}(\operatorname{expm1}(z)/a_{xx})\) with \(z=a_{xx}\Delta t\) so a finite result is not lost when \(z\) overflows to \(-\infty\) or when \(a_{yx}\Delta t\) overflows, and in log space when `expm1(z)` overflows at a finite \(z\); a zero continuous effect is exactly zero; the first-order product is the underflow limit of that equation, not the general discrete effect. Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-17T13:26Z) write the same discrete intercept as \(A^{-1}[e^{A\Delta t}-I]\xi\). Metric/weak invariance does not license latent-mean comparison. Meredith (1993) remains unread (Unpaywall/OpenAlex/Semantic Scholar/archive.org 2026-08-17T13:26Z: closed; Cambridge Core redirected to an HTML product page; Springer content/pdf was an HTML stub). +TEPP applies these sources to construct definition, score interpretation, reliability, validity evidence, uncertainty, consequences, longitudinal invariance, ESEM cross-loadings, and DSEM. Topic outputs are treated as fallible indicators or components only after their construct role is evaluated. Reflective, formative, and network classes remain distinct (Bollen & Lennox, 1991). Complete-data OLS loadings across posterior indicator draws are combined with Rubin (1996) \(T_m\); the arithmetic-mean helper remains a point estimate. Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T13:26Z: closed). The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title was opened 2026-08-17T12:04Z from archive.org; it is not the 1991 journal article and is not used as Mislevy plausible-value authority. Temporal precedence is not causal identification (Holland, 1986). Within/between OLS follows Enders and Tofighi (2007), Curran and Bauer (2011), and Hamaker et al. (2015). Enders and Tofighi (2007, Table 2, pp. 124–127; PDF opened 2026-08-17) show that the CWC cluster-mean coefficient is the contextual effect (`between − within`), not the between-cluster effect. Curran and Bauer (2011, pp. 607–608) reject person-mean subtraction on a raw autoregressive series as the lagged within-person residual; already-centered irregular residuals use the Voelkle et al. (2012, Eq. 7) / Driver et al. (2017, Eq. 3) scalar map. Discrete lags from unequal event intervals are remapped through that log-rate (Voelkle et al., 2012, ZORA accepted manuscript re-opened 2026-08-17T13:13Z) and are not pooled. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\) and restate the discrete intercept as a function of \(A\) and \(\Delta t\). A binary64 underflow of \(\exp(a\Delta t)\) to `+0` is refused because discrete auto-effects are strictly positive. The discrete effect of a constant predictor is Voelkle et al. (2012, Eq. 12; ZORA accepted manuscript re-opened 2026-08-17T14:20Z, Introducing Intercepts, manuscript p. 20), evaluated as \(a_{yx}(\operatorname{expm1}(z)/a_{xx})\) with \(z=a_{xx}\Delta t\) so a finite result is not lost when \(z\) overflows to \(-\infty\) or when \(a_{yx}\Delta t\) overflows, and in log space when `expm1(z)` overflows at a finite \(z\); a zero continuous effect is exactly zero; an overflowing \(a_{yx}/a_{xx}\) rewrite term fails closed; the first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. The discrete effect of a time-varying predictor with matched sampling and constancy intervals is Voelkle et al. (2012, Eq. 14; manuscript p. 21): \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product is not Eq. 12. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-17T13:26Z) write the same discrete intercept as \(A^{-1}[e^{A\Delta t}-I]\xi\). Metric/weak invariance does not license latent-mean comparison. Meredith (1993) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T14:20Z: closed). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T14:20Z: closed). ## Structural, correlated, dynamic, relational, and multilingual topic models diff --git a/docs/validation/temporal-event-foundation.md b/docs/validation/temporal-event-foundation.md index 9fed4a77..c5a07c10 100644 --- a/docs/validation/temporal-event-foundation.md +++ b/docs/validation/temporal-event-foundation.md @@ -23,7 +23,7 @@ This report tracks exact-head scientific and engineering evidence required befor | Truth corpora / manifests | `tepp_simulation` | implemented-main | — | deterministic generator tests | Task 10 / PR #18 | | Recovery metrics | `validation_core` | implemented-main | — | RMSE/bias/coverage/MC gates | Task 11 / PR #19 | | Versioned API/export contracts | `tepp_api` | implemented-main | naruon HTTP interchange | unknown-field/version/limit + naruon HTTPS interchange tests | Task 12 / PR #21; live HTTP service remaining | -| Psychometric structural input gates | `psychometric_core` | accepted-target | stacked psychometric PR | construct-class refusal + ALR/ILR boundary + true-loading RMSE + posterior-draw point-estimate mean + Rubin `T` + CWC within/between + CWC contextual effect + event-time log-rate + irregular already-centered residual lag + strong-gated latent means; full ESEM/DSEM remaining | ADR 0005; `docs/research/posterior-esem-input-gates.md`; `docs/research/multilevel-event-time-recovery.md`; `docs/research/rubin-total-variance.md`; `docs/research/strong-invariance-latent-means.md` | +| Psychometric structural input gates | `psychometric_core` | partial | stacked psychometric PR | construct-class refusal + ALR/ILR boundary + true-loading RMSE + posterior-draw point-estimate mean + Rubin `T` + CWC within/between + CWC contextual effect + event-time log-rate + constant- and time-varying-predictor discrete effects + irregular already-centered residual lag + strong-gated latent means; full ESEM/DSEM remaining | ADR 0005; `docs/research/posterior-esem-input-gates.md`; `docs/research/multilevel-event-time-recovery.md`; `docs/research/rubin-total-variance.md`; `docs/research/strong-invariance-latent-means.md` | | CWL modular connectors | `docs/connectors/*` | implemented-main | — | contract docs + examples | PR #22; live HTTP ports remaining | | Release SBOM/provenance generator | `scripts/release_evidence.py` | partial | — | generate+validate in CI | Task 13 partial / PR #28 | From 954612700af9aa99187d54e3341ce742f60ca23f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 17:25:48 +0000 Subject: [PATCH 17/62] feat(psychometric): recover Driver Eq. 3 discrete process noise MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exact scalar Q_Δt from Driver, Oud, and Voelkle (2017, Eq. 3). --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 1 + CLAUDE.md | 2 +- crates/psychometric_core/src/event_time.rs | 176 +++++++++++++++++- crates/psychometric_core/src/lib.rs | 5 +- ...multilevel_event_time_recovery_contract.rs | 53 +++++- .../scientific_claim_boundary_contract.rs | 27 ++- docs/TRACEABILITY.md | 2 +- docs/adr/0005-posterior-esem-dsem.md | 4 +- docs/adr/README.md | 2 +- .../multilevel-event-time-recovery.md | 17 +- docs/research/standards-and-literature.md | 2 +- docs/validation/temporal-event-foundation.md | 2 +- 13 files changed, 268 insertions(+), 27 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 7d7877b8..b31db86f 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -61,7 +61,7 @@ boundaries above remain the target modular MSA architecture. | `tepp_simulation` | known-truth temporal/event data generation | | `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics | | `tepp_api` | versioned DTO, schema, and export contracts | -| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means | +| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means | No crate exposes placeholder production behavior in Task 1. This prevents an empty façade from becoming a de facto public API before its invariants and tests diff --git a/CHANGELOG.md b/CHANGELOG.md index f362f8e3..ea725b5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang ### Added +- `psychometric_core` recovers the exact scalar discrete process noise of Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF opened 2026-08-17T17:19Z, p. 4): \(Q_{\Delta t}=q(\mathrm{e}^{2a\Delta t}-1)/(2a)\) for \(a\neq 0\) and \(q\Delta t\) for \(a=0\), with \(q=GG^{\top}\ge 0\). Evaluated as \(q(\operatorname{expm1}(z)/(2a))\) with \(z=2a\Delta t\). Binary64 underflow of \(z\) recovers \(q\Delta t\). \(z\to-\infty\) keeps the equilibrium variance \(-q/(2a)\). Finite-\(z\) `expm1` overflow rewrites in log space. A zero diffusion is exactly zero. \(z\to+\infty\) and a negative diffusion fail closed. Still not a Kalman filter, not DSEM, and not a matrix `expm`. - `psychometric_core` recovers Voelkle et al. (2012, Eq. 14; ZORA accepted manuscript re-opened 2026-08-17T14:20Z, Introducing Intercepts, manuscript p. 21): the discrete effect of a time-varying predictor whose sampling interval equals its constancy interval is \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). It does not depend on the predictor auto-effect. That product is not Eq. 12. Unmatched sampling and constancy intervals fail closed (Oud & Jansen, 2000, unread). An overflowing Eq. 12 rewrite scale \(a_{yx}/a_{xx}\) also fails closed. Still not DSEM. - `psychometric_core` recovers Voelkle et al. (2012, Eq. 12) when `expm1(z)` overflows to `+∞` at a finite `z`. The rewrite is `sign(a_{yx}/a_{xx})\exp(\ln|a_{yx}|+z-\ln|a_{xx}|)-a_{yx}/a_{xx}` (algebraically identical to `(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)`). A zero continuous effect is exactly zero even when `expm1` overflows (`0\cdot+\infty` is `NaN`). `z\to+\infty` remains fail-closed. ZORA accepted manuscript re-opened 2026-08-17T13:26Z, Introducing Intercepts, manuscript p. 20; Driver, Oud, and Voelkle (2017, Eq. 3) restated `A^{-1}[e^{A\Delta t}-I]\xi`. Still not DSEM. - `psychometric_core` evaluates Voelkle et al. (2012, Eq. 12) as \(a_{yx}(\operatorname{expm1}(z)/a_{xx})\) with \(z=a_{xx}\Delta t\). That order is algebraically identical to \((a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). Dividing the increment by the finite auto-effect keeps the equilibrium increment \(-a_{yx}/a_{xx}\) when \(z\) overflows to \(-\infty\) (ZORA accepted manuscript, Introducing Intercepts: the exponential vanishes as \(\Delta t\) grows; CodeRabbit finding on `7ffb65b`). The prior \(a_{yx}\Delta t\) overflow case remains finite. Binary64 underflow of \(z\) to `+0` recovers the Eq. 12 limit \(a_{yx}\Delta t\); the first-order product is still not the general discrete effect. Still not DSEM. diff --git a/CLAUDE.md b/CLAUDE.md index dab7426a..7699f05e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. -- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = q (expm1(2 a Δt) / (2 a))` with `q = G G⊤ ≥ 0`; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; this is not a Kalman filter. - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index 83a1816c..4757a5df 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -10,7 +10,8 @@ //! discrete effect of a constant predictor is Voelkle et al. (2012, //! Eq. 12). The discrete effect of a time-varying predictor whose //! sampling interval equals its constancy interval is their Eq. 14. -//! The difference quotient `(x(t+Δt) − x(t)) / Δt` (their +//! The exact scalar discrete process noise is the closed form of +//! Driver, Oud, and Voelkle (2017, Eq. 3) `Q_Δt`. The difference quotient `(x(t+Δt) − x(t)) / Δt` (their //! Eqs. 3–4) is refused. This is not DSEM and not a matrix `expm`. use std::collections::BTreeMap; @@ -390,6 +391,87 @@ pub fn refuse_unmatched_time_varying_predictor_interval( Err(PsychometricError::UnmatchedTimeVaryingInterval) } +/// Exact scalar discrete process noise on event time. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF opened 2026-08-17T17:19Z, +/// p. 4) write the discrete process-noise covariance +/// `Q_Δt = ∫_0^{Δt} expm(A(Δt−τ)) G G⊤ expm(A(Δt−τ))⊤ dτ`. +/// The noiseless scalar closed form with continuous diffusion +/// `q = G G⊤ ≥ 0` is `q (exp(2 a Δt) − 1) / (2 a)` for `a ≠ 0` and +/// `q Δt` for `a = 0`. The algebraically identical finite-`expm1` +/// evaluation is `q (expm1(z) / (2 a))` with `z = 2 a Δt`. When +/// binary64 `z` underflows to `+0`, the mathematical limit is `q Δt`. +/// When `z → −∞` the exponential vanishes and the result is the +/// equilibrium variance `−q / (2 a)` for stable `a < 0`. When +/// `expm1(z)` overflows to `+∞` at a finite `z`, rewrite as +/// `sign(q / (2 a)) exp(ln|q| + z − ln|2 a|) − q / (2 a)`. `z → +∞` +/// is an unstable process and fails closed unless `q = 0`. A zero +/// diffusion is exactly zero even if `expm1` overflows. This is not a +/// Kalman filter, not DSEM, and not a matrix `expm`. +/// +/// # Errors +/// +/// Returns [`PsychometricError::EventTimeRequired`] for any non-event clock, +/// [`PsychometricError::NonPositiveInterval`] when `event_delta` is not +/// strictly positive, and [`PsychometricError::InvalidNumericInput`] when +/// the diffusion is negative or non-finite, the log-rate is non-finite, or +/// the mapped variance is non-finite. +pub fn recover_discrete_process_noise( + continuous_diffusion: f64, + log_rate: f64, + event_delta: f64, + clock: LagClock, +) -> Result { + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + if !event_delta.is_finite() || event_delta <= 0.0 { + return Err(PsychometricError::NonPositiveInterval); + } + if !continuous_diffusion.is_finite() || continuous_diffusion < 0.0 || !log_rate.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + // Driver Eq. 3: the integral of a zero diffusion is zero. + // Direct 0 * (expm1(z) / (2 a)) is NaN when expm1 overflows. + if continuous_diffusion == 0.0 { + return Ok(0.0); + } + if log_rate == 0.0 { + return require_finite(continuous_diffusion * event_delta); + } + let twice_log_rate = 2.0 * log_rate; + if !twice_log_rate.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + let increment_argument = twice_log_rate * event_delta; + if increment_argument == 0.0 { + // Binary64 underflow of 2 a Δt. lim z→0 of Eq. 3 Q_Δt is q Δt. + return require_finite(continuous_diffusion * event_delta); + } + let increment = increment_argument.exp_m1(); + if increment.is_finite() { + // Divide expm1(z) by the finite 2 a, not by z. expm1(−∞)/−∞ + // is +0 and loses the equilibrium variance −q / (2 a). + return require_finite(continuous_diffusion * (increment / twice_log_rate)); + } + // expm1 overflowed. z → +∞ diverges (unstable auto-effect). + if !increment_argument.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + // Finite z, overflowed expm1. (q / (2 a))(exp(z) − 1) = + // sign(q / (2 a)) exp(ln|q| + z − ln|2 a|) − q / (2 a). + let scale = continuous_diffusion / twice_log_rate; + if !scale.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + let log_abs_dominant = + continuous_diffusion.abs().ln() + increment_argument - twice_log_rate.abs().ln(); + let dominant = require_finite( + continuous_diffusion.signum() * twice_log_rate.signum() * log_abs_dominant.exp(), + )?; + require_finite(dominant - scale) +} + /// Refuse the difference quotient as a continuous-time rate. /// /// Voelkle et al. (2012) discourage `(x(t+Δt) − x(t)) / Δt` as the drift. @@ -622,10 +704,10 @@ mod tests { ClusteredEventScore, EventOccasion, LagClock, LaggedWithinResidual, fit_scalar_log_rate, map_discrete_lag_across_event_intervals, recover_discrete_constant_predictor_effect, recover_discrete_lag_from_log_rate, recover_discrete_lag_one, - recover_discrete_time_varying_predictor_effect, recover_event_series_mean_log_rate, - recover_event_time_discrete_lag_and_log_rate, recover_irregular_centered_residual_log_rate, - recover_local_log_rate, recover_within_residual_event_time_log_rate, - refuse_difference_quotient_as_local_rate, + recover_discrete_process_noise, recover_discrete_time_varying_predictor_effect, + recover_event_series_mean_log_rate, recover_event_time_discrete_lag_and_log_rate, + recover_irregular_centered_residual_log_rate, recover_local_log_rate, + recover_within_residual_event_time_log_rate, refuse_difference_quotient_as_local_rate, refuse_pooled_discrete_lag_across_unequal_intervals, refuse_unmatched_time_varying_predictor_interval, }; @@ -1043,6 +1125,90 @@ mod tests { ); } + #[test] + fn discrete_process_noise_recovers_driver_equation_three() { + let diffusion = 0.4_f64; + let drift = -0.5_f64; + let delta = 1.0_f64; + let recovered = + recover_discrete_process_noise(diffusion, drift, delta, LagClock::EventTime) + .expect("q_dt"); + let expected = diffusion * ((2.0 * drift * delta).exp() - 1.0) / (2.0 * drift); + assert!((recovered - expected).abs() < 1e-15); + // a = 0 is the integral of a constant diffusion: q Δt. + assert_eq!( + recover_discrete_process_noise(diffusion, 0.0, 2.5, LagClock::EventTime), + Ok(diffusion * 2.5) + ); + // Binary64 underflow of 2 a Δt recovers the same limit. + let underflowed = recover_discrete_process_noise(1.0, 1e-308, 1e-308, LagClock::EventTime) + .expect("z underflow"); + assert!((underflowed - 1e-308).abs() < 1e-320); + // z → −∞ keeps the equilibrium variance −q / (2 a). + let equilibrium = + recover_discrete_process_noise(0.4, -1e300, 2.0, LagClock::EventTime).expect("eq var"); + assert!((equilibrium - (0.4 / (2.0 * 1e300))).abs() < 1e-315); + // Finite z, overflowed expm1: log-space rewrite stays finite. + let overflowed = recover_discrete_process_noise(1e-308, 400.0, 1.0, LagClock::EventTime) + .expect("expm1 overflow"); + let rewrite_scale = 1e-308 / 800.0; + let rewrite_log = (1e-308_f64).ln() + 800.0 - 800.0_f64.ln(); + let rewrite = rewrite_log.exp() - rewrite_scale; + assert!((overflowed - rewrite).abs() / rewrite.abs() < 1e-12); + assert_eq!( + recover_discrete_process_noise(0.0, 800.0, 1.0, LagClock::EventTime), + Ok(0.0) + ); + assert_eq!( + recover_discrete_process_noise(0.0, 1e308, 2.0, LagClock::EventTime), + Ok(0.0) + ); + } + + #[test] + fn discrete_process_noise_invalid_inputs_fail_closed() { + assert_eq!( + recover_discrete_process_noise(0.4, -0.5, 1.0, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_discrete_process_noise(0.4, -0.5, 0.0, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_process_noise(0.4, -0.5, -1.0, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_process_noise(0.4, -0.5, f64::NAN, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_process_noise(-0.1, -0.5, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_process_noise(f64::NAN, -0.5, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_process_noise(0.4, f64::NAN, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_process_noise(1.0, 800.0, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_process_noise(1.0, 1e308, 2.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_process_noise(1.0, 1e308, 1e-308, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + } + #[test] fn non_event_clocks_and_difference_quotient_fail_closed() { for clock in [ diff --git a/crates/psychometric_core/src/lib.rs b/crates/psychometric_core/src/lib.rs index 34af58b4..64ad04b1 100644 --- a/crates/psychometric_core/src/lib.rs +++ b/crates/psychometric_core/src/lib.rs @@ -14,7 +14,8 @@ //! lags across unequal event intervals through that log-rate, recovers the //! exact scalar discrete effect of a constant predictor, recovers the //! first-order discrete effect of a time-varying predictor with matched -//! sampling and constancy intervals, and refuses +//! sampling and constancy intervals, recovers the exact scalar discrete +//! process noise of Driver et al. (2017, Eq. 3), and refuses //! latent-mean comparison below strong invariance. mod causality; @@ -68,6 +69,8 @@ pub use event_time::recover_discrete_constant_predictor_effect; pub use event_time::recover_discrete_lag_from_log_rate; /// Noiseless scalar discrete lag `later / earlier`. pub use event_time::recover_discrete_lag_one; +/// Exact scalar discrete process noise `Q_Δt` on event time. +pub use event_time::recover_discrete_process_noise; /// First-order discrete effect of a time-varying event-time predictor. pub use event_time::recover_discrete_time_varying_predictor_effect; /// Mean local log-rate on a sorted event-time series. 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 614eb793..ba507749 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -6,10 +6,11 @@ use psychometric_core::{ LaggedWithinResidual, PsychometricError, map_discrete_lag_across_event_intervals, ordinary_least_squares_slope, recover_cluster_mean_within_between_slopes, recover_discrete_constant_predictor_effect, recover_discrete_lag_from_log_rate, - recover_discrete_time_varying_predictor_effect, recover_event_series_mean_log_rate, - recover_event_time_discrete_lag_and_log_rate, recover_irregular_centered_residual_log_rate, - recover_kish_weighted_slope, recover_within_residual_event_time_log_rate, - refuse_difference_quotient_as_local_rate, refuse_pooled_discrete_lag_across_unequal_intervals, + recover_discrete_process_noise, recover_discrete_time_varying_predictor_effect, + recover_event_series_mean_log_rate, recover_event_time_discrete_lag_and_log_rate, + recover_irregular_centered_residual_log_rate, recover_kish_weighted_slope, + recover_within_residual_event_time_log_rate, refuse_difference_quotient_as_local_rate, + refuse_pooled_discrete_lag_across_unequal_intervals, refuse_unmatched_time_varying_predictor_interval, }; @@ -282,6 +283,50 @@ fn time_varying_predictor_discrete_effect_recovers_equation_fourteen() { ); } +#[test] +fn discrete_process_noise_recovers_driver_equation_three() { + let diffusion = 0.4_f64; + let drift = -0.5_f64; + let delta = 1.0_f64; + let recovered = + recover_discrete_process_noise(diffusion, drift, delta, LagClock::EventTime).expect("q_dt"); + let expected = diffusion * ((2.0 * drift * delta).exp() - 1.0) / (2.0 * drift); + let error = rmse(&[expected], &[recovered]); + assert!(error < 1e-15, "Driver Eq. 3 Q_Δt RMSE {error}"); + let collapsed = rmse(&[expected], &[diffusion]); + assert!( + collapsed > error, + "continuous diffusion is not discrete process noise: collapsed RMSE {collapsed} must exceed {error}" + ); + assert_eq!( + recover_discrete_process_noise(diffusion, 0.0, 2.5, LagClock::EventTime), + Ok(diffusion * 2.5) + ); + let underflowed = recover_discrete_process_noise(1.0, 1e-308, 1e-308, LagClock::EventTime) + .expect("z underflow"); + assert!(rmse(&[1e-308], &[underflowed]) < 1e-320); + let equilibrium = + recover_discrete_process_noise(0.4, -1e300, 2.0, LagClock::EventTime).expect("eq var"); + assert!(rmse(&[0.4 / (2.0 * 1e300)], &[equilibrium]) < 1e-315); + let overflowed = + recover_discrete_process_noise(1e-308, 400.0, 1.0, LagClock::EventTime).expect("rewrite"); + let rewrite_scale = 1e-308 / 800.0; + let rewrite = ((1e-308_f64).ln() + 800.0 - 800.0_f64.ln()).exp() - rewrite_scale; + assert!(rmse(&[rewrite], &[overflowed]) / rewrite.abs() < 1e-12); + assert_eq!( + recover_discrete_process_noise(0.0, 800.0, 1.0, LagClock::EventTime), + Ok(0.0) + ); + assert_eq!( + recover_discrete_process_noise(1.0, 800.0, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_process_noise(0.4, -0.5, 1.0, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); +} + #[test] fn within_residual_event_time_log_rate_beats_pooled_levels() { let true_drift = -0.3_f64; diff --git a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs index 53d2dd50..cf2b6c4c 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -4,8 +4,9 @@ use psychometric_core::{ ClusteredEventScore, ClusteredScore, IndicatorKind, LagClock, LaggedWithinResidual, ordinary_least_squares_slope, posterior_draw_point_estimate_mean, recover_cluster_mean_within_between_slopes, recover_discrete_constant_predictor_effect, - recover_discrete_time_varying_predictor_effect, recover_irregular_centered_residual_log_rate, - recover_loading_point_estimate_mean, recover_within_residual_event_time_log_rate, + recover_discrete_process_noise, recover_discrete_time_varying_predictor_effect, + recover_irregular_centered_residual_log_rate, recover_loading_point_estimate_mean, + recover_within_residual_event_time_log_rate, }; #[test] @@ -158,3 +159,25 @@ fn time_varying_equation_fourteen_is_not_constant_equation_twelve() { ); assert!((time_varying - outcome_on_predictor * delta).abs() < 1e-15); } + +#[test] +fn discrete_process_noise_is_not_the_continuous_diffusion() { + let diffusion = 0.4_f64; + let drift = -0.5_f64; + let delta = 1.0_f64; + let discrete = + recover_discrete_process_noise(diffusion, drift, delta, LagClock::EventTime).expect("q_dt"); + assert!( + (discrete - diffusion).abs() > 1e-3, + "Driver et al. (2017, Eq. 3, p. 4): Q_Δt must not equal continuous G G⊤" + ); + let expected = diffusion * ((2.0 * drift * delta).exp() - 1.0) / (2.0 * drift); + assert!((discrete - expected).abs() < 1e-15); + let constant = + recover_discrete_constant_predictor_effect(diffusion, drift, delta, LagClock::EventTime) + .expect("eq 12"); + assert!( + (discrete - constant).abs() > 1e-3, + "Driver Eq. 3 Q_Δt is not Voelkle Eq. 12" + ); +} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index aeef37a7..f160e9a7 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -28,7 +28,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | report template/section/copied/style/modality method effects | ADR 0004/0012; PRD/TRD | simulation truth factors implemented; estimator-side method model remains future | partial | | candidate K statistical/Pareto gates + blinded LLM review | ADR 0012; research | future `model_selection` | accepted-target | | compositional topic correlation / stable clustering | ADR 0005/0012; research | future `network_analysis` | accepted-target | -| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR; full ESEM/DSEM remaining | partial | +| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR; full ESEM/DSEM remaining | partial | | CPU bounded multithreading + GPU/VRAM streaming/parity | ADR 0001/0006 | future `compute_backend` | accepted-target | | TDT detection/tracking vs CHRONOS schema/prediction/temporal consistency | ADR 0016; PRD/research | future `event_intelligence` | accepted-target | | evidence-bounded LLM interpretation | ADR 0010/0012; PRD | future `interpretation_gateway` | accepted-target | diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index 3e3353a9..5dde0291 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -1,7 +1,7 @@ # ADR 0005 — Posterior-aware ESEM/DSEM and structural interpretation **Decision status:** Accepted -**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target +**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target **Date:** 2026-08-05 **Supersedes:** None. ADR 0012 governs upstream topic measurement/network coordinates; this ADR governs higher-order psychometric structure and longitudinal interpretation. @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = q (expm1(2 a Δt) / (2 a))` with `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF opened 2026-08-17T17:19Z, p. 4; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−q / (2 a)`; a zero diffusion is exactly zero; `z → +∞` fails closed), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. 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/adr/README.md b/docs/adr/README.md index 0477eb78..231b9d21 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -10,7 +10,7 @@ Read [`ADR_POLICY.md`](ADR_POLICY.md) first. **Decision status and implementatio | [0002](0002-six-clock-temporal-semantics.md) | Six-clock temporal semantics and fail-closed historical leakage prevention | Accepted | active-PR | Unmerged PR #8 is the canonical Task 3 replacement implementing typed clocks/intervals against the current protected-main lineage; conflicted PR #5 is superseded lineage. Later graph/split enforcement remains target work. | | [0003](0003-relational-event-multiple-membership.md) | Relational event ontology and time-varying cross-classified multiple membership | Accepted | partial | Weighted time-varying membership network/roles are active-PR (PR #12); full multilevel estimators, graph ontology, and persistence remain accepted-target. ADR 0016 owns event-intelligence tasks. | | [0004](0004-shared-multilingual-latent-space.md) | One shared multilingual latent space with explicit invariance status | Accepted | accepted-target | ADR 0012 owns the full topic-estimator/backend/global-topic contract. | -| [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | partial | Input gates, posterior-draw point estimates, Rubin `T` on OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects, irregular already-centered residual lag, and strong-gated latent means are on the stacked psychometric PR; full ESEM/DSEM estimator remains accepted-target. | +| [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | partial | Input gates, posterior-draw point estimates, Rubin `T` on OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects, exact scalar discrete process noise, irregular already-centered residual lag, and strong-gated latent means are on the stacked psychometric PR; full ESEM/DSEM estimator remains accepted-target. | | [0006](0006-vram-gpu-nvidia-orchestration.md) | VRAM-adaptive GPU compute and model-credential boundary | Accepted | accepted-target | LLM orchestration policy superseded by ADR 0010; autonomous development authority governed by ADR 0015. | | [0007](0007-rust-workspace-quality-gates.md) | Explicit Rust workspace, pinned toolchains, and exact quality gates | Accepted | implemented-main | ADR 0014 governs scientific/product claim promotion beyond repository-quality tooling. | | [0008](0008-immutable-evidence-identities-digests-and-spans.md) | Immutable evidence identities, `SHA-256` digests, exact spans, and strict wire reconstruction | Accepted | implemented-main | ADR 0013 governs future persistence/reproducibility/split authority. | diff --git a/docs/research/multilevel-event-time-recovery.md b/docs/research/multilevel-event-time-recovery.md index feefebc4..bfad3508 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -12,15 +12,16 @@ This slice stays inside `psychometric_core`. It does not add a second invariance 6. refuse a binary64 underflow of that forward map to `+0` (not a discrete lag); 7. recover the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12); 8. recover the first-order discrete effect of a time-varying predictor whose sampling interval equals its constancy interval (Voelkle et al., 2012, Eq. 14); -9. refuse pooling discrete lags from unequal event intervals as one coefficient; -10. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); -11. refuse the difference quotient as a continuous-time rate; -12. apply the same event-time map to CWC residuals (still not DSEM); -13. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). +9. recover the exact scalar discrete process noise of Driver, Oud, and Voelkle (2017, Eq. 3); +10. refuse pooling discrete lags from unequal event intervals as one coefficient; +11. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); +12. refuse the difference quotient as a continuous-time rate; +13. apply the same event-time map to CWC residuals (still not DSEM); +14. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). ## Claim boundary -This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. +This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). ## Authoritative sources @@ -38,7 +39,7 @@ Kish, L. (1965). *Survey sampling*. John Wiley & Sons. Oud, J. H. L., & Jansen, R. A. R. G. (2000). Continuous time state space modeling of panel data by means of SEM. *Psychometrika, 65*(2), 199–215. https://doi.org/10.1007/BF02294374 (cited by Voelkle et al., 2012, Eq. 14 discussion; PDF not opened). -The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-17T14:20Z (https://www.zora.uzh.ch/handle/20.500.14742/72792; bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`). Page 16 writes discrete auto-effects as \(A^{*}(\Delta t)=\exp(A\Delta t)\) (Eq. 7). Introducing Intercepts (manuscript p. 20, Eq. 12) adds a continuous-time intercept \(b\) and writes the expected-value solution whose discrete increment is \(A^{-1}(\exp(A\Delta t)-I)b\); the scalar constant-predictor effect is \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). The next paragraph (manuscript p. 21, Eq. 14) writes the discrete effect of a **time-varying** predictor, when the sampling interval equals the constancy interval, as \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product does not depend on the predictor auto-effect. The manuscript calls Eq. 14 a first-order approximation that deteriorates as \(\Delta t\) grows, and defers unmatched sampling/constancy intervals to Oud and Jansen (2000), which is unread. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\) and restate the discrete intercept as a function of \(A\) and \(\Delta t\). Discrete auto-effects are strictly positive for finite real drift and interval; the inverse \(a=\ln\varphi/\Delta t\) therefore requires \(\varphi>0\). Binary64 `exp` of a large negative argument is `+0` and is refused. Equations 3–4 of Voelkle et al. (2012) are the discouraged difference-quotient approximation. Meredith (1993) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T14:20Z: closed; Springer content/pdf was HTML). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T14:20Z: closed). The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title is not the 1991 journal article. +The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-17T14:20Z (https://www.zora.uzh.ch/handle/20.500.14742/72792; bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`). Page 16 writes discrete auto-effects as \(A^{*}(\Delta t)=\exp(A\Delta t)\) (Eq. 7). Introducing Intercepts (manuscript p. 20, Eq. 12) adds a continuous-time intercept \(b\) and writes the expected-value solution whose discrete increment is \(A^{-1}(\exp(A\Delta t)-I)b\); the scalar constant-predictor effect is \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). The next paragraph (manuscript p. 21, Eq. 14) writes the discrete effect of a **time-varying** predictor, when the sampling interval equals the constancy interval, as \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product does not depend on the predictor auto-effect. The manuscript calls Eq. 14 a first-order approximation that deteriorates as \(\Delta t\) grows, and defers unmatched sampling/constancy intervals to Oud and Jansen (2000), which is unread. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4; JSS PDF opened 2026-08-17T17:19Z) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\), the discrete intercept \(b_{\Delta t}=A^{-1}[A_{\Delta t}-I]b\), and the discrete process-noise covariance \(Q_{\Delta t}=\int_{0}^{\Delta t}\operatorname{expm}(A(\Delta t-\tau))GG^{\top}\operatorname{expm}(A(\Delta t-\tau))^{\top}\,d\tau\). Discrete auto-effects are strictly positive for finite real drift and interval; the inverse \(a=\ln\varphi/\Delta t\) therefore requires \(\varphi>0\). Binary64 `exp` of a large negative argument is `+0` and is refused. Equations 3–4 of Voelkle et al. (2012) are the discouraged difference-quotient approximation. Meredith (1993) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T17:19Z: closed; Springer content/pdf was HTML). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T17:19Z: closed). ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title is not the 1991 journal article. Oud and Jansen (2000) remains unread (OpenAlex/Semantic Scholar 2026-08-17T17:19Z: closed). ## Formula notes @@ -49,6 +50,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-17T14:2 - **Unequal-interval remap.** Discrete \(\varphi(\Delta t_1)\) and \(\varphi(\Delta t_2)\) are not comparable when \(\Delta t_1\neq\Delta t_2\) (Voelkle et al., 2012, ZORA manuscript pp. 2, 16, 33). The licensed path is \(a=\ln\varphi_{\mathrm{src}}/\Delta t_{\mathrm{src}}\) then \(\varphi_{\mathrm{ref}}=\exp(a\,\Delta t_{\mathrm{ref}})\). Pooling those discrete lags fails closed. - **Constant-predictor discrete effect.** Voelkle et al. (2012, Eq. 12; ZORA accepted manuscript re-opened 2026-08-17T13:23Z, Introducing Intercepts): for a constant predictor with \(a_{xx}\neq 0\), \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). Driver, Oud, and Voelkle (2017, p. 4, after Eq. 3) restate the discrete intercept as a function of \(A\) and \(\Delta t\). The algebraically identical finite-`expm1` evaluation is \(a_{yx}(\operatorname{expm1}(z)/a_{xx})\) with \(z=a_{xx}\Delta t\). Dividing the increment by the finite auto-effect keeps the equilibrium increment \(-a_{yx}/a_{xx}\) when \(z\) overflows to \(-\infty\) (the manuscript notes that the exponential vanishes as \(\Delta t\) grows) and keeps a finite result when \(a_{yx}\Delta t\) overflows. When `expm1(z)` overflows to `+\infty` at a finite \(z\), rewrite as \(\operatorname{sign}(a_{yx}/a_{xx})\exp(\ln|a_{yx}|+z-\ln|a_{xx}|)-a_{yx}/a_{xx}\). A zero continuous effect is exactly zero even if `expm1` overflows. \(z\to+\infty\) fails closed unless \(a_{yx}=0\). When binary64 \(z\) underflows to `+0`, the mathematical limit of Eq. 12 is \(a_{yx}\Delta t\). That limit is IEEE-754 evaluation of Eq. 12, not a substitution of the first-order product as the general discrete effect. \(a_{xx}=0\) fails closed. This is not DSEM. - **Time-varying-predictor discrete effect.** Voelkle et al. (2012, Eq. 14; ZORA accepted manuscript re-opened 2026-08-17T14:20Z, Introducing Intercepts, manuscript p. 21): when the predictor can take a new value at each occasion and the sampling interval equals the constancy interval, \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). The effect does not depend on \(a_{xx}\). The manuscript states that this is a first-order approximation that deteriorates as \(\Delta t\) grows. It is not Eq. 12. Unmatched sampling and constancy intervals cite Oud and Jansen (2000), which is unread, and fail closed. +- **Discrete process noise.** Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF opened 2026-08-17T17:19Z, p. 4): \(Q_{\Delta t}=\int_{0}^{\Delta t}\operatorname{expm}(A(\Delta t-\tau))GG^{\top}\operatorname{expm}(A(\Delta t-\tau))^{\top}\,d\tau\). The scalar closed form with continuous diffusion \(q=GG^{\top}\ge 0\) is \(q(\mathrm{e}^{2a\Delta t}-1)/(2a)\) for \(a\neq 0\) and \(q\Delta t\) for \(a=0\). The algebraically identical finite-`expm1` evaluation is \(q(\operatorname{expm1}(z)/(2a))\) with \(z=2a\Delta t\). Binary64 underflow of \(z\) to `+0` recovers \(q\Delta t\). \(z\to-\infty\) keeps the equilibrium variance \(-q/(2a)\). When `expm1(z)` overflows at a finite \(z\), rewrite as \(\operatorname{sign}(q/(2a))\exp(\ln|q|+z-\ln|2a|)-q/(2a)\). A zero diffusion is exactly zero even if `expm1` overflows. \(z\to+\infty\) fails closed unless \(q=0\). Negative \(q\) fails closed. This is not a Kalman filter and not a matrix `expm`. - **CWC-then-lag.** Sample cluster means are removed first. Consecutive within residuals are then fitted by least squares to \(r_{t+\Delta t}\approx\exp(a\Delta t)\,r_{t}\) on event time. Same-sign pair-wise logs initialize the scalar Newton step. Sign-flipping \(T=2\) CWC pairs have no real logarithm and fail closed. Curran and Bauer (2011, pp. 607–608) show that this person-mean subtraction on a raw autoregressive series does **not** isolate the lagged within-person effect; the helper therefore does not claim to recover the raw-process drift. - **Already-centered irregular residual.** The caller supplies lagged within residuals. The mean of \(a=\ln(r_{t+\Delta t}/r_t)/\Delta t\) is the exact scalar map. Intervals may be irregular. The helper does not center again. This is not DSEM. @@ -62,6 +64,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-17T14:2 - a binary64 underflow of \(\exp(a\Delta t)\) to `+0` (direct forward map and large-interval remap) fails closed; - Voelkle et al. (2012, Eq. 12) recovers a known discrete constant-predictor effect at machine-scale RMSE, and that RMSE is smaller than the first-order product \(a_{yx}\Delta t\); \(a_{xx}=0\) fails closed; binary64 underflow of \(a_{xx}\Delta t\) to `+0` recovers the Eq. 12 limit \(a_{yx}\Delta t\); a finite Eq. 12 result whose first factor \(a_{yx}\Delta t\) overflows is recovered; a finite Eq. 12 equilibrium increment whose \(z=a_{xx}\Delta t\) overflows to \(-\infty\) is recovered as \(-a_{yx}/a_{xx}\); a finite Eq. 12 result whose `expm1(z)` overflows at a finite \(z\) is recovered in log space; a zero continuous effect is exactly zero; an overflowing \(a_{yx}/a_{xx}\) rewrite term fails closed; - Voelkle et al. (2012, Eq. 14) recovers \(a_{yx}\Delta t\) at machine-scale RMSE when sampling, constancy, and event intervals match, and that value is not the Eq. 12 constant-predictor effect; unmatched intervals fail closed; +- Driver et al. (2017, Eq. 3) recovers a known scalar \(Q_{\Delta t}\) at machine-scale RMSE, and that RMSE is smaller than treating the continuous diffusion as the discrete process noise; \(a=0\) and binary64 underflow of \(2a\Delta t\) recover \(q\Delta t\); \(z\to-\infty\) recovers \(-q/(2a)\); a finite result whose `expm1` overflows at a finite \(z\) is recovered in log space; a zero diffusion is exactly zero; a negative diffusion and \(z\to+\infty\) fail closed; - pooling discrete lags from unequal intervals fails closed; - CWC-then-lag on a two-cluster decaying series has smaller computed RMSE than a level-pooled series when the latter is identified; - already-centered irregular residuals recover a known drift at machine-scale RMSE, and that RMSE is smaller than CWC of the corresponding raw autoregressive series (Curran & Bauer, 2011, pp. 607–608); diff --git a/docs/research/standards-and-literature.md b/docs/research/standards-and-literature.md index 6bf59750..7ba5c48a 100644 --- a/docs/research/standards-and-literature.md +++ b/docs/research/standards-and-literature.md @@ -34,7 +34,7 @@ Meredith, W. (1993). Measurement invariance, factor analysis and factorial invar Holland, P. W. (1986). Statistics and causal inference. *Journal of the American Statistical Association, 81*(396), 945–960. https://doi.org/10.1080/01621459.1986.10478354 -TEPP applies these sources to construct definition, score interpretation, reliability, validity evidence, uncertainty, consequences, longitudinal invariance, ESEM cross-loadings, and DSEM. Topic outputs are treated as fallible indicators or components only after their construct role is evaluated. Reflective, formative, and network classes remain distinct (Bollen & Lennox, 1991). Complete-data OLS loadings across posterior indicator draws are combined with Rubin (1996) \(T_m\); the arithmetic-mean helper remains a point estimate. Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T13:26Z: closed). The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title was opened 2026-08-17T12:04Z from archive.org; it is not the 1991 journal article and is not used as Mislevy plausible-value authority. Temporal precedence is not causal identification (Holland, 1986). Within/between OLS follows Enders and Tofighi (2007), Curran and Bauer (2011), and Hamaker et al. (2015). Enders and Tofighi (2007, Table 2, pp. 124–127; PDF opened 2026-08-17) show that the CWC cluster-mean coefficient is the contextual effect (`between − within`), not the between-cluster effect. Curran and Bauer (2011, pp. 607–608) reject person-mean subtraction on a raw autoregressive series as the lagged within-person residual; already-centered irregular residuals use the Voelkle et al. (2012, Eq. 7) / Driver et al. (2017, Eq. 3) scalar map. Discrete lags from unequal event intervals are remapped through that log-rate (Voelkle et al., 2012, ZORA accepted manuscript re-opened 2026-08-17T13:13Z) and are not pooled. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\) and restate the discrete intercept as a function of \(A\) and \(\Delta t\). A binary64 underflow of \(\exp(a\Delta t)\) to `+0` is refused because discrete auto-effects are strictly positive. The discrete effect of a constant predictor is Voelkle et al. (2012, Eq. 12; ZORA accepted manuscript re-opened 2026-08-17T14:20Z, Introducing Intercepts, manuscript p. 20), evaluated as \(a_{yx}(\operatorname{expm1}(z)/a_{xx})\) with \(z=a_{xx}\Delta t\) so a finite result is not lost when \(z\) overflows to \(-\infty\) or when \(a_{yx}\Delta t\) overflows, and in log space when `expm1(z)` overflows at a finite \(z\); a zero continuous effect is exactly zero; an overflowing \(a_{yx}/a_{xx}\) rewrite term fails closed; the first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. The discrete effect of a time-varying predictor with matched sampling and constancy intervals is Voelkle et al. (2012, Eq. 14; manuscript p. 21): \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product is not Eq. 12. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-17T13:26Z) write the same discrete intercept as \(A^{-1}[e^{A\Delta t}-I]\xi\). Metric/weak invariance does not license latent-mean comparison. Meredith (1993) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T14:20Z: closed). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T14:20Z: closed). +TEPP applies these sources to construct definition, score interpretation, reliability, validity evidence, uncertainty, consequences, longitudinal invariance, ESEM cross-loadings, and DSEM. Topic outputs are treated as fallible indicators or components only after their construct role is evaluated. Reflective, formative, and network classes remain distinct (Bollen & Lennox, 1991). Complete-data OLS loadings across posterior indicator draws are combined with Rubin (1996) \(T_m\); the arithmetic-mean helper remains a point estimate. Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T13:26Z: closed). The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title was opened 2026-08-17T12:04Z from archive.org; it is not the 1991 journal article and is not used as Mislevy plausible-value authority. Temporal precedence is not causal identification (Holland, 1986). Within/between OLS follows Enders and Tofighi (2007), Curran and Bauer (2011), and Hamaker et al. (2015). Enders and Tofighi (2007, Table 2, pp. 124–127; PDF opened 2026-08-17) show that the CWC cluster-mean coefficient is the contextual effect (`between − within`), not the between-cluster effect. Curran and Bauer (2011, pp. 607–608) reject person-mean subtraction on a raw autoregressive series as the lagged within-person residual; already-centered irregular residuals use the Voelkle et al. (2012, Eq. 7) / Driver et al. (2017, Eq. 3) scalar map. Discrete lags from unequal event intervals are remapped through that log-rate (Voelkle et al., 2012, ZORA accepted manuscript re-opened 2026-08-17T13:13Z) and are not pooled. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\) and restate the discrete intercept as a function of \(A\) and \(\Delta t\). A binary64 underflow of \(\exp(a\Delta t)\) to `+0` is refused because discrete auto-effects are strictly positive. The discrete effect of a constant predictor is Voelkle et al. (2012, Eq. 12; ZORA accepted manuscript re-opened 2026-08-17T14:20Z, Introducing Intercepts, manuscript p. 20), evaluated as \(a_{yx}(\operatorname{expm1}(z)/a_{xx})\) with \(z=a_{xx}\Delta t\) so a finite result is not lost when \(z\) overflows to \(-\infty\) or when \(a_{yx}\Delta t\) overflows, and in log space when `expm1(z)` overflows at a finite \(z\); a zero continuous effect is exactly zero; an overflowing \(a_{yx}/a_{xx}\) rewrite term fails closed; the first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. The discrete effect of a time-varying predictor with matched sampling and constancy intervals is Voelkle et al. (2012, Eq. 14; manuscript p. 21): \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product is not Eq. 12. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). The exact scalar discrete process noise is Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF opened 2026-08-17T17:19Z, p. 4): \(Q_{\Delta t}=q(\mathrm{e}^{2a\Delta t}-1)/(2a)\) for \(a\neq 0\) with \(q=GG^{\top}\ge 0\); \(a=0\) recovers \(q\Delta t\). This is not a Kalman filter. Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-17T13:26Z) write the same discrete intercept as \(A^{-1}[e^{A\Delta t}-I]\xi\). Metric/weak invariance does not license latent-mean comparison. Meredith (1993) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T17:19Z: closed). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T17:19Z: closed). ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. Oud and Jansen (2000) remains unread (OpenAlex/Semantic Scholar 2026-08-17T17:19Z: closed). ## Structural, correlated, dynamic, relational, and multilingual topic models diff --git a/docs/validation/temporal-event-foundation.md b/docs/validation/temporal-event-foundation.md index c5a07c10..0d0c2011 100644 --- a/docs/validation/temporal-event-foundation.md +++ b/docs/validation/temporal-event-foundation.md @@ -23,7 +23,7 @@ This report tracks exact-head scientific and engineering evidence required befor | Truth corpora / manifests | `tepp_simulation` | implemented-main | — | deterministic generator tests | Task 10 / PR #18 | | Recovery metrics | `validation_core` | implemented-main | — | RMSE/bias/coverage/MC gates | Task 11 / PR #19 | | Versioned API/export contracts | `tepp_api` | implemented-main | naruon HTTP interchange | unknown-field/version/limit + naruon HTTPS interchange tests | Task 12 / PR #21; live HTTP service remaining | -| Psychometric structural input gates | `psychometric_core` | partial | stacked psychometric PR | construct-class refusal + ALR/ILR boundary + true-loading RMSE + posterior-draw point-estimate mean + Rubin `T` + CWC within/between + CWC contextual effect + event-time log-rate + constant- and time-varying-predictor discrete effects + irregular already-centered residual lag + strong-gated latent means; full ESEM/DSEM remaining | ADR 0005; `docs/research/posterior-esem-input-gates.md`; `docs/research/multilevel-event-time-recovery.md`; `docs/research/rubin-total-variance.md`; `docs/research/strong-invariance-latent-means.md` | +| Psychometric structural input gates | `psychometric_core` | partial | stacked psychometric PR | construct-class refusal + ALR/ILR boundary + true-loading RMSE + posterior-draw point-estimate mean + Rubin `T` + CWC within/between + CWC contextual effect + event-time log-rate + constant- and time-varying-predictor discrete effects + exact scalar discrete process noise + irregular already-centered residual lag + strong-gated latent means; full ESEM/DSEM remaining | ADR 0005; `docs/research/posterior-esem-input-gates.md`; `docs/research/multilevel-event-time-recovery.md`; `docs/research/rubin-total-variance.md`; `docs/research/strong-invariance-latent-means.md` | | CWL modular connectors | `docs/connectors/*` | implemented-main | — | contract docs + examples | PR #22; live HTTP ports remaining | | Release SBOM/provenance generator | `scripts/release_evidence.py` | partial | — | generate+validate in CI | Task 13 partial / PR #28 | From 797282e2f142e4b81e6cb2fab3e83f93915ed152 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 21:08:19 +0000 Subject: [PATCH 18/62] feat(psychometric): evaluate Driver Eq. 3 without twice-rate overflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Form z as 2(a Δt). Forming 2a first overflows when |a| is at the binary64 extreme even if a Δt and Q_Δt are finite. Algebraically identical to q(exp(2 a Δt) − 1)/(2 a). expm1(−∞) keeps −0.5 q/a. JSS PDF re-opened 2026-08-17T21:03Z, p. 4. Still not a Kalman filter, not DSEM, not matrix expm. Meredith (1993), Mislevy (1991), and Oud and Jansen (2000) remain unread. --- CHANGELOG.md | 4 +- CLAUDE.md | 2 +- crates/psychometric_core/src/event_time.rs | 73 +++++++++++-------- ...multilevel_event_time_recovery_contract.rs | 8 ++ .../scientific_claim_boundary_contract.rs | 8 ++ docs/adr/0005-posterior-esem-dsem.md | 2 +- .../multilevel-event-time-recovery.md | 6 +- docs/research/standards-and-literature.md | 2 +- .../strong-invariance-latent-means.md | 2 +- 9 files changed, 66 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea725b5f..476ffec8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang ## [Unreleased] -### Added - -- `psychometric_core` recovers the exact scalar discrete process noise of Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF opened 2026-08-17T17:19Z, p. 4): \(Q_{\Delta t}=q(\mathrm{e}^{2a\Delta t}-1)/(2a)\) for \(a\neq 0\) and \(q\Delta t\) for \(a=0\), with \(q=GG^{\top}\ge 0\). Evaluated as \(q(\operatorname{expm1}(z)/(2a))\) with \(z=2a\Delta t\). Binary64 underflow of \(z\) recovers \(q\Delta t\). \(z\to-\infty\) keeps the equilibrium variance \(-q/(2a)\). Finite-\(z\) `expm1` overflow rewrites in log space. A zero diffusion is exactly zero. \(z\to+\infty\) and a negative diffusion fail closed. Still not a Kalman filter, not DSEM, and not a matrix `expm`. +- `psychometric_core` evaluates Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-17T21:03Z, p. 4) as \(0.5 q(\operatorname{expm1}(z)/a)\) with \(z=2(a\Delta t)\). Forming \(2a\) first overflows when \(|a|\) is at the binary64 extreme even if \(a\Delta t\) and \(Q_{\Delta t}\) are finite (`a=1e308`, `Δt=1e-308` → \(0.5(\mathrm{e}^{2}-1)/10^{308}\); `a=-1e308`, `q=1e308`, `Δt=2` → \(0.5\)). Algebraically identical to \(q(\mathrm{e}^{2a\Delta t}-1)/(2a)\). `expm1(−∞)` keeps \(-0.5 q/a\). \(z\to+\infty\) still fails closed. Still not a Kalman filter, not DSEM, and not a matrix `expm`. Meredith (1993), Mislevy (1991), and Oud and Jansen (2000) remain unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T21:03Z: closed; ZORA Anubis-blocked). - `psychometric_core` recovers Voelkle et al. (2012, Eq. 14; ZORA accepted manuscript re-opened 2026-08-17T14:20Z, Introducing Intercepts, manuscript p. 21): the discrete effect of a time-varying predictor whose sampling interval equals its constancy interval is \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). It does not depend on the predictor auto-effect. That product is not Eq. 12. Unmatched sampling and constancy intervals fail closed (Oud & Jansen, 2000, unread). An overflowing Eq. 12 rewrite scale \(a_{yx}/a_{xx}\) also fails closed. Still not DSEM. - `psychometric_core` recovers Voelkle et al. (2012, Eq. 12) when `expm1(z)` overflows to `+∞` at a finite `z`. The rewrite is `sign(a_{yx}/a_{xx})\exp(\ln|a_{yx}|+z-\ln|a_{xx}|)-a_{yx}/a_{xx}` (algebraically identical to `(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)`). A zero continuous effect is exactly zero even when `expm1` overflows (`0\cdot+\infty` is `NaN`). `z\to+\infty` remains fail-closed. ZORA accepted manuscript re-opened 2026-08-17T13:26Z, Introducing Intercepts, manuscript p. 20; Driver, Oud, and Voelkle (2017, Eq. 3) restated `A^{-1}[e^{A\Delta t}-I]\xi`. Still not DSEM. - `psychometric_core` evaluates Voelkle et al. (2012, Eq. 12) as \(a_{yx}(\operatorname{expm1}(z)/a_{xx})\) with \(z=a_{xx}\Delta t\). That order is algebraically identical to \((a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). Dividing the increment by the finite auto-effect keeps the equilibrium increment \(-a_{yx}/a_{xx}\) when \(z\) overflows to \(-\infty\) (ZORA accepted manuscript, Introducing Intercepts: the exponential vanishes as \(\Delta t\) grows; CodeRabbit finding on `7ffb65b`). The prior \(a_{yx}\Delta t\) overflow case remains finite. Binary64 underflow of \(z\) to `+0` recovers the Eq. 12 limit \(a_{yx}\Delta t\); the first-order product is still not the general discrete effect. Still not DSEM. diff --git a/CLAUDE.md b/CLAUDE.md index 7699f05e..df19840e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. -- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = q (expm1(2 a Δt) / (2 a))` with `q = G G⊤ ≥ 0`; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; this is not a Kalman filter. +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; this is not a Kalman filter. - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index 4757a5df..c6add11d 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -393,21 +393,24 @@ pub fn refuse_unmatched_time_varying_predictor_interval( /// Exact scalar discrete process noise on event time. /// -/// Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF opened 2026-08-17T17:19Z, +/// Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-17T21:03Z, /// p. 4) write the discrete process-noise covariance /// `Q_Δt = ∫_0^{Δt} expm(A(Δt−τ)) G G⊤ expm(A(Δt−τ))⊤ dτ`. /// The noiseless scalar closed form with continuous diffusion /// `q = G G⊤ ≥ 0` is `q (exp(2 a Δt) − 1) / (2 a)` for `a ≠ 0` and /// `q Δt` for `a = 0`. The algebraically identical finite-`expm1` -/// evaluation is `q (expm1(z) / (2 a))` with `z = 2 a Δt`. When -/// binary64 `z` underflows to `+0`, the mathematical limit is `q Δt`. -/// When `z → −∞` the exponential vanishes and the result is the -/// equilibrium variance `−q / (2 a)` for stable `a < 0`. When -/// `expm1(z)` overflows to `+∞` at a finite `z`, rewrite as -/// `sign(q / (2 a)) exp(ln|q| + z − ln|2 a|) − q / (2 a)`. `z → +∞` -/// is an unstable process and fails closed unless `q = 0`. A zero -/// diffusion is exactly zero even if `expm1` overflows. This is not a -/// Kalman filter, not DSEM, and not a matrix `expm`. +/// evaluation is `0.5 q (expm1(z) / a)` with `z = 2 (a Δt)`. Form +/// `z` as twice the already-finite product `a Δt`. Forming `2 a` +/// first overflows when `|a|` is at the binary64 extreme even if +/// `a Δt` and `Q_Δt` are finite (`a = ±1e308`, `Δt = 1e-308`). +/// When binary64 `z` underflows to `+0`, the mathematical limit is +/// `q Δt`. When `z → −∞` the exponential vanishes and the result is +/// the equilibrium variance `−q / (2 a) = −0.5 q / a` for stable +/// `a < 0`. When `expm1(z)` overflows to `+∞` at a finite `z`, +/// rewrite as `sign(q / a) exp(ln|q| + z − ln|a| − ln 2) − 0.5 q / a`. +/// `z → +∞` is an unstable process and fails closed unless `q = 0`. +/// A zero diffusion is exactly zero even if `expm1` overflows. This +/// is not a Kalman filter, not DSEM, and not a matrix `expm`. /// /// # Errors /// @@ -439,37 +442,38 @@ pub fn recover_discrete_process_noise( if log_rate == 0.0 { return require_finite(continuous_diffusion * event_delta); } - let twice_log_rate = 2.0 * log_rate; - if !twice_log_rate.is_finite() { - return Err(PsychometricError::InvalidNumericInput); - } - let increment_argument = twice_log_rate * event_delta; + // z = 2 (a Δt), not (2 a) Δt. 2 a overflows at |a| = 1e308 even + // when a Δt is finite (Driver Eq. 3 scalar closed form). + let drift_interval = log_rate * event_delta; + let increment_argument = 2.0 * drift_interval; if increment_argument == 0.0 { // Binary64 underflow of 2 a Δt. lim z→0 of Eq. 3 Q_Δt is q Δt. return require_finite(continuous_diffusion * event_delta); } let increment = increment_argument.exp_m1(); if increment.is_finite() { - // Divide expm1(z) by the finite 2 a, not by z. expm1(−∞)/−∞ - // is +0 and loses the equilibrium variance −q / (2 a). - return require_finite(continuous_diffusion * (increment / twice_log_rate)); + // Q = q expm1(z) / (2 a) = 0.5 q (expm1(z) / a). Divide by + // the finite a, not by 2 a: 2 a overflows when |a| = 1e308. + // expm1(−∞) is −1, so this path also keeps −0.5 q / a. + return require_finite(0.5 * continuous_diffusion * (increment / log_rate)); } // expm1 overflowed. z → +∞ diverges (unstable auto-effect). + // z → −∞ is already handled above because expm1(−∞) is finite. if !increment_argument.is_finite() { return Err(PsychometricError::InvalidNumericInput); } // Finite z, overflowed expm1. (q / (2 a))(exp(z) − 1) = - // sign(q / (2 a)) exp(ln|q| + z − ln|2 a|) − q / (2 a). - let scale = continuous_diffusion / twice_log_rate; - if !scale.is_finite() { + // sign(q / a) exp(ln|q| + z − ln|a| − ln 2) − 0.5 q / a. + let half_scale = 0.5 * continuous_diffusion / log_rate; + if !half_scale.is_finite() { return Err(PsychometricError::InvalidNumericInput); } - let log_abs_dominant = - continuous_diffusion.abs().ln() + increment_argument - twice_log_rate.abs().ln(); - let dominant = require_finite( - continuous_diffusion.signum() * twice_log_rate.signum() * log_abs_dominant.exp(), - )?; - require_finite(dominant - scale) + let log_abs_dominant = continuous_diffusion.abs().ln() + increment_argument + - log_rate.abs().ln() + - std::f64::consts::LN_2; + let dominant = + require_finite(continuous_diffusion.signum() * log_rate.signum() * log_abs_dominant.exp())?; + require_finite(dominant - half_scale) } /// Refuse the difference quotient as a continuous-time rate. @@ -1163,6 +1167,17 @@ mod tests { recover_discrete_process_noise(0.0, 1e308, 2.0, LagClock::EventTime), Ok(0.0) ); + // Forming 2 a first overflows; z = 2 (a Δt) stays finite. + let twice_rate_overflow = + recover_discrete_process_noise(1.0, 1e308, 1e-308, LagClock::EventTime) + .expect("2a overflow"); + let expected_twice_rate = 0.5 * 2.0_f64.exp_m1() / 1e308; + assert!((twice_rate_overflow - expected_twice_rate).abs() / expected_twice_rate < 1e-12); + // 2 a overflows to −∞; expm1(−∞) = −1 keeps −0.5 q / a. + let overflowed_equilibrium = + recover_discrete_process_noise(1e308, -1e308, 2.0, LagClock::EventTime) + .expect("2a eq var"); + assert!((overflowed_equilibrium - 0.5).abs() < 1e-15); } #[test] @@ -1203,10 +1218,6 @@ mod tests { recover_discrete_process_noise(1.0, 1e308, 2.0, LagClock::EventTime), Err(PsychometricError::InvalidNumericInput) ); - assert_eq!( - recover_discrete_process_noise(1.0, 1e308, 1e-308, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) - ); } #[test] 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 ba507749..420f8339 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -317,6 +317,14 @@ fn discrete_process_noise_recovers_driver_equation_three() { recover_discrete_process_noise(0.0, 800.0, 1.0, LagClock::EventTime), Ok(0.0) ); + let twice_rate_overflow = + recover_discrete_process_noise(1.0, 1e308, 1e-308, LagClock::EventTime) + .expect("2a overflow"); + let expected_twice_rate = 0.5 * 2.0_f64.exp_m1() / 1e308; + assert!(rmse(&[expected_twice_rate], &[twice_rate_overflow]) / expected_twice_rate < 1e-12); + let overflowed_equilibrium = + recover_discrete_process_noise(1e308, -1e308, 2.0, LagClock::EventTime).expect("2a eq var"); + assert!(rmse(&[0.5], &[overflowed_equilibrium]) < 1e-15); assert_eq!( recover_discrete_process_noise(1.0, 800.0, 1.0, LagClock::EventTime), Err(PsychometricError::InvalidNumericInput) diff --git a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs index cf2b6c4c..7b08f8e6 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -173,6 +173,14 @@ fn discrete_process_noise_is_not_the_continuous_diffusion() { ); let expected = diffusion * ((2.0 * drift * delta).exp() - 1.0) / (2.0 * drift); assert!((discrete - expected).abs() < 1e-15); + let twice_rate_overflow = + recover_discrete_process_noise(1.0, 1e308, 1e-308, LagClock::EventTime) + .expect("2a overflow"); + let expected_twice_rate = 0.5 * 2.0_f64.exp_m1() / 1e308; + assert!((twice_rate_overflow - expected_twice_rate).abs() / expected_twice_rate < 1e-12); + let overflowed_equilibrium = + recover_discrete_process_noise(1e308, -1e308, 2.0, LagClock::EventTime).expect("2a eq var"); + assert!((overflowed_equilibrium - 0.5).abs() < 1e-15); let constant = recover_discrete_constant_predictor_effect(diffusion, drift, delta, LagClock::EventTime) .expect("eq 12"); diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index 5dde0291..f0133aac 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = q (expm1(2 a Δt) / (2 a))` with `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF opened 2026-08-17T17:19Z, p. 4; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−q / (2 a)`; a zero diffusion is exactly zero; `z → +∞` fails closed), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-17T21:03Z, p. 4; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` fails closed), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. 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 bfad3508..a4a5d2a5 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -39,7 +39,7 @@ Kish, L. (1965). *Survey sampling*. John Wiley & Sons. Oud, J. H. L., & Jansen, R. A. R. G. (2000). Continuous time state space modeling of panel data by means of SEM. *Psychometrika, 65*(2), 199–215. https://doi.org/10.1007/BF02294374 (cited by Voelkle et al., 2012, Eq. 14 discussion; PDF not opened). -The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-17T14:20Z (https://www.zora.uzh.ch/handle/20.500.14742/72792; bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`). Page 16 writes discrete auto-effects as \(A^{*}(\Delta t)=\exp(A\Delta t)\) (Eq. 7). Introducing Intercepts (manuscript p. 20, Eq. 12) adds a continuous-time intercept \(b\) and writes the expected-value solution whose discrete increment is \(A^{-1}(\exp(A\Delta t)-I)b\); the scalar constant-predictor effect is \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). The next paragraph (manuscript p. 21, Eq. 14) writes the discrete effect of a **time-varying** predictor, when the sampling interval equals the constancy interval, as \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product does not depend on the predictor auto-effect. The manuscript calls Eq. 14 a first-order approximation that deteriorates as \(\Delta t\) grows, and defers unmatched sampling/constancy intervals to Oud and Jansen (2000), which is unread. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4; JSS PDF opened 2026-08-17T17:19Z) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\), the discrete intercept \(b_{\Delta t}=A^{-1}[A_{\Delta t}-I]b\), and the discrete process-noise covariance \(Q_{\Delta t}=\int_{0}^{\Delta t}\operatorname{expm}(A(\Delta t-\tau))GG^{\top}\operatorname{expm}(A(\Delta t-\tau))^{\top}\,d\tau\). Discrete auto-effects are strictly positive for finite real drift and interval; the inverse \(a=\ln\varphi/\Delta t\) therefore requires \(\varphi>0\). Binary64 `exp` of a large negative argument is `+0` and is refused. Equations 3–4 of Voelkle et al. (2012) are the discouraged difference-quotient approximation. Meredith (1993) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T17:19Z: closed; Springer content/pdf was HTML). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T17:19Z: closed). ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title is not the 1991 journal article. Oud and Jansen (2000) remains unread (OpenAlex/Semantic Scholar 2026-08-17T17:19Z: closed). +The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-17T14:20Z (https://www.zora.uzh.ch/handle/20.500.14742/72792; bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`). Page 16 writes discrete auto-effects as \(A^{*}(\Delta t)=\exp(A\Delta t)\) (Eq. 7). Introducing Intercepts (manuscript p. 20, Eq. 12) adds a continuous-time intercept \(b\) and writes the expected-value solution whose discrete increment is \(A^{-1}(\exp(A\Delta t)-I)b\); the scalar constant-predictor effect is \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). The next paragraph (manuscript p. 21, Eq. 14) writes the discrete effect of a **time-varying** predictor, when the sampling interval equals the constancy interval, as \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product does not depend on the predictor auto-effect. The manuscript calls Eq. 14 a first-order approximation that deteriorates as \(\Delta t\) grows, and defers unmatched sampling/constancy intervals to Oud and Jansen (2000), which is unread. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4; JSS PDF opened 2026-08-17T17:19Z) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\), the discrete intercept \(b_{\Delta t}=A^{-1}[A_{\Delta t}-I]b\), and the discrete process-noise covariance \(Q_{\Delta t}=\int_{0}^{\Delta t}\operatorname{expm}(A(\Delta t-\tau))GG^{\top}\operatorname{expm}(A(\Delta t-\tau))^{\top}\,d\tau\). Discrete auto-effects are strictly positive for finite real drift and interval; the inverse \(a=\ln\varphi/\Delta t\) therefore requires \(\varphi>0\). Binary64 `exp` of a large negative argument is `+0` and is refused. Equations 3–4 of Voelkle et al. (2012) are the discouraged difference-quotient approximation. Meredith (1993) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T21:03Z: closed; Springer content/pdf was HTML). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T21:03Z: closed). ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title is not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T21:03Z: closed; Radboud/Leiden repositories 403). The Voelkle et al. (2012) ZORA bitstream was Anubis-blocked this cycle; the JSS PDF of Driver et al. (2017) was re-opened. ## Formula notes @@ -50,7 +50,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-17T14:2 - **Unequal-interval remap.** Discrete \(\varphi(\Delta t_1)\) and \(\varphi(\Delta t_2)\) are not comparable when \(\Delta t_1\neq\Delta t_2\) (Voelkle et al., 2012, ZORA manuscript pp. 2, 16, 33). The licensed path is \(a=\ln\varphi_{\mathrm{src}}/\Delta t_{\mathrm{src}}\) then \(\varphi_{\mathrm{ref}}=\exp(a\,\Delta t_{\mathrm{ref}})\). Pooling those discrete lags fails closed. - **Constant-predictor discrete effect.** Voelkle et al. (2012, Eq. 12; ZORA accepted manuscript re-opened 2026-08-17T13:23Z, Introducing Intercepts): for a constant predictor with \(a_{xx}\neq 0\), \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). Driver, Oud, and Voelkle (2017, p. 4, after Eq. 3) restate the discrete intercept as a function of \(A\) and \(\Delta t\). The algebraically identical finite-`expm1` evaluation is \(a_{yx}(\operatorname{expm1}(z)/a_{xx})\) with \(z=a_{xx}\Delta t\). Dividing the increment by the finite auto-effect keeps the equilibrium increment \(-a_{yx}/a_{xx}\) when \(z\) overflows to \(-\infty\) (the manuscript notes that the exponential vanishes as \(\Delta t\) grows) and keeps a finite result when \(a_{yx}\Delta t\) overflows. When `expm1(z)` overflows to `+\infty` at a finite \(z\), rewrite as \(\operatorname{sign}(a_{yx}/a_{xx})\exp(\ln|a_{yx}|+z-\ln|a_{xx}|)-a_{yx}/a_{xx}\). A zero continuous effect is exactly zero even if `expm1` overflows. \(z\to+\infty\) fails closed unless \(a_{yx}=0\). When binary64 \(z\) underflows to `+0`, the mathematical limit of Eq. 12 is \(a_{yx}\Delta t\). That limit is IEEE-754 evaluation of Eq. 12, not a substitution of the first-order product as the general discrete effect. \(a_{xx}=0\) fails closed. This is not DSEM. - **Time-varying-predictor discrete effect.** Voelkle et al. (2012, Eq. 14; ZORA accepted manuscript re-opened 2026-08-17T14:20Z, Introducing Intercepts, manuscript p. 21): when the predictor can take a new value at each occasion and the sampling interval equals the constancy interval, \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). The effect does not depend on \(a_{xx}\). The manuscript states that this is a first-order approximation that deteriorates as \(\Delta t\) grows. It is not Eq. 12. Unmatched sampling and constancy intervals cite Oud and Jansen (2000), which is unread, and fail closed. -- **Discrete process noise.** Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF opened 2026-08-17T17:19Z, p. 4): \(Q_{\Delta t}=\int_{0}^{\Delta t}\operatorname{expm}(A(\Delta t-\tau))GG^{\top}\operatorname{expm}(A(\Delta t-\tau))^{\top}\,d\tau\). The scalar closed form with continuous diffusion \(q=GG^{\top}\ge 0\) is \(q(\mathrm{e}^{2a\Delta t}-1)/(2a)\) for \(a\neq 0\) and \(q\Delta t\) for \(a=0\). The algebraically identical finite-`expm1` evaluation is \(q(\operatorname{expm1}(z)/(2a))\) with \(z=2a\Delta t\). Binary64 underflow of \(z\) to `+0` recovers \(q\Delta t\). \(z\to-\infty\) keeps the equilibrium variance \(-q/(2a)\). When `expm1(z)` overflows at a finite \(z\), rewrite as \(\operatorname{sign}(q/(2a))\exp(\ln|q|+z-\ln|2a|)-q/(2a)\). A zero diffusion is exactly zero even if `expm1` overflows. \(z\to+\infty\) fails closed unless \(q=0\). Negative \(q\) fails closed. This is not a Kalman filter and not a matrix `expm`. +- **Discrete process noise.** Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-17T21:03Z, p. 4): \(Q_{\Delta t}=\int_{0}^{\Delta t}\operatorname{expm}(A(\Delta t-\tau))GG^{\top}\operatorname{expm}(A(\Delta t-\tau))^{\top}\,d\tau\). The scalar closed form with continuous diffusion \(q=GG^{\top}\ge 0\) is \(q(\mathrm{e}^{2a\Delta t}-1)/(2a)\) for \(a\neq 0\) and \(q\Delta t\) for \(a=0\). The algebraically identical finite-`expm1` evaluation is \(0.5 q(\operatorname{expm1}(z)/a)\) with \(z=2(a\Delta t)\). Form \(z\) as twice the product \(a\Delta t\). Forming \(2a\) first overflows when \(|a|\) is at the binary64 extreme even if \(a\Delta t\) and \(Q_{\Delta t}\) are finite. Binary64 underflow of \(z\) to `+0` recovers \(q\Delta t\). \(z\to-\infty\) keeps the equilibrium variance \(-q/(2a)=-0.5 q/a\); `expm1(−∞)` is \(-1\), so that path stays on the finite increment. When `expm1(z)` overflows at a finite \(z\), rewrite as \(\operatorname{sign}(q/a)\exp(\ln|q|+z-\ln|a|-\ln 2)-0.5 q/a\). A zero diffusion is exactly zero even if `expm1` overflows. \(z\to+\infty\) fails closed unless \(q=0\). Negative \(q\) fails closed. This is not a Kalman filter and not a matrix `expm`. - **CWC-then-lag.** Sample cluster means are removed first. Consecutive within residuals are then fitted by least squares to \(r_{t+\Delta t}\approx\exp(a\Delta t)\,r_{t}\) on event time. Same-sign pair-wise logs initialize the scalar Newton step. Sign-flipping \(T=2\) CWC pairs have no real logarithm and fail closed. Curran and Bauer (2011, pp. 607–608) show that this person-mean subtraction on a raw autoregressive series does **not** isolate the lagged within-person effect; the helper therefore does not claim to recover the raw-process drift. - **Already-centered irregular residual.** The caller supplies lagged within residuals. The mean of \(a=\ln(r_{t+\Delta t}/r_t)/\Delta t\) is the exact scalar map. Intervals may be irregular. The helper does not center again. This is not DSEM. @@ -64,7 +64,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-17T14:2 - a binary64 underflow of \(\exp(a\Delta t)\) to `+0` (direct forward map and large-interval remap) fails closed; - Voelkle et al. (2012, Eq. 12) recovers a known discrete constant-predictor effect at machine-scale RMSE, and that RMSE is smaller than the first-order product \(a_{yx}\Delta t\); \(a_{xx}=0\) fails closed; binary64 underflow of \(a_{xx}\Delta t\) to `+0` recovers the Eq. 12 limit \(a_{yx}\Delta t\); a finite Eq. 12 result whose first factor \(a_{yx}\Delta t\) overflows is recovered; a finite Eq. 12 equilibrium increment whose \(z=a_{xx}\Delta t\) overflows to \(-\infty\) is recovered as \(-a_{yx}/a_{xx}\); a finite Eq. 12 result whose `expm1(z)` overflows at a finite \(z\) is recovered in log space; a zero continuous effect is exactly zero; an overflowing \(a_{yx}/a_{xx}\) rewrite term fails closed; - Voelkle et al. (2012, Eq. 14) recovers \(a_{yx}\Delta t\) at machine-scale RMSE when sampling, constancy, and event intervals match, and that value is not the Eq. 12 constant-predictor effect; unmatched intervals fail closed; -- Driver et al. (2017, Eq. 3) recovers a known scalar \(Q_{\Delta t}\) at machine-scale RMSE, and that RMSE is smaller than treating the continuous diffusion as the discrete process noise; \(a=0\) and binary64 underflow of \(2a\Delta t\) recover \(q\Delta t\); \(z\to-\infty\) recovers \(-q/(2a)\); a finite result whose `expm1` overflows at a finite \(z\) is recovered in log space; a zero diffusion is exactly zero; a negative diffusion and \(z\to+\infty\) fail closed; +- Driver et al. (2017, Eq. 3) recovers a known scalar \(Q_{\Delta t}\) at machine-scale RMSE, and that RMSE is smaller than treating the continuous diffusion as the discrete process noise; \(a=0\) and binary64 underflow of \(2a\Delta t\) recover \(q\Delta t\); \(z\to-\infty\) recovers \(-q/(2a)\); a finite result whose `expm1` overflows at a finite \(z\) is recovered in log space; a finite result whose \(2a\) overflows while \(a\Delta t\) stays finite is recovered as \(0.5 q(\operatorname{expm1}(z)/a)\); a zero diffusion is exactly zero; a negative diffusion and \(z\to+\infty\) fail closed; - pooling discrete lags from unequal intervals fails closed; - CWC-then-lag on a two-cluster decaying series has smaller computed RMSE than a level-pooled series when the latter is identified; - already-centered irregular residuals recover a known drift at machine-scale RMSE, and that RMSE is smaller than CWC of the corresponding raw autoregressive series (Curran & Bauer, 2011, pp. 607–608); diff --git a/docs/research/standards-and-literature.md b/docs/research/standards-and-literature.md index 7ba5c48a..95f53f39 100644 --- a/docs/research/standards-and-literature.md +++ b/docs/research/standards-and-literature.md @@ -34,7 +34,7 @@ Meredith, W. (1993). Measurement invariance, factor analysis and factorial invar Holland, P. W. (1986). Statistics and causal inference. *Journal of the American Statistical Association, 81*(396), 945–960. https://doi.org/10.1080/01621459.1986.10478354 -TEPP applies these sources to construct definition, score interpretation, reliability, validity evidence, uncertainty, consequences, longitudinal invariance, ESEM cross-loadings, and DSEM. Topic outputs are treated as fallible indicators or components only after their construct role is evaluated. Reflective, formative, and network classes remain distinct (Bollen & Lennox, 1991). Complete-data OLS loadings across posterior indicator draws are combined with Rubin (1996) \(T_m\); the arithmetic-mean helper remains a point estimate. Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T13:26Z: closed). The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title was opened 2026-08-17T12:04Z from archive.org; it is not the 1991 journal article and is not used as Mislevy plausible-value authority. Temporal precedence is not causal identification (Holland, 1986). Within/between OLS follows Enders and Tofighi (2007), Curran and Bauer (2011), and Hamaker et al. (2015). Enders and Tofighi (2007, Table 2, pp. 124–127; PDF opened 2026-08-17) show that the CWC cluster-mean coefficient is the contextual effect (`between − within`), not the between-cluster effect. Curran and Bauer (2011, pp. 607–608) reject person-mean subtraction on a raw autoregressive series as the lagged within-person residual; already-centered irregular residuals use the Voelkle et al. (2012, Eq. 7) / Driver et al. (2017, Eq. 3) scalar map. Discrete lags from unequal event intervals are remapped through that log-rate (Voelkle et al., 2012, ZORA accepted manuscript re-opened 2026-08-17T13:13Z) and are not pooled. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\) and restate the discrete intercept as a function of \(A\) and \(\Delta t\). A binary64 underflow of \(\exp(a\Delta t)\) to `+0` is refused because discrete auto-effects are strictly positive. The discrete effect of a constant predictor is Voelkle et al. (2012, Eq. 12; ZORA accepted manuscript re-opened 2026-08-17T14:20Z, Introducing Intercepts, manuscript p. 20), evaluated as \(a_{yx}(\operatorname{expm1}(z)/a_{xx})\) with \(z=a_{xx}\Delta t\) so a finite result is not lost when \(z\) overflows to \(-\infty\) or when \(a_{yx}\Delta t\) overflows, and in log space when `expm1(z)` overflows at a finite \(z\); a zero continuous effect is exactly zero; an overflowing \(a_{yx}/a_{xx}\) rewrite term fails closed; the first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. The discrete effect of a time-varying predictor with matched sampling and constancy intervals is Voelkle et al. (2012, Eq. 14; manuscript p. 21): \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product is not Eq. 12. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). The exact scalar discrete process noise is Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF opened 2026-08-17T17:19Z, p. 4): \(Q_{\Delta t}=q(\mathrm{e}^{2a\Delta t}-1)/(2a)\) for \(a\neq 0\) with \(q=GG^{\top}\ge 0\); \(a=0\) recovers \(q\Delta t\). This is not a Kalman filter. Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-17T13:26Z) write the same discrete intercept as \(A^{-1}[e^{A\Delta t}-I]\xi\). Metric/weak invariance does not license latent-mean comparison. Meredith (1993) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T17:19Z: closed). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T17:19Z: closed). ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. Oud and Jansen (2000) remains unread (OpenAlex/Semantic Scholar 2026-08-17T17:19Z: closed). +TEPP applies these sources to construct definition, score interpretation, reliability, validity evidence, uncertainty, consequences, longitudinal invariance, ESEM cross-loadings, and DSEM. Topic outputs are treated as fallible indicators or components only after their construct role is evaluated. Reflective, formative, and network classes remain distinct (Bollen & Lennox, 1991). Complete-data OLS loadings across posterior indicator draws are combined with Rubin (1996) \(T_m\); the arithmetic-mean helper remains a point estimate. Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T13:26Z: closed). The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title was opened 2026-08-17T12:04Z from archive.org; it is not the 1991 journal article and is not used as Mislevy plausible-value authority. Temporal precedence is not causal identification (Holland, 1986). Within/between OLS follows Enders and Tofighi (2007), Curran and Bauer (2011), and Hamaker et al. (2015). Enders and Tofighi (2007, Table 2, pp. 124–127; PDF opened 2026-08-17) show that the CWC cluster-mean coefficient is the contextual effect (`between − within`), not the between-cluster effect. Curran and Bauer (2011, pp. 607–608) reject person-mean subtraction on a raw autoregressive series as the lagged within-person residual; already-centered irregular residuals use the Voelkle et al. (2012, Eq. 7) / Driver et al. (2017, Eq. 3) scalar map. Discrete lags from unequal event intervals are remapped through that log-rate (Voelkle et al., 2012, ZORA accepted manuscript re-opened 2026-08-17T13:13Z) and are not pooled. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\) and restate the discrete intercept as a function of \(A\) and \(\Delta t\). A binary64 underflow of \(\exp(a\Delta t)\) to `+0` is refused because discrete auto-effects are strictly positive. The discrete effect of a constant predictor is Voelkle et al. (2012, Eq. 12; ZORA accepted manuscript re-opened 2026-08-17T14:20Z, Introducing Intercepts, manuscript p. 20), evaluated as \(a_{yx}(\operatorname{expm1}(z)/a_{xx})\) with \(z=a_{xx}\Delta t\) so a finite result is not lost when \(z\) overflows to \(-\infty\) or when \(a_{yx}\Delta t\) overflows, and in log space when `expm1(z)` overflows at a finite \(z\); a zero continuous effect is exactly zero; an overflowing \(a_{yx}/a_{xx}\) rewrite term fails closed; the first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. The discrete effect of a time-varying predictor with matched sampling and constancy intervals is Voelkle et al. (2012, Eq. 14; manuscript p. 21): \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product is not Eq. 12. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). The exact scalar discrete process noise is Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-17T21:03Z, p. 4): \(Q_{\Delta t}=0.5 q(\operatorname{expm1}(z)/a)\) with \(z=2(a\Delta t)\) for \(a\neq 0\) and \(q=GG^{\top}\ge 0\); do not form \(2a\) first; \(a=0\) recovers \(q\Delta t\). This is not a Kalman filter. Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-17T13:26Z) write the same discrete intercept as \(A^{-1}[e^{A\Delta t}-I]\xi\). Metric/weak invariance does not license latent-mean comparison. Meredith (1993) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T21:03Z: closed). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T21:03Z: closed). ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T21:03Z: closed). ## Structural, correlated, dynamic, relational, and multilingual topic models diff --git a/docs/research/strong-invariance-latent-means.md b/docs/research/strong-invariance-latent-means.md index 2d182ea4..9bcbf1fa 100644 --- a/docs/research/strong-invariance-latent-means.md +++ b/docs/research/strong-invariance-latent-means.md @@ -12,7 +12,7 @@ This slice does **not** import the unpublished `measurement_invariance` crate on - `#84` `scalar` is the strong/scalar status (equal loading and intercept). That status licenses latent means. - Strict (also equal residual variance) also licenses latent means. - This is two-group OLS, not MGCFA, not partial invariance, and not alignment optimization. -- Meredith (1993) names weak/strong/strict are used only as conventional labels. That PDF was not opened (Unpaywall/OpenAlex/Semantic Scholar/archive.org/ERIC 2026-08-17T11:18Z: closed; Springer remains an HTML stub). Do not cite Meredith equations as having been read. +- Meredith (1993) names weak/strong/strict are used only as conventional labels. That PDF was not opened (Unpaywall/OpenAlex/Semantic Scholar/archive.org/ERIC 2026-08-17T21:03Z: closed; Springer remains an HTML stub). Do not cite Meredith equations as having been read. ## Authoritative sources used for the mean gate From 6c13dfbf3af237263e1e28f003e6e920f8733062 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 03:11:21 +0000 Subject: [PATCH 19/62] feat(psychometric): refuse overflowing Driver Eq. 3 rewrite scale MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-18, p. 4) write Q_Δt as the integral that closes to q (e^{2 a Δt} − 1) / (2 a). When expm1(z) overflows at a finite z = 2 (a Δt) and 0.5 q / a is non-finite, that discrete variance is not finite and fails closed. Meredith (1993) and Mislevy (1991) remain unread. ERIC ED334221 is Singer and Willett (1991). This is not a Kalman filter and not a matrix expm. --- CHANGELOG.md | 1 + CLAUDE.md | 2 +- crates/psychometric_core/src/event_time.rs | 11 +++++++++++ .../tests/multilevel_event_time_recovery_contract.rs | 4 ++++ .../tests/scientific_claim_boundary_contract.rs | 4 ++++ docs/adr/0005-posterior-esem-dsem.md | 2 +- docs/research/multilevel-event-time-recovery.md | 6 +++--- docs/research/rubin-total-variance.md | 2 +- docs/research/standards-and-literature.md | 2 +- docs/research/strong-invariance-latent-means.md | 2 +- 10 files changed, 28 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 476ffec8..b09706a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang ## [Unreleased] +- `psychometric_core` refuses an overflowing Driver Eq. 3 rewrite scale `0.5 q / a` (JSS PDF re-opened 2026-08-18T03:07Z, p. 4). When `expm1(z)` overflows at a finite `z = 2(a Δt)` and `0.5 q / a` is non-finite (`q = 1e308`, `a = 0.1`, `Δt = 4000` → `z = 800`), `Q_Δt = q(e^{2aΔt}−1)/(2a)` is not finite and fails closed. Algebraically identical to the licensed integral. Still not a Kalman filter, not DSEM, and not a matrix `expm`. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall/OpenAlex/Semantic Scholar/CORE 2026-08-18T03:07Z: closed). ERIC ED334221 is Singer and Willett (1991), not Mislevy (1991). ERIC ED333032 remains Mislevy, Sheehan, and Wingersky (1990). ZORA Anubis-blocked. Oud and Jansen (2000) remains unread. - `psychometric_core` evaluates Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-17T21:03Z, p. 4) as \(0.5 q(\operatorname{expm1}(z)/a)\) with \(z=2(a\Delta t)\). Forming \(2a\) first overflows when \(|a|\) is at the binary64 extreme even if \(a\Delta t\) and \(Q_{\Delta t}\) are finite (`a=1e308`, `Δt=1e-308` → \(0.5(\mathrm{e}^{2}-1)/10^{308}\); `a=-1e308`, `q=1e308`, `Δt=2` → \(0.5\)). Algebraically identical to \(q(\mathrm{e}^{2a\Delta t}-1)/(2a)\). `expm1(−∞)` keeps \(-0.5 q/a\). \(z\to+\infty\) still fails closed. Still not a Kalman filter, not DSEM, and not a matrix `expm`. Meredith (1993), Mislevy (1991), and Oud and Jansen (2000) remain unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T21:03Z: closed; ZORA Anubis-blocked). - `psychometric_core` recovers Voelkle et al. (2012, Eq. 14; ZORA accepted manuscript re-opened 2026-08-17T14:20Z, Introducing Intercepts, manuscript p. 21): the discrete effect of a time-varying predictor whose sampling interval equals its constancy interval is \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). It does not depend on the predictor auto-effect. That product is not Eq. 12. Unmatched sampling and constancy intervals fail closed (Oud & Jansen, 2000, unread). An overflowing Eq. 12 rewrite scale \(a_{yx}/a_{xx}\) also fails closed. Still not DSEM. - `psychometric_core` recovers Voelkle et al. (2012, Eq. 12) when `expm1(z)` overflows to `+∞` at a finite `z`. The rewrite is `sign(a_{yx}/a_{xx})\exp(\ln|a_{yx}|+z-\ln|a_{xx}|)-a_{yx}/a_{xx}` (algebraically identical to `(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)`). A zero continuous effect is exactly zero even when `expm1` overflows (`0\cdot+\infty` is `NaN`). `z\to+\infty` remains fail-closed. ZORA accepted manuscript re-opened 2026-08-17T13:26Z, Introducing Intercepts, manuscript p. 20; Driver, Oud, and Voelkle (2017, Eq. 3) restated `A^{-1}[e^{A\Delta t}-I]\xi`. Still not DSEM. diff --git a/CLAUDE.md b/CLAUDE.md index df19840e..2b045810 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. -- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; this is not a Kalman filter. +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index c6add11d..539042fe 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -408,6 +408,8 @@ pub fn refuse_unmatched_time_varying_predictor_interval( /// the equilibrium variance `−q / (2 a) = −0.5 q / a` for stable /// `a < 0`. When `expm1(z)` overflows to `+∞` at a finite `z`, /// rewrite as `sign(q / a) exp(ln|q| + z − ln|a| − ln 2) − 0.5 q / a`. +/// An overflowing rewrite scale `0.5 q / a` is not a finite `Q_Δt` +/// (`q = 1e308`, `a = 0.1`, `Δt = 4000` → `z = 800`, `0.5 q / a = +∞`). /// `z → +∞` is an unstable process and fails closed unless `q = 0`. /// A zero diffusion is exactly zero even if `expm1` overflows. This /// is not a Kalman filter, not DSEM, and not a matrix `expm`. @@ -464,6 +466,9 @@ pub fn recover_discrete_process_noise( } // Finite z, overflowed expm1. (q / (2 a))(exp(z) − 1) = // sign(q / a) exp(ln|q| + z − ln|a| − ln 2) − 0.5 q / a. + // Driver Eq. 3 (JSS PDF re-opened 2026-08-18T03:07Z, p. 4): + // Q_Δt is that integral. If 0.5 q / a overflows, the rewrite + // scale is not finite and Q_Δt is not finite. let half_scale = 0.5 * continuous_diffusion / log_rate; if !half_scale.is_finite() { return Err(PsychometricError::InvalidNumericInput); @@ -1218,6 +1223,12 @@ mod tests { recover_discrete_process_noise(1.0, 1e308, 2.0, LagClock::EventTime), Err(PsychometricError::InvalidNumericInput) ); + // Finite z, overflowed expm1, overflowing 0.5 q / a. + // q (e^{2 a Δt} − 1) / (2 a) is then non-finite (Driver Eq. 3). + assert_eq!( + recover_discrete_process_noise(1e308, 0.1, 4000.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); } #[test] 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 420f8339..661c9817 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -329,6 +329,10 @@ fn discrete_process_noise_recovers_driver_equation_three() { recover_discrete_process_noise(1.0, 800.0, 1.0, LagClock::EventTime), Err(PsychometricError::InvalidNumericInput) ); + assert_eq!( + recover_discrete_process_noise(1e308, 0.1, 4000.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); assert_eq!( recover_discrete_process_noise(0.4, -0.5, 1.0, LagClock::SystemTime), 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 7b08f8e6..58e6d98e 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -181,6 +181,10 @@ fn discrete_process_noise_is_not_the_continuous_diffusion() { let overflowed_equilibrium = recover_discrete_process_noise(1e308, -1e308, 2.0, LagClock::EventTime).expect("2a eq var"); assert!((overflowed_equilibrium - 0.5).abs() < 1e-15); + assert_eq!( + recover_discrete_process_noise(1e308, 0.1, 4000.0, LagClock::EventTime), + Err(psychometric_core::PsychometricError::InvalidNumericInput) + ); let constant = recover_discrete_constant_predictor_effect(diffusion, drift, delta, LagClock::EventTime) .expect("eq 12"); diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index f0133aac..04b8d4b4 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-17T21:03Z, p. 4; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` fails closed), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T03:07Z, p. 4; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. 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 a4a5d2a5..3b826198 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -39,7 +39,7 @@ Kish, L. (1965). *Survey sampling*. John Wiley & Sons. Oud, J. H. L., & Jansen, R. A. R. G. (2000). Continuous time state space modeling of panel data by means of SEM. *Psychometrika, 65*(2), 199–215. https://doi.org/10.1007/BF02294374 (cited by Voelkle et al., 2012, Eq. 14 discussion; PDF not opened). -The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-17T14:20Z (https://www.zora.uzh.ch/handle/20.500.14742/72792; bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`). Page 16 writes discrete auto-effects as \(A^{*}(\Delta t)=\exp(A\Delta t)\) (Eq. 7). Introducing Intercepts (manuscript p. 20, Eq. 12) adds a continuous-time intercept \(b\) and writes the expected-value solution whose discrete increment is \(A^{-1}(\exp(A\Delta t)-I)b\); the scalar constant-predictor effect is \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). The next paragraph (manuscript p. 21, Eq. 14) writes the discrete effect of a **time-varying** predictor, when the sampling interval equals the constancy interval, as \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product does not depend on the predictor auto-effect. The manuscript calls Eq. 14 a first-order approximation that deteriorates as \(\Delta t\) grows, and defers unmatched sampling/constancy intervals to Oud and Jansen (2000), which is unread. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4; JSS PDF opened 2026-08-17T17:19Z) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\), the discrete intercept \(b_{\Delta t}=A^{-1}[A_{\Delta t}-I]b\), and the discrete process-noise covariance \(Q_{\Delta t}=\int_{0}^{\Delta t}\operatorname{expm}(A(\Delta t-\tau))GG^{\top}\operatorname{expm}(A(\Delta t-\tau))^{\top}\,d\tau\). Discrete auto-effects are strictly positive for finite real drift and interval; the inverse \(a=\ln\varphi/\Delta t\) therefore requires \(\varphi>0\). Binary64 `exp` of a large negative argument is `+0` and is refused. Equations 3–4 of Voelkle et al. (2012) are the discouraged difference-quotient approximation. Meredith (1993) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T21:03Z: closed; Springer content/pdf was HTML). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T21:03Z: closed). ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title is not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T21:03Z: closed; Radboud/Leiden repositories 403). The Voelkle et al. (2012) ZORA bitstream was Anubis-blocked this cycle; the JSS PDF of Driver et al. (2017) was re-opened. +The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-17T14:20Z (https://www.zora.uzh.ch/handle/20.500.14742/72792; bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`). Page 16 writes discrete auto-effects as \(A^{*}(\Delta t)=\exp(A\Delta t)\) (Eq. 7). Introducing Intercepts (manuscript p. 20, Eq. 12) adds a continuous-time intercept \(b\) and writes the expected-value solution whose discrete increment is \(A^{-1}(\exp(A\Delta t)-I)b\); the scalar constant-predictor effect is \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). The next paragraph (manuscript p. 21, Eq. 14) writes the discrete effect of a **time-varying** predictor, when the sampling interval equals the constancy interval, as \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product does not depend on the predictor auto-effect. The manuscript calls Eq. 14 a first-order approximation that deteriorates as \(\Delta t\) grows, and defers unmatched sampling/constancy intervals to Oud and Jansen (2000), which is unread. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4; JSS PDF opened 2026-08-17T17:19Z) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\), the discrete intercept \(b_{\Delta t}=A^{-1}[A_{\Delta t}-I]b\), and the discrete process-noise covariance \(Q_{\Delta t}=\int_{0}^{\Delta t}\operatorname{expm}(A(\Delta t-\tau))GG^{\top}\operatorname{expm}(A(\Delta t-\tau))^{\top}\,d\tau\). Discrete auto-effects are strictly positive for finite real drift and interval; the inverse \(a=\ln\varphi/\Delta t\) therefore requires \(\varphi>0\). Binary64 `exp` of a large negative argument is `+0` and is refused. Equations 3–4 of Voelkle et al. (2012) are the discouraged difference-quotient approximation. Meredith (1993) remains unread (Unpaywall/OpenAlex/Semantic Scholar/CORE/OpenAIRE 2026-08-18T03:07Z: closed; Springer remains an HTML stub; archive.org title search empty). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-18T03:07Z: closed; ETS landing page is HTML). ERIC ED334221 is Singer and Willett (1991), *From whether to when*, not the 1991 journal article. ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title is not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T21:03Z: closed; Radboud/Leiden repositories 403). The Voelkle et al. (2012) ZORA bitstream was Anubis-blocked this cycle; the JSS PDF of Driver et al. (2017) was re-opened 2026-08-18T03:07Z. ## Formula notes @@ -50,7 +50,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-17T14:2 - **Unequal-interval remap.** Discrete \(\varphi(\Delta t_1)\) and \(\varphi(\Delta t_2)\) are not comparable when \(\Delta t_1\neq\Delta t_2\) (Voelkle et al., 2012, ZORA manuscript pp. 2, 16, 33). The licensed path is \(a=\ln\varphi_{\mathrm{src}}/\Delta t_{\mathrm{src}}\) then \(\varphi_{\mathrm{ref}}=\exp(a\,\Delta t_{\mathrm{ref}})\). Pooling those discrete lags fails closed. - **Constant-predictor discrete effect.** Voelkle et al. (2012, Eq. 12; ZORA accepted manuscript re-opened 2026-08-17T13:23Z, Introducing Intercepts): for a constant predictor with \(a_{xx}\neq 0\), \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). Driver, Oud, and Voelkle (2017, p. 4, after Eq. 3) restate the discrete intercept as a function of \(A\) and \(\Delta t\). The algebraically identical finite-`expm1` evaluation is \(a_{yx}(\operatorname{expm1}(z)/a_{xx})\) with \(z=a_{xx}\Delta t\). Dividing the increment by the finite auto-effect keeps the equilibrium increment \(-a_{yx}/a_{xx}\) when \(z\) overflows to \(-\infty\) (the manuscript notes that the exponential vanishes as \(\Delta t\) grows) and keeps a finite result when \(a_{yx}\Delta t\) overflows. When `expm1(z)` overflows to `+\infty` at a finite \(z\), rewrite as \(\operatorname{sign}(a_{yx}/a_{xx})\exp(\ln|a_{yx}|+z-\ln|a_{xx}|)-a_{yx}/a_{xx}\). A zero continuous effect is exactly zero even if `expm1` overflows. \(z\to+\infty\) fails closed unless \(a_{yx}=0\). When binary64 \(z\) underflows to `+0`, the mathematical limit of Eq. 12 is \(a_{yx}\Delta t\). That limit is IEEE-754 evaluation of Eq. 12, not a substitution of the first-order product as the general discrete effect. \(a_{xx}=0\) fails closed. This is not DSEM. - **Time-varying-predictor discrete effect.** Voelkle et al. (2012, Eq. 14; ZORA accepted manuscript re-opened 2026-08-17T14:20Z, Introducing Intercepts, manuscript p. 21): when the predictor can take a new value at each occasion and the sampling interval equals the constancy interval, \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). The effect does not depend on \(a_{xx}\). The manuscript states that this is a first-order approximation that deteriorates as \(\Delta t\) grows. It is not Eq. 12. Unmatched sampling and constancy intervals cite Oud and Jansen (2000), which is unread, and fail closed. -- **Discrete process noise.** Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-17T21:03Z, p. 4): \(Q_{\Delta t}=\int_{0}^{\Delta t}\operatorname{expm}(A(\Delta t-\tau))GG^{\top}\operatorname{expm}(A(\Delta t-\tau))^{\top}\,d\tau\). The scalar closed form with continuous diffusion \(q=GG^{\top}\ge 0\) is \(q(\mathrm{e}^{2a\Delta t}-1)/(2a)\) for \(a\neq 0\) and \(q\Delta t\) for \(a=0\). The algebraically identical finite-`expm1` evaluation is \(0.5 q(\operatorname{expm1}(z)/a)\) with \(z=2(a\Delta t)\). Form \(z\) as twice the product \(a\Delta t\). Forming \(2a\) first overflows when \(|a|\) is at the binary64 extreme even if \(a\Delta t\) and \(Q_{\Delta t}\) are finite. Binary64 underflow of \(z\) to `+0` recovers \(q\Delta t\). \(z\to-\infty\) keeps the equilibrium variance \(-q/(2a)=-0.5 q/a\); `expm1(−∞)` is \(-1\), so that path stays on the finite increment. When `expm1(z)` overflows at a finite \(z\), rewrite as \(\operatorname{sign}(q/a)\exp(\ln|q|+z-\ln|a|-\ln 2)-0.5 q/a\). A zero diffusion is exactly zero even if `expm1` overflows. \(z\to+\infty\) fails closed unless \(q=0\). Negative \(q\) fails closed. This is not a Kalman filter and not a matrix `expm`. +- **Discrete process noise.** Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-17T21:03Z, p. 4): \(Q_{\Delta t}=\int_{0}^{\Delta t}\operatorname{expm}(A(\Delta t-\tau))GG^{\top}\operatorname{expm}(A(\Delta t-\tau))^{\top}\,d\tau\). The scalar closed form with continuous diffusion \(q=GG^{\top}\ge 0\) is \(q(\mathrm{e}^{2a\Delta t}-1)/(2a)\) for \(a\neq 0\) and \(q\Delta t\) for \(a=0\). The algebraically identical finite-`expm1` evaluation is \(0.5 q(\operatorname{expm1}(z)/a)\) with \(z=2(a\Delta t)\). Form \(z\) as twice the product \(a\Delta t\). Forming \(2a\) first overflows when \(|a|\) is at the binary64 extreme even if \(a\Delta t\) and \(Q_{\Delta t}\) are finite. Binary64 underflow of \(z\) to `+0` recovers \(q\Delta t\). \(z\to-\infty\) keeps the equilibrium variance \(-q/(2a)=-0.5 q/a\); `expm1(−∞)` is \(-1\), so that path stays on the finite increment. When `expm1(z)` overflows at a finite \(z\), rewrite as \(\operatorname{sign}(q/a)\exp(\ln|q|+z-\ln|a|-\ln 2)-0.5 q/a\). An overflowing rewrite scale \(0.5 q/a\) is not a finite \(Q_{\Delta t}\) and fails closed (`q=1e308`, `a=0.1`, `Δt=4000` → `z=800`; JSS PDF re-opened 2026-08-18T03:07Z, p. 4). A zero diffusion is exactly zero even if `expm1` overflows. \(z\to+\infty\) fails closed unless \(q=0\). Negative \(q\) fails closed. This is not a Kalman filter and not a matrix `expm`. - **CWC-then-lag.** Sample cluster means are removed first. Consecutive within residuals are then fitted by least squares to \(r_{t+\Delta t}\approx\exp(a\Delta t)\,r_{t}\) on event time. Same-sign pair-wise logs initialize the scalar Newton step. Sign-flipping \(T=2\) CWC pairs have no real logarithm and fail closed. Curran and Bauer (2011, pp. 607–608) show that this person-mean subtraction on a raw autoregressive series does **not** isolate the lagged within-person effect; the helper therefore does not claim to recover the raw-process drift. - **Already-centered irregular residual.** The caller supplies lagged within residuals. The mean of \(a=\ln(r_{t+\Delta t}/r_t)/\Delta t\) is the exact scalar map. Intervals may be irregular. The helper does not center again. This is not DSEM. @@ -64,7 +64,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-17T14:2 - a binary64 underflow of \(\exp(a\Delta t)\) to `+0` (direct forward map and large-interval remap) fails closed; - Voelkle et al. (2012, Eq. 12) recovers a known discrete constant-predictor effect at machine-scale RMSE, and that RMSE is smaller than the first-order product \(a_{yx}\Delta t\); \(a_{xx}=0\) fails closed; binary64 underflow of \(a_{xx}\Delta t\) to `+0` recovers the Eq. 12 limit \(a_{yx}\Delta t\); a finite Eq. 12 result whose first factor \(a_{yx}\Delta t\) overflows is recovered; a finite Eq. 12 equilibrium increment whose \(z=a_{xx}\Delta t\) overflows to \(-\infty\) is recovered as \(-a_{yx}/a_{xx}\); a finite Eq. 12 result whose `expm1(z)` overflows at a finite \(z\) is recovered in log space; a zero continuous effect is exactly zero; an overflowing \(a_{yx}/a_{xx}\) rewrite term fails closed; - Voelkle et al. (2012, Eq. 14) recovers \(a_{yx}\Delta t\) at machine-scale RMSE when sampling, constancy, and event intervals match, and that value is not the Eq. 12 constant-predictor effect; unmatched intervals fail closed; -- Driver et al. (2017, Eq. 3) recovers a known scalar \(Q_{\Delta t}\) at machine-scale RMSE, and that RMSE is smaller than treating the continuous diffusion as the discrete process noise; \(a=0\) and binary64 underflow of \(2a\Delta t\) recover \(q\Delta t\); \(z\to-\infty\) recovers \(-q/(2a)\); a finite result whose `expm1` overflows at a finite \(z\) is recovered in log space; a finite result whose \(2a\) overflows while \(a\Delta t\) stays finite is recovered as \(0.5 q(\operatorname{expm1}(z)/a)\); a zero diffusion is exactly zero; a negative diffusion and \(z\to+\infty\) fail closed; +- Driver et al. (2017, Eq. 3) recovers a known scalar \(Q_{\Delta t}\) at machine-scale RMSE, and that RMSE is smaller than treating the continuous diffusion as the discrete process noise; \(a=0\) and binary64 underflow of \(2a\Delta t\) recover \(q\Delta t\); \(z\to-\infty\) recovers \(-q/(2a)\); a finite result whose `expm1` overflows at a finite \(z\) is recovered in log space; a finite result whose \(2a\) overflows while \(a\Delta t\) stays finite is recovered as \(0.5 q(\operatorname{expm1}(z)/a)\); a zero diffusion is exactly zero; a negative diffusion, \(z\to+\infty\), and an overflowing rewrite scale \(0.5 q/a\) fail closed; - pooling discrete lags from unequal intervals fails closed; - CWC-then-lag on a two-cluster decaying series has smaller computed RMSE than a level-pooled series when the latter is identified; - already-centered irregular residuals recover a known drift at machine-scale RMSE, and that RMSE is smaller than CWC of the corresponding raw autoregressive series (Curran & Bauer, 2011, pp. 607–608); diff --git a/docs/research/rubin-total-variance.md b/docs/research/rubin-total-variance.md index 10cafc5d..0b6e158d 100644 --- a/docs/research/rubin-total-variance.md +++ b/docs/research/rubin-total-variance.md @@ -6,7 +6,7 @@ Adds Rubin combining for complete-data OLS loadings across posterior indicator d ## Claim boundary -`T_m = \bar U_m + (1+1/m)B_m` is the complete-data combining rule. This slice does **not** implement Mislevy plausible values. The 1991 *Psychometrika* paper was not opened in this cycle (Unpaywall/OpenAlex/Semantic Scholar/archive.org 2026-08-17T11:18Z: closed; Cambridge Core PDF redirected to an HTML stub; ETS landing page is HTML). Do not cite that paper as having been read. +`T_m = \bar U_m + (1+1/m)B_m` is the complete-data combining rule. This slice does **not** implement Mislevy plausible values. The 1991 *Psychometrika* paper was not opened in this cycle (Unpaywall/OpenAlex/Semantic Scholar/archive.org 2026-08-18T03:07Z: closed; ETS landing page is HTML; ERIC ED334221 is Singer & Willett, 1991). Do not cite that paper as having been read. ## Authoritative sources diff --git a/docs/research/standards-and-literature.md b/docs/research/standards-and-literature.md index 95f53f39..bca58c73 100644 --- a/docs/research/standards-and-literature.md +++ b/docs/research/standards-and-literature.md @@ -34,7 +34,7 @@ Meredith, W. (1993). Measurement invariance, factor analysis and factorial invar Holland, P. W. (1986). Statistics and causal inference. *Journal of the American Statistical Association, 81*(396), 945–960. https://doi.org/10.1080/01621459.1986.10478354 -TEPP applies these sources to construct definition, score interpretation, reliability, validity evidence, uncertainty, consequences, longitudinal invariance, ESEM cross-loadings, and DSEM. Topic outputs are treated as fallible indicators or components only after their construct role is evaluated. Reflective, formative, and network classes remain distinct (Bollen & Lennox, 1991). Complete-data OLS loadings across posterior indicator draws are combined with Rubin (1996) \(T_m\); the arithmetic-mean helper remains a point estimate. Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T13:26Z: closed). The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title was opened 2026-08-17T12:04Z from archive.org; it is not the 1991 journal article and is not used as Mislevy plausible-value authority. Temporal precedence is not causal identification (Holland, 1986). Within/between OLS follows Enders and Tofighi (2007), Curran and Bauer (2011), and Hamaker et al. (2015). Enders and Tofighi (2007, Table 2, pp. 124–127; PDF opened 2026-08-17) show that the CWC cluster-mean coefficient is the contextual effect (`between − within`), not the between-cluster effect. Curran and Bauer (2011, pp. 607–608) reject person-mean subtraction on a raw autoregressive series as the lagged within-person residual; already-centered irregular residuals use the Voelkle et al. (2012, Eq. 7) / Driver et al. (2017, Eq. 3) scalar map. Discrete lags from unequal event intervals are remapped through that log-rate (Voelkle et al., 2012, ZORA accepted manuscript re-opened 2026-08-17T13:13Z) and are not pooled. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\) and restate the discrete intercept as a function of \(A\) and \(\Delta t\). A binary64 underflow of \(\exp(a\Delta t)\) to `+0` is refused because discrete auto-effects are strictly positive. The discrete effect of a constant predictor is Voelkle et al. (2012, Eq. 12; ZORA accepted manuscript re-opened 2026-08-17T14:20Z, Introducing Intercepts, manuscript p. 20), evaluated as \(a_{yx}(\operatorname{expm1}(z)/a_{xx})\) with \(z=a_{xx}\Delta t\) so a finite result is not lost when \(z\) overflows to \(-\infty\) or when \(a_{yx}\Delta t\) overflows, and in log space when `expm1(z)` overflows at a finite \(z\); a zero continuous effect is exactly zero; an overflowing \(a_{yx}/a_{xx}\) rewrite term fails closed; the first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. The discrete effect of a time-varying predictor with matched sampling and constancy intervals is Voelkle et al. (2012, Eq. 14; manuscript p. 21): \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product is not Eq. 12. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). The exact scalar discrete process noise is Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-17T21:03Z, p. 4): \(Q_{\Delta t}=0.5 q(\operatorname{expm1}(z)/a)\) with \(z=2(a\Delta t)\) for \(a\neq 0\) and \(q=GG^{\top}\ge 0\); do not form \(2a\) first; \(a=0\) recovers \(q\Delta t\). This is not a Kalman filter. Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-17T13:26Z) write the same discrete intercept as \(A^{-1}[e^{A\Delta t}-I]\xi\). Metric/weak invariance does not license latent-mean comparison. Meredith (1993) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T21:03Z: closed). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T21:03Z: closed). ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T21:03Z: closed). +TEPP applies these sources to construct definition, score interpretation, reliability, validity evidence, uncertainty, consequences, longitudinal invariance, ESEM cross-loadings, and DSEM. Topic outputs are treated as fallible indicators or components only after their construct role is evaluated. Reflective, formative, and network classes remain distinct (Bollen & Lennox, 1991). Complete-data OLS loadings across posterior indicator draws are combined with Rubin (1996) \(T_m\); the arithmetic-mean helper remains a point estimate. Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-18T03:07Z: closed). The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title was opened 2026-08-17T12:04Z from archive.org; it is not the 1991 journal article and is not used as Mislevy plausible-value authority. Temporal precedence is not causal identification (Holland, 1986). Within/between OLS follows Enders and Tofighi (2007), Curran and Bauer (2011), and Hamaker et al. (2015). Enders and Tofighi (2007, Table 2, pp. 124–127; PDF opened 2026-08-17) show that the CWC cluster-mean coefficient is the contextual effect (`between − within`), not the between-cluster effect. Curran and Bauer (2011, pp. 607–608) reject person-mean subtraction on a raw autoregressive series as the lagged within-person residual; already-centered irregular residuals use the Voelkle et al. (2012, Eq. 7) / Driver et al. (2017, Eq. 3) scalar map. Discrete lags from unequal event intervals are remapped through that log-rate (Voelkle et al., 2012, ZORA accepted manuscript re-opened 2026-08-17T13:13Z) and are not pooled. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\) and restate the discrete intercept as a function of \(A\) and \(\Delta t\). A binary64 underflow of \(\exp(a\Delta t)\) to `+0` is refused because discrete auto-effects are strictly positive. The discrete effect of a constant predictor is Voelkle et al. (2012, Eq. 12; ZORA accepted manuscript re-opened 2026-08-17T14:20Z, Introducing Intercepts, manuscript p. 20), evaluated as \(a_{yx}(\operatorname{expm1}(z)/a_{xx})\) with \(z=a_{xx}\Delta t\) so a finite result is not lost when \(z\) overflows to \(-\infty\) or when \(a_{yx}\Delta t\) overflows, and in log space when `expm1(z)` overflows at a finite \(z\); a zero continuous effect is exactly zero; an overflowing \(a_{yx}/a_{xx}\) rewrite term fails closed; the first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. The discrete effect of a time-varying predictor with matched sampling and constancy intervals is Voelkle et al. (2012, Eq. 14; manuscript p. 21): \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product is not Eq. 12. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). The exact scalar discrete process noise is Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-18T03:07Z, p. 4): \(Q_{\Delta t}=0.5 q(\operatorname{expm1}(z)/a)\) with \(z=2(a\Delta t)\) for \(a\neq 0\) and \(q=GG^{\top}\ge 0\); do not form \(2a\) first; \(a=0\) recovers \(q\Delta t\); an overflowing rewrite scale \(0.5 q/a\) fails closed. This is not a Kalman filter. Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-17T13:26Z) write the same discrete intercept as \(A^{-1}[e^{A\Delta t}-I]\xi\). Metric/weak invariance does not license latent-mean comparison. Meredith (1993) remains unread (Unpaywall/OpenAlex/Semantic Scholar/CORE 2026-08-18T03:07Z: closed). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-18T03:07Z: closed). ERIC ED334221 is Singer and Willett (1991), not the 1991 journal article. ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T21:03Z: closed). ## Structural, correlated, dynamic, relational, and multilingual topic models diff --git a/docs/research/strong-invariance-latent-means.md b/docs/research/strong-invariance-latent-means.md index 9bcbf1fa..327fa7a8 100644 --- a/docs/research/strong-invariance-latent-means.md +++ b/docs/research/strong-invariance-latent-means.md @@ -12,7 +12,7 @@ This slice does **not** import the unpublished `measurement_invariance` crate on - `#84` `scalar` is the strong/scalar status (equal loading and intercept). That status licenses latent means. - Strict (also equal residual variance) also licenses latent means. - This is two-group OLS, not MGCFA, not partial invariance, and not alignment optimization. -- Meredith (1993) names weak/strong/strict are used only as conventional labels. That PDF was not opened (Unpaywall/OpenAlex/Semantic Scholar/archive.org/ERIC 2026-08-17T21:03Z: closed; Springer remains an HTML stub). Do not cite Meredith equations as having been read. +- Meredith (1993) names weak/strong/strict are used only as conventional labels. That PDF was not opened (Unpaywall/OpenAlex/Semantic Scholar/CORE/OpenAIRE/archive.org 2026-08-18T03:07Z: closed; Springer remains an HTML stub). Do not cite Meredith equations as having been read. ## Authoritative sources used for the mean gate From 6f24124bf3f45db955b6632d706201e5dee7b584 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 07:20:23 +0000 Subject: [PATCH 20/62] test(psychometric): close remaining event-time branch coverage Lib-instantiation fail-closed arms for Voelkle et al. (2012, Eq. 14) and Driver, Oud, and Voelkle (2017, Eq. 3) now live in the integration contract. The four leftover False sides on 6c13dfb were assert!(a && b) in the unit-test module; they cannot take False on a passing test and are now independent asserts. Nightly branch coverage on psychometric_core is 238/238. Scalar L remains 1. Still not a Kalman filter, not DSEM, and not a matrix expm. --- CHANGELOG.md | 1 + crates/psychometric_core/src/event_time.rs | 9 +- ...multilevel_event_time_recovery_contract.rs | 137 ++++++++++++++++++ .../scientific_claim_boundary_contract.rs | 20 +++ docs/adr/0005-posterior-esem-dsem.md | 2 +- .../multilevel-event-time-recovery.md | 6 +- 6 files changed, 168 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b09706a4..fd4e7d72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang ## [Unreleased] +- `psychometric_core` closes nightly branch coverage on `event_time.rs` (160/164 on `6c13dfb`). The four remaining False sides were `assert!(a && b)` in the unit-test module (Eq. 12 equilibrium-increment and expm1-overflow oracles); they cannot take False on a passing test. Split into independent asserts. Lib-instantiation fail-closed arms for Voelkle et al. (2012, Eq. 14) and Driver, Oud, and Voelkle (2017, Eq. 3) stay in the integration contract. Driver JSS PDF already opened 2026-08-18T07:06Z, p. 4 (`L` remains identity; this is not a Kalman filter). Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall/OpenAlex 2026-08-18T07:17Z: closed; Springer `content/pdf` is HTML 200, not a PDF). ERIC ED334221 is Singer and Willett (1991). ZORA Anubis-blocked. Oud and Jansen (2000) remains unread. - `psychometric_core` refuses an overflowing Driver Eq. 3 rewrite scale `0.5 q / a` (JSS PDF re-opened 2026-08-18T03:07Z, p. 4). When `expm1(z)` overflows at a finite `z = 2(a Δt)` and `0.5 q / a` is non-finite (`q = 1e308`, `a = 0.1`, `Δt = 4000` → `z = 800`), `Q_Δt = q(e^{2aΔt}−1)/(2a)` is not finite and fails closed. Algebraically identical to the licensed integral. Still not a Kalman filter, not DSEM, and not a matrix `expm`. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall/OpenAlex/Semantic Scholar/CORE 2026-08-18T03:07Z: closed). ERIC ED334221 is Singer and Willett (1991), not Mislevy (1991). ERIC ED333032 remains Mislevy, Sheehan, and Wingersky (1990). ZORA Anubis-blocked. Oud and Jansen (2000) remains unread. - `psychometric_core` evaluates Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-17T21:03Z, p. 4) as \(0.5 q(\operatorname{expm1}(z)/a)\) with \(z=2(a\Delta t)\). Forming \(2a\) first overflows when \(|a|\) is at the binary64 extreme even if \(a\Delta t\) and \(Q_{\Delta t}\) are finite (`a=1e308`, `Δt=1e-308` → \(0.5(\mathrm{e}^{2}-1)/10^{308}\); `a=-1e308`, `q=1e308`, `Δt=2` → \(0.5\)). Algebraically identical to \(q(\mathrm{e}^{2a\Delta t}-1)/(2a)\). `expm1(−∞)` keeps \(-0.5 q/a\). \(z\to+\infty\) still fails closed. Still not a Kalman filter, not DSEM, and not a matrix `expm`. Meredith (1993), Mislevy (1991), and Oud and Jansen (2000) remain unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T21:03Z: closed; ZORA Anubis-blocked). - `psychometric_core` recovers Voelkle et al. (2012, Eq. 14; ZORA accepted manuscript re-opened 2026-08-17T14:20Z, Introducing Intercepts, manuscript p. 21): the discrete effect of a time-varying predictor whose sampling interval equals its constancy interval is \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). It does not depend on the predictor auto-effect. That product is not Eq. 12. Unmatched sampling and constancy intervals fail closed (Oud & Jansen, 2000, unread). An overflowing Eq. 12 rewrite scale \(a_{yx}/a_{xx}\) also fails closed. Still not DSEM. diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index 539042fe..5f04b865 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -395,7 +395,8 @@ pub fn refuse_unmatched_time_varying_predictor_interval( /// /// Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-17T21:03Z, /// p. 4) write the discrete process-noise covariance -/// `Q_Δt = ∫_0^{Δt} expm(A(Δt−τ)) G G⊤ expm(A(Δt−τ))⊤ dτ`. +/// `Q_Δt = ∫_0^{Δt} expm(A(Δt−τ)) L G G⊤ L⊤ expm(A(Δt−τ))⊤ dτ`. +/// This slice takes scalar `L = 1` (every latent subject to system noise). /// The noiseless scalar closed form with continuous diffusion /// `q = G G⊤ ≥ 0` is `q (exp(2 a Δt) − 1) / (2 a)` for `a ≠ 0` and /// `q Δt` for `a = 0`. The algebraically identical finite-`expm1` @@ -944,7 +945,8 @@ mod tests { // z → -∞: expm1(z)/z * Δt is +0; Eq. 12 → -a_yx/a_xx (Voelkle // 2012, Introducing Intercepts equilibrium increment). let increment_argument = -1e308_f64 * 2.0; - assert!(increment_argument.is_infinite() && increment_argument.is_sign_negative()); + assert!(increment_argument.is_infinite()); + assert!(increment_argument.is_sign_negative()); let lost_scale = increment_argument.exp_m1() / increment_argument * 2.0; assert_eq!(lost_scale.to_bits(), 0.0_f64.to_bits()); let negative_overflow = @@ -966,7 +968,8 @@ mod tests { .expect("eq 12 log-space"); let expected = (1e-308_f64.ln() + 800.0 - 800.0_f64.ln()).exp() - 1e-308 / 800.0; assert!((recovered - expected).abs() / expected < 1e-12); - assert!(recovered.is_finite() && recovered > 0.0); + assert!(recovered.is_finite()); + assert!(recovered > 0.0); let negative = recover_discrete_constant_predictor_effect(-1e-308, 800.0, 1.0, LagClock::EventTime) .expect("eq 12 signed log-space"); 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 661c9817..7769707d 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -283,6 +283,115 @@ fn time_varying_predictor_discrete_effect_recovers_equation_fourteen() { ); } +#[test] +fn time_varying_predictor_equation_fourteen_intervals_fail_closed() { + let outcome_on_predictor = 0.2_f64; + assert_eq!( + recover_discrete_time_varying_predictor_effect( + outcome_on_predictor, + 1.0, + 1.0, + 1.0, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_discrete_time_varying_predictor_effect( + outcome_on_predictor, + f64::NAN, + 1.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_time_varying_predictor_effect( + outcome_on_predictor, + 0.0, + 1.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_time_varying_predictor_effect( + outcome_on_predictor, + 1.0, + f64::NAN, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_time_varying_predictor_effect( + outcome_on_predictor, + 1.0, + 0.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_time_varying_predictor_effect( + outcome_on_predictor, + 1.0, + 1.0, + f64::NAN, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_time_varying_predictor_effect( + outcome_on_predictor, + 1.0, + 1.0, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_time_varying_predictor_effect( + outcome_on_predictor, + 2.0, + 2.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::UnmatchedTimeVaryingInterval) + ); +} + +#[test] +fn time_varying_predictor_equation_fourteen_numeric_inputs_fail_closed() { + assert_eq!( + recover_discrete_time_varying_predictor_effect( + f64::NAN, + 1.0, + 1.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_time_varying_predictor_effect( + 1e308, + 10.0, + 10.0, + 10.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); +} + #[test] fn discrete_process_noise_recovers_driver_equation_three() { let diffusion = 0.4_f64; @@ -333,6 +442,34 @@ fn discrete_process_noise_recovers_driver_equation_three() { recover_discrete_process_noise(1e308, 0.1, 4000.0, LagClock::EventTime), Err(PsychometricError::InvalidNumericInput) ); + assert_eq!( + recover_discrete_process_noise(1.0, 1e308, 2.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_process_noise(0.4, -0.5, 0.0, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_process_noise(0.4, -0.5, -1.0, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_process_noise(0.4, -0.5, f64::NAN, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_process_noise(-0.1, -0.5, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_process_noise(f64::NAN, -0.5, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_process_noise(0.4, f64::NAN, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); assert_eq!( recover_discrete_process_noise(0.4, -0.5, 1.0, LagClock::SystemTime), 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 58e6d98e..edcdfad1 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -185,6 +185,26 @@ fn discrete_process_noise_is_not_the_continuous_diffusion() { recover_discrete_process_noise(1e308, 0.1, 4000.0, LagClock::EventTime), Err(psychometric_core::PsychometricError::InvalidNumericInput) ); + assert_eq!( + recover_discrete_process_noise(1.0, 1e308, 2.0, LagClock::EventTime), + Err(psychometric_core::PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_process_noise(0.4, -0.5, f64::NAN, LagClock::EventTime), + Err(psychometric_core::PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_process_noise(-0.1, -0.5, 1.0, LagClock::EventTime), + Err(psychometric_core::PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_process_noise(f64::NAN, -0.5, 1.0, LagClock::EventTime), + Err(psychometric_core::PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_process_noise(0.4, f64::NAN, 1.0, LagClock::EventTime), + Err(psychometric_core::PsychometricError::InvalidNumericInput) + ); let constant = recover_discrete_constant_predictor_effect(diffusion, drift, delta, LagClock::EventTime) .expect("eq 12"); diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index 04b8d4b4..d9fbb776 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T03:07Z, p. 4; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. 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 3b826198..d039bde5 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -39,7 +39,7 @@ Kish, L. (1965). *Survey sampling*. John Wiley & Sons. Oud, J. H. L., & Jansen, R. A. R. G. (2000). Continuous time state space modeling of panel data by means of SEM. *Psychometrika, 65*(2), 199–215. https://doi.org/10.1007/BF02294374 (cited by Voelkle et al., 2012, Eq. 14 discussion; PDF not opened). -The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-17T14:20Z (https://www.zora.uzh.ch/handle/20.500.14742/72792; bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`). Page 16 writes discrete auto-effects as \(A^{*}(\Delta t)=\exp(A\Delta t)\) (Eq. 7). Introducing Intercepts (manuscript p. 20, Eq. 12) adds a continuous-time intercept \(b\) and writes the expected-value solution whose discrete increment is \(A^{-1}(\exp(A\Delta t)-I)b\); the scalar constant-predictor effect is \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). The next paragraph (manuscript p. 21, Eq. 14) writes the discrete effect of a **time-varying** predictor, when the sampling interval equals the constancy interval, as \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product does not depend on the predictor auto-effect. The manuscript calls Eq. 14 a first-order approximation that deteriorates as \(\Delta t\) grows, and defers unmatched sampling/constancy intervals to Oud and Jansen (2000), which is unread. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4; JSS PDF opened 2026-08-17T17:19Z) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\), the discrete intercept \(b_{\Delta t}=A^{-1}[A_{\Delta t}-I]b\), and the discrete process-noise covariance \(Q_{\Delta t}=\int_{0}^{\Delta t}\operatorname{expm}(A(\Delta t-\tau))GG^{\top}\operatorname{expm}(A(\Delta t-\tau))^{\top}\,d\tau\). Discrete auto-effects are strictly positive for finite real drift and interval; the inverse \(a=\ln\varphi/\Delta t\) therefore requires \(\varphi>0\). Binary64 `exp` of a large negative argument is `+0` and is refused. Equations 3–4 of Voelkle et al. (2012) are the discouraged difference-quotient approximation. Meredith (1993) remains unread (Unpaywall/OpenAlex/Semantic Scholar/CORE/OpenAIRE 2026-08-18T03:07Z: closed; Springer remains an HTML stub; archive.org title search empty). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-18T03:07Z: closed; ETS landing page is HTML). ERIC ED334221 is Singer and Willett (1991), *From whether to when*, not the 1991 journal article. ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title is not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T21:03Z: closed; Radboud/Leiden repositories 403). The Voelkle et al. (2012) ZORA bitstream was Anubis-blocked this cycle; the JSS PDF of Driver et al. (2017) was re-opened 2026-08-18T03:07Z. +The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-17T14:20Z (https://www.zora.uzh.ch/handle/20.500.14742/72792; bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`). Page 16 writes discrete auto-effects as \(A^{*}(\Delta t)=\exp(A\Delta t)\) (Eq. 7). Introducing Intercepts (manuscript p. 20, Eq. 12) adds a continuous-time intercept \(b\) and writes the expected-value solution whose discrete increment is \(A^{-1}(\exp(A\Delta t)-I)b\); the scalar constant-predictor effect is \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). The next paragraph (manuscript p. 21, Eq. 14) writes the discrete effect of a **time-varying** predictor, when the sampling interval equals the constancy interval, as \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product does not depend on the predictor auto-effect. The manuscript calls Eq. 14 a first-order approximation that deteriorates as \(\Delta t\) grows, and defers unmatched sampling/constancy intervals to Oud and Jansen (2000), which is unread. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4; JSS PDF re-opened 2026-08-18T07:06Z from https://www.jstatsoft.org/index.php/jss/article/view/v077i05/v77i05.pdf) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\), the discrete intercept \(b_{\Delta t}=A^{-1}[A_{\Delta t}-I]b\), and the discrete process-noise covariance \(Q_{\Delta t}=\int_{0}^{\Delta t}\operatorname{expm}(A(\Delta t-\tau))LGG^{\top}L^{\top}\operatorname{expm}(A(\Delta t-\tau))^{\top}\,d\tau\). This slice takes scalar \(L=1\) (every latent subject to system noise); it does not implement the 0/1 selector matrix and is not a Kalman filter. Discrete auto-effects are strictly positive for finite real drift and interval; the inverse \(a=\ln\varphi/\Delta t\) therefore requires \(\varphi>0\). Binary64 `exp` of a large negative argument is `+0` and is refused. Equations 3–4 of Voelkle et al. (2012) are the discouraged difference-quotient approximation. Meredith (1993) remains unread (Unpaywall/OpenAlex 2026-08-18T07:17Z: closed; Springer `content/pdf` is HTML 200, not a PDF; archive.org title search empty). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex 2026-08-18T07:17Z: closed; Springer `content/pdf` is HTML 200; ETS landing page is HTML). ERIC ED334221 is Singer and Willett (1991), *From whether to when*, not the 1991 journal article. ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title is not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex 2026-08-18T07:17Z: closed; Radboud landing is not a PDF). The Voelkle et al. (2012) ZORA bitstream was Anubis-blocked this cycle. ## Formula notes @@ -63,8 +63,8 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-17T14:2 - the forward map inverts the log-rate, remaps \(\varphi(1)\) onto \(\varphi(2)\) at machine-scale RMSE, and that RMSE is smaller than treating \(\varphi(1)\) as \(\varphi(2)\); - a binary64 underflow of \(\exp(a\Delta t)\) to `+0` (direct forward map and large-interval remap) fails closed; - Voelkle et al. (2012, Eq. 12) recovers a known discrete constant-predictor effect at machine-scale RMSE, and that RMSE is smaller than the first-order product \(a_{yx}\Delta t\); \(a_{xx}=0\) fails closed; binary64 underflow of \(a_{xx}\Delta t\) to `+0` recovers the Eq. 12 limit \(a_{yx}\Delta t\); a finite Eq. 12 result whose first factor \(a_{yx}\Delta t\) overflows is recovered; a finite Eq. 12 equilibrium increment whose \(z=a_{xx}\Delta t\) overflows to \(-\infty\) is recovered as \(-a_{yx}/a_{xx}\); a finite Eq. 12 result whose `expm1(z)` overflows at a finite \(z\) is recovered in log space; a zero continuous effect is exactly zero; an overflowing \(a_{yx}/a_{xx}\) rewrite term fails closed; -- Voelkle et al. (2012, Eq. 14) recovers \(a_{yx}\Delta t\) at machine-scale RMSE when sampling, constancy, and event intervals match, and that value is not the Eq. 12 constant-predictor effect; unmatched intervals fail closed; -- Driver et al. (2017, Eq. 3) recovers a known scalar \(Q_{\Delta t}\) at machine-scale RMSE, and that RMSE is smaller than treating the continuous diffusion as the discrete process noise; \(a=0\) and binary64 underflow of \(2a\Delta t\) recover \(q\Delta t\); \(z\to-\infty\) recovers \(-q/(2a)\); a finite result whose `expm1` overflows at a finite \(z\) is recovered in log space; a finite result whose \(2a\) overflows while \(a\Delta t\) stays finite is recovered as \(0.5 q(\operatorname{expm1}(z)/a)\); a zero diffusion is exactly zero; a negative diffusion, \(z\to+\infty\), and an overflowing rewrite scale \(0.5 q/a\) fail closed; +- Voelkle et al. (2012, Eq. 14) recovers \(a_{yx}\Delta t\) at machine-scale RMSE when sampling, constancy, and event intervals match, and that value is not the Eq. 12 constant-predictor effect; unmatched intervals, a non-event clock, a non-finite continuous effect, and an overflowing product fail closed; +- Driver et al. (2017, Eq. 3) recovers a known scalar \(Q_{\Delta t}\) at machine-scale RMSE, and that RMSE is smaller than treating the continuous diffusion as the discrete process noise; \(a=0\) and binary64 underflow of \(2a\Delta t\) recover \(q\Delta t\); \(z\to-\infty\) recovers \(-q/(2a)\); a finite result whose `expm1` overflows at a finite \(z\) is recovered in log space; a finite result whose \(2a\) overflows while \(a\Delta t\) stays finite is recovered as \(0.5 q(\operatorname{expm1}(z)/a)\); a zero diffusion is exactly zero; a negative diffusion, \(z\to+\infty\), and an overflowing rewrite scale \(0.5 q/a\) fail closed; non-finite event interval, non-positive sampling interval, and non-finite constancy interval fail closed on Eq. 14; non-finite / non-positive event interval, non-finite / negative diffusion, and non-finite log-rate fail closed on Eq. 3; - pooling discrete lags from unequal intervals fails closed; - CWC-then-lag on a two-cluster decaying series has smaller computed RMSE than a level-pooled series when the latter is identified; - already-centered irregular residuals recover a known drift at machine-scale RMSE, and that RMSE is smaller than CWC of the corresponding raw autoregressive series (Curran & Bauer, 2011, pp. 607–608); From 321568a862898329130e8b53dd4011b6ebd414b3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 11:20:44 +0000 Subject: [PATCH 21/62] feat(psychometric): recover Driver Eq. 3-4 lagged covariance and variance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Map cov(η_t, η_{t-1}) = exp(a Δt) p and Var(η_t) = exp(2 a Δt) p + Q_Δt from the opened Driver, Oud, and Voelkle (2017) JSS solution (Eq. 3) and integral covariance (Eq. 4, pp. 4-5). Q_Δt stays the homogeneous-process conditional residual and is refused as Var(η_t). The JSS article has no numbered §2.2. Finite-exp product overflow and rewrite overflow fail closed. Still not a Kalman filter, not DSEM, and not a matrix expm. --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 1 + CLAUDE.md | 2 +- crates/psychometric_core/src/error.rs | 10 + crates/psychometric_core/src/event_time.rs | 283 +++++++++++++++++- crates/psychometric_core/src/lib.rs | 11 +- .../tests/esem_input_recovery_contract.rs | 4 + ...multilevel_event_time_recovery_contract.rs | 72 +++++ .../scientific_claim_boundary_contract.rs | 29 +- docs/TRACEABILITY.md | 2 +- docs/adr/0005-posterior-esem-dsem.md | 4 +- docs/adr/README.md | 2 +- .../multilevel-event-time-recovery.md | 19 +- docs/validation/temporal-event-foundation.md | 2 +- 14 files changed, 426 insertions(+), 17 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index b31db86f..3ae8ce52 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -61,7 +61,7 @@ boundaries above remain the target modular MSA architecture. | `tepp_simulation` | known-truth temporal/event data generation | | `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics | | `tepp_api` | versioned DTO, schema, and export contracts | -| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means | +| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means | No crate exposes placeholder production behavior in Task 1. This prevents an empty façade from becoming a de facto public API before its invariants and tests diff --git a/CHANGELOG.md b/CHANGELOG.md index fd4e7d72..aeaf6595 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T11:20Z) lagged latent covariance `cov(η_ti, η_{t-1,i}) = A_Δt cov(η_{t-1,i})` and the law-of-total-variance map `Var(η_ti) = A_Δt Var(η_{t-1,i}) A_Δt⊤ + Q_Δt`. Eq. 3 writes `η(t) = exp(A Δt) η(t0) + … +` the stochastic integral; Eq. 4 writes that the integral exhibits covariance `Q_Δt`. `Q_Δt` remains `cov(η_ti | η_{t-1,i})` for the homogeneous process (`ξ`, `z` given) and is refused as the unconditional variance. The JSS article has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). Scalar `exp(a Δt) p` underflow to `+0` is a vanishing covariance and is kept. Finite-`a Δt` exponential overflow rewrites as `exp(ln p + a Δt)`. A finite `exp(a Δt)` whose product with `p` overflows fails closed. Still not a Kalman filter, not DSEM, and not a matrix `expm`. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-18T11:05Z: closed). Oud and Jansen (2000) remains unread (Radboud 403). ZORA Anubis-blocked. - `psychometric_core` closes nightly branch coverage on `event_time.rs` (160/164 on `6c13dfb`). The four remaining False sides were `assert!(a && b)` in the unit-test module (Eq. 12 equilibrium-increment and expm1-overflow oracles); they cannot take False on a passing test. Split into independent asserts. Lib-instantiation fail-closed arms for Voelkle et al. (2012, Eq. 14) and Driver, Oud, and Voelkle (2017, Eq. 3) stay in the integration contract. Driver JSS PDF already opened 2026-08-18T07:06Z, p. 4 (`L` remains identity; this is not a Kalman filter). Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall/OpenAlex 2026-08-18T07:17Z: closed; Springer `content/pdf` is HTML 200, not a PDF). ERIC ED334221 is Singer and Willett (1991). ZORA Anubis-blocked. Oud and Jansen (2000) remains unread. - `psychometric_core` refuses an overflowing Driver Eq. 3 rewrite scale `0.5 q / a` (JSS PDF re-opened 2026-08-18T03:07Z, p. 4). When `expm1(z)` overflows at a finite `z = 2(a Δt)` and `0.5 q / a` is non-finite (`q = 1e308`, `a = 0.1`, `Δt = 4000` → `z = 800`), `Q_Δt = q(e^{2aΔt}−1)/(2a)` is not finite and fails closed. Algebraically identical to the licensed integral. Still not a Kalman filter, not DSEM, and not a matrix `expm`. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall/OpenAlex/Semantic Scholar/CORE 2026-08-18T03:07Z: closed). ERIC ED334221 is Singer and Willett (1991), not Mislevy (1991). ERIC ED333032 remains Mislevy, Sheehan, and Wingersky (1990). ZORA Anubis-blocked. Oud and Jansen (2000) remains unread. - `psychometric_core` evaluates Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-17T21:03Z, p. 4) as \(0.5 q(\operatorname{expm1}(z)/a)\) with \(z=2(a\Delta t)\). Forming \(2a\) first overflows when \(|a|\) is at the binary64 extreme even if \(a\Delta t\) and \(Q_{\Delta t}\) are finite (`a=1e308`, `Δt=1e-308` → \(0.5(\mathrm{e}^{2}-1)/10^{308}\); `a=-1e308`, `q=1e308`, `Δt=2` → \(0.5\)). Algebraically identical to \(q(\mathrm{e}^{2a\Delta t}-1)/(2a)\). `expm1(−∞)` keeps \(-0.5 q/a\). \(z\to+\infty\) still fails closed. Still not a Kalman filter, not DSEM, and not a matrix `expm`. Meredith (1993), Mislevy (1991), and Oud and Jansen (2000) remain unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T21:03Z: closed; ZORA Anubis-blocked). diff --git a/CLAUDE.md b/CLAUDE.md index 2b045810..9c3188f2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. -- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. diff --git a/crates/psychometric_core/src/error.rs b/crates/psychometric_core/src/error.rs index 7c3bfbfc..6bce7d3e 100644 --- a/crates/psychometric_core/src/error.rs +++ b/crates/psychometric_core/src/error.rs @@ -44,6 +44,9 @@ pub enum PsychometricError { InsufficientDraws, /// Latent-mean comparison was requested at metric/weak invariance. StrongInvarianceRequired, + /// Driver Eq. 3 process noise was treated as the unconditional latent + /// variance. `Q_Δt` is `cov(η_ti | η_{t-1,i})`. + ProcessNoiseIsConditionalVariance, } impl fmt::Display for PsychometricError { @@ -81,6 +84,9 @@ impl fmt::Display for PsychometricError { Self::StrongInvarianceRequired => { "latent-mean comparison requires strong or strict invariance; metric/weak is not enough" } + Self::ProcessNoiseIsConditionalVariance => { + "discrete process noise is the conditional residual variance, not the unconditional latent variance" + } }; formatter.write_str(message) } @@ -158,5 +164,9 @@ mod tests { PsychometricError::StrongInvarianceRequired.to_string(), "latent-mean comparison requires strong or strict invariance; metric/weak is not enough" ); + assert_eq!( + PsychometricError::ProcessNoiseIsConditionalVariance.to_string(), + "discrete process noise is the conditional residual variance, not the unconditional latent variance" + ); } } diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index 5f04b865..9389417a 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -11,7 +11,16 @@ //! Eq. 12). The discrete effect of a time-varying predictor whose //! sampling interval equals its constancy interval is their Eq. 14. //! The exact scalar discrete process noise is the closed form of -//! Driver, Oud, and Voelkle (2017, Eq. 3) `Q_Δt`. The difference quotient `(x(t+Δt) − x(t)) / Δt` (their +//! Driver, Oud, and Voelkle (2017, Eq. 3–4, pp. 4–5) `Q_Δt`. +//! Equation 3 writes `η(t) = exp(A Δt) η(t0) + … +` the stochastic +//! integral. Equation 4 writes that the integral exhibits covariance +//! `Q_Δt`. The homogeneous-process consequence (`ξ`, `z` given) is +//! `Q_Δt = cov(η_ti | η_{t-1,i})` and +//! `cov(η_ti, η_{t-1,i}) = A_Δt cov(η_{t-1,i})`. The law of total +//! variance on that pair is +//! `Var(η_ti) = A_Δt Var(η_{t-1,i}) A_Δt⊤ + Q_Δt`. The JSS article +//! has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). +//! The difference quotient `(x(t+Δt) − x(t)) / Δt` (their //! Eqs. 3–4) is refused. This is not DSEM and not a matrix `expm`. use std::collections::BTreeMap; @@ -482,6 +491,132 @@ pub fn recover_discrete_process_noise( require_finite(dominant - half_scale) } +/// Exact scalar lagged latent covariance on event time. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 3–4, pp. 4–5; JSS PDF +/// re-opened 2026-08-18T11:20Z) write `η(t) = exp(A Δt) η(t0) + … +` +/// the stochastic integral (Eq. 3) and that the integral exhibits +/// covariance `Q_Δt` (Eq. 4). The homogeneous-process consequence is +/// `cov(η_ti, η_{t-1,i}) = A_Δt cov(η_{t-1,i})`. The scalar map is +/// `exp(a Δt) p` with prior variance `p ≥ 0`. This is not `Q_Δt`. +/// Binary64 underflow of `exp(a Δt)` to `+0` is a vanishing +/// covariance and is kept. A zero prior variance is exactly zero even +/// if the exponential overflows. When `exp(a Δt)` overflows at a +/// finite `a Δt`, rewrite as `exp(ln p + a Δt)`. An overflowing +/// rewrite fails closed. A finite `exp(a Δt)` whose product with `p` +/// overflows also fails closed. The JSS article has no numbered §2.2. +/// This is not a Kalman filter, not DSEM, and not a matrix `expm`. +/// +/// # Errors +/// +/// Returns [`PsychometricError::EventTimeRequired`] for any non-event clock, +/// [`PsychometricError::NonPositiveInterval`] when `event_delta` is not +/// strictly positive, and [`PsychometricError::InvalidNumericInput`] when +/// the prior variance is negative or non-finite, the log-rate is +/// non-finite, or the mapped covariance is non-finite. +pub fn recover_discrete_lagged_latent_covariance( + prior_variance: f64, + log_rate: f64, + event_delta: f64, + clock: LagClock, +) -> Result { + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + if !event_delta.is_finite() || event_delta <= 0.0 { + return Err(PsychometricError::NonPositiveInterval); + } + if !prior_variance.is_finite() || prior_variance < 0.0 || !log_rate.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + // 0 * +∞ is NaN. Driver Eq. 3: A_Δt * 0 = 0. + if prior_variance == 0.0 { + return Ok(0.0); + } + let drift_interval = log_rate * event_delta; + let auto_effect = drift_interval.exp(); + if auto_effect.is_finite() { + // +0 underflow is a vanishing lagged covariance. + return require_finite(auto_effect * prior_variance); + } + if !drift_interval.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + // Finite a Δt, overflowed exp. exp(a Δt) p = exp(ln p + a Δt). + require_finite((prior_variance.ln() + drift_interval).exp()) +} + +/// Exact scalar discrete latent variance on event time. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 3–4, pp. 4–5; JSS PDF +/// re-opened 2026-08-18T11:20Z) write `Q_Δt` as the covariance of the +/// stochastic integral (Eq. 4) after the homogeneous map +/// `η(t) = exp(A Δt) η(t0) + …` (Eq. 3). That pair is +/// `Q_Δt = cov(η_ti | η_{t-1,i})` and +/// `cov(η_ti, η_{t-1,i}) = A_Δt cov(η_{t-1,i})` when `ξ` and `z` are +/// given. The law of total variance on that pair is +/// `Var(η_ti) = A_Δt Var(η_{t-1,i}) A_Δt⊤ + Q_Δt`. The scalar map is +/// `exp(2 a Δt) p + Q_Δt`. This is not `Q_Δt` alone and not a Kalman +/// measurement update. A zero prior variance is exactly `Q_Δt`. +/// Binary64 underflow of `exp(2 a Δt)` keeps `Q_Δt`. When `exp(z)` +/// overflows at a finite `z = 2 (a Δt)`, rewrite as +/// `exp(ln p + z) + Q_Δt`. An overflowing rewrite fails closed. +/// A finite `exp(z) p` whose sum with `Q_Δt` overflows fails closed. +/// The JSS article has no numbered §2.2. +/// +/// # Errors +/// +/// Propagates [`recover_discrete_process_noise`]. Returns +/// [`PsychometricError::InvalidNumericInput`] when the prior variance is +/// negative or non-finite or the mapped variance is non-finite. +pub fn recover_discrete_latent_variance( + prior_variance: f64, + continuous_diffusion: f64, + log_rate: f64, + event_delta: f64, + clock: LagClock, +) -> Result { + let process_noise = + recover_discrete_process_noise(continuous_diffusion, log_rate, event_delta, clock)?; + if !prior_variance.is_finite() || prior_variance < 0.0 { + return Err(PsychometricError::InvalidNumericInput); + } + if prior_variance == 0.0 { + return Ok(process_noise); + } + let increment_argument = 2.0 * (log_rate * event_delta); + if increment_argument == 0.0 { + return require_finite(prior_variance + process_noise); + } + let auto_effect_square = increment_argument.exp(); + if auto_effect_square.is_finite() { + return require_finite(auto_effect_square * prior_variance + process_noise); + } + if !increment_argument.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + let carried = require_finite((prior_variance.ln() + increment_argument).exp())?; + require_finite(carried + process_noise) +} + +/// Refuse treating Driver Eq. 3 process noise as the unconditional variance. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 3–4, pp. 4–5): +/// `Q_Δt = cov(η_ti | η_{t-1,i})` for the homogeneous process. That +/// residual variance is not `Var(η_ti)` when the previous state is +/// random. The JSS article has no numbered §2.2. +/// +/// # Errors +/// +/// Always returns [`PsychometricError::ProcessNoiseIsConditionalVariance`]. +pub fn refuse_process_noise_as_unconditional_variance( + process_noise: f64, + prior_variance: f64, +) -> Result { + let _ = (process_noise, prior_variance); + Err(PsychometricError::ProcessNoiseIsConditionalVariance) +} + /// Refuse the difference quotient as a continuous-time rate. /// /// Voelkle et al. (2012) discourage `(x(t+Δt) − x(t)) / Δt` as the drift. @@ -714,11 +849,13 @@ mod tests { ClusteredEventScore, EventOccasion, LagClock, LaggedWithinResidual, fit_scalar_log_rate, map_discrete_lag_across_event_intervals, recover_discrete_constant_predictor_effect, recover_discrete_lag_from_log_rate, recover_discrete_lag_one, + recover_discrete_lagged_latent_covariance, recover_discrete_latent_variance, recover_discrete_process_noise, recover_discrete_time_varying_predictor_effect, recover_event_series_mean_log_rate, recover_event_time_discrete_lag_and_log_rate, recover_irregular_centered_residual_log_rate, recover_local_log_rate, recover_within_residual_event_time_log_rate, refuse_difference_quotient_as_local_rate, refuse_pooled_discrete_lag_across_unequal_intervals, + refuse_process_noise_as_unconditional_variance, refuse_unmatched_time_varying_predictor_interval, }; use crate::error::PsychometricError; @@ -1234,6 +1371,150 @@ mod tests { ); } + #[test] + fn lagged_covariance_and_latent_variance_follow_driver_equations_three_and_four() { + let prior = 2.0_f64; + let diffusion = 0.4_f64; + let drift = -0.5_f64; + let delta = 1.0_f64; + let lagged = + recover_discrete_lagged_latent_covariance(prior, drift, delta, LagClock::EventTime) + .expect("lagged cov"); + let expected_lagged = (drift * delta).exp() * prior; + assert!((lagged - expected_lagged).abs() < 1e-15); + let process_noise = + recover_discrete_process_noise(diffusion, drift, delta, LagClock::EventTime) + .expect("q_dt"); + let latent = + recover_discrete_latent_variance(prior, diffusion, drift, delta, LagClock::EventTime) + .expect("var"); + let expected_var = (2.0 * drift * delta).exp() * prior + process_noise; + assert!((latent - expected_var).abs() < 1e-15); + assert!((latent - process_noise).abs() > 1e-3); + assert_eq!( + refuse_process_noise_as_unconditional_variance(process_noise, prior), + Err(PsychometricError::ProcessNoiseIsConditionalVariance) + ); + assert_eq!( + recover_discrete_lagged_latent_covariance(0.0, 800.0, 1.0, LagClock::EventTime), + Ok(0.0) + ); + let underflowed_lagged = + recover_discrete_lagged_latent_covariance(2.0, -1e308, 2.0, LagClock::EventTime) + .expect("underflow lagged"); + assert_eq!(underflowed_lagged.to_bits(), 0.0_f64.to_bits()); + let rewritten = + recover_discrete_lagged_latent_covariance(1e-308, 800.0, 1.0, LagClock::EventTime) + .expect("rewrite lagged"); + let expected_rewrite = (1e-308_f64.ln() + 800.0).exp(); + assert!((rewritten - expected_rewrite).abs() / expected_rewrite < 1e-12); + let zero_prior = + recover_discrete_latent_variance(0.0, diffusion, drift, delta, LagClock::EventTime) + .expect("zero prior"); + assert!((zero_prior - process_noise).abs() < 1e-15); + let drifted_zero = + recover_discrete_latent_variance(2.0, diffusion, 0.0, 2.5, LagClock::EventTime) + .expect("a=0"); + assert!((drifted_zero - (2.0 + diffusion * 2.5)).abs() < 1e-15); + let underflowed_var = + recover_discrete_latent_variance(2.0, 1.0, 1e-308, 1e-308, LagClock::EventTime) + .expect("z underflow"); + assert!((underflowed_var - (2.0 + 1.0 * 1e-308)).abs() < 1e-15); + let vanished = + recover_discrete_latent_variance(2.0, 1e308, -1e308, 2.0, LagClock::EventTime) + .expect("phi_sq underflow"); + assert!((vanished - 0.5).abs() < 1e-15); + let rewritten_var = + recover_discrete_latent_variance(1e-308, 1e-308, 400.0, 1.0, LagClock::EventTime) + .expect("rewrite var"); + assert!(rewritten_var.is_finite()); + assert!(rewritten_var > 0.0); + } + + #[test] + fn lagged_covariance_and_latent_variance_overflow_paths_fail_closed() { + assert_eq!( + recover_discrete_lagged_latent_covariance(1e308, 800.0, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_lagged_latent_covariance(2.0, 1e308, 2.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_latent_variance(1e308, 1e-308, 400.0, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_latent_variance(2.0, 1.0, 1e308, 2.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_lagged_latent_covariance(1e308, 700.0, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_latent_variance(1e308, 1e-308, 350.0, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_latent_variance(1e308, 1e308, 0.0, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + let carried = (-90.622_f64).exp(); + let diffusion_sum = (-83.938_f64).exp(); + assert_eq!( + recover_discrete_latent_variance( + carried, + diffusion_sum, + 400.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_lagged_latent_covariance(-0.1, -0.5, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_lagged_latent_covariance(f64::NAN, -0.5, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_lagged_latent_covariance(2.0, f64::NAN, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_lagged_latent_covariance(2.0, -0.5, 0.0, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_lagged_latent_covariance(2.0, -0.5, -1.0, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_lagged_latent_covariance(2.0, -0.5, f64::NAN, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_lagged_latent_covariance(2.0, -0.5, 1.0, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_discrete_latent_variance(-0.1, 0.4, -0.5, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_latent_variance(f64::NAN, 0.4, -0.5, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_latent_variance(2.0, 0.4, -0.5, 1.0, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + } + #[test] fn non_event_clocks_and_difference_quotient_fail_closed() { for clock in [ diff --git a/crates/psychometric_core/src/lib.rs b/crates/psychometric_core/src/lib.rs index 64ad04b1..99770550 100644 --- a/crates/psychometric_core/src/lib.rs +++ b/crates/psychometric_core/src/lib.rs @@ -15,7 +15,10 @@ //! exact scalar discrete effect of a constant predictor, recovers the //! first-order discrete effect of a time-varying predictor with matched //! sampling and constancy intervals, recovers the exact scalar discrete -//! process noise of Driver et al. (2017, Eq. 3), and refuses +//! process noise of Driver et al. (2017, Eq. 3), recovers the lagged +//! latent covariance and unconditional latent variance licensed by +//! their Eq. 3–4, +//! and refuses //! latent-mean comparison below strong invariance. mod causality; @@ -69,6 +72,10 @@ pub use event_time::recover_discrete_constant_predictor_effect; pub use event_time::recover_discrete_lag_from_log_rate; /// Noiseless scalar discrete lag `later / earlier`. pub use event_time::recover_discrete_lag_one; +/// Exact scalar lagged latent covariance `A_Δt cov(η_{t-1})`. +pub use event_time::recover_discrete_lagged_latent_covariance; +/// Exact scalar discrete latent variance `A_Δt P A_Δt⊤ + Q_Δt`. +pub use event_time::recover_discrete_latent_variance; /// Exact scalar discrete process noise `Q_Δt` on event time. pub use event_time::recover_discrete_process_noise; /// First-order discrete effect of a time-varying event-time predictor. @@ -87,6 +94,8 @@ pub use event_time::recover_within_residual_event_time_log_rate; pub use event_time::refuse_difference_quotient_as_local_rate; /// Refuse pooling discrete lags from unequal event intervals. pub use event_time::refuse_pooled_discrete_lag_across_unequal_intervals; +/// Refuse treating Driver Eq. 3 process noise as the unconditional variance. +pub use event_time::refuse_process_noise_as_unconditional_variance; /// Refuse a time-varying predictor whose sampling and constancy intervals differ. pub use event_time::refuse_unmatched_time_varying_predictor_interval; /// Indicator coordinate kind. diff --git a/crates/psychometric_core/tests/esem_input_recovery_contract.rs b/crates/psychometric_core/tests/esem_input_recovery_contract.rs index 19bd1e1e..6e755cfa 100644 --- a/crates/psychometric_core/tests/esem_input_recovery_contract.rs +++ b/crates/psychometric_core/tests/esem_input_recovery_contract.rs @@ -296,4 +296,8 @@ fn finite_alr_correlation_and_error_messages_are_stable() { PsychometricError::StrongInvarianceRequired.to_string(), "latent-mean comparison requires strong or strict invariance; metric/weak is not enough" ); + assert_eq!( + PsychometricError::ProcessNoiseIsConditionalVariance.to_string(), + "discrete process noise is the conditional residual variance, not the unconditional latent variance" + ); } 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 7769707d..001cf128 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -6,11 +6,13 @@ use psychometric_core::{ LaggedWithinResidual, PsychometricError, map_discrete_lag_across_event_intervals, ordinary_least_squares_slope, recover_cluster_mean_within_between_slopes, recover_discrete_constant_predictor_effect, recover_discrete_lag_from_log_rate, + recover_discrete_lagged_latent_covariance, recover_discrete_latent_variance, recover_discrete_process_noise, recover_discrete_time_varying_predictor_effect, recover_event_series_mean_log_rate, recover_event_time_discrete_lag_and_log_rate, recover_irregular_centered_residual_log_rate, recover_kish_weighted_slope, recover_within_residual_event_time_log_rate, refuse_difference_quotient_as_local_rate, refuse_pooled_discrete_lag_across_unequal_intervals, + refuse_process_noise_as_unconditional_variance, refuse_unmatched_time_varying_predictor_interval, }; @@ -565,6 +567,76 @@ fn irregular_centered_residuals_recover_known_drift_better_than_cwc_of_raw_ar() ); } +#[test] +fn discrete_latent_variance_recovers_driver_equations_three_and_four() { + let prior = 2.0_f64; + let diffusion = 0.4_f64; + let drift = -0.5_f64; + let delta = 1.0_f64; + let process_noise = + recover_discrete_process_noise(diffusion, drift, delta, LagClock::EventTime).expect("q_dt"); + let lagged = + recover_discrete_lagged_latent_covariance(prior, drift, delta, LagClock::EventTime) + .expect("lagged"); + let latent = + recover_discrete_latent_variance(prior, diffusion, drift, delta, LagClock::EventTime) + .expect("var"); + let expected_lagged = (drift * delta).exp() * prior; + let expected_var = (2.0 * drift * delta).exp() * prior + process_noise; + let lagged_error = rmse(&[expected_lagged], &[lagged]); + let var_error = rmse(&[expected_var], &[latent]); + assert!( + lagged_error < 1e-15, + "Driver Eq. 3-4 lagged RMSE {lagged_error}" + ); + assert!( + var_error < 1e-15, + "Driver Eq. 3-4 variance RMSE {var_error}" + ); + let collapsed = rmse(&[expected_var], &[process_noise]); + assert!( + collapsed > var_error, + "Q_Δt is not Var(η_t): collapsed RMSE {collapsed} must exceed {var_error}" + ); + assert_eq!( + refuse_process_noise_as_unconditional_variance(process_noise, prior), + Err(PsychometricError::ProcessNoiseIsConditionalVariance) + ); + assert_eq!( + recover_discrete_lagged_latent_covariance(0.0, 800.0, 1.0, LagClock::EventTime), + Ok(0.0) + ); + let rewritten = + recover_discrete_lagged_latent_covariance(1e-308, 800.0, 1.0, LagClock::EventTime) + .expect("rewrite"); + assert!(rewritten.is_finite()); + assert!(rewritten > 0.0); + assert_eq!( + recover_discrete_lagged_latent_covariance(2.0, 1e308, 2.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_latent_variance(-1.0, diffusion, drift, delta, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_lagged_latent_covariance(2.0, drift, 0.0, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_lagged_latent_covariance(2.0, drift, 1.0, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_discrete_lagged_latent_covariance(1e308, 700.0, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_latent_variance(1e308, 1e308, 0.0, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); +} + #[test] fn admitted_coordinates_still_required_for_multilevel_weights() { assert_eq!( diff --git a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs index edcdfad1..21b92b38 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -4,9 +4,10 @@ use psychometric_core::{ ClusteredEventScore, ClusteredScore, IndicatorKind, LagClock, LaggedWithinResidual, ordinary_least_squares_slope, posterior_draw_point_estimate_mean, recover_cluster_mean_within_between_slopes, recover_discrete_constant_predictor_effect, + recover_discrete_lagged_latent_covariance, recover_discrete_latent_variance, recover_discrete_process_noise, recover_discrete_time_varying_predictor_effect, recover_irregular_centered_residual_log_rate, recover_loading_point_estimate_mean, - recover_within_residual_event_time_log_rate, + recover_within_residual_event_time_log_rate, refuse_process_noise_as_unconditional_variance, }; #[test] @@ -213,3 +214,29 @@ fn discrete_process_noise_is_not_the_continuous_diffusion() { "Driver Eq. 3 Q_Δt is not Voelkle Eq. 12" ); } + +#[test] +fn process_noise_is_not_the_unconditional_latent_variance() { + let prior = 2.0_f64; + let diffusion = 0.4_f64; + let drift = -0.5_f64; + let delta = 1.0_f64; + let process_noise = + recover_discrete_process_noise(diffusion, drift, delta, LagClock::EventTime).expect("q_dt"); + let lagged = + recover_discrete_lagged_latent_covariance(prior, drift, delta, LagClock::EventTime) + .expect("lagged"); + let latent = + recover_discrete_latent_variance(prior, diffusion, drift, delta, LagClock::EventTime) + .expect("var"); + assert!( + (process_noise - latent).abs() > 1e-3, + "Driver et al. (2017, Eq. 3-4, pp. 4-5): Q_Δt is cov(η_t | η_{{t-1}}), not Var(η_t)" + ); + assert!((lagged - (drift * delta).exp() * prior).abs() < 1e-15); + assert!((latent - ((2.0 * drift * delta).exp() * prior + process_noise)).abs() < 1e-15); + assert_eq!( + refuse_process_noise_as_unconditional_variance(process_noise, prior), + Err(psychometric_core::PsychometricError::ProcessNoiseIsConditionalVariance) + ); +} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index f160e9a7..052ac630 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -28,7 +28,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | report template/section/copied/style/modality method effects | ADR 0004/0012; PRD/TRD | simulation truth factors implemented; estimator-side method model remains future | partial | | candidate K statistical/Pareto gates + blinded LLM review | ADR 0012; research | future `model_selection` | accepted-target | | compositional topic correlation / stable clustering | ADR 0005/0012; research | future `network_analysis` | accepted-target | -| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR; full ESEM/DSEM remaining | partial | +| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR; full ESEM/DSEM remaining | partial | | CPU bounded multithreading + GPU/VRAM streaming/parity | ADR 0001/0006 | future `compute_backend` | accepted-target | | TDT detection/tracking vs CHRONOS schema/prediction/temporal consistency | ADR 0016; PRD/research | future `event_intelligence` | accepted-target | | evidence-bounded LLM interpretation | ADR 0010/0012; PRD | future `interpretation_gateway` | accepted-target | diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index d9fbb776..5d1353aa 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -1,7 +1,7 @@ # ADR 0005 — Posterior-aware ESEM/DSEM and structural interpretation **Decision status:** Accepted -**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target +**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target **Date:** 2026-08-05 **Supersedes:** None. ADR 0012 governs upstream topic measurement/network coordinates; this ADR governs higher-order psychometric structure and longitudinal interpretation. @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T11:20Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. 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/adr/README.md b/docs/adr/README.md index 231b9d21..447eb77e 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -10,7 +10,7 @@ Read [`ADR_POLICY.md`](ADR_POLICY.md) first. **Decision status and implementatio | [0002](0002-six-clock-temporal-semantics.md) | Six-clock temporal semantics and fail-closed historical leakage prevention | Accepted | active-PR | Unmerged PR #8 is the canonical Task 3 replacement implementing typed clocks/intervals against the current protected-main lineage; conflicted PR #5 is superseded lineage. Later graph/split enforcement remains target work. | | [0003](0003-relational-event-multiple-membership.md) | Relational event ontology and time-varying cross-classified multiple membership | Accepted | partial | Weighted time-varying membership network/roles are active-PR (PR #12); full multilevel estimators, graph ontology, and persistence remain accepted-target. ADR 0016 owns event-intelligence tasks. | | [0004](0004-shared-multilingual-latent-space.md) | One shared multilingual latent space with explicit invariance status | Accepted | accepted-target | ADR 0012 owns the full topic-estimator/backend/global-topic contract. | -| [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | partial | Input gates, posterior-draw point estimates, Rubin `T` on OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects, exact scalar discrete process noise, irregular already-centered residual lag, and strong-gated latent means are on the stacked psychometric PR; full ESEM/DSEM estimator remains accepted-target. | +| [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | partial | Input gates, posterior-draw point estimates, Rubin `T` on OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects, exact scalar discrete process noise, lagged latent covariance and unconditional latent variance, irregular already-centered residual lag, and strong-gated latent means are on the stacked psychometric PR; full ESEM/DSEM estimator remains accepted-target. | | [0006](0006-vram-gpu-nvidia-orchestration.md) | VRAM-adaptive GPU compute and model-credential boundary | Accepted | accepted-target | LLM orchestration policy superseded by ADR 0010; autonomous development authority governed by ADR 0015. | | [0007](0007-rust-workspace-quality-gates.md) | Explicit Rust workspace, pinned toolchains, and exact quality gates | Accepted | implemented-main | ADR 0014 governs scientific/product claim promotion beyond repository-quality tooling. | | [0008](0008-immutable-evidence-identities-digests-and-spans.md) | Immutable evidence identities, `SHA-256` digests, exact spans, and strict wire reconstruction | Accepted | implemented-main | ADR 0013 governs future persistence/reproducibility/split authority. | diff --git a/docs/research/multilevel-event-time-recovery.md b/docs/research/multilevel-event-time-recovery.md index d039bde5..891fed59 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -13,15 +13,17 @@ This slice stays inside `psychometric_core`. It does not add a second invariance 7. recover the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12); 8. recover the first-order discrete effect of a time-varying predictor whose sampling interval equals its constancy interval (Voelkle et al., 2012, Eq. 14); 9. recover the exact scalar discrete process noise of Driver, Oud, and Voelkle (2017, Eq. 3); -10. refuse pooling discrete lags from unequal event intervals as one coefficient; -11. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); -12. refuse the difference quotient as a continuous-time rate; -13. apply the same event-time map to CWC residuals (still not DSEM); -14. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). +10. recover the exact scalar lagged latent covariance `A_Δt cov(η_{t-1})` (Driver et al., 2017, Eq. 3–4); +11. recover the exact scalar discrete latent variance `A_Δt P A_Δt⊤ + Q_Δt` and refuse `Q_Δt` as that unconditional variance; +12. refuse pooling discrete lags from unequal event intervals as one coefficient; +13. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); +14. refuse the difference quotient as a continuous-time rate; +15. apply the same event-time map to CWC residuals (still not DSEM); +16. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). ## Claim boundary -This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). +This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). ## Authoritative sources @@ -39,7 +41,7 @@ Kish, L. (1965). *Survey sampling*. John Wiley & Sons. Oud, J. H. L., & Jansen, R. A. R. G. (2000). Continuous time state space modeling of panel data by means of SEM. *Psychometrika, 65*(2), 199–215. https://doi.org/10.1007/BF02294374 (cited by Voelkle et al., 2012, Eq. 14 discussion; PDF not opened). -The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-17T14:20Z (https://www.zora.uzh.ch/handle/20.500.14742/72792; bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`). Page 16 writes discrete auto-effects as \(A^{*}(\Delta t)=\exp(A\Delta t)\) (Eq. 7). Introducing Intercepts (manuscript p. 20, Eq. 12) adds a continuous-time intercept \(b\) and writes the expected-value solution whose discrete increment is \(A^{-1}(\exp(A\Delta t)-I)b\); the scalar constant-predictor effect is \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). The next paragraph (manuscript p. 21, Eq. 14) writes the discrete effect of a **time-varying** predictor, when the sampling interval equals the constancy interval, as \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product does not depend on the predictor auto-effect. The manuscript calls Eq. 14 a first-order approximation that deteriorates as \(\Delta t\) grows, and defers unmatched sampling/constancy intervals to Oud and Jansen (2000), which is unread. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4; JSS PDF re-opened 2026-08-18T07:06Z from https://www.jstatsoft.org/index.php/jss/article/view/v077i05/v77i05.pdf) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\), the discrete intercept \(b_{\Delta t}=A^{-1}[A_{\Delta t}-I]b\), and the discrete process-noise covariance \(Q_{\Delta t}=\int_{0}^{\Delta t}\operatorname{expm}(A(\Delta t-\tau))LGG^{\top}L^{\top}\operatorname{expm}(A(\Delta t-\tau))^{\top}\,d\tau\). This slice takes scalar \(L=1\) (every latent subject to system noise); it does not implement the 0/1 selector matrix and is not a Kalman filter. Discrete auto-effects are strictly positive for finite real drift and interval; the inverse \(a=\ln\varphi/\Delta t\) therefore requires \(\varphi>0\). Binary64 `exp` of a large negative argument is `+0` and is refused. Equations 3–4 of Voelkle et al. (2012) are the discouraged difference-quotient approximation. Meredith (1993) remains unread (Unpaywall/OpenAlex 2026-08-18T07:17Z: closed; Springer `content/pdf` is HTML 200, not a PDF; archive.org title search empty). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex 2026-08-18T07:17Z: closed; Springer `content/pdf` is HTML 200; ETS landing page is HTML). ERIC ED334221 is Singer and Willett (1991), *From whether to when*, not the 1991 journal article. ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title is not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex 2026-08-18T07:17Z: closed; Radboud landing is not a PDF). The Voelkle et al. (2012) ZORA bitstream was Anubis-blocked this cycle. +The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-17T14:20Z (https://www.zora.uzh.ch/handle/20.500.14742/72792; bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`). Page 16 writes discrete auto-effects as \(A^{*}(\Delta t)=\exp(A\Delta t)\) (Eq. 7). Introducing Intercepts (manuscript p. 20, Eq. 12) adds a continuous-time intercept \(b\) and writes the expected-value solution whose discrete increment is \(A^{-1}(\exp(A\Delta t)-I)b\); the scalar constant-predictor effect is \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). The next paragraph (manuscript p. 21, Eq. 14) writes the discrete effect of a **time-varying** predictor, when the sampling interval equals the constancy interval, as \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product does not depend on the predictor auto-effect. The manuscript calls Eq. 14 a first-order approximation that deteriorates as \(\Delta t\) grows, and defers unmatched sampling/constancy intervals to Oud and Jansen (2000), which is unread. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4; JSS PDF re-opened 2026-08-18T11:05Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\), the discrete intercept \(b_{\Delta t}=A^{-1}[A_{\Delta t}-I]b\), and the discrete process-noise covariance \(Q_{\Delta t}=\int_{0}^{\Delta t}\operatorname{expm}(A(\Delta t-\tau))LGG^{\top}L^{\top}\operatorname{expm}(A(\Delta t-\tau))^{\top}\,d\tau\). Equation 4 (p. 5) writes that the integral exhibits covariance \(Q_{\Delta t}\). The homogeneous-process consequence (`ξ`, `z` given) is \(Q_{\Delta t}=\operatorname{cov}(\eta_{ti}\mid\eta_{t-1,i})\) and \(\operatorname{cov}(\eta_{ti},\eta_{t-1,i})=A_{\Delta t}\operatorname{cov}(\eta_{t-1,i})\). The law of total variance on that pair is \(\operatorname{Var}(\eta_{ti})=A_{\Delta t}\operatorname{Var}(\eta_{t-1,i})A_{\Delta t}^{\top}+Q_{\Delta t}\). The JSS article has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). This slice takes scalar \(L=1\) (every latent subject to system noise); it does not implement the 0/1 selector matrix and is not a Kalman filter. Discrete auto-effects are strictly positive for finite real drift and interval; the inverse \(a=\ln\varphi/\Delta t\) therefore requires \(\varphi>0\). Binary64 `exp` of a large negative argument is `+0` and is refused as a discrete lag; the same underflow is a vanishing lagged covariance and is kept. Equations 3–4 of Voelkle et al. (2012) are the discouraged difference-quotient approximation. Meredith (1993) remains unread (Unpaywall/OpenAlex 2026-08-18T11:05Z: closed; Springer `content/pdf` is HTML 200, not a PDF; archive.org title search empty). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex 2026-08-18T11:05Z: closed; Springer `content/pdf` is HTML 200; ETS landing page is HTML). ERIC ED334221 is Singer and Willett (1991), *From whether to when*, not the 1991 journal article. ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title is not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex 2026-08-18T11:05Z: closed; Radboud landing and bitstream 403). The Voelkle et al. (2012) ZORA bitstream was Anubis-blocked this cycle. ## Formula notes @@ -51,6 +53,8 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-17T14:2 - **Constant-predictor discrete effect.** Voelkle et al. (2012, Eq. 12; ZORA accepted manuscript re-opened 2026-08-17T13:23Z, Introducing Intercepts): for a constant predictor with \(a_{xx}\neq 0\), \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). Driver, Oud, and Voelkle (2017, p. 4, after Eq. 3) restate the discrete intercept as a function of \(A\) and \(\Delta t\). The algebraically identical finite-`expm1` evaluation is \(a_{yx}(\operatorname{expm1}(z)/a_{xx})\) with \(z=a_{xx}\Delta t\). Dividing the increment by the finite auto-effect keeps the equilibrium increment \(-a_{yx}/a_{xx}\) when \(z\) overflows to \(-\infty\) (the manuscript notes that the exponential vanishes as \(\Delta t\) grows) and keeps a finite result when \(a_{yx}\Delta t\) overflows. When `expm1(z)` overflows to `+\infty` at a finite \(z\), rewrite as \(\operatorname{sign}(a_{yx}/a_{xx})\exp(\ln|a_{yx}|+z-\ln|a_{xx}|)-a_{yx}/a_{xx}\). A zero continuous effect is exactly zero even if `expm1` overflows. \(z\to+\infty\) fails closed unless \(a_{yx}=0\). When binary64 \(z\) underflows to `+0`, the mathematical limit of Eq. 12 is \(a_{yx}\Delta t\). That limit is IEEE-754 evaluation of Eq. 12, not a substitution of the first-order product as the general discrete effect. \(a_{xx}=0\) fails closed. This is not DSEM. - **Time-varying-predictor discrete effect.** Voelkle et al. (2012, Eq. 14; ZORA accepted manuscript re-opened 2026-08-17T14:20Z, Introducing Intercepts, manuscript p. 21): when the predictor can take a new value at each occasion and the sampling interval equals the constancy interval, \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). The effect does not depend on \(a_{xx}\). The manuscript states that this is a first-order approximation that deteriorates as \(\Delta t\) grows. It is not Eq. 12. Unmatched sampling and constancy intervals cite Oud and Jansen (2000), which is unread, and fail closed. - **Discrete process noise.** Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-17T21:03Z, p. 4): \(Q_{\Delta t}=\int_{0}^{\Delta t}\operatorname{expm}(A(\Delta t-\tau))GG^{\top}\operatorname{expm}(A(\Delta t-\tau))^{\top}\,d\tau\). The scalar closed form with continuous diffusion \(q=GG^{\top}\ge 0\) is \(q(\mathrm{e}^{2a\Delta t}-1)/(2a)\) for \(a\neq 0\) and \(q\Delta t\) for \(a=0\). The algebraically identical finite-`expm1` evaluation is \(0.5 q(\operatorname{expm1}(z)/a)\) with \(z=2(a\Delta t)\). Form \(z\) as twice the product \(a\Delta t\). Forming \(2a\) first overflows when \(|a|\) is at the binary64 extreme even if \(a\Delta t\) and \(Q_{\Delta t}\) are finite. Binary64 underflow of \(z\) to `+0` recovers \(q\Delta t\). \(z\to-\infty\) keeps the equilibrium variance \(-q/(2a)=-0.5 q/a\); `expm1(−∞)` is \(-1\), so that path stays on the finite increment. When `expm1(z)` overflows at a finite \(z\), rewrite as \(\operatorname{sign}(q/a)\exp(\ln|q|+z-\ln|a|-\ln 2)-0.5 q/a\). An overflowing rewrite scale \(0.5 q/a\) is not a finite \(Q_{\Delta t}\) and fails closed (`q=1e308`, `a=0.1`, `Δt=4000` → `z=800`; JSS PDF re-opened 2026-08-18T03:07Z, p. 4). A zero diffusion is exactly zero even if `expm1` overflows. \(z\to+\infty\) fails closed unless \(q=0\). Negative \(q\) fails closed. This is not a Kalman filter and not a matrix `expm`. +- **Lagged latent covariance.** Driver, Oud, and Voelkle (2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T11:20Z): \(\operatorname{cov}(\eta_{ti},\eta_{t-1,i})=A_{\Delta t}\operatorname{cov}(\eta_{t-1,i})\) for the homogeneous process. The scalar map is \(\mathrm{e}^{a\Delta t}p\) with prior variance \(p\ge 0\). This is not \(Q_{\Delta t}\). Binary64 underflow of \(\mathrm{e}^{a\Delta t}\) to `+0` is a vanishing covariance and is kept. A zero prior variance is exactly zero. Finite-\(a\Delta t\) exponential overflow rewrites as \(\exp(\ln p+a\Delta t)\). A finite \(\mathrm{e}^{a\Delta t}\) whose product with \(p\) overflows fails closed. The JSS article has no numbered §2.2. +- **Discrete latent variance.** Equations 3–4 write \(Q_{\Delta t}\) as the covariance of the stochastic integral, so \(Q_{\Delta t}=\operatorname{cov}(\eta_{ti}\mid\eta_{t-1,i})\) when \(\xi\) and \(z\) are given. The law of total variance on that pair is \(\operatorname{Var}(\eta_{ti})=A_{\Delta t}\operatorname{Var}(\eta_{t-1,i})A_{\Delta t}^{\top}+Q_{\Delta t}\). The scalar map is \(\mathrm{e}^{2a\Delta t}p+Q_{\Delta t}\). This is not a Kalman measurement update. A zero prior variance is exactly \(Q_{\Delta t}\). Binary64 underflow of \(\mathrm{e}^{2a\Delta t}\) keeps \(Q_{\Delta t}\). Finite-\(z\) exponential overflow rewrites as \(\exp(\ln p+z)+Q_{\Delta t}\). Treating \(Q_{\Delta t}\) as \(\operatorname{Var}(\eta_{t})\) fails closed. - **CWC-then-lag.** Sample cluster means are removed first. Consecutive within residuals are then fitted by least squares to \(r_{t+\Delta t}\approx\exp(a\Delta t)\,r_{t}\) on event time. Same-sign pair-wise logs initialize the scalar Newton step. Sign-flipping \(T=2\) CWC pairs have no real logarithm and fail closed. Curran and Bauer (2011, pp. 607–608) show that this person-mean subtraction on a raw autoregressive series does **not** isolate the lagged within-person effect; the helper therefore does not claim to recover the raw-process drift. - **Already-centered irregular residual.** The caller supplies lagged within residuals. The mean of \(a=\ln(r_{t+\Delta t}/r_t)/\Delta t\) is the exact scalar map. Intervals may be irregular. The helper does not center again. This is not DSEM. @@ -65,6 +69,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-17T14:2 - Voelkle et al. (2012, Eq. 12) recovers a known discrete constant-predictor effect at machine-scale RMSE, and that RMSE is smaller than the first-order product \(a_{yx}\Delta t\); \(a_{xx}=0\) fails closed; binary64 underflow of \(a_{xx}\Delta t\) to `+0` recovers the Eq. 12 limit \(a_{yx}\Delta t\); a finite Eq. 12 result whose first factor \(a_{yx}\Delta t\) overflows is recovered; a finite Eq. 12 equilibrium increment whose \(z=a_{xx}\Delta t\) overflows to \(-\infty\) is recovered as \(-a_{yx}/a_{xx}\); a finite Eq. 12 result whose `expm1(z)` overflows at a finite \(z\) is recovered in log space; a zero continuous effect is exactly zero; an overflowing \(a_{yx}/a_{xx}\) rewrite term fails closed; - Voelkle et al. (2012, Eq. 14) recovers \(a_{yx}\Delta t\) at machine-scale RMSE when sampling, constancy, and event intervals match, and that value is not the Eq. 12 constant-predictor effect; unmatched intervals, a non-event clock, a non-finite continuous effect, and an overflowing product fail closed; - Driver et al. (2017, Eq. 3) recovers a known scalar \(Q_{\Delta t}\) at machine-scale RMSE, and that RMSE is smaller than treating the continuous diffusion as the discrete process noise; \(a=0\) and binary64 underflow of \(2a\Delta t\) recover \(q\Delta t\); \(z\to-\infty\) recovers \(-q/(2a)\); a finite result whose `expm1` overflows at a finite \(z\) is recovered in log space; a finite result whose \(2a\) overflows while \(a\Delta t\) stays finite is recovered as \(0.5 q(\operatorname{expm1}(z)/a)\); a zero diffusion is exactly zero; a negative diffusion, \(z\to+\infty\), and an overflowing rewrite scale \(0.5 q/a\) fail closed; non-finite event interval, non-positive sampling interval, and non-finite constancy interval fail closed on Eq. 14; non-finite / non-positive event interval, non-finite / negative diffusion, and non-finite log-rate fail closed on Eq. 3; +- Driver et al. (2017, Eq. 3–4) recovers a known lagged covariance \(\mathrm{e}^{a\Delta t}p\) and a known latent variance \(\mathrm{e}^{2a\Delta t}p+Q_{\Delta t}\) at machine-scale RMSE, and that variance RMSE is smaller than treating \(Q_{\Delta t}\) as \(\operatorname{Var}(\eta_{t})\); a zero prior variance is exactly zero (lagged) or \(Q_{\Delta t}\) (unconditional); binary64 underflow of \(\mathrm{e}^{a\Delta t}\) keeps a vanishing lagged covariance; a finite \(\mathrm{e}^{a\Delta t}\) whose product with \(p\) overflows fails closed; treating \(Q_{\Delta t}\) as the unconditional variance fails closed; - pooling discrete lags from unequal intervals fails closed; - CWC-then-lag on a two-cluster decaying series has smaller computed RMSE than a level-pooled series when the latter is identified; - already-centered irregular residuals recover a known drift at machine-scale RMSE, and that RMSE is smaller than CWC of the corresponding raw autoregressive series (Curran & Bauer, 2011, pp. 607–608); diff --git a/docs/validation/temporal-event-foundation.md b/docs/validation/temporal-event-foundation.md index 0d0c2011..a697bfe0 100644 --- a/docs/validation/temporal-event-foundation.md +++ b/docs/validation/temporal-event-foundation.md @@ -23,7 +23,7 @@ This report tracks exact-head scientific and engineering evidence required befor | Truth corpora / manifests | `tepp_simulation` | implemented-main | — | deterministic generator tests | Task 10 / PR #18 | | Recovery metrics | `validation_core` | implemented-main | — | RMSE/bias/coverage/MC gates | Task 11 / PR #19 | | Versioned API/export contracts | `tepp_api` | implemented-main | naruon HTTP interchange | unknown-field/version/limit + naruon HTTPS interchange tests | Task 12 / PR #21; live HTTP service remaining | -| Psychometric structural input gates | `psychometric_core` | partial | stacked psychometric PR | construct-class refusal + ALR/ILR boundary + true-loading RMSE + posterior-draw point-estimate mean + Rubin `T` + CWC within/between + CWC contextual effect + event-time log-rate + constant- and time-varying-predictor discrete effects + exact scalar discrete process noise + irregular already-centered residual lag + strong-gated latent means; full ESEM/DSEM remaining | ADR 0005; `docs/research/posterior-esem-input-gates.md`; `docs/research/multilevel-event-time-recovery.md`; `docs/research/rubin-total-variance.md`; `docs/research/strong-invariance-latent-means.md` | +| Psychometric structural input gates | `psychometric_core` | partial | stacked psychometric PR | construct-class refusal + ALR/ILR boundary + true-loading RMSE + posterior-draw point-estimate mean + Rubin `T` + CWC within/between + CWC contextual effect + event-time log-rate + constant- and time-varying-predictor discrete effects + exact scalar discrete process noise + lagged latent covariance and unconditional latent variance + irregular already-centered residual lag + strong-gated latent means; full ESEM/DSEM remaining | ADR 0005; `docs/research/posterior-esem-input-gates.md`; `docs/research/multilevel-event-time-recovery.md`; `docs/research/rubin-total-variance.md`; `docs/research/strong-invariance-latent-means.md` | | CWL modular connectors | `docs/connectors/*` | implemented-main | — | contract docs + examples | PR #22; live HTTP ports remaining | | Release SBOM/provenance generator | `scripts/release_evidence.py` | partial | — | generate+validate in CI | Task 13 partial / PR #28 | From 9439b43ccb0f4f8d754d37d7d717f37585adf2b0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 14:08:05 +0000 Subject: [PATCH 22/62] test(psychometric): refuse zero-diffusion Driver Eq. 3-4 overflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zero diffusion is exactly Q_Δt = 0 (Driver, Oud, and Voelkle 2017, Eq. 3). That skip does not license Var(η_t) = exp(2 a Δt) p when 2 (a Δt) overflows to +∞ (p = 2, q = 0, a = 1e308, Δt = 2). Nightly uncovered production line event_time.rs:596 on 321568a. JSS PDF re-opened 2026-08-18T14:04Z, pp. 4-5. Still not a Kalman filter, not DSEM, and not a matrix expm. Meredith (1993) and Mislevy (1991) remain unread. --- CHANGELOG.md | 1 + CLAUDE.md | 2 +- crates/psychometric_core/src/event_time.rs | 11 ++++++++++- .../tests/multilevel_event_time_recovery_contract.rs | 6 ++++++ docs/adr/0005-posterior-esem-dsem.md | 2 +- docs/research/multilevel-event-time-recovery.md | 4 ++-- docs/research/standards-and-literature.md | 2 +- 7 files changed, 22 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aeaf6595..0f62aa07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang ## [Unreleased] +- `psychometric_core` refuses a zero-diffusion Driver, Oud, and Voelkle (2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T14:04Z) latent variance when `2 (a Δt)` overflows to `+∞`. Zero diffusion is exactly `Q_Δt = 0` (Eq. 3 integral of a zero `G`). That skip does not license `Var(η_t) = exp(2 a Δt) p + 0` when the carried term is non-finite (`p = 2`, `q = 0`, `a = 1e308`, `Δt = 2`). Nightly uncovered production line `event_time.rs:596` on predecessor `321568a` is this arm. Still not a Kalman filter, not DSEM, and not a matrix `expm`. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-18T14:04Z: closed; Springer `content/pdf` is HTML 200). Oud and Jansen (2000) remains unread. - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T11:20Z) lagged latent covariance `cov(η_ti, η_{t-1,i}) = A_Δt cov(η_{t-1,i})` and the law-of-total-variance map `Var(η_ti) = A_Δt Var(η_{t-1,i}) A_Δt⊤ + Q_Δt`. Eq. 3 writes `η(t) = exp(A Δt) η(t0) + … +` the stochastic integral; Eq. 4 writes that the integral exhibits covariance `Q_Δt`. `Q_Δt` remains `cov(η_ti | η_{t-1,i})` for the homogeneous process (`ξ`, `z` given) and is refused as the unconditional variance. The JSS article has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). Scalar `exp(a Δt) p` underflow to `+0` is a vanishing covariance and is kept. Finite-`a Δt` exponential overflow rewrites as `exp(ln p + a Δt)`. A finite `exp(a Δt)` whose product with `p` overflows fails closed. Still not a Kalman filter, not DSEM, and not a matrix `expm`. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-18T11:05Z: closed). Oud and Jansen (2000) remains unread (Radboud 403). ZORA Anubis-blocked. - `psychometric_core` closes nightly branch coverage on `event_time.rs` (160/164 on `6c13dfb`). The four remaining False sides were `assert!(a && b)` in the unit-test module (Eq. 12 equilibrium-increment and expm1-overflow oracles); they cannot take False on a passing test. Split into independent asserts. Lib-instantiation fail-closed arms for Voelkle et al. (2012, Eq. 14) and Driver, Oud, and Voelkle (2017, Eq. 3) stay in the integration contract. Driver JSS PDF already opened 2026-08-18T07:06Z, p. 4 (`L` remains identity; this is not a Kalman filter). Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall/OpenAlex 2026-08-18T07:17Z: closed; Springer `content/pdf` is HTML 200, not a PDF). ERIC ED334221 is Singer and Willett (1991). ZORA Anubis-blocked. Oud and Jansen (2000) remains unread. - `psychometric_core` refuses an overflowing Driver Eq. 3 rewrite scale `0.5 q / a` (JSS PDF re-opened 2026-08-18T03:07Z, p. 4). When `expm1(z)` overflows at a finite `z = 2(a Δt)` and `0.5 q / a` is non-finite (`q = 1e308`, `a = 0.1`, `Δt = 4000` → `z = 800`), `Q_Δt = q(e^{2aΔt}−1)/(2a)` is not finite and fails closed. Algebraically identical to the licensed integral. Still not a Kalman filter, not DSEM, and not a matrix `expm`. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall/OpenAlex/Semantic Scholar/CORE 2026-08-18T03:07Z: closed). ERIC ED334221 is Singer and Willett (1991), not Mislevy (1991). ERIC ED333032 remains Mislevy, Sheehan, and Wingersky (1990). ZORA Anubis-blocked. Oud and Jansen (2000) remains unread. diff --git a/CLAUDE.md b/CLAUDE.md index 9c3188f2..e8e3c285 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. -- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index 9389417a..dedde63c 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -562,7 +562,10 @@ pub fn recover_discrete_lagged_latent_covariance( /// overflows at a finite `z = 2 (a Δt)`, rewrite as /// `exp(ln p + z) + Q_Δt`. An overflowing rewrite fails closed. /// A finite `exp(z) p` whose sum with `Q_Δt` overflows fails closed. -/// The JSS article has no numbered §2.2. +/// A zero diffusion skips the process-noise `z → +∞` refusal; the +/// carried term `exp(2 a Δt) p` is then still non-finite when +/// `2 (a Δt)` overflows to `+∞` (`p = 2`, `q = 0`, `a = 1e308`, +/// `Δt = 2`) and fails closed. The JSS article has no numbered §2.2. /// /// # Errors /// @@ -1449,6 +1452,12 @@ mod tests { recover_discrete_latent_variance(2.0, 1.0, 1e308, 2.0, LagClock::EventTime), Err(PsychometricError::InvalidNumericInput) ); + // Zero diffusion is exactly Q_Δt = 0 (Driver Eq. 3). That skip + // does not license exp(2 a Δt) p when 2 (a Δt) overflows to +∞. + assert_eq!( + recover_discrete_latent_variance(2.0, 0.0, 1e308, 2.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); assert_eq!( recover_discrete_lagged_latent_covariance(1e308, 700.0, 1.0, LagClock::EventTime), Err(PsychometricError::InvalidNumericInput) 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 001cf128..b98fde89 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -635,6 +635,12 @@ fn discrete_latent_variance_recovers_driver_equations_three_and_four() { recover_discrete_latent_variance(1e308, 1e308, 0.0, 1.0, LagClock::EventTime), Err(PsychometricError::InvalidNumericInput) ); + // Zero diffusion skips process-noise z→+∞. exp(2 a Δt) p is then + // non-finite (Driver Eq. 3–4). + assert_eq!( + recover_discrete_latent_variance(2.0, 0.0, 1e308, 2.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); } #[test] diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index 5d1353aa..b1d6abbd 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T11:20Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T14:04Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. 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 891fed59..fc3e63d3 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -54,7 +54,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-17T14:2 - **Time-varying-predictor discrete effect.** Voelkle et al. (2012, Eq. 14; ZORA accepted manuscript re-opened 2026-08-17T14:20Z, Introducing Intercepts, manuscript p. 21): when the predictor can take a new value at each occasion and the sampling interval equals the constancy interval, \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). The effect does not depend on \(a_{xx}\). The manuscript states that this is a first-order approximation that deteriorates as \(\Delta t\) grows. It is not Eq. 12. Unmatched sampling and constancy intervals cite Oud and Jansen (2000), which is unread, and fail closed. - **Discrete process noise.** Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-17T21:03Z, p. 4): \(Q_{\Delta t}=\int_{0}^{\Delta t}\operatorname{expm}(A(\Delta t-\tau))GG^{\top}\operatorname{expm}(A(\Delta t-\tau))^{\top}\,d\tau\). The scalar closed form with continuous diffusion \(q=GG^{\top}\ge 0\) is \(q(\mathrm{e}^{2a\Delta t}-1)/(2a)\) for \(a\neq 0\) and \(q\Delta t\) for \(a=0\). The algebraically identical finite-`expm1` evaluation is \(0.5 q(\operatorname{expm1}(z)/a)\) with \(z=2(a\Delta t)\). Form \(z\) as twice the product \(a\Delta t\). Forming \(2a\) first overflows when \(|a|\) is at the binary64 extreme even if \(a\Delta t\) and \(Q_{\Delta t}\) are finite. Binary64 underflow of \(z\) to `+0` recovers \(q\Delta t\). \(z\to-\infty\) keeps the equilibrium variance \(-q/(2a)=-0.5 q/a\); `expm1(−∞)` is \(-1\), so that path stays on the finite increment. When `expm1(z)` overflows at a finite \(z\), rewrite as \(\operatorname{sign}(q/a)\exp(\ln|q|+z-\ln|a|-\ln 2)-0.5 q/a\). An overflowing rewrite scale \(0.5 q/a\) is not a finite \(Q_{\Delta t}\) and fails closed (`q=1e308`, `a=0.1`, `Δt=4000` → `z=800`; JSS PDF re-opened 2026-08-18T03:07Z, p. 4). A zero diffusion is exactly zero even if `expm1` overflows. \(z\to+\infty\) fails closed unless \(q=0\). Negative \(q\) fails closed. This is not a Kalman filter and not a matrix `expm`. - **Lagged latent covariance.** Driver, Oud, and Voelkle (2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T11:20Z): \(\operatorname{cov}(\eta_{ti},\eta_{t-1,i})=A_{\Delta t}\operatorname{cov}(\eta_{t-1,i})\) for the homogeneous process. The scalar map is \(\mathrm{e}^{a\Delta t}p\) with prior variance \(p\ge 0\). This is not \(Q_{\Delta t}\). Binary64 underflow of \(\mathrm{e}^{a\Delta t}\) to `+0` is a vanishing covariance and is kept. A zero prior variance is exactly zero. Finite-\(a\Delta t\) exponential overflow rewrites as \(\exp(\ln p+a\Delta t)\). A finite \(\mathrm{e}^{a\Delta t}\) whose product with \(p\) overflows fails closed. The JSS article has no numbered §2.2. -- **Discrete latent variance.** Equations 3–4 write \(Q_{\Delta t}\) as the covariance of the stochastic integral, so \(Q_{\Delta t}=\operatorname{cov}(\eta_{ti}\mid\eta_{t-1,i})\) when \(\xi\) and \(z\) are given. The law of total variance on that pair is \(\operatorname{Var}(\eta_{ti})=A_{\Delta t}\operatorname{Var}(\eta_{t-1,i})A_{\Delta t}^{\top}+Q_{\Delta t}\). The scalar map is \(\mathrm{e}^{2a\Delta t}p+Q_{\Delta t}\). This is not a Kalman measurement update. A zero prior variance is exactly \(Q_{\Delta t}\). Binary64 underflow of \(\mathrm{e}^{2a\Delta t}\) keeps \(Q_{\Delta t}\). Finite-\(z\) exponential overflow rewrites as \(\exp(\ln p+z)+Q_{\Delta t}\). Treating \(Q_{\Delta t}\) as \(\operatorname{Var}(\eta_{t})\) fails closed. +- **Discrete latent variance.** Equations 3–4 write \(Q_{\Delta t}\) as the covariance of the stochastic integral, so \(Q_{\Delta t}=\operatorname{cov}(\eta_{ti}\mid\eta_{t-1,i})\) when \(\xi\) and \(z\) are given. The law of total variance on that pair is \(\operatorname{Var}(\eta_{ti})=A_{\Delta t}\operatorname{Var}(\eta_{t-1,i})A_{\Delta t}^{\top}+Q_{\Delta t}\). The scalar map is \(\mathrm{e}^{2a\Delta t}p+Q_{\Delta t}\). This is not a Kalman measurement update. A zero prior variance is exactly \(Q_{\Delta t}\). Binary64 underflow of \(\mathrm{e}^{2a\Delta t}\) keeps \(Q_{\Delta t}\). Finite-\(z\) exponential overflow rewrites as \(\exp(\ln p+z)+Q_{\Delta t}\). A zero diffusion is exactly \(Q_{\Delta t}=0\); that skip does not license a non-finite carried term when \(2(a\Delta t)\) overflows to \(+\infty\). Treating \(Q_{\Delta t}\) as \(\operatorname{Var}(\eta_{t})\) fails closed. - **CWC-then-lag.** Sample cluster means are removed first. Consecutive within residuals are then fitted by least squares to \(r_{t+\Delta t}\approx\exp(a\Delta t)\,r_{t}\) on event time. Same-sign pair-wise logs initialize the scalar Newton step. Sign-flipping \(T=2\) CWC pairs have no real logarithm and fail closed. Curran and Bauer (2011, pp. 607–608) show that this person-mean subtraction on a raw autoregressive series does **not** isolate the lagged within-person effect; the helper therefore does not claim to recover the raw-process drift. - **Already-centered irregular residual.** The caller supplies lagged within residuals. The mean of \(a=\ln(r_{t+\Delta t}/r_t)/\Delta t\) is the exact scalar map. Intervals may be irregular. The helper does not center again. This is not DSEM. @@ -69,7 +69,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-17T14:2 - Voelkle et al. (2012, Eq. 12) recovers a known discrete constant-predictor effect at machine-scale RMSE, and that RMSE is smaller than the first-order product \(a_{yx}\Delta t\); \(a_{xx}=0\) fails closed; binary64 underflow of \(a_{xx}\Delta t\) to `+0` recovers the Eq. 12 limit \(a_{yx}\Delta t\); a finite Eq. 12 result whose first factor \(a_{yx}\Delta t\) overflows is recovered; a finite Eq. 12 equilibrium increment whose \(z=a_{xx}\Delta t\) overflows to \(-\infty\) is recovered as \(-a_{yx}/a_{xx}\); a finite Eq. 12 result whose `expm1(z)` overflows at a finite \(z\) is recovered in log space; a zero continuous effect is exactly zero; an overflowing \(a_{yx}/a_{xx}\) rewrite term fails closed; - Voelkle et al. (2012, Eq. 14) recovers \(a_{yx}\Delta t\) at machine-scale RMSE when sampling, constancy, and event intervals match, and that value is not the Eq. 12 constant-predictor effect; unmatched intervals, a non-event clock, a non-finite continuous effect, and an overflowing product fail closed; - Driver et al. (2017, Eq. 3) recovers a known scalar \(Q_{\Delta t}\) at machine-scale RMSE, and that RMSE is smaller than treating the continuous diffusion as the discrete process noise; \(a=0\) and binary64 underflow of \(2a\Delta t\) recover \(q\Delta t\); \(z\to-\infty\) recovers \(-q/(2a)\); a finite result whose `expm1` overflows at a finite \(z\) is recovered in log space; a finite result whose \(2a\) overflows while \(a\Delta t\) stays finite is recovered as \(0.5 q(\operatorname{expm1}(z)/a)\); a zero diffusion is exactly zero; a negative diffusion, \(z\to+\infty\), and an overflowing rewrite scale \(0.5 q/a\) fail closed; non-finite event interval, non-positive sampling interval, and non-finite constancy interval fail closed on Eq. 14; non-finite / non-positive event interval, non-finite / negative diffusion, and non-finite log-rate fail closed on Eq. 3; -- Driver et al. (2017, Eq. 3–4) recovers a known lagged covariance \(\mathrm{e}^{a\Delta t}p\) and a known latent variance \(\mathrm{e}^{2a\Delta t}p+Q_{\Delta t}\) at machine-scale RMSE, and that variance RMSE is smaller than treating \(Q_{\Delta t}\) as \(\operatorname{Var}(\eta_{t})\); a zero prior variance is exactly zero (lagged) or \(Q_{\Delta t}\) (unconditional); binary64 underflow of \(\mathrm{e}^{a\Delta t}\) keeps a vanishing lagged covariance; a finite \(\mathrm{e}^{a\Delta t}\) whose product with \(p\) overflows fails closed; treating \(Q_{\Delta t}\) as the unconditional variance fails closed; +- Driver et al. (2017, Eq. 3–4) recovers a known lagged covariance \(\mathrm{e}^{a\Delta t}p\) and a known latent variance \(\mathrm{e}^{2a\Delta t}p+Q_{\Delta t}\) at machine-scale RMSE, and that variance RMSE is smaller than treating \(Q_{\Delta t}\) as \(\operatorname{Var}(\eta_{t})\); a zero prior variance is exactly zero (lagged) or \(Q_{\Delta t}\) (unconditional); binary64 underflow of \(\mathrm{e}^{a\Delta t}\) keeps a vanishing lagged covariance; a finite \(\mathrm{e}^{a\Delta t}\) whose product with \(p\) overflows fails closed; a zero diffusion whose \(2(a\Delta t)\) overflows to \(+\infty\) fails closed; treating \(Q_{\Delta t}\) as the unconditional variance fails closed; - pooling discrete lags from unequal intervals fails closed; - CWC-then-lag on a two-cluster decaying series has smaller computed RMSE than a level-pooled series when the latter is identified; - already-centered irregular residuals recover a known drift at machine-scale RMSE, and that RMSE is smaller than CWC of the corresponding raw autoregressive series (Curran & Bauer, 2011, pp. 607–608); diff --git a/docs/research/standards-and-literature.md b/docs/research/standards-and-literature.md index bca58c73..19db025e 100644 --- a/docs/research/standards-and-literature.md +++ b/docs/research/standards-and-literature.md @@ -34,7 +34,7 @@ Meredith, W. (1993). Measurement invariance, factor analysis and factorial invar Holland, P. W. (1986). Statistics and causal inference. *Journal of the American Statistical Association, 81*(396), 945–960. https://doi.org/10.1080/01621459.1986.10478354 -TEPP applies these sources to construct definition, score interpretation, reliability, validity evidence, uncertainty, consequences, longitudinal invariance, ESEM cross-loadings, and DSEM. Topic outputs are treated as fallible indicators or components only after their construct role is evaluated. Reflective, formative, and network classes remain distinct (Bollen & Lennox, 1991). Complete-data OLS loadings across posterior indicator draws are combined with Rubin (1996) \(T_m\); the arithmetic-mean helper remains a point estimate. Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-18T03:07Z: closed). The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title was opened 2026-08-17T12:04Z from archive.org; it is not the 1991 journal article and is not used as Mislevy plausible-value authority. Temporal precedence is not causal identification (Holland, 1986). Within/between OLS follows Enders and Tofighi (2007), Curran and Bauer (2011), and Hamaker et al. (2015). Enders and Tofighi (2007, Table 2, pp. 124–127; PDF opened 2026-08-17) show that the CWC cluster-mean coefficient is the contextual effect (`between − within`), not the between-cluster effect. Curran and Bauer (2011, pp. 607–608) reject person-mean subtraction on a raw autoregressive series as the lagged within-person residual; already-centered irregular residuals use the Voelkle et al. (2012, Eq. 7) / Driver et al. (2017, Eq. 3) scalar map. Discrete lags from unequal event intervals are remapped through that log-rate (Voelkle et al., 2012, ZORA accepted manuscript re-opened 2026-08-17T13:13Z) and are not pooled. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\) and restate the discrete intercept as a function of \(A\) and \(\Delta t\). A binary64 underflow of \(\exp(a\Delta t)\) to `+0` is refused because discrete auto-effects are strictly positive. The discrete effect of a constant predictor is Voelkle et al. (2012, Eq. 12; ZORA accepted manuscript re-opened 2026-08-17T14:20Z, Introducing Intercepts, manuscript p. 20), evaluated as \(a_{yx}(\operatorname{expm1}(z)/a_{xx})\) with \(z=a_{xx}\Delta t\) so a finite result is not lost when \(z\) overflows to \(-\infty\) or when \(a_{yx}\Delta t\) overflows, and in log space when `expm1(z)` overflows at a finite \(z\); a zero continuous effect is exactly zero; an overflowing \(a_{yx}/a_{xx}\) rewrite term fails closed; the first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. The discrete effect of a time-varying predictor with matched sampling and constancy intervals is Voelkle et al. (2012, Eq. 14; manuscript p. 21): \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product is not Eq. 12. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). The exact scalar discrete process noise is Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-18T03:07Z, p. 4): \(Q_{\Delta t}=0.5 q(\operatorname{expm1}(z)/a)\) with \(z=2(a\Delta t)\) for \(a\neq 0\) and \(q=GG^{\top}\ge 0\); do not form \(2a\) first; \(a=0\) recovers \(q\Delta t\); an overflowing rewrite scale \(0.5 q/a\) fails closed. This is not a Kalman filter. Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-17T13:26Z) write the same discrete intercept as \(A^{-1}[e^{A\Delta t}-I]\xi\). Metric/weak invariance does not license latent-mean comparison. Meredith (1993) remains unread (Unpaywall/OpenAlex/Semantic Scholar/CORE 2026-08-18T03:07Z: closed). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-18T03:07Z: closed). ERIC ED334221 is Singer and Willett (1991), not the 1991 journal article. ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T21:03Z: closed). +TEPP applies these sources to construct definition, score interpretation, reliability, validity evidence, uncertainty, consequences, longitudinal invariance, ESEM cross-loadings, and DSEM. Topic outputs are treated as fallible indicators or components only after their construct role is evaluated. Reflective, formative, and network classes remain distinct (Bollen & Lennox, 1991). Complete-data OLS loadings across posterior indicator draws are combined with Rubin (1996) \(T_m\); the arithmetic-mean helper remains a point estimate. Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-18T03:07Z: closed). The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title was opened 2026-08-17T12:04Z from archive.org; it is not the 1991 journal article and is not used as Mislevy plausible-value authority. Temporal precedence is not causal identification (Holland, 1986). Within/between OLS follows Enders and Tofighi (2007), Curran and Bauer (2011), and Hamaker et al. (2015). Enders and Tofighi (2007, Table 2, pp. 124–127; PDF opened 2026-08-17) show that the CWC cluster-mean coefficient is the contextual effect (`between − within`), not the between-cluster effect. Curran and Bauer (2011, pp. 607–608) reject person-mean subtraction on a raw autoregressive series as the lagged within-person residual; already-centered irregular residuals use the Voelkle et al. (2012, Eq. 7) / Driver et al. (2017, Eq. 3) scalar map. Discrete lags from unequal event intervals are remapped through that log-rate (Voelkle et al., 2012, ZORA accepted manuscript re-opened 2026-08-17T13:13Z) and are not pooled. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\) and restate the discrete intercept as a function of \(A\) and \(\Delta t\). A binary64 underflow of \(\exp(a\Delta t)\) to `+0` is refused because discrete auto-effects are strictly positive. The discrete effect of a constant predictor is Voelkle et al. (2012, Eq. 12; ZORA accepted manuscript re-opened 2026-08-17T14:20Z, Introducing Intercepts, manuscript p. 20), evaluated as \(a_{yx}(\operatorname{expm1}(z)/a_{xx})\) with \(z=a_{xx}\Delta t\) so a finite result is not lost when \(z\) overflows to \(-\infty\) or when \(a_{yx}\Delta t\) overflows, and in log space when `expm1(z)` overflows at a finite \(z\); a zero continuous effect is exactly zero; an overflowing \(a_{yx}/a_{xx}\) rewrite term fails closed; the first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. The discrete effect of a time-varying predictor with matched sampling and constancy intervals is Voelkle et al. (2012, Eq. 14; manuscript p. 21): \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product is not Eq. 12. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). The exact scalar discrete process noise is Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-18T14:04Z, p. 4): \(Q_{\Delta t}=0.5 q(\operatorname{expm1}(z)/a)\) with \(z=2(a\Delta t)\) for \(a\neq 0\) and \(q=GG^{\top}\ge 0\); do not form \(2a\) first; \(a=0\) recovers \(q\Delta t\); an overflowing rewrite scale \(0.5 q/a\) fails closed. This is not a Kalman filter. Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-18T14:04Z) write the same discrete intercept as \(A^{-1}[e^{A\Delta t}-I]\xi\). The lagged covariance is \(\mathrm{e}^{a\Delta t}p\) and the unconditional variance is \(\mathrm{e}^{2a\Delta t}p+Q_{\Delta t}\) (Driver et al., 2017, Eq. 3–4, pp. 4–5); a zero diffusion whose \(2(a\Delta t)\) overflows to \(+\infty\) fails closed. Metric/weak invariance does not license latent-mean comparison. Meredith (1993) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-18T14:04Z: closed; Springer `content/pdf` is HTML 200). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-18T14:04Z: closed). ERIC ED334221 is Singer and Willett (1991), not the 1991 journal article. ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T21:03Z: closed). ## Structural, correlated, dynamic, relational, and multilingual topic models From 105e1e4d667cc962bf8acebdc529229b0976f1d3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 18:10:13 +0000 Subject: [PATCH 23/62] feat(psychometric): recover Driver Eq. 4 stationary within-subject variance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Driver, Oud, and Voelkle (2017, Eq. 4, p. 5; §4.3 pp. 9-10; p. 16 asymDIFFUSION; JSS PDF re-opened 2026-08-18T18:03Z). For stable a < 0 the Δt→∞ limit of Q_Δt is -q/(2a). Form -0.5 q/a; do not form 2a first (a = -1e308, q = 1e308 → 0.5). Starting from that variance, Var(η_t) is invariant across finite event intervals. a ≥ 0 has no finite stationary variance. Finite-interval Q_Δt is not asymDIFFUSION. Still not a Kalman filter, not DSEM, and not a matrix expm. Meredith (1993) and Mislevy (1991) remain unread. --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 1 + CLAUDE.md | 2 +- crates/psychometric_core/src/error.rs | 22 +++ crates/psychometric_core/src/event_time.rs | 166 +++++++++++++++++- crates/psychometric_core/src/lib.rs | 8 +- .../tests/esem_input_recovery_contract.rs | 8 + ...multilevel_event_time_recovery_contract.rs | 61 ++++++- .../scientific_claim_boundary_contract.rs | 31 +++- docs/TRACEABILITY.md | 2 +- docs/adr/0005-posterior-esem-dsem.md | 4 +- docs/adr/README.md | 2 +- .../multilevel-event-time-recovery.md | 17 +- docs/research/standards-and-literature.md | 2 +- 14 files changed, 309 insertions(+), 19 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 3ae8ce52..ad608914 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -61,7 +61,7 @@ boundaries above remain the target modular MSA architecture. | `tepp_simulation` | known-truth temporal/event data generation | | `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics | | `tepp_api` | versioned DTO, schema, and export contracts | -| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means | +| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means | No crate exposes placeholder production behavior in Task 1. This prevents an empty façade from becoming a de facto public API before its invariants and tests diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f62aa07..e5261f11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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, Eq. 4, p. 5; §4.3 pp. 9–10; p. 16 `asymDIFFUSION`; JSS PDF re-opened 2026-08-18T18:03Z) scalar stationary within-subject variance. Eq. 4 writes `Q_Δt = irow(A#^{-1}[e^{A# Δt} − I] row(Q))` with `A# = A ⊗ I + I ⊗ A`. The scalar Kronecker sum is `2 a`. As `Δt → ∞` with stable `a < 0`, that limit is `-q / (2 a)`. Form `-0.5 q / a`; do not form `2 a` first (`a = -1e308`, `q = 1e308` → `0.5`). Starting from that variance, `Var(η_t)` is invariant across finite event intervals. A zero diffusion is exactly zero. `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not `asymDIFFUSION`. Still not a Kalman filter, not DSEM, not a matrix `expm`, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall/OpenAlex 2026-08-18T18:03Z: closed). Oud and Jansen (2000) remains unread. ZORA Anubis-blocked. - `psychometric_core` refuses a zero-diffusion Driver, Oud, and Voelkle (2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T14:04Z) latent variance when `2 (a Δt)` overflows to `+∞`. Zero diffusion is exactly `Q_Δt = 0` (Eq. 3 integral of a zero `G`). That skip does not license `Var(η_t) = exp(2 a Δt) p + 0` when the carried term is non-finite (`p = 2`, `q = 0`, `a = 1e308`, `Δt = 2`). Nightly uncovered production line `event_time.rs:596` on predecessor `321568a` is this arm. Still not a Kalman filter, not DSEM, and not a matrix `expm`. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-18T14:04Z: closed; Springer `content/pdf` is HTML 200). Oud and Jansen (2000) remains unread. - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T11:20Z) lagged latent covariance `cov(η_ti, η_{t-1,i}) = A_Δt cov(η_{t-1,i})` and the law-of-total-variance map `Var(η_ti) = A_Δt Var(η_{t-1,i}) A_Δt⊤ + Q_Δt`. Eq. 3 writes `η(t) = exp(A Δt) η(t0) + … +` the stochastic integral; Eq. 4 writes that the integral exhibits covariance `Q_Δt`. `Q_Δt` remains `cov(η_ti | η_{t-1,i})` for the homogeneous process (`ξ`, `z` given) and is refused as the unconditional variance. The JSS article has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). Scalar `exp(a Δt) p` underflow to `+0` is a vanishing covariance and is kept. Finite-`a Δt` exponential overflow rewrites as `exp(ln p + a Δt)`. A finite `exp(a Δt)` whose product with `p` overflows fails closed. Still not a Kalman filter, not DSEM, and not a matrix `expm`. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-18T11:05Z: closed). Oud and Jansen (2000) remains unread (Radboud 403). ZORA Anubis-blocked. - `psychometric_core` closes nightly branch coverage on `event_time.rs` (160/164 on `6c13dfb`). The four remaining False sides were `assert!(a && b)` in the unit-test module (Eq. 12 equilibrium-increment and expm1-overflow oracles); they cannot take False on a passing test. Split into independent asserts. Lib-instantiation fail-closed arms for Voelkle et al. (2012, Eq. 14) and Driver, Oud, and Voelkle (2017, Eq. 3) stay in the integration contract. Driver JSS PDF already opened 2026-08-18T07:06Z, p. 4 (`L` remains identity; this is not a Kalman filter). Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall/OpenAlex 2026-08-18T07:17Z: closed; Springer `content/pdf` is HTML 200, not a PDF). ERIC ED334221 is Singer and Willett (1991). ZORA Anubis-blocked. Oud and Jansen (2000) remains unread. diff --git a/CLAUDE.md b/CLAUDE.md index e8e3c285..33bfe25c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. -- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). Form `-0.5 q / a`. `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. diff --git a/crates/psychometric_core/src/error.rs b/crates/psychometric_core/src/error.rs index 6bce7d3e..6d73cd99 100644 --- a/crates/psychometric_core/src/error.rs +++ b/crates/psychometric_core/src/error.rs @@ -47,6 +47,14 @@ pub enum PsychometricError { /// Driver Eq. 3 process noise was treated as the unconditional latent /// variance. `Q_Δt` is `cov(η_ti | η_{t-1,i})`. ProcessNoiseIsConditionalVariance, + /// Stationary within-subject variance was requested for a non-stable + /// drift. Driver et al. (2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 + /// `asymDIFFUSION`) require `a < 0`. + StationaryVarianceRequiresStableDrift, + /// Finite-interval Driver Eq. 3 process noise was treated as the + /// asymptotic within-subject variance. `Q_Δt` at a finite `Δt` is not + /// `asymDIFFUSION`. + FiniteIntervalProcessNoiseIsNotStationary, } impl fmt::Display for PsychometricError { @@ -87,6 +95,12 @@ impl fmt::Display for PsychometricError { Self::ProcessNoiseIsConditionalVariance => { "discrete process noise is the conditional residual variance, not the unconditional latent variance" } + Self::StationaryVarianceRequiresStableDrift => { + "stationary within-subject variance requires a stable negative drift" + } + Self::FiniteIntervalProcessNoiseIsNotStationary => { + "finite-interval process noise is not the asymptotic within-subject variance" + } }; formatter.write_str(message) } @@ -168,5 +182,13 @@ mod tests { PsychometricError::ProcessNoiseIsConditionalVariance.to_string(), "discrete process noise is the conditional residual variance, not the unconditional latent variance" ); + assert_eq!( + PsychometricError::StationaryVarianceRequiresStableDrift.to_string(), + "stationary within-subject variance requires a stable negative drift" + ); + assert_eq!( + PsychometricError::FiniteIntervalProcessNoiseIsNotStationary.to_string(), + "finite-interval process noise is not the asymptotic within-subject variance" + ); } } diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index dedde63c..4d1cf111 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -18,7 +18,10 @@ //! `Q_Δt = cov(η_ti | η_{t-1,i})` and //! `cov(η_ti, η_{t-1,i}) = A_Δt cov(η_{t-1,i})`. The law of total //! variance on that pair is -//! `Var(η_ti) = A_Δt Var(η_{t-1,i}) A_Δt⊤ + Q_Δt`. The JSS article +//! `Var(η_ti) = A_Δt Var(η_{t-1,i}) A_Δt⊤ + Q_Δt`. As `Δt → ∞` with +//! stable `a < 0`, Eq. 4 and the JSS `asymDIFFUSION` summary (p. 16; +//! §4.3 T0VAR stationarity) give the scalar Lyapunov solution +//! `-q / (2 a)`. The JSS article //! has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). //! The difference quotient `(x(t+Δt) − x(t)) / Δt` (their //! Eqs. 3–4) is refused. This is not DSEM and not a matrix `expm`. @@ -602,6 +605,75 @@ pub fn recover_discrete_latent_variance( require_finite(carried + process_noise) } +/// Exact scalar stationary within-subject variance on event time. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 4, p. 5; JSS PDF re-opened +/// 2026-08-18T18:03Z) write `Q_Δt` as +/// `irow(A#^{-1}[e^{A# Δt} − I] row(Q))` with `A# = A ⊗ I + I ⊗ A`. +/// The scalar Kronecker sum is `2 a`. As `Δt → ∞` with stable +/// `a < 0`, `e^{2 a Δt} → 0` and Eq. 4 becomes `-q / (2 a)`. The +/// JSS summary names that limit `asymDIFFUSION` and takes it as the +/// total within-subject variance (p. 16). Section 4.3 (pp. 9–10) +/// constrains a stationary `T0VAR` to that same model-predicted +/// variance. Form `-0.5 q / a`. Do not form `2 a` first: at +/// `a = -1e308`, `q = 1e308`, `2 a` overflows and `-q / (2 a)` +/// collapses to `+0`, but `-0.5 q / a = 0.5`. A zero diffusion is +/// exactly zero. `a ≥ 0` has no finite stationary variance +/// (including Brownian `a = 0`, whose variance grows as `q Δt`). +/// An overflowing `-0.5 q / a` fails closed. This is not a Kalman +/// filter, not DSEM, not a matrix `expm`, and not ctsem estimation. +/// +/// # Errors +/// +/// Returns [`PsychometricError::EventTimeRequired`] for any non-event +/// clock, [`PsychometricError::StationaryVarianceRequiresStableDrift`] +/// when the log-rate is not strictly negative, and +/// [`PsychometricError::InvalidNumericInput`] when the diffusion is +/// negative or non-finite, the log-rate is non-finite, or the mapped +/// variance is non-finite. +pub fn recover_stationary_latent_variance( + continuous_diffusion: f64, + log_rate: f64, + clock: LagClock, +) -> Result { + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + if !continuous_diffusion.is_finite() || continuous_diffusion < 0.0 || !log_rate.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + if log_rate >= 0.0 { + return Err(PsychometricError::StationaryVarianceRequiresStableDrift); + } + // Driver Eq. 4 as Δt → ∞: (0 − 1) q / (2 a) = −q / (2 a). + // Direct 0 * (1 / (2 a)) is not needed; a zero diffusion is zero. + if continuous_diffusion == 0.0 { + return Ok(0.0); + } + // −q / (2 a) = −0.5 q / a. Do not form 2 a first. + require_finite(-0.5 * continuous_diffusion / log_rate) +} + +/// Refuse treating finite-interval process noise as `asymDIFFUSION`. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 4 and p. 16): `Q_Δt` at a +/// finite event interval is the covariance of the stochastic integral +/// over that interval. The asymptotic within-subject variance is the +/// `Δt → ∞` limit. Section 4.3 distinguishes that stationary +/// constraint from a predetermined `T0VAR`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::FiniteIntervalProcessNoiseIsNotStationary`]. +pub fn refuse_finite_interval_process_noise_as_stationary_variance( + process_noise: f64, + event_delta: f64, +) -> Result { + let _ = (process_noise, event_delta); + Err(PsychometricError::FiniteIntervalProcessNoiseIsNotStationary) +} + /// Refuse treating Driver Eq. 3 process noise as the unconditional variance. /// /// Driver, Oud, and Voelkle (2017, Eq. 3–4, pp. 4–5): @@ -856,7 +928,9 @@ mod tests { recover_discrete_process_noise, recover_discrete_time_varying_predictor_effect, recover_event_series_mean_log_rate, recover_event_time_discrete_lag_and_log_rate, recover_irregular_centered_residual_log_rate, recover_local_log_rate, - recover_within_residual_event_time_log_rate, refuse_difference_quotient_as_local_rate, + recover_stationary_latent_variance, recover_within_residual_event_time_log_rate, + refuse_difference_quotient_as_local_rate, + refuse_finite_interval_process_noise_as_stationary_variance, refuse_pooled_discrete_lag_across_unequal_intervals, refuse_process_noise_as_unconditional_variance, refuse_unmatched_time_varying_predictor_interval, @@ -1524,6 +1598,94 @@ mod tests { ); } + #[test] + fn stationary_variance_recovers_driver_equation_four_asymptote() { + let diffusion = 0.4_f64; + let drift = -0.5_f64; + let recovered = recover_stationary_latent_variance(diffusion, drift, LagClock::EventTime) + .expect("asym"); + let expected = -0.5 * diffusion / drift; + assert!((recovered - expected).abs() < 1e-15); + assert!((recovered - 0.4).abs() < 1e-15); + // Starting from p_∞, Var(η_t) is invariant across finite Δt. + for delta in [0.5_f64, 1.0, 2.0, 10.0] { + let evolved = recover_discrete_latent_variance( + recovered, + diffusion, + drift, + delta, + LagClock::EventTime, + ) + .expect("invariant"); + assert!( + (evolved - recovered).abs() < 1e-12, + "stationary variance must be invariant at Δt={delta}" + ); + } + let finite_noise = + recover_discrete_process_noise(diffusion, drift, 1.0, LagClock::EventTime) + .expect("finite q_dt"); + assert!((finite_noise - recovered).abs() > 1e-3); + assert_eq!( + refuse_finite_interval_process_noise_as_stationary_variance(finite_noise, 1.0), + Err(PsychometricError::FiniteIntervalProcessNoiseIsNotStationary) + ); + assert_eq!( + refuse_finite_interval_process_noise_as_stationary_variance(recovered, 1.0), + Err(PsychometricError::FiniteIntervalProcessNoiseIsNotStationary) + ); + assert_eq!( + recover_stationary_latent_variance(0.0, drift, LagClock::EventTime), + Ok(0.0) + ); + // Do not form 2 a first: 2*(-1e308) overflows; -0.5 q / a is 0.5. + let twice_rate_overflow = + recover_stationary_latent_variance(1e308, -1e308, LagClock::EventTime) + .expect("2a overflow"); + assert!((twice_rate_overflow - 0.5).abs() < 1e-15); + assert!(!(2.0 * -1e308_f64).is_finite()); + let lost = -1e308_f64 / (2.0 * -1e308_f64); + assert!(lost.abs() < 1e-15); + } + + #[test] + fn stationary_variance_unstable_and_invalid_inputs_fail_closed() { + assert_eq!( + recover_stationary_latent_variance(0.4, -0.5, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_stationary_latent_variance(0.4, 0.0, LagClock::EventTime), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + recover_stationary_latent_variance(0.4, 0.5, LagClock::EventTime), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + recover_stationary_latent_variance(0.0, 0.0, LagClock::EventTime), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + recover_stationary_latent_variance(-0.1, -0.5, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_stationary_latent_variance(f64::NAN, -0.5, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_stationary_latent_variance(0.4, f64::NAN, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + // −0.5 q / a overflows when q is huge relative to |a|. + assert!(!(-0.5 * 1e308_f64 / -1e-10_f64).is_finite()); + assert_eq!( + recover_stationary_latent_variance(1e308, -1e-10, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + } + #[test] fn non_event_clocks_and_difference_quotient_fail_closed() { for clock in [ diff --git a/crates/psychometric_core/src/lib.rs b/crates/psychometric_core/src/lib.rs index 99770550..4cd57d09 100644 --- a/crates/psychometric_core/src/lib.rs +++ b/crates/psychometric_core/src/lib.rs @@ -17,7 +17,9 @@ //! sampling and constancy intervals, recovers the exact scalar discrete //! process noise of Driver et al. (2017, Eq. 3), recovers the lagged //! latent covariance and unconditional latent variance licensed by -//! their Eq. 3–4, +//! their Eq. 3–4, recovers the scalar stationary within-subject +//! variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 +//! `asymDIFFUSION`), //! and refuses //! latent-mean comparison below strong invariance. @@ -88,10 +90,14 @@ pub use event_time::recover_event_time_discrete_lag_and_log_rate; pub use event_time::recover_irregular_centered_residual_log_rate; /// Exact scalar inverse `a = ln(φ) / Δt`. pub use event_time::recover_local_log_rate; +/// Exact scalar stationary within-subject variance `-q / (2 a)`. +pub use event_time::recover_stationary_latent_variance; /// CWC-then-event-time local log-rate (not DSEM; not raw-process AR drift). pub use event_time::recover_within_residual_event_time_log_rate; /// Refuse the difference quotient as a continuous-time rate. pub use event_time::refuse_difference_quotient_as_local_rate; +/// Refuse treating finite-interval `Q_Δt` as `asymDIFFUSION`. +pub use event_time::refuse_finite_interval_process_noise_as_stationary_variance; /// Refuse pooling discrete lags from unequal event intervals. pub use event_time::refuse_pooled_discrete_lag_across_unequal_intervals; /// Refuse treating Driver Eq. 3 process noise as the unconditional variance. diff --git a/crates/psychometric_core/tests/esem_input_recovery_contract.rs b/crates/psychometric_core/tests/esem_input_recovery_contract.rs index 6e755cfa..3ca8fe5b 100644 --- a/crates/psychometric_core/tests/esem_input_recovery_contract.rs +++ b/crates/psychometric_core/tests/esem_input_recovery_contract.rs @@ -300,4 +300,12 @@ fn finite_alr_correlation_and_error_messages_are_stable() { PsychometricError::ProcessNoiseIsConditionalVariance.to_string(), "discrete process noise is the conditional residual variance, not the unconditional latent variance" ); + assert_eq!( + PsychometricError::StationaryVarianceRequiresStableDrift.to_string(), + "stationary within-subject variance requires a stable negative drift" + ); + assert_eq!( + PsychometricError::FiniteIntervalProcessNoiseIsNotStationary.to_string(), + "finite-interval process noise is not the asymptotic within-subject variance" + ); } 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 b98fde89..991b6f63 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -10,7 +10,9 @@ use psychometric_core::{ recover_discrete_process_noise, recover_discrete_time_varying_predictor_effect, recover_event_series_mean_log_rate, recover_event_time_discrete_lag_and_log_rate, recover_irregular_centered_residual_log_rate, recover_kish_weighted_slope, - recover_within_residual_event_time_log_rate, refuse_difference_quotient_as_local_rate, + recover_stationary_latent_variance, recover_within_residual_event_time_log_rate, + refuse_difference_quotient_as_local_rate, + refuse_finite_interval_process_noise_as_stationary_variance, refuse_pooled_discrete_lag_across_unequal_intervals, refuse_process_noise_as_unconditional_variance, refuse_unmatched_time_varying_predictor_interval, @@ -643,6 +645,63 @@ fn discrete_latent_variance_recovers_driver_equations_three_and_four() { ); } +#[test] +fn stationary_variance_recovers_driver_equation_four_asymptote() { + let diffusion = 0.4_f64; + let drift = -0.5_f64; + let stationary = + recover_stationary_latent_variance(diffusion, drift, LagClock::EventTime).expect("asym"); + let expected = -0.5 * diffusion / drift; + let error = rmse(&[expected], &[stationary]); + assert!(error < 1e-15, "Driver Eq. 4 asymDIFFUSION RMSE {error}"); + for delta in [0.5_f64, 1.0, 2.0, 10.0] { + let evolved = recover_discrete_latent_variance( + stationary, + diffusion, + drift, + delta, + LagClock::EventTime, + ) + .expect("invariant"); + let evolved_error = rmse(&[stationary], &[evolved]); + assert!( + evolved_error < 1e-12, + "stationary variance must be invariant at Δt={delta}: RMSE {evolved_error}" + ); + } + let finite_noise = + recover_discrete_process_noise(diffusion, drift, 1.0, LagClock::EventTime).expect("q_dt"); + let collapsed = rmse(&[stationary], &[finite_noise]); + assert!( + collapsed > error, + "finite-Δt Q_Δt is not asymDIFFUSION: collapsed RMSE {collapsed} must exceed {error}" + ); + assert_eq!( + refuse_finite_interval_process_noise_as_stationary_variance(finite_noise, 1.0), + Err(PsychometricError::FiniteIntervalProcessNoiseIsNotStationary) + ); + assert_eq!( + recover_stationary_latent_variance(diffusion, 0.0, LagClock::EventTime), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + recover_stationary_latent_variance(diffusion, 0.5, LagClock::EventTime), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + recover_stationary_latent_variance(1e308, -1e308, LagClock::EventTime), + Ok(0.5) + ); + assert_eq!( + recover_stationary_latent_variance(diffusion, drift, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_stationary_latent_variance(1e308, -1e-10, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); +} + #[test] fn admitted_coordinates_still_required_for_multilevel_weights() { assert_eq!( diff --git a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs index 21b92b38..f5a1356d 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -7,7 +7,9 @@ use psychometric_core::{ recover_discrete_lagged_latent_covariance, recover_discrete_latent_variance, recover_discrete_process_noise, recover_discrete_time_varying_predictor_effect, recover_irregular_centered_residual_log_rate, recover_loading_point_estimate_mean, - recover_within_residual_event_time_log_rate, refuse_process_noise_as_unconditional_variance, + recover_stationary_latent_variance, recover_within_residual_event_time_log_rate, + refuse_finite_interval_process_noise_as_stationary_variance, + refuse_process_noise_as_unconditional_variance, }; #[test] @@ -240,3 +242,30 @@ fn process_noise_is_not_the_unconditional_latent_variance() { Err(psychometric_core::PsychometricError::ProcessNoiseIsConditionalVariance) ); } + +#[test] +fn finite_interval_process_noise_is_not_the_stationary_variance() { + let diffusion = 0.4_f64; + let drift = -0.5_f64; + let delta = 1.0_f64; + let process_noise = + recover_discrete_process_noise(diffusion, drift, delta, LagClock::EventTime).expect("q_dt"); + let stationary = + recover_stationary_latent_variance(diffusion, drift, LagClock::EventTime).expect("asym"); + assert!( + (process_noise - stationary).abs() > 1e-3, + "Driver et al. (2017, Eq. 4 / p. 16): finite-Δt Q_Δt is not asymDIFFUSION" + ); + let evolved = + recover_discrete_latent_variance(stationary, diffusion, drift, delta, LagClock::EventTime) + .expect("invariant"); + assert!((evolved - stationary).abs() < 1e-12); + assert_eq!( + refuse_finite_interval_process_noise_as_stationary_variance(process_noise, delta), + Err(psychometric_core::PsychometricError::FiniteIntervalProcessNoiseIsNotStationary) + ); + assert_eq!( + recover_stationary_latent_variance(diffusion, 0.0, LagClock::EventTime), + Err(psychometric_core::PsychometricError::StationaryVarianceRequiresStableDrift) + ); +} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 052ac630..29b6aec5 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -28,7 +28,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | report template/section/copied/style/modality method effects | ADR 0004/0012; PRD/TRD | simulation truth factors implemented; estimator-side method model remains future | partial | | candidate K statistical/Pareto gates + blinded LLM review | ADR 0012; research | future `model_selection` | accepted-target | | compositional topic correlation / stable clustering | ADR 0005/0012; research | future `network_analysis` | accepted-target | -| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR; full ESEM/DSEM remaining | partial | +| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR; full ESEM/DSEM remaining | partial | | CPU bounded multithreading + GPU/VRAM streaming/parity | ADR 0001/0006 | future `compute_backend` | accepted-target | | TDT detection/tracking vs CHRONOS schema/prediction/temporal consistency | ADR 0016; PRD/research | future `event_intelligence` | accepted-target | | evidence-bounded LLM interpretation | ADR 0010/0012; PRD | future `interpretation_gateway` | accepted-target | diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index b1d6abbd..ba862db3 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -1,7 +1,7 @@ # ADR 0005 — Posterior-aware ESEM/DSEM and structural interpretation **Decision status:** Accepted -**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target +**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target **Date:** 2026-08-05 **Supersedes:** None. ADR 0012 governs upstream topic measurement/network coordinates; this ADR governs higher-order psychometric structure and longitudinal interpretation. @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T14:04Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; form `-0.5 q / a`; do not form `2 a` first; `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. 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/adr/README.md b/docs/adr/README.md index 447eb77e..35e91b32 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -10,7 +10,7 @@ Read [`ADR_POLICY.md`](ADR_POLICY.md) first. **Decision status and implementatio | [0002](0002-six-clock-temporal-semantics.md) | Six-clock temporal semantics and fail-closed historical leakage prevention | Accepted | active-PR | Unmerged PR #8 is the canonical Task 3 replacement implementing typed clocks/intervals against the current protected-main lineage; conflicted PR #5 is superseded lineage. Later graph/split enforcement remains target work. | | [0003](0003-relational-event-multiple-membership.md) | Relational event ontology and time-varying cross-classified multiple membership | Accepted | partial | Weighted time-varying membership network/roles are active-PR (PR #12); full multilevel estimators, graph ontology, and persistence remain accepted-target. ADR 0016 owns event-intelligence tasks. | | [0004](0004-shared-multilingual-latent-space.md) | One shared multilingual latent space with explicit invariance status | Accepted | accepted-target | ADR 0012 owns the full topic-estimator/backend/global-topic contract. | -| [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | partial | Input gates, posterior-draw point estimates, Rubin `T` on OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects, exact scalar discrete process noise, lagged latent covariance and unconditional latent variance, irregular already-centered residual lag, and strong-gated latent means are on the stacked psychometric PR; full ESEM/DSEM estimator remains accepted-target. | +| [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | partial | Input gates, posterior-draw point estimates, Rubin `T` on OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects, exact scalar discrete process noise, lagged latent covariance and unconditional latent variance, stationary within-subject variance (`asymDIFFUSION`), irregular already-centered residual lag, and strong-gated latent means are on the stacked psychometric PR; full ESEM/DSEM estimator remains accepted-target. | | [0006](0006-vram-gpu-nvidia-orchestration.md) | VRAM-adaptive GPU compute and model-credential boundary | Accepted | accepted-target | LLM orchestration policy superseded by ADR 0010; autonomous development authority governed by ADR 0015. | | [0007](0007-rust-workspace-quality-gates.md) | Explicit Rust workspace, pinned toolchains, and exact quality gates | Accepted | implemented-main | ADR 0014 governs scientific/product claim promotion beyond repository-quality tooling. | | [0008](0008-immutable-evidence-identities-digests-and-spans.md) | Immutable evidence identities, `SHA-256` digests, exact spans, and strict wire reconstruction | Accepted | implemented-main | ADR 0013 governs future persistence/reproducibility/split authority. | diff --git a/docs/research/multilevel-event-time-recovery.md b/docs/research/multilevel-event-time-recovery.md index fc3e63d3..e2cb3139 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -15,15 +15,16 @@ This slice stays inside `psychometric_core`. It does not add a second invariance 9. recover the exact scalar discrete process noise of Driver, Oud, and Voelkle (2017, Eq. 3); 10. recover the exact scalar lagged latent covariance `A_Δt cov(η_{t-1})` (Driver et al., 2017, Eq. 3–4); 11. recover the exact scalar discrete latent variance `A_Δt P A_Δt⊤ + Q_Δt` and refuse `Q_Δt` as that unconditional variance; -12. refuse pooling discrete lags from unequal event intervals as one coefficient; -13. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); -14. refuse the difference quotient as a continuous-time rate; -15. apply the same event-time map to CWC residuals (still not DSEM); -16. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). +12. recover the exact scalar stationary within-subject variance as the `Δt → ∞` limit of Eq. 4 (`asymDIFFUSION`; §4.3 T0VAR stationarity) and refuse finite-interval `Q_Δt` as that limit; +13. refuse pooling discrete lags from unequal event intervals as one coefficient; +14. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); +15. refuse the difference quotient as a continuous-time rate; +16. apply the same event-time map to CWC residuals (still not DSEM); +17. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). ## Claim boundary -This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). +This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. ## Authoritative sources @@ -41,7 +42,7 @@ Kish, L. (1965). *Survey sampling*. John Wiley & Sons. Oud, J. H. L., & Jansen, R. A. R. G. (2000). Continuous time state space modeling of panel data by means of SEM. *Psychometrika, 65*(2), 199–215. https://doi.org/10.1007/BF02294374 (cited by Voelkle et al., 2012, Eq. 14 discussion; PDF not opened). -The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-17T14:20Z (https://www.zora.uzh.ch/handle/20.500.14742/72792; bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`). Page 16 writes discrete auto-effects as \(A^{*}(\Delta t)=\exp(A\Delta t)\) (Eq. 7). Introducing Intercepts (manuscript p. 20, Eq. 12) adds a continuous-time intercept \(b\) and writes the expected-value solution whose discrete increment is \(A^{-1}(\exp(A\Delta t)-I)b\); the scalar constant-predictor effect is \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). The next paragraph (manuscript p. 21, Eq. 14) writes the discrete effect of a **time-varying** predictor, when the sampling interval equals the constancy interval, as \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product does not depend on the predictor auto-effect. The manuscript calls Eq. 14 a first-order approximation that deteriorates as \(\Delta t\) grows, and defers unmatched sampling/constancy intervals to Oud and Jansen (2000), which is unread. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4; JSS PDF re-opened 2026-08-18T11:05Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\), the discrete intercept \(b_{\Delta t}=A^{-1}[A_{\Delta t}-I]b\), and the discrete process-noise covariance \(Q_{\Delta t}=\int_{0}^{\Delta t}\operatorname{expm}(A(\Delta t-\tau))LGG^{\top}L^{\top}\operatorname{expm}(A(\Delta t-\tau))^{\top}\,d\tau\). Equation 4 (p. 5) writes that the integral exhibits covariance \(Q_{\Delta t}\). The homogeneous-process consequence (`ξ`, `z` given) is \(Q_{\Delta t}=\operatorname{cov}(\eta_{ti}\mid\eta_{t-1,i})\) and \(\operatorname{cov}(\eta_{ti},\eta_{t-1,i})=A_{\Delta t}\operatorname{cov}(\eta_{t-1,i})\). The law of total variance on that pair is \(\operatorname{Var}(\eta_{ti})=A_{\Delta t}\operatorname{Var}(\eta_{t-1,i})A_{\Delta t}^{\top}+Q_{\Delta t}\). The JSS article has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). This slice takes scalar \(L=1\) (every latent subject to system noise); it does not implement the 0/1 selector matrix and is not a Kalman filter. Discrete auto-effects are strictly positive for finite real drift and interval; the inverse \(a=\ln\varphi/\Delta t\) therefore requires \(\varphi>0\). Binary64 `exp` of a large negative argument is `+0` and is refused as a discrete lag; the same underflow is a vanishing lagged covariance and is kept. Equations 3–4 of Voelkle et al. (2012) are the discouraged difference-quotient approximation. Meredith (1993) remains unread (Unpaywall/OpenAlex 2026-08-18T11:05Z: closed; Springer `content/pdf` is HTML 200, not a PDF; archive.org title search empty). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex 2026-08-18T11:05Z: closed; Springer `content/pdf` is HTML 200; ETS landing page is HTML). ERIC ED334221 is Singer and Willett (1991), *From whether to when*, not the 1991 journal article. ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title is not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex 2026-08-18T11:05Z: closed; Radboud landing and bitstream 403). The Voelkle et al. (2012) ZORA bitstream was Anubis-blocked this cycle. +The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-17T14:20Z (https://www.zora.uzh.ch/handle/20.500.14742/72792; bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`). Page 16 writes discrete auto-effects as \(A^{*}(\Delta t)=\exp(A\Delta t)\) (Eq. 7). Introducing Intercepts (manuscript p. 20, Eq. 12) adds a continuous-time intercept \(b\) and writes the expected-value solution whose discrete increment is \(A^{-1}(\exp(A\Delta t)-I)b\); the scalar constant-predictor effect is \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). The next paragraph (manuscript p. 21, Eq. 14) writes the discrete effect of a **time-varying** predictor, when the sampling interval equals the constancy interval, as \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product does not depend on the predictor auto-effect. The manuscript calls Eq. 14 a first-order approximation that deteriorates as \(\Delta t\) grows, and defers unmatched sampling/constancy intervals to Oud and Jansen (2000), which is unread. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4; JSS PDF re-opened 2026-08-18T18:03Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\), the discrete intercept \(b_{\Delta t}=A^{-1}[A_{\Delta t}-I]b\), and the discrete process-noise covariance \(Q_{\Delta t}=\int_{0}^{\Delta t}\operatorname{expm}(A(\Delta t-\tau))LGG^{\top}L^{\top}\operatorname{expm}(A(\Delta t-\tau))^{\top}\,d\tau\). Equation 4 (p. 5) writes that the integral exhibits covariance \(Q_{\Delta t}=\operatorname{irow}(A^{\#^{-1}}[e^{A^{\#}\Delta t}-I]\operatorname{row}(Q))\) with \(A^{\#}=A\otimes I+I\otimes A\). The homogeneous-process consequence (`ξ`, `z` given) is \(Q_{\Delta t}=\operatorname{cov}(\eta_{ti}\mid\eta_{t-1,i})\) and \(\operatorname{cov}(\eta_{ti},\eta_{t-1,i})=A_{\Delta t}\operatorname{cov}(\eta_{t-1,i})\). The law of total variance on that pair is \(\operatorname{Var}(\eta_{ti})=A_{\Delta t}\operatorname{Var}(\eta_{t-1,i})A_{\Delta t}^{\top}+Q_{\Delta t}\). As \(\Delta t\to\infty\) with stable \(a<0\), Eq. 4 becomes \(-q/(2a)\). The JSS summary names that limit `asymDIFFUSION` and takes it as the total within-subject variance (p. 16). Section 4.3 (pp. 9–10) constrains a stationary `T0VAR` to that model-predicted variance and distinguishes it from a predetermined first occasion. The JSS article has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). This slice takes scalar \(L=1\) (every latent subject to system noise); it does not implement the 0/1 selector matrix and is not a Kalman filter. Discrete auto-effects are strictly positive for finite real drift and interval; the inverse \(a=\ln\varphi/\Delta t\) therefore requires \(\varphi>0\). Binary64 `exp` of a large negative argument is `+0` and is refused as a discrete lag; the same underflow is a vanishing lagged covariance and is kept. Equations 3–4 of Voelkle et al. (2012) are the discouraged difference-quotient approximation. Meredith (1993) remains unread (Unpaywall/OpenAlex 2026-08-18T18:03Z: closed; Springer `content/pdf` is HTML 200, not a PDF; archive.org title search empty). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex 2026-08-18T18:03Z: closed; Springer `content/pdf` is HTML 200; ETS landing page is HTML). ERIC ED334221 is Singer and Willett (1991), *From whether to when*, not the 1991 journal article. ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title is not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex 2026-08-18T18:03Z: closed; Radboud landing and bitstream 403). The Voelkle et al. (2012) ZORA bitstream was Anubis-blocked this cycle. ## Formula notes @@ -55,6 +56,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-17T14:2 - **Discrete process noise.** Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-17T21:03Z, p. 4): \(Q_{\Delta t}=\int_{0}^{\Delta t}\operatorname{expm}(A(\Delta t-\tau))GG^{\top}\operatorname{expm}(A(\Delta t-\tau))^{\top}\,d\tau\). The scalar closed form with continuous diffusion \(q=GG^{\top}\ge 0\) is \(q(\mathrm{e}^{2a\Delta t}-1)/(2a)\) for \(a\neq 0\) and \(q\Delta t\) for \(a=0\). The algebraically identical finite-`expm1` evaluation is \(0.5 q(\operatorname{expm1}(z)/a)\) with \(z=2(a\Delta t)\). Form \(z\) as twice the product \(a\Delta t\). Forming \(2a\) first overflows when \(|a|\) is at the binary64 extreme even if \(a\Delta t\) and \(Q_{\Delta t}\) are finite. Binary64 underflow of \(z\) to `+0` recovers \(q\Delta t\). \(z\to-\infty\) keeps the equilibrium variance \(-q/(2a)=-0.5 q/a\); `expm1(−∞)` is \(-1\), so that path stays on the finite increment. When `expm1(z)` overflows at a finite \(z\), rewrite as \(\operatorname{sign}(q/a)\exp(\ln|q|+z-\ln|a|-\ln 2)-0.5 q/a\). An overflowing rewrite scale \(0.5 q/a\) is not a finite \(Q_{\Delta t}\) and fails closed (`q=1e308`, `a=0.1`, `Δt=4000` → `z=800`; JSS PDF re-opened 2026-08-18T03:07Z, p. 4). A zero diffusion is exactly zero even if `expm1` overflows. \(z\to+\infty\) fails closed unless \(q=0\). Negative \(q\) fails closed. This is not a Kalman filter and not a matrix `expm`. - **Lagged latent covariance.** Driver, Oud, and Voelkle (2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T11:20Z): \(\operatorname{cov}(\eta_{ti},\eta_{t-1,i})=A_{\Delta t}\operatorname{cov}(\eta_{t-1,i})\) for the homogeneous process. The scalar map is \(\mathrm{e}^{a\Delta t}p\) with prior variance \(p\ge 0\). This is not \(Q_{\Delta t}\). Binary64 underflow of \(\mathrm{e}^{a\Delta t}\) to `+0` is a vanishing covariance and is kept. A zero prior variance is exactly zero. Finite-\(a\Delta t\) exponential overflow rewrites as \(\exp(\ln p+a\Delta t)\). A finite \(\mathrm{e}^{a\Delta t}\) whose product with \(p\) overflows fails closed. The JSS article has no numbered §2.2. - **Discrete latent variance.** Equations 3–4 write \(Q_{\Delta t}\) as the covariance of the stochastic integral, so \(Q_{\Delta t}=\operatorname{cov}(\eta_{ti}\mid\eta_{t-1,i})\) when \(\xi\) and \(z\) are given. The law of total variance on that pair is \(\operatorname{Var}(\eta_{ti})=A_{\Delta t}\operatorname{Var}(\eta_{t-1,i})A_{\Delta t}^{\top}+Q_{\Delta t}\). The scalar map is \(\mathrm{e}^{2a\Delta t}p+Q_{\Delta t}\). This is not a Kalman measurement update. A zero prior variance is exactly \(Q_{\Delta t}\). Binary64 underflow of \(\mathrm{e}^{2a\Delta t}\) keeps \(Q_{\Delta t}\). Finite-\(z\) exponential overflow rewrites as \(\exp(\ln p+z)+Q_{\Delta t}\). A zero diffusion is exactly \(Q_{\Delta t}=0\); that skip does not license a non-finite carried term when \(2(a\Delta t)\) overflows to \(+\infty\). Treating \(Q_{\Delta t}\) as \(\operatorname{Var}(\eta_{t})\) fails closed. +- **Stationary within-subject variance.** Driver et al. (2017, Eq. 4 as \(\Delta t\to\infty\); §4.3 pp. 9–10; p. 16 `asymDIFFUSION`): for stable \(a<0\), \(\lim_{\Delta t\to\infty}Q_{\Delta t}=-q/(2a)\). The algebraically identical evaluation is \(-0.5 q/a\). Forming \(2a\) first overflows when \(|a|\) is at the binary64 extreme (`a=-1e308`, `q=1e308` → `0.5`). Starting from that variance, \(\operatorname{Var}(\eta_{t})\) is invariant across finite event intervals. A zero diffusion is exactly zero. \(a\ge 0\) has no finite stationary variance (Brownian \(a=0\) grows as \(q\Delta t\)). An overflowing \(-0.5 q/a\) fails closed. Finite-interval \(Q_{\Delta t}\) is not that limit. This is not ctsem estimation and not a Kalman filter. - **CWC-then-lag.** Sample cluster means are removed first. Consecutive within residuals are then fitted by least squares to \(r_{t+\Delta t}\approx\exp(a\Delta t)\,r_{t}\) on event time. Same-sign pair-wise logs initialize the scalar Newton step. Sign-flipping \(T=2\) CWC pairs have no real logarithm and fail closed. Curran and Bauer (2011, pp. 607–608) show that this person-mean subtraction on a raw autoregressive series does **not** isolate the lagged within-person effect; the helper therefore does not claim to recover the raw-process drift. - **Already-centered irregular residual.** The caller supplies lagged within residuals. The mean of \(a=\ln(r_{t+\Delta t}/r_t)/\Delta t\) is the exact scalar map. Intervals may be irregular. The helper does not center again. This is not DSEM. @@ -70,6 +72,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-17T14:2 - Voelkle et al. (2012, Eq. 14) recovers \(a_{yx}\Delta t\) at machine-scale RMSE when sampling, constancy, and event intervals match, and that value is not the Eq. 12 constant-predictor effect; unmatched intervals, a non-event clock, a non-finite continuous effect, and an overflowing product fail closed; - Driver et al. (2017, Eq. 3) recovers a known scalar \(Q_{\Delta t}\) at machine-scale RMSE, and that RMSE is smaller than treating the continuous diffusion as the discrete process noise; \(a=0\) and binary64 underflow of \(2a\Delta t\) recover \(q\Delta t\); \(z\to-\infty\) recovers \(-q/(2a)\); a finite result whose `expm1` overflows at a finite \(z\) is recovered in log space; a finite result whose \(2a\) overflows while \(a\Delta t\) stays finite is recovered as \(0.5 q(\operatorname{expm1}(z)/a)\); a zero diffusion is exactly zero; a negative diffusion, \(z\to+\infty\), and an overflowing rewrite scale \(0.5 q/a\) fail closed; non-finite event interval, non-positive sampling interval, and non-finite constancy interval fail closed on Eq. 14; non-finite / non-positive event interval, non-finite / negative diffusion, and non-finite log-rate fail closed on Eq. 3; - Driver et al. (2017, Eq. 3–4) recovers a known lagged covariance \(\mathrm{e}^{a\Delta t}p\) and a known latent variance \(\mathrm{e}^{2a\Delta t}p+Q_{\Delta t}\) at machine-scale RMSE, and that variance RMSE is smaller than treating \(Q_{\Delta t}\) as \(\operatorname{Var}(\eta_{t})\); a zero prior variance is exactly zero (lagged) or \(Q_{\Delta t}\) (unconditional); binary64 underflow of \(\mathrm{e}^{a\Delta t}\) keeps a vanishing lagged covariance; a finite \(\mathrm{e}^{a\Delta t}\) whose product with \(p\) overflows fails closed; a zero diffusion whose \(2(a\Delta t)\) overflows to \(+\infty\) fails closed; treating \(Q_{\Delta t}\) as the unconditional variance fails closed; +- Driver et al. (2017, Eq. 4 as \(\Delta t\to\infty\); p. 16 `asymDIFFUSION`; §4.3) recovers a known stationary variance \(-q/(2a)\) at machine-scale RMSE, and that RMSE is smaller than treating finite-interval \(Q_{\Delta t}\) as the stationary variance; starting from that variance, \(\operatorname{Var}(\eta_{t})\) is invariant across finite \(\Delta t\); a zero diffusion is exactly zero; forming \(2a\) first overflows while \(-0.5 q/a\) stays finite; \(a\ge 0\), a non-event clock, a negative diffusion, and an overflowing \(-0.5 q/a\) fail closed; - pooling discrete lags from unequal intervals fails closed; - CWC-then-lag on a two-cluster decaying series has smaller computed RMSE than a level-pooled series when the latter is identified; - already-centered irregular residuals recover a known drift at machine-scale RMSE, and that RMSE is smaller than CWC of the corresponding raw autoregressive series (Curran & Bauer, 2011, pp. 607–608); diff --git a/docs/research/standards-and-literature.md b/docs/research/standards-and-literature.md index 19db025e..b80317d5 100644 --- a/docs/research/standards-and-literature.md +++ b/docs/research/standards-and-literature.md @@ -34,7 +34,7 @@ Meredith, W. (1993). Measurement invariance, factor analysis and factorial invar Holland, P. W. (1986). Statistics and causal inference. *Journal of the American Statistical Association, 81*(396), 945–960. https://doi.org/10.1080/01621459.1986.10478354 -TEPP applies these sources to construct definition, score interpretation, reliability, validity evidence, uncertainty, consequences, longitudinal invariance, ESEM cross-loadings, and DSEM. Topic outputs are treated as fallible indicators or components only after their construct role is evaluated. Reflective, formative, and network classes remain distinct (Bollen & Lennox, 1991). Complete-data OLS loadings across posterior indicator draws are combined with Rubin (1996) \(T_m\); the arithmetic-mean helper remains a point estimate. Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-18T03:07Z: closed). The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title was opened 2026-08-17T12:04Z from archive.org; it is not the 1991 journal article and is not used as Mislevy plausible-value authority. Temporal precedence is not causal identification (Holland, 1986). Within/between OLS follows Enders and Tofighi (2007), Curran and Bauer (2011), and Hamaker et al. (2015). Enders and Tofighi (2007, Table 2, pp. 124–127; PDF opened 2026-08-17) show that the CWC cluster-mean coefficient is the contextual effect (`between − within`), not the between-cluster effect. Curran and Bauer (2011, pp. 607–608) reject person-mean subtraction on a raw autoregressive series as the lagged within-person residual; already-centered irregular residuals use the Voelkle et al. (2012, Eq. 7) / Driver et al. (2017, Eq. 3) scalar map. Discrete lags from unequal event intervals are remapped through that log-rate (Voelkle et al., 2012, ZORA accepted manuscript re-opened 2026-08-17T13:13Z) and are not pooled. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\) and restate the discrete intercept as a function of \(A\) and \(\Delta t\). A binary64 underflow of \(\exp(a\Delta t)\) to `+0` is refused because discrete auto-effects are strictly positive. The discrete effect of a constant predictor is Voelkle et al. (2012, Eq. 12; ZORA accepted manuscript re-opened 2026-08-17T14:20Z, Introducing Intercepts, manuscript p. 20), evaluated as \(a_{yx}(\operatorname{expm1}(z)/a_{xx})\) with \(z=a_{xx}\Delta t\) so a finite result is not lost when \(z\) overflows to \(-\infty\) or when \(a_{yx}\Delta t\) overflows, and in log space when `expm1(z)` overflows at a finite \(z\); a zero continuous effect is exactly zero; an overflowing \(a_{yx}/a_{xx}\) rewrite term fails closed; the first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. The discrete effect of a time-varying predictor with matched sampling and constancy intervals is Voelkle et al. (2012, Eq. 14; manuscript p. 21): \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product is not Eq. 12. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). The exact scalar discrete process noise is Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-18T14:04Z, p. 4): \(Q_{\Delta t}=0.5 q(\operatorname{expm1}(z)/a)\) with \(z=2(a\Delta t)\) for \(a\neq 0\) and \(q=GG^{\top}\ge 0\); do not form \(2a\) first; \(a=0\) recovers \(q\Delta t\); an overflowing rewrite scale \(0.5 q/a\) fails closed. This is not a Kalman filter. Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-18T14:04Z) write the same discrete intercept as \(A^{-1}[e^{A\Delta t}-I]\xi\). The lagged covariance is \(\mathrm{e}^{a\Delta t}p\) and the unconditional variance is \(\mathrm{e}^{2a\Delta t}p+Q_{\Delta t}\) (Driver et al., 2017, Eq. 3–4, pp. 4–5); a zero diffusion whose \(2(a\Delta t)\) overflows to \(+\infty\) fails closed. Metric/weak invariance does not license latent-mean comparison. Meredith (1993) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-18T14:04Z: closed; Springer `content/pdf` is HTML 200). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-18T14:04Z: closed). ERIC ED334221 is Singer and Willett (1991), not the 1991 journal article. ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-17T21:03Z: closed). +TEPP applies these sources to construct definition, score interpretation, reliability, validity evidence, uncertainty, consequences, longitudinal invariance, ESEM cross-loadings, and DSEM. Topic outputs are treated as fallible indicators or components only after their construct role is evaluated. Reflective, formative, and network classes remain distinct (Bollen & Lennox, 1991). Complete-data OLS loadings across posterior indicator draws are combined with Rubin (1996) \(T_m\); the arithmetic-mean helper remains a point estimate. Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-18T03:07Z: closed). The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title was opened 2026-08-17T12:04Z from archive.org; it is not the 1991 journal article and is not used as Mislevy plausible-value authority. Temporal precedence is not causal identification (Holland, 1986). Within/between OLS follows Enders and Tofighi (2007), Curran and Bauer (2011), and Hamaker et al. (2015). Enders and Tofighi (2007, Table 2, pp. 124–127; PDF opened 2026-08-17) show that the CWC cluster-mean coefficient is the contextual effect (`between − within`), not the between-cluster effect. Curran and Bauer (2011, pp. 607–608) reject person-mean subtraction on a raw autoregressive series as the lagged within-person residual; already-centered irregular residuals use the Voelkle et al. (2012, Eq. 7) / Driver et al. (2017, Eq. 3) scalar map. Discrete lags from unequal event intervals are remapped through that log-rate (Voelkle et al., 2012, ZORA accepted manuscript re-opened 2026-08-17T13:13Z) and are not pooled. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\) and restate the discrete intercept as a function of \(A\) and \(\Delta t\). A binary64 underflow of \(\exp(a\Delta t)\) to `+0` is refused because discrete auto-effects are strictly positive. The discrete effect of a constant predictor is Voelkle et al. (2012, Eq. 12; ZORA accepted manuscript re-opened 2026-08-17T14:20Z, Introducing Intercepts, manuscript p. 20), evaluated as \(a_{yx}(\operatorname{expm1}(z)/a_{xx})\) with \(z=a_{xx}\Delta t\) so a finite result is not lost when \(z\) overflows to \(-\infty\) or when \(a_{yx}\Delta t\) overflows, and in log space when `expm1(z)` overflows at a finite \(z\); a zero continuous effect is exactly zero; an overflowing \(a_{yx}/a_{xx}\) rewrite term fails closed; the first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. The discrete effect of a time-varying predictor with matched sampling and constancy intervals is Voelkle et al. (2012, Eq. 14; manuscript p. 21): \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product is not Eq. 12. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). The exact scalar discrete process noise is Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-18T14:04Z, p. 4): \(Q_{\Delta t}=0.5 q(\operatorname{expm1}(z)/a)\) with \(z=2(a\Delta t)\) for \(a\neq 0\) and \(q=GG^{\top}\ge 0\); do not form \(2a\) first; \(a=0\) recovers \(q\Delta t\); an overflowing rewrite scale \(0.5 q/a\) fails closed. This is not a Kalman filter. Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-18T14:04Z) write the same discrete intercept as \(A^{-1}[e^{A\Delta t}-I]\xi\). The lagged covariance is \(\mathrm{e}^{a\Delta t}p\) and the unconditional variance is \(\mathrm{e}^{2a\Delta t}p+Q_{\Delta t}\) (Driver et al., 2017, Eq. 3–4, pp. 4–5); a zero diffusion whose \(2(a\Delta t)\) overflows to \(+\infty\) fails closed. The stationary within-subject variance is the \(\Delta t\to\infty\) limit of Eq. 4: \(-q/(2a)\) for stable \(a<0\) (JSS p. 16 `asymDIFFUSION`; §4.3; PDF re-opened 2026-08-18T18:03Z). Finite-interval \(Q_{\Delta t}\) is not that limit. Metric/weak invariance does not license latent-mean comparison. Meredith (1993) remains unread (Unpaywall/OpenAlex 2026-08-18T18:03Z: closed; Springer `content/pdf` is HTML 200). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex 2026-08-18T18:03Z: closed). ERIC ED334221 is Singer and Willett (1991), not the 1991 journal article. ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex 2026-08-18T18:03Z: closed). ## Structural, correlated, dynamic, relational, and multilingual topic models From 556e23d5a14a519d35feed2d5ef31117761726e6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 21:13:33 +0000 Subject: [PATCH 24/62] =?UTF-8?q?feat(psychometric):=20recover=20Driver=20?= =?UTF-8?q?=C2=A74.3=20trait-plus-state=20variance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Driver, Oud, and Voelkle (2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z). A stable trait has DRIFT and DIFFUSION fixed to zero, so Var = trait + state and cov(t, t-1) = trait + exp(a Δt) p. The ctsem TRAITVAR rewrite that adds the trait to DIFFUSION does not license treating trait variance as process noise. Trait variance is not asymDIFFUSION. Evolving the summed variance as if it were all state is not this map. Still not RI-CLPM, not a Kalman filter, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991) remain unread. --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 1 + CLAUDE.md | 2 +- crates/psychometric_core/src/error.rs | 21 ++ crates/psychometric_core/src/event_time.rs | 234 +++++++++++++++++- crates/psychometric_core/src/lib.rs | 12 +- ...multilevel_event_time_recovery_contract.rs | 80 +++++- .../scientific_claim_boundary_contract.rs | 42 +++- docs/TRACEABILITY.md | 2 +- docs/adr/0005-posterior-esem-dsem.md | 4 +- docs/adr/README.md | 2 +- .../multilevel-event-time-recovery.md | 17 +- docs/research/standards-and-literature.md | 2 +- docs/validation/temporal-event-foundation.md | 2 +- 14 files changed, 400 insertions(+), 23 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index ad608914..b7e195e4 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -61,7 +61,7 @@ boundaries above remain the target modular MSA architecture. | `tepp_simulation` | known-truth temporal/event data generation | | `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics | | `tepp_api` | versioned DTO, schema, and export contracts | -| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means | +| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means | No crate exposes placeholder production behavior in Task 1. This prevents an empty façade from becoming a de facto public API before its invariants and tests diff --git a/CHANGELOG.md b/CHANGELOG.md index e5261f11..19011f87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z) scalar trait-plus-state latent variance and lagged covariance. A stable trait process has `DRIFT` and `DIFFUSION` fixed to zero, so `Var = trait + state` and `cov(t, t-1) = trait + exp(a Δt) p`. The ctsem `TRAITVAR` rewrite that adds the trait to `DIFFUSION` does not license treating trait variance as process noise. Trait variance is not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not this map. Still not RI-CLPM, not a Kalman filter, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall/OpenAlex 2026-08-18T21:07Z: closed). Oud and Jansen (2000) remains unread. ZORA accepted manuscript re-opened via bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`. - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 4, p. 5; §4.3 pp. 9–10; p. 16 `asymDIFFUSION`; JSS PDF re-opened 2026-08-18T18:03Z) scalar stationary within-subject variance. Eq. 4 writes `Q_Δt = irow(A#^{-1}[e^{A# Δt} − I] row(Q))` with `A# = A ⊗ I + I ⊗ A`. The scalar Kronecker sum is `2 a`. As `Δt → ∞` with stable `a < 0`, that limit is `-q / (2 a)`. Form `-0.5 q / a`; do not form `2 a` first (`a = -1e308`, `q = 1e308` → `0.5`). Starting from that variance, `Var(η_t)` is invariant across finite event intervals. A zero diffusion is exactly zero. `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not `asymDIFFUSION`. Still not a Kalman filter, not DSEM, not a matrix `expm`, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall/OpenAlex 2026-08-18T18:03Z: closed). Oud and Jansen (2000) remains unread. ZORA Anubis-blocked. - `psychometric_core` refuses a zero-diffusion Driver, Oud, and Voelkle (2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T14:04Z) latent variance when `2 (a Δt)` overflows to `+∞`. Zero diffusion is exactly `Q_Δt = 0` (Eq. 3 integral of a zero `G`). That skip does not license `Var(η_t) = exp(2 a Δt) p + 0` when the carried term is non-finite (`p = 2`, `q = 0`, `a = 1e308`, `Δt = 2`). Nightly uncovered production line `event_time.rs:596` on predecessor `321568a` is this arm. Still not a Kalman filter, not DSEM, and not a matrix `expm`. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-18T14:04Z: closed; Springer `content/pdf` is HTML 200). Oud and Jansen (2000) remains unread. - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T11:20Z) lagged latent covariance `cov(η_ti, η_{t-1,i}) = A_Δt cov(η_{t-1,i})` and the law-of-total-variance map `Var(η_ti) = A_Δt Var(η_{t-1,i}) A_Δt⊤ + Q_Δt`. Eq. 3 writes `η(t) = exp(A Δt) η(t0) + … +` the stochastic integral; Eq. 4 writes that the integral exhibits covariance `Q_Δt`. `Q_Δt` remains `cov(η_ti | η_{t-1,i})` for the homogeneous process (`ξ`, `z` given) and is refused as the unconditional variance. The JSS article has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). Scalar `exp(a Δt) p` underflow to `+0` is a vanishing covariance and is kept. Finite-`a Δt` exponential overflow rewrites as `exp(ln p + a Δt)`. A finite `exp(a Δt)` whose product with `p` overflows fails closed. Still not a Kalman filter, not DSEM, and not a matrix `expm`. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-18T11:05Z: closed). Oud and Jansen (2000) remains unread (Radboud 403). ZORA Anubis-blocked. diff --git a/CLAUDE.md b/CLAUDE.md index 33bfe25c..ce4467b2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. -- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). Form `-0.5 q / a`. `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). Form `-0.5 q / a`. `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. diff --git a/crates/psychometric_core/src/error.rs b/crates/psychometric_core/src/error.rs index 6d73cd99..1f1cd2c5 100644 --- a/crates/psychometric_core/src/error.rs +++ b/crates/psychometric_core/src/error.rs @@ -55,6 +55,13 @@ pub enum PsychometricError { /// asymptotic within-subject variance. `Q_Δt` at a finite `Δt` is not /// `asymDIFFUSION`. FiniteIntervalProcessNoiseIsNotStationary, + /// Driver §4.3 trait variance was treated as process noise or diffusion. + /// A stable trait has `DRIFT` and `DIFFUSION` fixed to zero. + TraitVarianceIsNotProcessNoise, + /// Driver §4.3 trait variance was treated as the stationary + /// within-subject variance. `TRAITVAR` is between-subject and + /// time-invariant; `asymDIFFUSION` is the `Δt → ∞` state variance. + TraitVarianceIsNotStationaryWithinSubject, } impl fmt::Display for PsychometricError { @@ -101,6 +108,12 @@ impl fmt::Display for PsychometricError { Self::FiniteIntervalProcessNoiseIsNotStationary => { "finite-interval process noise is not the asymptotic within-subject variance" } + Self::TraitVarianceIsNotProcessNoise => { + "trait variance is not process noise and is not a diffusion" + } + Self::TraitVarianceIsNotStationaryWithinSubject => { + "trait variance is not the stationary within-subject variance" + } }; formatter.write_str(message) } @@ -190,5 +203,13 @@ mod tests { PsychometricError::FiniteIntervalProcessNoiseIsNotStationary.to_string(), "finite-interval process noise is not the asymptotic within-subject variance" ); + assert_eq!( + PsychometricError::TraitVarianceIsNotProcessNoise.to_string(), + "trait variance is not process noise and is not a diffusion" + ); + assert_eq!( + PsychometricError::TraitVarianceIsNotStationaryWithinSubject.to_string(), + "trait variance is not the stationary within-subject variance" + ); } } diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index 4d1cf111..6eaaa46f 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -21,7 +21,10 @@ //! `Var(η_ti) = A_Δt Var(η_{t-1,i}) A_Δt⊤ + Q_Δt`. As `Δt → ∞` with //! stable `a < 0`, Eq. 4 and the JSS `asymDIFFUSION` summary (p. 16; //! §4.3 T0VAR stationarity) give the scalar Lyapunov solution -//! `-q / (2 a)`. The JSS article +//! `-q / (2 a)`. Section 4.3 (p. 9) then adds a stable trait process +//! with `DRIFT` and `DIFFUSION` fixed to zero. That `TRAITVAR` is +//! time-invariant between-subject variance; it is not process noise +//! and not `asymDIFFUSION`. The JSS article //! has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). //! The difference quotient `(x(t+Δt) − x(t)) / Δt` (their //! Eqs. 3–4) is refused. This is not DSEM and not a matrix `expm`. @@ -674,6 +677,108 @@ pub fn refuse_finite_interval_process_noise_as_stationary_variance( Err(PsychometricError::FiniteIntervalProcessNoiseIsNotStationary) } +/// Exact scalar trait-plus-state latent variance. +/// +/// Driver, Oud, and Voelkle (2017, §4.3, p. 9; JSS PDF re-opened +/// 2026-08-18T21:07Z) add a stable trait process with `DRIFT` and +/// `DIFFUSION` fixed to zero. The scalar sum is `trait + state`. The +/// ctsem `TRAITVAR` parameterization that adds the trait to the +/// `DIFFUSION` matrix is a software rewrite; it does not license +/// treating trait variance as process noise. This is not RI-CLPM, not +/// a Kalman filter, and not ctsem estimation. +/// +/// # Errors +/// +/// Returns [`PsychometricError::InvalidNumericInput`] when either +/// variance is negative or non-finite, or the sum overflows. +pub fn recover_trait_plus_state_latent_variance( + trait_variance: f64, + state_variance: f64, +) -> Result { + if !trait_variance.is_finite() || trait_variance < 0.0 { + return Err(PsychometricError::InvalidNumericInput); + } + if !state_variance.is_finite() || state_variance < 0.0 { + return Err(PsychometricError::InvalidNumericInput); + } + if trait_variance == 0.0 { + return Ok(state_variance); + } + if state_variance == 0.0 { + return Ok(trait_variance); + } + require_finite(trait_variance + state_variance) +} + +/// Exact scalar trait-plus-state lagged latent covariance. +/// +/// Driver, Oud, and Voelkle (2017, §4.3, p. 9): a stable trait has no +/// temporal dynamics, so `cov(trait_t, trait_{t-1}) = trait`. The +/// state lagged covariance remains `exp(a Δt) p` (Eq. 3–4). The +/// scalar sum is `trait + exp(a Δt) p`. Evolving the summed variance +/// as if it were all state is not this map. This is not RI-CLPM. +/// +/// # Errors +/// +/// Propagates [`recover_discrete_lagged_latent_covariance`]. Returns +/// [`PsychometricError::InvalidNumericInput`] when the trait variance +/// is negative or non-finite or the sum overflows. +pub fn recover_trait_plus_state_lagged_covariance( + trait_variance: f64, + state_prior_variance: f64, + log_rate: f64, + event_delta: f64, + clock: LagClock, +) -> Result { + if !trait_variance.is_finite() || trait_variance < 0.0 { + return Err(PsychometricError::InvalidNumericInput); + } + let state_lagged = recover_discrete_lagged_latent_covariance( + state_prior_variance, + log_rate, + event_delta, + clock, + )?; + if trait_variance == 0.0 { + return Ok(state_lagged); + } + require_finite(trait_variance + state_lagged) +} + +/// Refuse treating Driver §4.3 trait variance as process noise. +/// +/// A stable trait has `DIFFUSION` fixed to zero. The ctsem +/// `TRAITVAR` rewrite that adds the trait to `DIFFUSION` is not a +/// license to treat trait variance as `Q_Δt`. +/// +/// # Errors +/// +/// Always returns [`PsychometricError::TraitVarianceIsNotProcessNoise`]. +pub fn refuse_trait_variance_as_process_noise( + trait_variance: f64, + process_noise: f64, +) -> Result { + let _ = (trait_variance, process_noise); + Err(PsychometricError::TraitVarianceIsNotProcessNoise) +} + +/// Refuse treating Driver §4.3 trait variance as `asymDIFFUSION`. +/// +/// Trait variance is time-invariant between-subject variance. The +/// stationary within-subject variance is the `Δt → ∞` limit of Eq. 4. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::TraitVarianceIsNotStationaryWithinSubject`]. +pub fn refuse_trait_variance_as_stationary_within_subject( + trait_variance: f64, + stationary_state_variance: f64, +) -> Result { + let _ = (trait_variance, stationary_state_variance); + Err(PsychometricError::TraitVarianceIsNotStationaryWithinSubject) +} + /// Refuse treating Driver Eq. 3 process noise as the unconditional variance. /// /// Driver, Oud, and Voelkle (2017, Eq. 3–4, pp. 4–5): @@ -928,11 +1033,13 @@ mod tests { recover_discrete_process_noise, recover_discrete_time_varying_predictor_effect, recover_event_series_mean_log_rate, recover_event_time_discrete_lag_and_log_rate, recover_irregular_centered_residual_log_rate, recover_local_log_rate, - recover_stationary_latent_variance, recover_within_residual_event_time_log_rate, + recover_stationary_latent_variance, recover_trait_plus_state_lagged_covariance, + recover_trait_plus_state_latent_variance, recover_within_residual_event_time_log_rate, refuse_difference_quotient_as_local_rate, refuse_finite_interval_process_noise_as_stationary_variance, refuse_pooled_discrete_lag_across_unequal_intervals, - refuse_process_noise_as_unconditional_variance, + refuse_process_noise_as_unconditional_variance, refuse_trait_variance_as_process_noise, + refuse_trait_variance_as_stationary_within_subject, refuse_unmatched_time_varying_predictor_interval, }; use crate::error::PsychometricError; @@ -1686,6 +1793,127 @@ mod tests { ); } + #[test] + fn trait_plus_state_recovers_driver_section_four_point_three() { + let trait_variance = 1.5_f64; + let diffusion = 0.4_f64; + let drift = -0.5_f64; + let delta = 1.0_f64; + let state = recover_stationary_latent_variance(diffusion, drift, LagClock::EventTime) + .expect("state"); + let total = recover_trait_plus_state_latent_variance(trait_variance, state).expect("sum"); + assert!((total - (trait_variance + state)).abs() < 1e-15); + let lagged = recover_trait_plus_state_lagged_covariance( + trait_variance, + state, + drift, + delta, + LagClock::EventTime, + ) + .expect("lagged"); + let state_lagged = + recover_discrete_lagged_latent_covariance(state, drift, delta, LagClock::EventTime) + .expect("state lagged"); + assert!((lagged - (trait_variance + state_lagged)).abs() < 1e-15); + // Evolving the summed variance as if it were all state is not + // the trait-plus-state map (Driver §4.3; Hamaker et al., 2015). + let evolved_as_state = + recover_discrete_latent_variance(total, diffusion, drift, delta, LagClock::EventTime) + .expect("wrong"); + let evolved_state = + recover_discrete_latent_variance(state, diffusion, drift, delta, LagClock::EventTime) + .expect("state evolved"); + let evolved_right = + recover_trait_plus_state_latent_variance(trait_variance, evolved_state).expect("right"); + assert!((evolved_right - total).abs() < 1e-12); + assert!((evolved_as_state - evolved_right).abs() > 1e-3); + assert_eq!( + recover_trait_plus_state_latent_variance(0.0, state), + Ok(state) + ); + assert_eq!( + recover_trait_plus_state_latent_variance(trait_variance, 0.0), + Ok(trait_variance) + ); + assert_eq!( + recover_trait_plus_state_lagged_covariance( + 0.0, + state, + drift, + delta, + LagClock::EventTime + ), + Ok(state_lagged) + ); + assert_eq!( + recover_trait_plus_state_lagged_covariance( + trait_variance, + 0.0, + drift, + delta, + LagClock::EventTime + ), + Ok(trait_variance) + ); + let process_noise = + recover_discrete_process_noise(diffusion, drift, delta, LagClock::EventTime) + .expect("q_dt"); + assert_eq!( + refuse_trait_variance_as_process_noise(trait_variance, process_noise), + Err(PsychometricError::TraitVarianceIsNotProcessNoise) + ); + assert_eq!( + refuse_trait_variance_as_stationary_within_subject(trait_variance, state), + Err(PsychometricError::TraitVarianceIsNotStationaryWithinSubject) + ); + } + + #[test] + fn trait_plus_state_invalid_inputs_fail_closed() { + assert_eq!( + recover_trait_plus_state_latent_variance(-0.1, 0.4), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_trait_plus_state_latent_variance(0.4, -0.1), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_trait_plus_state_latent_variance(f64::NAN, 0.4), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_trait_plus_state_latent_variance(0.4, f64::NAN), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_trait_plus_state_latent_variance(1e308, 1e308), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_trait_plus_state_lagged_covariance(-0.1, 0.4, -0.5, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_trait_plus_state_lagged_covariance( + f64::NAN, + 0.4, + -0.5, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_trait_plus_state_lagged_covariance(0.4, 0.4, -0.5, 1.0, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_trait_plus_state_lagged_covariance(1e308, 1e308, 0.0, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + } + #[test] fn non_event_clocks_and_difference_quotient_fail_closed() { for clock in [ diff --git a/crates/psychometric_core/src/lib.rs b/crates/psychometric_core/src/lib.rs index 4cd57d09..9c8e374e 100644 --- a/crates/psychometric_core/src/lib.rs +++ b/crates/psychometric_core/src/lib.rs @@ -19,7 +19,9 @@ //! latent covariance and unconditional latent variance licensed by //! their Eq. 3–4, recovers the scalar stationary within-subject //! variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 -//! `asymDIFFUSION`), +//! `asymDIFFUSION`), recovers the Driver §4.3 trait-plus-state +//! variance and lagged covariance (`TRAITVAR` is not process noise +//! and not `asymDIFFUSION`), //! and refuses //! latent-mean comparison below strong invariance. @@ -92,6 +94,10 @@ pub use event_time::recover_irregular_centered_residual_log_rate; pub use event_time::recover_local_log_rate; /// Exact scalar stationary within-subject variance `-q / (2 a)`. pub use event_time::recover_stationary_latent_variance; +/// Exact scalar trait-plus-state lagged covariance. +pub use event_time::recover_trait_plus_state_lagged_covariance; +/// Exact scalar trait-plus-state latent variance. +pub use event_time::recover_trait_plus_state_latent_variance; /// CWC-then-event-time local log-rate (not DSEM; not raw-process AR drift). pub use event_time::recover_within_residual_event_time_log_rate; /// Refuse the difference quotient as a continuous-time rate. @@ -102,6 +108,10 @@ pub use event_time::refuse_finite_interval_process_noise_as_stationary_variance; pub use event_time::refuse_pooled_discrete_lag_across_unequal_intervals; /// Refuse treating Driver Eq. 3 process noise as the unconditional variance. pub use event_time::refuse_process_noise_as_unconditional_variance; +/// Refuse treating Driver §4.3 trait variance as process noise. +pub use event_time::refuse_trait_variance_as_process_noise; +/// Refuse treating Driver §4.3 trait variance as `asymDIFFUSION`. +pub use event_time::refuse_trait_variance_as_stationary_within_subject; /// Refuse a time-varying predictor whose sampling and constancy intervals differ. pub use event_time::refuse_unmatched_time_varying_predictor_interval; /// Indicator coordinate kind. diff --git a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs index 991b6f63..160ebbf2 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -10,11 +10,13 @@ use psychometric_core::{ recover_discrete_process_noise, recover_discrete_time_varying_predictor_effect, recover_event_series_mean_log_rate, recover_event_time_discrete_lag_and_log_rate, recover_irregular_centered_residual_log_rate, recover_kish_weighted_slope, - recover_stationary_latent_variance, recover_within_residual_event_time_log_rate, + recover_stationary_latent_variance, recover_trait_plus_state_lagged_covariance, + recover_trait_plus_state_latent_variance, recover_within_residual_event_time_log_rate, refuse_difference_quotient_as_local_rate, refuse_finite_interval_process_noise_as_stationary_variance, refuse_pooled_discrete_lag_across_unequal_intervals, - refuse_process_noise_as_unconditional_variance, + refuse_process_noise_as_unconditional_variance, refuse_trait_variance_as_process_noise, + refuse_trait_variance_as_stationary_within_subject, refuse_unmatched_time_varying_predictor_interval, }; @@ -702,6 +704,80 @@ fn stationary_variance_recovers_driver_equation_four_asymptote() { ); } +#[test] +fn trait_plus_state_recovers_driver_section_four_point_three() { + let trait_variance = 1.5_f64; + let diffusion = 0.4_f64; + let drift = -0.5_f64; + let delta = 1.0_f64; + let state = + recover_stationary_latent_variance(diffusion, drift, LagClock::EventTime).expect("state"); + let total = recover_trait_plus_state_latent_variance(trait_variance, state).expect("sum"); + let expected_total = trait_variance + state; + let error = rmse(&[expected_total], &[total]); + assert!(error < 1e-15, "Driver §4.3 trait+state RMSE {error}"); + let lagged = recover_trait_plus_state_lagged_covariance( + trait_variance, + state, + drift, + delta, + LagClock::EventTime, + ) + .expect("lagged"); + let state_lagged = + recover_discrete_lagged_latent_covariance(state, drift, delta, LagClock::EventTime) + .expect("state lagged"); + let lagged_error = rmse(&[trait_variance + state_lagged], &[lagged]); + assert!( + lagged_error < 1e-15, + "Driver §4.3 trait+state lagged RMSE {lagged_error}" + ); + let evolved_as_state = + recover_discrete_latent_variance(total, diffusion, drift, delta, LagClock::EventTime) + .expect("wrong"); + let evolved_state = + recover_discrete_latent_variance(state, diffusion, drift, delta, LagClock::EventTime) + .expect("state evolved"); + let evolved_right = + recover_trait_plus_state_latent_variance(trait_variance, evolved_state).expect("right"); + let right_error = rmse(&[total], &[evolved_right]); + assert!( + right_error < 1e-12, + "trait + stationary state must stay invariant: RMSE {right_error}" + ); + let collapsed = rmse(&[evolved_right], &[evolved_as_state]); + assert!( + collapsed > error, + "evolving trait+state as all-state is not Driver §4.3: collapsed RMSE {collapsed} must exceed {error}" + ); + let process_noise = + recover_discrete_process_noise(diffusion, drift, delta, LagClock::EventTime).expect("q_dt"); + assert_eq!( + refuse_trait_variance_as_process_noise(trait_variance, process_noise), + Err(PsychometricError::TraitVarianceIsNotProcessNoise) + ); + assert_eq!( + refuse_trait_variance_as_stationary_within_subject(trait_variance, state), + Err(PsychometricError::TraitVarianceIsNotStationaryWithinSubject) + ); + assert_eq!( + recover_trait_plus_state_latent_variance(0.0, state), + Ok(state) + ); + assert_eq!( + recover_trait_plus_state_latent_variance(trait_variance, 0.0), + Ok(trait_variance) + ); + assert_eq!( + recover_trait_plus_state_lagged_covariance(1e308, 1e308, 0.0, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_trait_plus_state_lagged_covariance(0.4, 0.4, -0.5, 1.0, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); +} + #[test] fn admitted_coordinates_still_required_for_multilevel_weights() { assert_eq!( diff --git a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs index f5a1356d..231b567e 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -7,9 +7,11 @@ use psychometric_core::{ recover_discrete_lagged_latent_covariance, recover_discrete_latent_variance, recover_discrete_process_noise, recover_discrete_time_varying_predictor_effect, recover_irregular_centered_residual_log_rate, recover_loading_point_estimate_mean, - recover_stationary_latent_variance, recover_within_residual_event_time_log_rate, + recover_stationary_latent_variance, recover_trait_plus_state_latent_variance, + recover_within_residual_event_time_log_rate, refuse_finite_interval_process_noise_as_stationary_variance, - refuse_process_noise_as_unconditional_variance, + refuse_process_noise_as_unconditional_variance, refuse_trait_variance_as_process_noise, + refuse_trait_variance_as_stationary_within_subject, }; #[test] @@ -269,3 +271,39 @@ fn finite_interval_process_noise_is_not_the_stationary_variance() { Err(psychometric_core::PsychometricError::StationaryVarianceRequiresStableDrift) ); } + +#[test] +fn trait_variance_is_not_process_noise_or_stationary_within_subject() { + let trait_variance = 1.5_f64; + let diffusion = 0.4_f64; + let drift = -0.5_f64; + let delta = 1.0_f64; + let state = + recover_stationary_latent_variance(diffusion, drift, LagClock::EventTime).expect("state"); + let total = recover_trait_plus_state_latent_variance(trait_variance, state).expect("sum"); + let process_noise = + recover_discrete_process_noise(diffusion, drift, delta, LagClock::EventTime).expect("q_dt"); + assert!( + (trait_variance - process_noise).abs() > 1e-3, + "Driver et al. (2017, §4.3, p. 9): TRAITVAR is not Q_Δt" + ); + assert!( + (trait_variance - state).abs() > 1e-3, + "Driver et al. (2017, §4.3, p. 9): TRAITVAR is not asymDIFFUSION" + ); + let evolved_as_state = + recover_discrete_latent_variance(total, diffusion, drift, delta, LagClock::EventTime) + .expect("wrong"); + assert!( + (evolved_as_state - total).abs() > 1e-3, + "Driver et al. (2017, §4.3): evolving trait+state as all-state is not the trait map" + ); + assert_eq!( + refuse_trait_variance_as_process_noise(trait_variance, process_noise), + Err(psychometric_core::PsychometricError::TraitVarianceIsNotProcessNoise) + ); + assert_eq!( + refuse_trait_variance_as_stationary_within_subject(trait_variance, state), + Err(psychometric_core::PsychometricError::TraitVarianceIsNotStationaryWithinSubject) + ); +} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 29b6aec5..1a87c80c 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -28,7 +28,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | report template/section/copied/style/modality method effects | ADR 0004/0012; PRD/TRD | simulation truth factors implemented; estimator-side method model remains future | partial | | candidate K statistical/Pareto gates + blinded LLM review | ADR 0012; research | future `model_selection` | accepted-target | | compositional topic correlation / stable clustering | ADR 0005/0012; research | future `network_analysis` | accepted-target | -| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR; full ESEM/DSEM remaining | partial | +| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR; full ESEM/DSEM remaining | partial | | CPU bounded multithreading + GPU/VRAM streaming/parity | ADR 0001/0006 | future `compute_backend` | accepted-target | | TDT detection/tracking vs CHRONOS schema/prediction/temporal consistency | ADR 0016; PRD/research | future `event_intelligence` | accepted-target | | evidence-bounded LLM interpretation | ADR 0010/0012; PRD | future `interpretation_gateway` | accepted-target | diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index ba862db3..d3eca1d7 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -1,7 +1,7 @@ # ADR 0005 — Posterior-aware ESEM/DSEM and structural interpretation **Decision status:** Accepted -**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target +**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target **Date:** 2026-08-05 **Supersedes:** None. ADR 0012 governs upstream topic measurement/network coordinates; this ADR governs higher-order psychometric structure and longitudinal interpretation. @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; form `-0.5 q / a`; do not form `2 a` first; `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; form `-0.5 q / a`; do not form `2 a` first; `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. 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/adr/README.md b/docs/adr/README.md index 35e91b32..f82d0b8e 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -10,7 +10,7 @@ Read [`ADR_POLICY.md`](ADR_POLICY.md) first. **Decision status and implementatio | [0002](0002-six-clock-temporal-semantics.md) | Six-clock temporal semantics and fail-closed historical leakage prevention | Accepted | active-PR | Unmerged PR #8 is the canonical Task 3 replacement implementing typed clocks/intervals against the current protected-main lineage; conflicted PR #5 is superseded lineage. Later graph/split enforcement remains target work. | | [0003](0003-relational-event-multiple-membership.md) | Relational event ontology and time-varying cross-classified multiple membership | Accepted | partial | Weighted time-varying membership network/roles are active-PR (PR #12); full multilevel estimators, graph ontology, and persistence remain accepted-target. ADR 0016 owns event-intelligence tasks. | | [0004](0004-shared-multilingual-latent-space.md) | One shared multilingual latent space with explicit invariance status | Accepted | accepted-target | ADR 0012 owns the full topic-estimator/backend/global-topic contract. | -| [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | partial | Input gates, posterior-draw point estimates, Rubin `T` on OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects, exact scalar discrete process noise, lagged latent covariance and unconditional latent variance, stationary within-subject variance (`asymDIFFUSION`), irregular already-centered residual lag, and strong-gated latent means are on the stacked psychometric PR; full ESEM/DSEM estimator remains accepted-target. | +| [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | partial | Input gates, posterior-draw point estimates, Rubin `T` on OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects, exact scalar discrete process noise, lagged latent covariance and unconditional latent variance, stationary within-subject variance (`asymDIFFUSION`), trait-plus-state variance (`TRAITVAR`; not process noise), irregular already-centered residual lag, and strong-gated latent means are on the stacked psychometric PR; full ESEM/DSEM estimator remains accepted-target. | | [0006](0006-vram-gpu-nvidia-orchestration.md) | VRAM-adaptive GPU compute and model-credential boundary | Accepted | accepted-target | LLM orchestration policy superseded by ADR 0010; autonomous development authority governed by ADR 0015. | | [0007](0007-rust-workspace-quality-gates.md) | Explicit Rust workspace, pinned toolchains, and exact quality gates | Accepted | implemented-main | ADR 0014 governs scientific/product claim promotion beyond repository-quality tooling. | | [0008](0008-immutable-evidence-identities-digests-and-spans.md) | Immutable evidence identities, `SHA-256` digests, exact spans, and strict wire reconstruction | Accepted | implemented-main | ADR 0013 governs future persistence/reproducibility/split authority. | diff --git a/docs/research/multilevel-event-time-recovery.md b/docs/research/multilevel-event-time-recovery.md index e2cb3139..ce72bb01 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -16,15 +16,16 @@ This slice stays inside `psychometric_core`. It does not add a second invariance 10. recover the exact scalar lagged latent covariance `A_Δt cov(η_{t-1})` (Driver et al., 2017, Eq. 3–4); 11. recover the exact scalar discrete latent variance `A_Δt P A_Δt⊤ + Q_Δt` and refuse `Q_Δt` as that unconditional variance; 12. recover the exact scalar stationary within-subject variance as the `Δt → ∞` limit of Eq. 4 (`asymDIFFUSION`; §4.3 T0VAR stationarity) and refuse finite-interval `Q_Δt` as that limit; -13. refuse pooling discrete lags from unequal event intervals as one coefficient; -14. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); -15. refuse the difference quotient as a continuous-time rate; -16. apply the same event-time map to CWC residuals (still not DSEM); -17. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). +13. recover the exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`) and refuse treating trait variance as process noise or as `asymDIFFUSION`; +14. refuse pooling discrete lags from unequal event intervals as one coefficient; +15. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); +16. refuse the difference quotient as a continuous-time rate; +17. apply the same event-time map to CWC residuals (still not DSEM); +18. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). ## Claim boundary -This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. +This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. ## Authoritative sources @@ -42,7 +43,7 @@ Kish, L. (1965). *Survey sampling*. John Wiley & Sons. Oud, J. H. L., & Jansen, R. A. R. G. (2000). Continuous time state space modeling of panel data by means of SEM. *Psychometrika, 65*(2), 199–215. https://doi.org/10.1007/BF02294374 (cited by Voelkle et al., 2012, Eq. 14 discussion; PDF not opened). -The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-17T14:20Z (https://www.zora.uzh.ch/handle/20.500.14742/72792; bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`). Page 16 writes discrete auto-effects as \(A^{*}(\Delta t)=\exp(A\Delta t)\) (Eq. 7). Introducing Intercepts (manuscript p. 20, Eq. 12) adds a continuous-time intercept \(b\) and writes the expected-value solution whose discrete increment is \(A^{-1}(\exp(A\Delta t)-I)b\); the scalar constant-predictor effect is \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). The next paragraph (manuscript p. 21, Eq. 14) writes the discrete effect of a **time-varying** predictor, when the sampling interval equals the constancy interval, as \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product does not depend on the predictor auto-effect. The manuscript calls Eq. 14 a first-order approximation that deteriorates as \(\Delta t\) grows, and defers unmatched sampling/constancy intervals to Oud and Jansen (2000), which is unread. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4; JSS PDF re-opened 2026-08-18T18:03Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\), the discrete intercept \(b_{\Delta t}=A^{-1}[A_{\Delta t}-I]b\), and the discrete process-noise covariance \(Q_{\Delta t}=\int_{0}^{\Delta t}\operatorname{expm}(A(\Delta t-\tau))LGG^{\top}L^{\top}\operatorname{expm}(A(\Delta t-\tau))^{\top}\,d\tau\). Equation 4 (p. 5) writes that the integral exhibits covariance \(Q_{\Delta t}=\operatorname{irow}(A^{\#^{-1}}[e^{A^{\#}\Delta t}-I]\operatorname{row}(Q))\) with \(A^{\#}=A\otimes I+I\otimes A\). The homogeneous-process consequence (`ξ`, `z` given) is \(Q_{\Delta t}=\operatorname{cov}(\eta_{ti}\mid\eta_{t-1,i})\) and \(\operatorname{cov}(\eta_{ti},\eta_{t-1,i})=A_{\Delta t}\operatorname{cov}(\eta_{t-1,i})\). The law of total variance on that pair is \(\operatorname{Var}(\eta_{ti})=A_{\Delta t}\operatorname{Var}(\eta_{t-1,i})A_{\Delta t}^{\top}+Q_{\Delta t}\). As \(\Delta t\to\infty\) with stable \(a<0\), Eq. 4 becomes \(-q/(2a)\). The JSS summary names that limit `asymDIFFUSION` and takes it as the total within-subject variance (p. 16). Section 4.3 (pp. 9–10) constrains a stationary `T0VAR` to that model-predicted variance and distinguishes it from a predetermined first occasion. The JSS article has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). This slice takes scalar \(L=1\) (every latent subject to system noise); it does not implement the 0/1 selector matrix and is not a Kalman filter. Discrete auto-effects are strictly positive for finite real drift and interval; the inverse \(a=\ln\varphi/\Delta t\) therefore requires \(\varphi>0\). Binary64 `exp` of a large negative argument is `+0` and is refused as a discrete lag; the same underflow is a vanishing lagged covariance and is kept. Equations 3–4 of Voelkle et al. (2012) are the discouraged difference-quotient approximation. Meredith (1993) remains unread (Unpaywall/OpenAlex 2026-08-18T18:03Z: closed; Springer `content/pdf` is HTML 200, not a PDF; archive.org title search empty). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex 2026-08-18T18:03Z: closed; Springer `content/pdf` is HTML 200; ETS landing page is HTML). ERIC ED334221 is Singer and Willett (1991), *From whether to when*, not the 1991 journal article. ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title is not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex 2026-08-18T18:03Z: closed; Radboud landing and bitstream 403). The Voelkle et al. (2012) ZORA bitstream was Anubis-blocked this cycle. +The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:07Z (https://www.zora.uzh.ch/handle/20.500.14742/72792; bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`). Page 16 writes discrete auto-effects as \(A^{*}(\Delta t)=\exp(A\Delta t)\) (Eq. 7). Introducing Intercepts (manuscript p. 20, Eq. 12) adds a continuous-time intercept \(b\) and writes the expected-value solution whose discrete increment is \(A^{-1}(\exp(A\Delta t)-I)b\); the scalar constant-predictor effect is \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). The next paragraph (manuscript p. 21, Eq. 14) writes the discrete effect of a **time-varying** predictor, when the sampling interval equals the constancy interval, as \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product does not depend on the predictor auto-effect. The manuscript calls Eq. 14 a first-order approximation that deteriorates as \(\Delta t\) grows, and defers unmatched sampling/constancy intervals to Oud and Jansen (2000), which is unread. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4; JSS PDF re-opened 2026-08-18T21:07Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\), the discrete intercept \(b_{\Delta t}=A^{-1}[A_{\Delta t}-I]b\), and the discrete process-noise covariance \(Q_{\Delta t}=\int_{0}^{\Delta t}\operatorname{expm}(A(\Delta t-\tau))LGG^{\top}L^{\top}\operatorname{expm}(A(\Delta t-\tau))^{\top}\,d\tau\). Equation 4 (p. 5) writes that the integral exhibits covariance \(Q_{\Delta t}=\operatorname{irow}(A^{\#^{-1}}[e^{A^{\#}\Delta t}-I]\operatorname{row}(Q))\) with \(A^{\#}=A\otimes I+I\otimes A\). The homogeneous-process consequence (`ξ`, `z` given) is \(Q_{\Delta t}=\operatorname{cov}(\eta_{ti}\mid\eta_{t-1,i})\) and \(\operatorname{cov}(\eta_{ti},\eta_{t-1,i})=A_{\Delta t}\operatorname{cov}(\eta_{t-1,i})\). The law of total variance on that pair is \(\operatorname{Var}(\eta_{ti})=A_{\Delta t}\operatorname{Var}(\eta_{t-1,i})A_{\Delta t}^{\top}+Q_{\Delta t}\). As \(\Delta t\to\infty\) with stable \(a<0\), Eq. 4 becomes \(-q/(2a)\). The JSS summary names that limit `asymDIFFUSION` and takes it as the total within-subject variance (p. 16). Section 4.3 (pp. 9–10) constrains a stationary `T0VAR` to that model-predicted variance and distinguishes it from a predetermined first occasion. The same section (p. 9) adds a stable trait process with `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is that time-invariant between-subject variance. The JSS article has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). This slice takes scalar \(L=1\) (every latent subject to system noise); it does not implement the 0/1 selector matrix and is not a Kalman filter. Discrete auto-effects are strictly positive for finite real drift and interval; the inverse \(a=\ln\varphi/\Delta t\) therefore requires \(\varphi>0\). Binary64 `exp` of a large negative argument is `+0` and is refused as a discrete lag; the same underflow is a vanishing lagged covariance and is kept. Equations 3–4 of Voelkle et al. (2012) are the discouraged difference-quotient approximation. Meredith (1993) remains unread (Unpaywall/OpenAlex 2026-08-18T21:07Z: closed; Springer `content/pdf` is HTML 200, not a PDF; archive.org title search empty). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex 2026-08-18T21:07Z: closed; Springer `content/pdf` is HTML 200; ETS landing page is HTML). ERIC ED334221 is Singer and Willett (1991), *From whether to when*, not the 1991 journal article. ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title is not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex 2026-08-18T21:07Z: closed; Radboud landing and bitstream 403). ## Formula notes @@ -57,6 +58,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-17T14:2 - **Lagged latent covariance.** Driver, Oud, and Voelkle (2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T11:20Z): \(\operatorname{cov}(\eta_{ti},\eta_{t-1,i})=A_{\Delta t}\operatorname{cov}(\eta_{t-1,i})\) for the homogeneous process. The scalar map is \(\mathrm{e}^{a\Delta t}p\) with prior variance \(p\ge 0\). This is not \(Q_{\Delta t}\). Binary64 underflow of \(\mathrm{e}^{a\Delta t}\) to `+0` is a vanishing covariance and is kept. A zero prior variance is exactly zero. Finite-\(a\Delta t\) exponential overflow rewrites as \(\exp(\ln p+a\Delta t)\). A finite \(\mathrm{e}^{a\Delta t}\) whose product with \(p\) overflows fails closed. The JSS article has no numbered §2.2. - **Discrete latent variance.** Equations 3–4 write \(Q_{\Delta t}\) as the covariance of the stochastic integral, so \(Q_{\Delta t}=\operatorname{cov}(\eta_{ti}\mid\eta_{t-1,i})\) when \(\xi\) and \(z\) are given. The law of total variance on that pair is \(\operatorname{Var}(\eta_{ti})=A_{\Delta t}\operatorname{Var}(\eta_{t-1,i})A_{\Delta t}^{\top}+Q_{\Delta t}\). The scalar map is \(\mathrm{e}^{2a\Delta t}p+Q_{\Delta t}\). This is not a Kalman measurement update. A zero prior variance is exactly \(Q_{\Delta t}\). Binary64 underflow of \(\mathrm{e}^{2a\Delta t}\) keeps \(Q_{\Delta t}\). Finite-\(z\) exponential overflow rewrites as \(\exp(\ln p+z)+Q_{\Delta t}\). A zero diffusion is exactly \(Q_{\Delta t}=0\); that skip does not license a non-finite carried term when \(2(a\Delta t)\) overflows to \(+\infty\). Treating \(Q_{\Delta t}\) as \(\operatorname{Var}(\eta_{t})\) fails closed. - **Stationary within-subject variance.** Driver et al. (2017, Eq. 4 as \(\Delta t\to\infty\); §4.3 pp. 9–10; p. 16 `asymDIFFUSION`): for stable \(a<0\), \(\lim_{\Delta t\to\infty}Q_{\Delta t}=-q/(2a)\). The algebraically identical evaluation is \(-0.5 q/a\). Forming \(2a\) first overflows when \(|a|\) is at the binary64 extreme (`a=-1e308`, `q=1e308` → `0.5`). Starting from that variance, \(\operatorname{Var}(\eta_{t})\) is invariant across finite event intervals. A zero diffusion is exactly zero. \(a\ge 0\) has no finite stationary variance (Brownian \(a=0\) grows as \(q\Delta t\)). An overflowing \(-0.5 q/a\) fails closed. Finite-interval \(Q_{\Delta t}\) is not that limit. This is not ctsem estimation and not a Kalman filter. +- **Trait-plus-state variance.** Driver et al. (2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z): a stable trait process has `DRIFT` and `DIFFUSION` fixed to zero. The scalar maps are \(\operatorname{Var}=\mathrm{trait}+\mathrm{state}\) and \(\operatorname{cov}(t,t-1)=\mathrm{trait}+\mathrm{e}^{a\Delta t}p\). The ctsem `TRAITVAR` rewrite that adds the trait to `DIFFUSION` does not license treating trait variance as \(Q_{\Delta t}\). Trait variance is not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not this map. A zero trait is exactly the state. A zero state is exactly the trait. An overflowing sum fails closed. This is not RI-CLPM and not ctsem estimation. - **CWC-then-lag.** Sample cluster means are removed first. Consecutive within residuals are then fitted by least squares to \(r_{t+\Delta t}\approx\exp(a\Delta t)\,r_{t}\) on event time. Same-sign pair-wise logs initialize the scalar Newton step. Sign-flipping \(T=2\) CWC pairs have no real logarithm and fail closed. Curran and Bauer (2011, pp. 607–608) show that this person-mean subtraction on a raw autoregressive series does **not** isolate the lagged within-person effect; the helper therefore does not claim to recover the raw-process drift. - **Already-centered irregular residual.** The caller supplies lagged within residuals. The mean of \(a=\ln(r_{t+\Delta t}/r_t)/\Delta t\) is the exact scalar map. Intervals may be irregular. The helper does not center again. This is not DSEM. @@ -73,6 +75,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-17T14:2 - Driver et al. (2017, Eq. 3) recovers a known scalar \(Q_{\Delta t}\) at machine-scale RMSE, and that RMSE is smaller than treating the continuous diffusion as the discrete process noise; \(a=0\) and binary64 underflow of \(2a\Delta t\) recover \(q\Delta t\); \(z\to-\infty\) recovers \(-q/(2a)\); a finite result whose `expm1` overflows at a finite \(z\) is recovered in log space; a finite result whose \(2a\) overflows while \(a\Delta t\) stays finite is recovered as \(0.5 q(\operatorname{expm1}(z)/a)\); a zero diffusion is exactly zero; a negative diffusion, \(z\to+\infty\), and an overflowing rewrite scale \(0.5 q/a\) fail closed; non-finite event interval, non-positive sampling interval, and non-finite constancy interval fail closed on Eq. 14; non-finite / non-positive event interval, non-finite / negative diffusion, and non-finite log-rate fail closed on Eq. 3; - Driver et al. (2017, Eq. 3–4) recovers a known lagged covariance \(\mathrm{e}^{a\Delta t}p\) and a known latent variance \(\mathrm{e}^{2a\Delta t}p+Q_{\Delta t}\) at machine-scale RMSE, and that variance RMSE is smaller than treating \(Q_{\Delta t}\) as \(\operatorname{Var}(\eta_{t})\); a zero prior variance is exactly zero (lagged) or \(Q_{\Delta t}\) (unconditional); binary64 underflow of \(\mathrm{e}^{a\Delta t}\) keeps a vanishing lagged covariance; a finite \(\mathrm{e}^{a\Delta t}\) whose product with \(p\) overflows fails closed; a zero diffusion whose \(2(a\Delta t)\) overflows to \(+\infty\) fails closed; treating \(Q_{\Delta t}\) as the unconditional variance fails closed; - Driver et al. (2017, Eq. 4 as \(\Delta t\to\infty\); p. 16 `asymDIFFUSION`; §4.3) recovers a known stationary variance \(-q/(2a)\) at machine-scale RMSE, and that RMSE is smaller than treating finite-interval \(Q_{\Delta t}\) as the stationary variance; starting from that variance, \(\operatorname{Var}(\eta_{t})\) is invariant across finite \(\Delta t\); a zero diffusion is exactly zero; forming \(2a\) first overflows while \(-0.5 q/a\) stays finite; \(a\ge 0\), a non-event clock, a negative diffusion, and an overflowing \(-0.5 q/a\) fail closed; +- Driver et al. (2017, §4.3, p. 9) recovers a known trait-plus-state variance and lagged covariance at machine-scale RMSE, and that RMSE is smaller than evolving the summed variance as if it were all state; a zero trait is the state; a zero state is the trait; treating trait variance as process noise or as `asymDIFFUSION` fails closed; an overflowing sum and a non-event clock fail closed; - pooling discrete lags from unequal intervals fails closed; - CWC-then-lag on a two-cluster decaying series has smaller computed RMSE than a level-pooled series when the latter is identified; - already-centered irregular residuals recover a known drift at machine-scale RMSE, and that RMSE is smaller than CWC of the corresponding raw autoregressive series (Curran & Bauer, 2011, pp. 607–608); diff --git a/docs/research/standards-and-literature.md b/docs/research/standards-and-literature.md index b80317d5..ad713f92 100644 --- a/docs/research/standards-and-literature.md +++ b/docs/research/standards-and-literature.md @@ -34,7 +34,7 @@ Meredith, W. (1993). Measurement invariance, factor analysis and factorial invar Holland, P. W. (1986). Statistics and causal inference. *Journal of the American Statistical Association, 81*(396), 945–960. https://doi.org/10.1080/01621459.1986.10478354 -TEPP applies these sources to construct definition, score interpretation, reliability, validity evidence, uncertainty, consequences, longitudinal invariance, ESEM cross-loadings, and DSEM. Topic outputs are treated as fallible indicators or components only after their construct role is evaluated. Reflective, formative, and network classes remain distinct (Bollen & Lennox, 1991). Complete-data OLS loadings across posterior indicator draws are combined with Rubin (1996) \(T_m\); the arithmetic-mean helper remains a point estimate. Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-18T03:07Z: closed). The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title was opened 2026-08-17T12:04Z from archive.org; it is not the 1991 journal article and is not used as Mislevy plausible-value authority. Temporal precedence is not causal identification (Holland, 1986). Within/between OLS follows Enders and Tofighi (2007), Curran and Bauer (2011), and Hamaker et al. (2015). Enders and Tofighi (2007, Table 2, pp. 124–127; PDF opened 2026-08-17) show that the CWC cluster-mean coefficient is the contextual effect (`between − within`), not the between-cluster effect. Curran and Bauer (2011, pp. 607–608) reject person-mean subtraction on a raw autoregressive series as the lagged within-person residual; already-centered irregular residuals use the Voelkle et al. (2012, Eq. 7) / Driver et al. (2017, Eq. 3) scalar map. Discrete lags from unequal event intervals are remapped through that log-rate (Voelkle et al., 2012, ZORA accepted manuscript re-opened 2026-08-17T13:13Z) and are not pooled. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\) and restate the discrete intercept as a function of \(A\) and \(\Delta t\). A binary64 underflow of \(\exp(a\Delta t)\) to `+0` is refused because discrete auto-effects are strictly positive. The discrete effect of a constant predictor is Voelkle et al. (2012, Eq. 12; ZORA accepted manuscript re-opened 2026-08-17T14:20Z, Introducing Intercepts, manuscript p. 20), evaluated as \(a_{yx}(\operatorname{expm1}(z)/a_{xx})\) with \(z=a_{xx}\Delta t\) so a finite result is not lost when \(z\) overflows to \(-\infty\) or when \(a_{yx}\Delta t\) overflows, and in log space when `expm1(z)` overflows at a finite \(z\); a zero continuous effect is exactly zero; an overflowing \(a_{yx}/a_{xx}\) rewrite term fails closed; the first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. The discrete effect of a time-varying predictor with matched sampling and constancy intervals is Voelkle et al. (2012, Eq. 14; manuscript p. 21): \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product is not Eq. 12. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). The exact scalar discrete process noise is Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-18T14:04Z, p. 4): \(Q_{\Delta t}=0.5 q(\operatorname{expm1}(z)/a)\) with \(z=2(a\Delta t)\) for \(a\neq 0\) and \(q=GG^{\top}\ge 0\); do not form \(2a\) first; \(a=0\) recovers \(q\Delta t\); an overflowing rewrite scale \(0.5 q/a\) fails closed. This is not a Kalman filter. Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-18T14:04Z) write the same discrete intercept as \(A^{-1}[e^{A\Delta t}-I]\xi\). The lagged covariance is \(\mathrm{e}^{a\Delta t}p\) and the unconditional variance is \(\mathrm{e}^{2a\Delta t}p+Q_{\Delta t}\) (Driver et al., 2017, Eq. 3–4, pp. 4–5); a zero diffusion whose \(2(a\Delta t)\) overflows to \(+\infty\) fails closed. The stationary within-subject variance is the \(\Delta t\to\infty\) limit of Eq. 4: \(-q/(2a)\) for stable \(a<0\) (JSS p. 16 `asymDIFFUSION`; §4.3; PDF re-opened 2026-08-18T18:03Z). Finite-interval \(Q_{\Delta t}\) is not that limit. Metric/weak invariance does not license latent-mean comparison. Meredith (1993) remains unread (Unpaywall/OpenAlex 2026-08-18T18:03Z: closed; Springer `content/pdf` is HTML 200). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex 2026-08-18T18:03Z: closed). ERIC ED334221 is Singer and Willett (1991), not the 1991 journal article. ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex 2026-08-18T18:03Z: closed). +TEPP applies these sources to construct definition, score interpretation, reliability, validity evidence, uncertainty, consequences, longitudinal invariance, ESEM cross-loadings, and DSEM. Topic outputs are treated as fallible indicators or components only after their construct role is evaluated. Reflective, formative, and network classes remain distinct (Bollen & Lennox, 1991). Complete-data OLS loadings across posterior indicator draws are combined with Rubin (1996) \(T_m\); the arithmetic-mean helper remains a point estimate. Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-18T03:07Z: closed). The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title was opened 2026-08-17T12:04Z from archive.org; it is not the 1991 journal article and is not used as Mislevy plausible-value authority. Temporal precedence is not causal identification (Holland, 1986). Within/between OLS follows Enders and Tofighi (2007), Curran and Bauer (2011), and Hamaker et al. (2015). Enders and Tofighi (2007, Table 2, pp. 124–127; PDF opened 2026-08-17) show that the CWC cluster-mean coefficient is the contextual effect (`between − within`), not the between-cluster effect. Curran and Bauer (2011, pp. 607–608) reject person-mean subtraction on a raw autoregressive series as the lagged within-person residual; already-centered irregular residuals use the Voelkle et al. (2012, Eq. 7) / Driver et al. (2017, Eq. 3) scalar map. Discrete lags from unequal event intervals are remapped through that log-rate (Voelkle et al., 2012, ZORA accepted manuscript re-opened 2026-08-17T13:13Z) and are not pooled. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\) and restate the discrete intercept as a function of \(A\) and \(\Delta t\). A binary64 underflow of \(\exp(a\Delta t)\) to `+0` is refused because discrete auto-effects are strictly positive. The discrete effect of a constant predictor is Voelkle et al. (2012, Eq. 12; ZORA accepted manuscript re-opened 2026-08-17T14:20Z, Introducing Intercepts, manuscript p. 20), evaluated as \(a_{yx}(\operatorname{expm1}(z)/a_{xx})\) with \(z=a_{xx}\Delta t\) so a finite result is not lost when \(z\) overflows to \(-\infty\) or when \(a_{yx}\Delta t\) overflows, and in log space when `expm1(z)` overflows at a finite \(z\); a zero continuous effect is exactly zero; an overflowing \(a_{yx}/a_{xx}\) rewrite term fails closed; the first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. The discrete effect of a time-varying predictor with matched sampling and constancy intervals is Voelkle et al. (2012, Eq. 14; manuscript p. 21): \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product is not Eq. 12. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). The exact scalar discrete process noise is Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-18T14:04Z, p. 4): \(Q_{\Delta t}=0.5 q(\operatorname{expm1}(z)/a)\) with \(z=2(a\Delta t)\) for \(a\neq 0\) and \(q=GG^{\top}\ge 0\); do not form \(2a\) first; \(a=0\) recovers \(q\Delta t\); an overflowing rewrite scale \(0.5 q/a\) fails closed. This is not a Kalman filter. Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-18T14:04Z) write the same discrete intercept as \(A^{-1}[e^{A\Delta t}-I]\xi\). The lagged covariance is \(\mathrm{e}^{a\Delta t}p\) and the unconditional variance is \(\mathrm{e}^{2a\Delta t}p+Q_{\Delta t}\) (Driver et al., 2017, Eq. 3–4, pp. 4–5); a zero diffusion whose \(2(a\Delta t)\) overflows to \(+\infty\) fails closed. The stationary within-subject variance is the \(\Delta t\to\infty\) limit of Eq. 4: \(-q/(2a)\) for stable \(a<0\) (JSS p. 16 `asymDIFFUSION`; §4.3; PDF re-opened 2026-08-18T18:03Z). Finite-interval \(Q_{\Delta t}\) is not that limit. Trait-plus-state variance is \(\mathrm{trait}+\mathrm{state}\) and lagged covariance is \(\mathrm{trait}+\mathrm{e}^{a\Delta t}p\) (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z). Trait variance is not process noise and not `asymDIFFUSION`. Metric/weak invariance does not license latent-mean comparison. Meredith (1993) remains unread (Unpaywall/OpenAlex 2026-08-18T21:07Z: closed; Springer `content/pdf` is HTML 200). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex 2026-08-18T21:07Z: closed). ERIC ED334221 is Singer and Willett (1991), not the 1991 journal article. ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex 2026-08-18T21:07Z: closed). ## Structural, correlated, dynamic, relational, and multilingual topic models diff --git a/docs/validation/temporal-event-foundation.md b/docs/validation/temporal-event-foundation.md index a697bfe0..98c20753 100644 --- a/docs/validation/temporal-event-foundation.md +++ b/docs/validation/temporal-event-foundation.md @@ -23,7 +23,7 @@ This report tracks exact-head scientific and engineering evidence required befor | Truth corpora / manifests | `tepp_simulation` | implemented-main | — | deterministic generator tests | Task 10 / PR #18 | | Recovery metrics | `validation_core` | implemented-main | — | RMSE/bias/coverage/MC gates | Task 11 / PR #19 | | Versioned API/export contracts | `tepp_api` | implemented-main | naruon HTTP interchange | unknown-field/version/limit + naruon HTTPS interchange tests | Task 12 / PR #21; live HTTP service remaining | -| Psychometric structural input gates | `psychometric_core` | partial | stacked psychometric PR | construct-class refusal + ALR/ILR boundary + true-loading RMSE + posterior-draw point-estimate mean + Rubin `T` + CWC within/between + CWC contextual effect + event-time log-rate + constant- and time-varying-predictor discrete effects + exact scalar discrete process noise + lagged latent covariance and unconditional latent variance + irregular already-centered residual lag + strong-gated latent means; full ESEM/DSEM remaining | ADR 0005; `docs/research/posterior-esem-input-gates.md`; `docs/research/multilevel-event-time-recovery.md`; `docs/research/rubin-total-variance.md`; `docs/research/strong-invariance-latent-means.md` | +| Psychometric structural input gates | `psychometric_core` | partial | stacked psychometric PR | construct-class refusal + ALR/ILR boundary + true-loading RMSE + posterior-draw point-estimate mean + Rubin `T` + CWC within/between + CWC contextual effect + event-time log-rate + constant- and time-varying-predictor discrete effects + exact scalar discrete process noise + lagged latent covariance and unconditional latent variance + stationary within-subject variance + trait-plus-state variance + irregular already-centered residual lag + strong-gated latent means; full ESEM/DSEM remaining | ADR 0005; `docs/research/posterior-esem-input-gates.md`; `docs/research/multilevel-event-time-recovery.md`; `docs/research/rubin-total-variance.md`; `docs/research/strong-invariance-latent-means.md` | | CWL modular connectors | `docs/connectors/*` | implemented-main | — | contract docs + examples | PR #22; live HTTP ports remaining | | Release SBOM/provenance generator | `scripts/release_evidence.py` | partial | — | generate+validate in CI | Task 13 partial / PR #28 | From 17fe4f7db475c33b444e51cf13038f2c77befe83 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 00:17:38 +0000 Subject: [PATCH 25/62] feat(psychometric): form Driver Eq. 4 stationary ratio first MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Driver, Oud, and Voelkle (2017, Eq. 4, p. 5; JSS PDF re-opened 2026-08-19T00:14Z). The Δt→∞ limit is -q/(2a). Form (q/a)*-0.5. Do not form 2a first (a = -1e308 overflows). Do not form 0.5 q first (q = from_bits(1), a = -from_bits(1) underflows to +0; representable solution is 0.5). CodeRabbit finding on 556e23d. Still not a Kalman filter, not DSEM, and not a matrix expm. Meredith (1993) and Mislevy (1991) remain unread. --- CHANGELOG.md | 1 + CLAUDE.md | 2 +- crates/psychometric_core/src/event_time.rs | 33 +++++++++++++------ ...multilevel_event_time_recovery_contract.rs | 7 +++- .../scientific_claim_boundary_contract.rs | 5 +++ docs/adr/0005-posterior-esem-dsem.md | 2 +- .../multilevel-event-time-recovery.md | 6 ++-- 7 files changed, 40 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19011f87..ccae05c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang ## [Unreleased] +- `psychometric_core` evaluates the Driver, Oud, and Voelkle (2017, Eq. 4, p. 5; JSS PDF re-opened 2026-08-19T00:14Z) stationary within-subject variance as `(q / a) * -0.5`. The paper limit is `-q / (2 a)`. Forming `2 a` first overflows at `a = -1e308`. Forming `0.5 q` first underflows at `q = from_bits(1)`, `a = -from_bits(1)` and returns `+0` (CodeRabbit finding on `556e23d`); the representable Lyapunov solution is `0.5`. Still not a Kalman filter, not DSEM, not a matrix `expm`, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall/OpenAlex 2026-08-19T00:14Z: closed; Springer `content/pdf` is HTML 200). Oud and Jansen (2000) remains unread. ZORA Anubis-blocked. - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z) scalar trait-plus-state latent variance and lagged covariance. A stable trait process has `DRIFT` and `DIFFUSION` fixed to zero, so `Var = trait + state` and `cov(t, t-1) = trait + exp(a Δt) p`. The ctsem `TRAITVAR` rewrite that adds the trait to `DIFFUSION` does not license treating trait variance as process noise. Trait variance is not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not this map. Still not RI-CLPM, not a Kalman filter, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall/OpenAlex 2026-08-18T21:07Z: closed). Oud and Jansen (2000) remains unread. ZORA accepted manuscript re-opened via bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`. - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 4, p. 5; §4.3 pp. 9–10; p. 16 `asymDIFFUSION`; JSS PDF re-opened 2026-08-18T18:03Z) scalar stationary within-subject variance. Eq. 4 writes `Q_Δt = irow(A#^{-1}[e^{A# Δt} − I] row(Q))` with `A# = A ⊗ I + I ⊗ A`. The scalar Kronecker sum is `2 a`. As `Δt → ∞` with stable `a < 0`, that limit is `-q / (2 a)`. Form `-0.5 q / a`; do not form `2 a` first (`a = -1e308`, `q = 1e308` → `0.5`). Starting from that variance, `Var(η_t)` is invariant across finite event intervals. A zero diffusion is exactly zero. `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not `asymDIFFUSION`. Still not a Kalman filter, not DSEM, not a matrix `expm`, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall/OpenAlex 2026-08-18T18:03Z: closed). Oud and Jansen (2000) remains unread. ZORA Anubis-blocked. - `psychometric_core` refuses a zero-diffusion Driver, Oud, and Voelkle (2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T14:04Z) latent variance when `2 (a Δt)` overflows to `+∞`. Zero diffusion is exactly `Q_Δt = 0` (Eq. 3 integral of a zero `G`). That skip does not license `Var(η_t) = exp(2 a Δt) p + 0` when the carried term is non-finite (`p = 2`, `q = 0`, `a = 1e308`, `Δt = 2`). Nightly uncovered production line `event_time.rs:596` on predecessor `321568a` is this arm. Still not a Kalman filter, not DSEM, and not a matrix `expm`. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-18T14:04Z: closed; Springer `content/pdf` is HTML 200). Oud and Jansen (2000) remains unread. diff --git a/CLAUDE.md b/CLAUDE.md index ce4467b2..f8148f00 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. -- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). Form `-0.5 q / a`. `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). Form `(q / a) * -0.5`. Do not form `2 a` first. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index 6eaaa46f..f080ce5a 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -611,19 +611,22 @@ pub fn recover_discrete_latent_variance( /// Exact scalar stationary within-subject variance on event time. /// /// Driver, Oud, and Voelkle (2017, Eq. 4, p. 5; JSS PDF re-opened -/// 2026-08-18T18:03Z) write `Q_Δt` as +/// 2026-08-19T00:14Z) write `Q_Δt` as /// `irow(A#^{-1}[e^{A# Δt} − I] row(Q))` with `A# = A ⊗ I + I ⊗ A`. /// The scalar Kronecker sum is `2 a`. As `Δt → ∞` with stable /// `a < 0`, `e^{2 a Δt} → 0` and Eq. 4 becomes `-q / (2 a)`. The /// JSS summary names that limit `asymDIFFUSION` and takes it as the /// total within-subject variance (p. 16). Section 4.3 (pp. 9–10) /// constrains a stationary `T0VAR` to that same model-predicted -/// variance. Form `-0.5 q / a`. Do not form `2 a` first: at +/// variance. Form `(q / a) * -0.5`. Do not form `2 a` first: at /// `a = -1e308`, `q = 1e308`, `2 a` overflows and `-q / (2 a)` -/// collapses to `+0`, but `-0.5 q / a = 0.5`. A zero diffusion is +/// collapses to `+0`, but `(q / a) * -0.5 = 0.5`. Do not form +/// `0.5 q` first: at `q = from_bits(1)`, `a = -from_bits(1)`, +/// `-0.5 * q` underflows to `-0` and the quotient is `+0`, but +/// the representable Lyapunov solution is `0.5`. A zero diffusion is /// exactly zero. `a ≥ 0` has no finite stationary variance /// (including Brownian `a = 0`, whose variance grows as `q Δt`). -/// An overflowing `-0.5 q / a` fails closed. This is not a Kalman +/// An overflowing `(q / a) * -0.5` fails closed. This is not a Kalman /// filter, not DSEM, not a matrix `expm`, and not ctsem estimation. /// /// # Errors @@ -653,8 +656,9 @@ pub fn recover_stationary_latent_variance( if continuous_diffusion == 0.0 { return Ok(0.0); } - // −q / (2 a) = −0.5 q / a. Do not form 2 a first. - require_finite(-0.5 * continuous_diffusion / log_rate) + // −q / (2 a). Form the ratio first. Do not form 2 a (overflows + // at |a| = 1e308). Do not form 0.5 q (underflows at min subnormal). + require_finite((continuous_diffusion / log_rate) * -0.5) } /// Refuse treating finite-interval process noise as `asymDIFFUSION`. @@ -1711,7 +1715,7 @@ mod tests { let drift = -0.5_f64; let recovered = recover_stationary_latent_variance(diffusion, drift, LagClock::EventTime) .expect("asym"); - let expected = -0.5 * diffusion / drift; + let expected = (diffusion / drift) * -0.5; assert!((recovered - expected).abs() < 1e-15); assert!((recovered - 0.4).abs() < 1e-15); // Starting from p_∞, Var(η_t) is invariant across finite Δt. @@ -1745,7 +1749,7 @@ mod tests { recover_stationary_latent_variance(0.0, drift, LagClock::EventTime), Ok(0.0) ); - // Do not form 2 a first: 2*(-1e308) overflows; -0.5 q / a is 0.5. + // Do not form 2 a first: 2*(-1e308) overflows; (q/a)*-0.5 is 0.5. let twice_rate_overflow = recover_stationary_latent_variance(1e308, -1e308, LagClock::EventTime) .expect("2a overflow"); @@ -1753,6 +1757,15 @@ mod tests { assert!(!(2.0 * -1e308_f64).is_finite()); let lost = -1e308_f64 / (2.0 * -1e308_f64); assert!(lost.abs() < 1e-15); + // Do not form 0.5 q first: 0.5 * from_bits(1) underflows. + let min_subnormal = f64::from_bits(1); + assert!((0.5 * min_subnormal).abs() < 1e-300); + assert!((-0.5 * min_subnormal / -min_subnormal).abs() < 1e-300); + let subnormal_ratio = + recover_stationary_latent_variance(min_subnormal, -min_subnormal, LagClock::EventTime) + .expect("subnormal ratio"); + assert!((subnormal_ratio - 0.5).abs() < 1e-15); + assert!(((min_subnormal / -min_subnormal) * -0.5 - 0.5).abs() < 1e-15); } #[test] @@ -1785,8 +1798,8 @@ mod tests { recover_stationary_latent_variance(0.4, f64::NAN, LagClock::EventTime), Err(PsychometricError::InvalidNumericInput) ); - // −0.5 q / a overflows when q is huge relative to |a|. - assert!(!(-0.5 * 1e308_f64 / -1e-10_f64).is_finite()); + // (q / a) * -0.5 overflows when q is huge relative to |a|. + assert!(!((1e308_f64 / -1e-10_f64) * -0.5).is_finite()); assert_eq!( recover_stationary_latent_variance(1e308, -1e-10, LagClock::EventTime), Err(PsychometricError::InvalidNumericInput) 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 160ebbf2..ae711c0b 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -653,7 +653,7 @@ fn stationary_variance_recovers_driver_equation_four_asymptote() { let drift = -0.5_f64; let stationary = recover_stationary_latent_variance(diffusion, drift, LagClock::EventTime).expect("asym"); - let expected = -0.5 * diffusion / drift; + let expected = (diffusion / drift) * -0.5; let error = rmse(&[expected], &[stationary]); assert!(error < 1e-15, "Driver Eq. 4 asymDIFFUSION RMSE {error}"); for delta in [0.5_f64, 1.0, 2.0, 10.0] { @@ -694,6 +694,11 @@ fn stationary_variance_recovers_driver_equation_four_asymptote() { recover_stationary_latent_variance(1e308, -1e308, LagClock::EventTime), Ok(0.5) ); + let min_subnormal = f64::from_bits(1); + assert_eq!( + recover_stationary_latent_variance(min_subnormal, -min_subnormal, LagClock::EventTime), + Ok(0.5) + ); assert_eq!( recover_stationary_latent_variance(diffusion, drift, LagClock::SystemTime), 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 231b567e..95266239 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -270,6 +270,11 @@ fn finite_interval_process_noise_is_not_the_stationary_variance() { recover_stationary_latent_variance(diffusion, 0.0, LagClock::EventTime), Err(psychometric_core::PsychometricError::StationaryVarianceRequiresStableDrift) ); + let min_subnormal = f64::from_bits(1); + let subnormal_ratio = + recover_stationary_latent_variance(min_subnormal, -min_subnormal, LagClock::EventTime) + .expect("subnormal ratio"); + assert!((subnormal_ratio - 0.5).abs() < 1e-15); } #[test] diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index d3eca1d7..8c8d0c82 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; form `-0.5 q / a`; do not form `2 a` first; `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; form `(q / a) * -0.5`; do not form `2 a` first; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. 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 ce72bb01..4f2174cc 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -43,7 +43,7 @@ Kish, L. (1965). *Survey sampling*. John Wiley & Sons. Oud, J. H. L., & Jansen, R. A. R. G. (2000). Continuous time state space modeling of panel data by means of SEM. *Psychometrika, 65*(2), 199–215. https://doi.org/10.1007/BF02294374 (cited by Voelkle et al., 2012, Eq. 14 discussion; PDF not opened). -The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:07Z (https://www.zora.uzh.ch/handle/20.500.14742/72792; bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`). Page 16 writes discrete auto-effects as \(A^{*}(\Delta t)=\exp(A\Delta t)\) (Eq. 7). Introducing Intercepts (manuscript p. 20, Eq. 12) adds a continuous-time intercept \(b\) and writes the expected-value solution whose discrete increment is \(A^{-1}(\exp(A\Delta t)-I)b\); the scalar constant-predictor effect is \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). The next paragraph (manuscript p. 21, Eq. 14) writes the discrete effect of a **time-varying** predictor, when the sampling interval equals the constancy interval, as \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product does not depend on the predictor auto-effect. The manuscript calls Eq. 14 a first-order approximation that deteriorates as \(\Delta t\) grows, and defers unmatched sampling/constancy intervals to Oud and Jansen (2000), which is unread. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4; JSS PDF re-opened 2026-08-18T21:07Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\), the discrete intercept \(b_{\Delta t}=A^{-1}[A_{\Delta t}-I]b\), and the discrete process-noise covariance \(Q_{\Delta t}=\int_{0}^{\Delta t}\operatorname{expm}(A(\Delta t-\tau))LGG^{\top}L^{\top}\operatorname{expm}(A(\Delta t-\tau))^{\top}\,d\tau\). Equation 4 (p. 5) writes that the integral exhibits covariance \(Q_{\Delta t}=\operatorname{irow}(A^{\#^{-1}}[e^{A^{\#}\Delta t}-I]\operatorname{row}(Q))\) with \(A^{\#}=A\otimes I+I\otimes A\). The homogeneous-process consequence (`ξ`, `z` given) is \(Q_{\Delta t}=\operatorname{cov}(\eta_{ti}\mid\eta_{t-1,i})\) and \(\operatorname{cov}(\eta_{ti},\eta_{t-1,i})=A_{\Delta t}\operatorname{cov}(\eta_{t-1,i})\). The law of total variance on that pair is \(\operatorname{Var}(\eta_{ti})=A_{\Delta t}\operatorname{Var}(\eta_{t-1,i})A_{\Delta t}^{\top}+Q_{\Delta t}\). As \(\Delta t\to\infty\) with stable \(a<0\), Eq. 4 becomes \(-q/(2a)\). The JSS summary names that limit `asymDIFFUSION` and takes it as the total within-subject variance (p. 16). Section 4.3 (pp. 9–10) constrains a stationary `T0VAR` to that model-predicted variance and distinguishes it from a predetermined first occasion. The same section (p. 9) adds a stable trait process with `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is that time-invariant between-subject variance. The JSS article has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). This slice takes scalar \(L=1\) (every latent subject to system noise); it does not implement the 0/1 selector matrix and is not a Kalman filter. Discrete auto-effects are strictly positive for finite real drift and interval; the inverse \(a=\ln\varphi/\Delta t\) therefore requires \(\varphi>0\). Binary64 `exp` of a large negative argument is `+0` and is refused as a discrete lag; the same underflow is a vanishing lagged covariance and is kept. Equations 3–4 of Voelkle et al. (2012) are the discouraged difference-quotient approximation. Meredith (1993) remains unread (Unpaywall/OpenAlex 2026-08-18T21:07Z: closed; Springer `content/pdf` is HTML 200, not a PDF; archive.org title search empty). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex 2026-08-18T21:07Z: closed; Springer `content/pdf` is HTML 200; ETS landing page is HTML). ERIC ED334221 is Singer and Willett (1991), *From whether to when*, not the 1991 journal article. ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title is not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex 2026-08-18T21:07Z: closed; Radboud landing and bitstream 403). +The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:07Z (https://www.zora.uzh.ch/handle/20.500.14742/72792; bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`). Page 16 writes discrete auto-effects as \(A^{*}(\Delta t)=\exp(A\Delta t)\) (Eq. 7). Introducing Intercepts (manuscript p. 20, Eq. 12) adds a continuous-time intercept \(b\) and writes the expected-value solution whose discrete increment is \(A^{-1}(\exp(A\Delta t)-I)b\); the scalar constant-predictor effect is \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). The next paragraph (manuscript p. 21, Eq. 14) writes the discrete effect of a **time-varying** predictor, when the sampling interval equals the constancy interval, as \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product does not depend on the predictor auto-effect. The manuscript calls Eq. 14 a first-order approximation that deteriorates as \(\Delta t\) grows, and defers unmatched sampling/constancy intervals to Oud and Jansen (2000), which is unread. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4; JSS PDF re-opened 2026-08-18T21:07Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\), the discrete intercept \(b_{\Delta t}=A^{-1}[A_{\Delta t}-I]b\), and the discrete process-noise covariance \(Q_{\Delta t}=\int_{0}^{\Delta t}\operatorname{expm}(A(\Delta t-\tau))LGG^{\top}L^{\top}\operatorname{expm}(A(\Delta t-\tau))^{\top}\,d\tau\). Equation 4 (p. 5) writes that the integral exhibits covariance \(Q_{\Delta t}=\operatorname{irow}(A^{\#^{-1}}[e^{A^{\#}\Delta t}-I]\operatorname{row}(Q))\) with \(A^{\#}=A\otimes I+I\otimes A\). The homogeneous-process consequence (`ξ`, `z` given) is \(Q_{\Delta t}=\operatorname{cov}(\eta_{ti}\mid\eta_{t-1,i})\) and \(\operatorname{cov}(\eta_{ti},\eta_{t-1,i})=A_{\Delta t}\operatorname{cov}(\eta_{t-1,i})\). The law of total variance on that pair is \(\operatorname{Var}(\eta_{ti})=A_{\Delta t}\operatorname{Var}(\eta_{t-1,i})A_{\Delta t}^{\top}+Q_{\Delta t}\). As \(\Delta t\to\infty\) with stable \(a<0\), Eq. 4 becomes \(-q/(2a)\). The JSS summary names that limit `asymDIFFUSION` and takes it as the total within-subject variance (p. 16). Section 4.3 (pp. 9–10) constrains a stationary `T0VAR` to that model-predicted variance and distinguishes it from a predetermined first occasion. The same section (p. 9) adds a stable trait process with `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is that time-invariant between-subject variance. The JSS article has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). This slice takes scalar \(L=1\) (every latent subject to system noise); it does not implement the 0/1 selector matrix and is not a Kalman filter. Discrete auto-effects are strictly positive for finite real drift and interval; the inverse \(a=\ln\varphi/\Delta t\) therefore requires \(\varphi>0\). Binary64 `exp` of a large negative argument is `+0` and is refused as a discrete lag; the same underflow is a vanishing lagged covariance and is kept. Equations 3–4 of Voelkle et al. (2012) are the discouraged difference-quotient approximation. Meredith (1993) remains unread (Unpaywall/OpenAlex 2026-08-19T00:14Z: closed; Springer `content/pdf` is HTML 200, not a PDF; archive.org title search empty). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex 2026-08-19T00:14Z: closed; Springer `content/pdf` is HTML 200; ETS landing page is HTML). ERIC ED334221 is Singer and Willett (1991), *From whether to when*, not the 1991 journal article. ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title is not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex 2026-08-18T21:07Z: closed; Radboud landing and bitstream 403). ## Formula notes @@ -57,7 +57,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - **Discrete process noise.** Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-17T21:03Z, p. 4): \(Q_{\Delta t}=\int_{0}^{\Delta t}\operatorname{expm}(A(\Delta t-\tau))GG^{\top}\operatorname{expm}(A(\Delta t-\tau))^{\top}\,d\tau\). The scalar closed form with continuous diffusion \(q=GG^{\top}\ge 0\) is \(q(\mathrm{e}^{2a\Delta t}-1)/(2a)\) for \(a\neq 0\) and \(q\Delta t\) for \(a=0\). The algebraically identical finite-`expm1` evaluation is \(0.5 q(\operatorname{expm1}(z)/a)\) with \(z=2(a\Delta t)\). Form \(z\) as twice the product \(a\Delta t\). Forming \(2a\) first overflows when \(|a|\) is at the binary64 extreme even if \(a\Delta t\) and \(Q_{\Delta t}\) are finite. Binary64 underflow of \(z\) to `+0` recovers \(q\Delta t\). \(z\to-\infty\) keeps the equilibrium variance \(-q/(2a)=-0.5 q/a\); `expm1(−∞)` is \(-1\), so that path stays on the finite increment. When `expm1(z)` overflows at a finite \(z\), rewrite as \(\operatorname{sign}(q/a)\exp(\ln|q|+z-\ln|a|-\ln 2)-0.5 q/a\). An overflowing rewrite scale \(0.5 q/a\) is not a finite \(Q_{\Delta t}\) and fails closed (`q=1e308`, `a=0.1`, `Δt=4000` → `z=800`; JSS PDF re-opened 2026-08-18T03:07Z, p. 4). A zero diffusion is exactly zero even if `expm1` overflows. \(z\to+\infty\) fails closed unless \(q=0\). Negative \(q\) fails closed. This is not a Kalman filter and not a matrix `expm`. - **Lagged latent covariance.** Driver, Oud, and Voelkle (2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T11:20Z): \(\operatorname{cov}(\eta_{ti},\eta_{t-1,i})=A_{\Delta t}\operatorname{cov}(\eta_{t-1,i})\) for the homogeneous process. The scalar map is \(\mathrm{e}^{a\Delta t}p\) with prior variance \(p\ge 0\). This is not \(Q_{\Delta t}\). Binary64 underflow of \(\mathrm{e}^{a\Delta t}\) to `+0` is a vanishing covariance and is kept. A zero prior variance is exactly zero. Finite-\(a\Delta t\) exponential overflow rewrites as \(\exp(\ln p+a\Delta t)\). A finite \(\mathrm{e}^{a\Delta t}\) whose product with \(p\) overflows fails closed. The JSS article has no numbered §2.2. - **Discrete latent variance.** Equations 3–4 write \(Q_{\Delta t}\) as the covariance of the stochastic integral, so \(Q_{\Delta t}=\operatorname{cov}(\eta_{ti}\mid\eta_{t-1,i})\) when \(\xi\) and \(z\) are given. The law of total variance on that pair is \(\operatorname{Var}(\eta_{ti})=A_{\Delta t}\operatorname{Var}(\eta_{t-1,i})A_{\Delta t}^{\top}+Q_{\Delta t}\). The scalar map is \(\mathrm{e}^{2a\Delta t}p+Q_{\Delta t}\). This is not a Kalman measurement update. A zero prior variance is exactly \(Q_{\Delta t}\). Binary64 underflow of \(\mathrm{e}^{2a\Delta t}\) keeps \(Q_{\Delta t}\). Finite-\(z\) exponential overflow rewrites as \(\exp(\ln p+z)+Q_{\Delta t}\). A zero diffusion is exactly \(Q_{\Delta t}=0\); that skip does not license a non-finite carried term when \(2(a\Delta t)\) overflows to \(+\infty\). Treating \(Q_{\Delta t}\) as \(\operatorname{Var}(\eta_{t})\) fails closed. -- **Stationary within-subject variance.** Driver et al. (2017, Eq. 4 as \(\Delta t\to\infty\); §4.3 pp. 9–10; p. 16 `asymDIFFUSION`): for stable \(a<0\), \(\lim_{\Delta t\to\infty}Q_{\Delta t}=-q/(2a)\). The algebraically identical evaluation is \(-0.5 q/a\). Forming \(2a\) first overflows when \(|a|\) is at the binary64 extreme (`a=-1e308`, `q=1e308` → `0.5`). Starting from that variance, \(\operatorname{Var}(\eta_{t})\) is invariant across finite event intervals. A zero diffusion is exactly zero. \(a\ge 0\) has no finite stationary variance (Brownian \(a=0\) grows as \(q\Delta t\)). An overflowing \(-0.5 q/a\) fails closed. Finite-interval \(Q_{\Delta t}\) is not that limit. This is not ctsem estimation and not a Kalman filter. +- **Stationary within-subject variance.** Driver et al. (2017, Eq. 4 as \(\Delta t\to\infty\); §4.3 pp. 9–10; p. 16 `asymDIFFUSION`; JSS PDF re-opened 2026-08-19T00:14Z, p. 5): for stable \(a<0\), \(\lim_{\Delta t\to\infty}Q_{\Delta t}=-q/(2a)\). Form the ratio \((q/a)\times-0.5\). Forming \(2a\) first overflows when \(|a|\) is at the binary64 extreme (`a=-1e308`, `q=1e308` → `0.5`). Forming \(0.5q\) first underflows at the minimum subnormal (`q=from_bits(1)`, `a=-from_bits(1)` → naive `+0`; representable solution `0.5`). Starting from that variance, \(\operatorname{Var}(\eta_{t})\) is invariant across finite event intervals. A zero diffusion is exactly zero. \(a\ge 0\) has no finite stationary variance (Brownian \(a=0\) grows as \(q\Delta t\)). An overflowing \((q/a)\times-0.5\) fails closed. Finite-interval \(Q_{\Delta t}\) is not that limit. This is not ctsem estimation and not a Kalman filter. - **Trait-plus-state variance.** Driver et al. (2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z): a stable trait process has `DRIFT` and `DIFFUSION` fixed to zero. The scalar maps are \(\operatorname{Var}=\mathrm{trait}+\mathrm{state}\) and \(\operatorname{cov}(t,t-1)=\mathrm{trait}+\mathrm{e}^{a\Delta t}p\). The ctsem `TRAITVAR` rewrite that adds the trait to `DIFFUSION` does not license treating trait variance as \(Q_{\Delta t}\). Trait variance is not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not this map. A zero trait is exactly the state. A zero state is exactly the trait. An overflowing sum fails closed. This is not RI-CLPM and not ctsem estimation. - **CWC-then-lag.** Sample cluster means are removed first. Consecutive within residuals are then fitted by least squares to \(r_{t+\Delta t}\approx\exp(a\Delta t)\,r_{t}\) on event time. Same-sign pair-wise logs initialize the scalar Newton step. Sign-flipping \(T=2\) CWC pairs have no real logarithm and fail closed. Curran and Bauer (2011, pp. 607–608) show that this person-mean subtraction on a raw autoregressive series does **not** isolate the lagged within-person effect; the helper therefore does not claim to recover the raw-process drift. - **Already-centered irregular residual.** The caller supplies lagged within residuals. The mean of \(a=\ln(r_{t+\Delta t}/r_t)/\Delta t\) is the exact scalar map. Intervals may be irregular. The helper does not center again. This is not DSEM. @@ -74,7 +74,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - Voelkle et al. (2012, Eq. 14) recovers \(a_{yx}\Delta t\) at machine-scale RMSE when sampling, constancy, and event intervals match, and that value is not the Eq. 12 constant-predictor effect; unmatched intervals, a non-event clock, a non-finite continuous effect, and an overflowing product fail closed; - Driver et al. (2017, Eq. 3) recovers a known scalar \(Q_{\Delta t}\) at machine-scale RMSE, and that RMSE is smaller than treating the continuous diffusion as the discrete process noise; \(a=0\) and binary64 underflow of \(2a\Delta t\) recover \(q\Delta t\); \(z\to-\infty\) recovers \(-q/(2a)\); a finite result whose `expm1` overflows at a finite \(z\) is recovered in log space; a finite result whose \(2a\) overflows while \(a\Delta t\) stays finite is recovered as \(0.5 q(\operatorname{expm1}(z)/a)\); a zero diffusion is exactly zero; a negative diffusion, \(z\to+\infty\), and an overflowing rewrite scale \(0.5 q/a\) fail closed; non-finite event interval, non-positive sampling interval, and non-finite constancy interval fail closed on Eq. 14; non-finite / non-positive event interval, non-finite / negative diffusion, and non-finite log-rate fail closed on Eq. 3; - Driver et al. (2017, Eq. 3–4) recovers a known lagged covariance \(\mathrm{e}^{a\Delta t}p\) and a known latent variance \(\mathrm{e}^{2a\Delta t}p+Q_{\Delta t}\) at machine-scale RMSE, and that variance RMSE is smaller than treating \(Q_{\Delta t}\) as \(\operatorname{Var}(\eta_{t})\); a zero prior variance is exactly zero (lagged) or \(Q_{\Delta t}\) (unconditional); binary64 underflow of \(\mathrm{e}^{a\Delta t}\) keeps a vanishing lagged covariance; a finite \(\mathrm{e}^{a\Delta t}\) whose product with \(p\) overflows fails closed; a zero diffusion whose \(2(a\Delta t)\) overflows to \(+\infty\) fails closed; treating \(Q_{\Delta t}\) as the unconditional variance fails closed; -- Driver et al. (2017, Eq. 4 as \(\Delta t\to\infty\); p. 16 `asymDIFFUSION`; §4.3) recovers a known stationary variance \(-q/(2a)\) at machine-scale RMSE, and that RMSE is smaller than treating finite-interval \(Q_{\Delta t}\) as the stationary variance; starting from that variance, \(\operatorname{Var}(\eta_{t})\) is invariant across finite \(\Delta t\); a zero diffusion is exactly zero; forming \(2a\) first overflows while \(-0.5 q/a\) stays finite; \(a\ge 0\), a non-event clock, a negative diffusion, and an overflowing \(-0.5 q/a\) fail closed; +- Driver et al. (2017, Eq. 4 as \(\Delta t\to\infty\); p. 16 `asymDIFFUSION`; §4.3) recovers a known stationary variance \(-q/(2a)\) at machine-scale RMSE, and that RMSE is smaller than treating finite-interval \(Q_{\Delta t}\) as the stationary variance; starting from that variance, \(\operatorname{Var}(\eta_{t})\) is invariant across finite \(\Delta t\); a zero diffusion is exactly zero; forming \(2a\) first overflows while \((q/a)\times-0.5\) stays finite; forming \(0.5q\) first underflows at the minimum subnormal while \((q/a)\times-0.5=0.5\); \(a\ge 0\), a non-event clock, a negative diffusion, and an overflowing \((q/a)\times-0.5\) fail closed; - Driver et al. (2017, §4.3, p. 9) recovers a known trait-plus-state variance and lagged covariance at machine-scale RMSE, and that RMSE is smaller than evolving the summed variance as if it were all state; a zero trait is the state; a zero state is the trait; treating trait variance as process noise or as `asymDIFFUSION` fails closed; an overflowing sum and a non-event clock fail closed; - pooling discrete lags from unequal intervals fails closed; - CWC-then-lag on a two-cluster decaying series has smaller computed RMSE than a level-pooled series when the latter is identified; From 75ecdd3d43079381174302b87c1c966ee584cbd2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 00:22:39 +0000 Subject: [PATCH 26/62] feat(psychometric): recover Driver Eq. 1 observed-indicator variance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Driver, Oud, and Voelkle (2017, Eq. 1, p. 4; p. 16 MANIFESTVAR; JSS PDF re-opened 2026-08-19T00:14Z). y = Λη + τ + ε with ε ~ N(0, Θ), so Var(y) = λ² Var(η) + θ. Form (λ p) λ then add θ. Do not form λ² first (λ = 1e308, p = 1e-308 → 1e308). MANIFESTVAR is not Var(y). Var(η) is not Var(y). Still not a Kalman filter, not ESEM, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991) remain unread. --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 1 + CLAUDE.md | 2 +- crates/psychometric_core/src/error.rs | 20 +++ crates/psychometric_core/src/event_time.rs | 158 +++++++++++++++++- crates/psychometric_core/src/lib.rs | 10 +- ...multilevel_event_time_recovery_contract.rs | 50 +++++- .../scientific_claim_boundary_contract.rs | 30 +++- docs/TRACEABILITY.md | 2 +- docs/adr/0005-posterior-esem-dsem.md | 4 +- docs/adr/README.md | 2 +- .../multilevel-event-time-recovery.md | 15 +- docs/validation/temporal-event-foundation.md | 2 +- 13 files changed, 275 insertions(+), 23 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index b7e195e4..a087c12c 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -61,7 +61,7 @@ boundaries above remain the target modular MSA architecture. | `tepp_simulation` | known-truth temporal/event data generation | | `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics | | `tepp_api` | versioned DTO, schema, and export contracts | -| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means | +| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance (Driver et al., 2017, Eq. 1; `MANIFESTVAR` is not `Var(y)`), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means | No crate exposes placeholder production behavior in Task 1. This prevents an empty façade from becoming a de facto public API before its invariants and tests diff --git a/CHANGELOG.md b/CHANGELOG.md index ccae05c5..ab732246 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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, Eq. 1, p. 4; p. 16 `MANIFESTVAR`; JSS PDF re-opened 2026-08-19T00:14Z) scalar observed-indicator variance. Equation 1 writes `y_i(t) = Λ η_i(t) + τ + ε_i(t)` with `ε ~ N(0, Θ)`. The scalar map is `Var(y) = λ² Var(η) + θ`. Form `(λ p) λ` then add `θ`. Do not form `λ²` first (`λ = 1e308`, `p = 1e-308` → `1e308`). `MANIFESTVAR` is not `Var(y)`. `Var(η)` is not `Var(y)`. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall/OpenAlex 2026-08-19T00:14Z: closed). Oud and Jansen (2000) remains unread. ZORA Anubis-blocked. - `psychometric_core` evaluates the Driver, Oud, and Voelkle (2017, Eq. 4, p. 5; JSS PDF re-opened 2026-08-19T00:14Z) stationary within-subject variance as `(q / a) * -0.5`. The paper limit is `-q / (2 a)`. Forming `2 a` first overflows at `a = -1e308`. Forming `0.5 q` first underflows at `q = from_bits(1)`, `a = -from_bits(1)` and returns `+0` (CodeRabbit finding on `556e23d`); the representable Lyapunov solution is `0.5`. Still not a Kalman filter, not DSEM, not a matrix `expm`, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall/OpenAlex 2026-08-19T00:14Z: closed; Springer `content/pdf` is HTML 200). Oud and Jansen (2000) remains unread. ZORA Anubis-blocked. - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z) scalar trait-plus-state latent variance and lagged covariance. A stable trait process has `DRIFT` and `DIFFUSION` fixed to zero, so `Var = trait + state` and `cov(t, t-1) = trait + exp(a Δt) p`. The ctsem `TRAITVAR` rewrite that adds the trait to `DIFFUSION` does not license treating trait variance as process noise. Trait variance is not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not this map. Still not RI-CLPM, not a Kalman filter, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall/OpenAlex 2026-08-18T21:07Z: closed). Oud and Jansen (2000) remains unread. ZORA accepted manuscript re-opened via bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`. - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 4, p. 5; §4.3 pp. 9–10; p. 16 `asymDIFFUSION`; JSS PDF re-opened 2026-08-18T18:03Z) scalar stationary within-subject variance. Eq. 4 writes `Q_Δt = irow(A#^{-1}[e^{A# Δt} − I] row(Q))` with `A# = A ⊗ I + I ⊗ A`. The scalar Kronecker sum is `2 a`. As `Δt → ∞` with stable `a < 0`, that limit is `-q / (2 a)`. Form `-0.5 q / a`; do not form `2 a` first (`a = -1e308`, `q = 1e308` → `0.5`). Starting from that variance, `Var(η_t)` is invariant across finite event intervals. A zero diffusion is exactly zero. `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not `asymDIFFUSION`. Still not a Kalman filter, not DSEM, not a matrix `expm`, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall/OpenAlex 2026-08-18T18:03Z: closed). Oud and Jansen (2000) remains unread. ZORA Anubis-blocked. diff --git a/CLAUDE.md b/CLAUDE.md index f8148f00..1cab9917 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. -- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). Form `(q / a) * -0.5`. Do not form `2 a` first. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). Form `(q / a) * -0.5`. Do not form `2 a` first. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` (Driver et al., 2017, Eq. 1, p. 4; p. 16 `MANIFESTVAR`). Form `(λ p) λ` then add `θ`. `MANIFESTVAR` is not `Var(y)`. `Var(η)` is not `Var(y)`. - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. diff --git a/crates/psychometric_core/src/error.rs b/crates/psychometric_core/src/error.rs index 1f1cd2c5..86ce9a6b 100644 --- a/crates/psychometric_core/src/error.rs +++ b/crates/psychometric_core/src/error.rs @@ -62,6 +62,12 @@ pub enum PsychometricError { /// within-subject variance. `TRAITVAR` is between-subject and /// time-invariant; `asymDIFFUSION` is the `Δt → ∞` state variance. TraitVarianceIsNotStationaryWithinSubject, + /// Driver Eq. 1 measurement-error variance was treated as the + /// observed-indicator variance. `MANIFESTVAR` is `Θ`, not `Var(y)`. + MeasurementErrorIsNotObservedVariance, + /// Driver Eq. 1 latent variance was treated as the observed-indicator + /// variance. `Var(η)` is not `Var(y)`. + LatentVarianceIsNotObservedVariance, } impl fmt::Display for PsychometricError { @@ -114,6 +120,12 @@ impl fmt::Display for PsychometricError { Self::TraitVarianceIsNotStationaryWithinSubject => { "trait variance is not the stationary within-subject variance" } + Self::MeasurementErrorIsNotObservedVariance => { + "measurement-error variance is not the observed-indicator variance" + } + Self::LatentVarianceIsNotObservedVariance => { + "latent variance is not the observed-indicator variance" + } }; formatter.write_str(message) } @@ -211,5 +223,13 @@ mod tests { PsychometricError::TraitVarianceIsNotStationaryWithinSubject.to_string(), "trait variance is not the stationary within-subject variance" ); + assert_eq!( + PsychometricError::MeasurementErrorIsNotObservedVariance.to_string(), + "measurement-error variance is not the observed-indicator variance" + ); + assert_eq!( + PsychometricError::LatentVarianceIsNotObservedVariance.to_string(), + "latent variance is not the observed-indicator variance" + ); } } diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index f080ce5a..52a173ac 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -24,7 +24,11 @@ //! `-q / (2 a)`. Section 4.3 (p. 9) then adds a stable trait process //! with `DRIFT` and `DIFFUSION` fixed to zero. That `TRAITVAR` is //! time-invariant between-subject variance; it is not process noise -//! and not `asymDIFFUSION`. The JSS article +//! and not `asymDIFFUSION`. Equation 1 (p. 4) writes +//! `y_i(t) = Λ η_i(t) + τ + ε_i(t)` with `ε ~ N(0, Θ)`. The JSS +//! summary (p. 16) names `Θ` `MANIFESTVAR`. The scalar observed +//! variance is `λ² Var(η) + θ`. `MANIFESTVAR` is not `Var(y)` and +//! `Var(η)` is not `Var(y)`. The JSS article //! has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). //! The difference quotient `(x(t+Δt) − x(t)) / Δt` (their //! Eqs. 3–4) is refused. This is not DSEM and not a matrix `expm`. @@ -783,6 +787,81 @@ pub fn refuse_trait_variance_as_stationary_within_subject( Err(PsychometricError::TraitVarianceIsNotStationaryWithinSubject) } +/// Exact scalar observed-indicator variance from Driver Equation 1. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 1, p. 4; JSS PDF re-opened +/// 2026-08-19T00:14Z) write `y_i(t) = Λ η_i(t) + τ + ε_i(t)` with +/// `ε ~ N(0, Θ)`. The JSS summary (p. 16) names `Θ` `MANIFESTVAR`. +/// The scalar map is `Var(y) = λ² Var(η) + θ`. Form `(λ p) λ` then +/// add `θ`. Do not form `λ²` first: at `λ = 1e308`, `p = 1e-308`, +/// `λ²` overflows and `λ² p` is non-finite, but `(λ p) λ = 1e308`. +/// A zero loading or zero latent variance is exactly `θ`. A zero +/// measurement error is exactly `λ² p`. Negative latent or +/// measurement-error variance fails closed. An overflowing product +/// or sum fails closed. This is not a Kalman filter, not ESEM +/// estimation, and not ctsem estimation. +/// +/// # Errors +/// +/// Returns [`PsychometricError::InvalidNumericInput`] when the loading +/// is non-finite, either variance is negative or non-finite, or the +/// mapped variance is non-finite. +pub fn recover_manifest_observed_variance( + loading: f64, + latent_variance: f64, + measurement_error_variance: f64, +) -> Result { + if !loading.is_finite() + || !latent_variance.is_finite() + || latent_variance < 0.0 + || !measurement_error_variance.is_finite() + || measurement_error_variance < 0.0 + { + return Err(PsychometricError::InvalidNumericInput); + } + if loading == 0.0 || latent_variance == 0.0 { + return Ok(measurement_error_variance); + } + let explained = require_finite((loading * latent_variance) * loading)?; + if measurement_error_variance == 0.0 { + return Ok(explained); + } + require_finite(explained + measurement_error_variance) +} + +/// Refuse treating Driver Eq. 1 measurement error as `Var(y)`. +/// +/// `MANIFESTVAR` is `Θ`, the variance of `ε`. Equation 1 maps +/// `Var(y) = λ² Var(η) + θ`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::MeasurementErrorIsNotObservedVariance`]. +pub fn refuse_measurement_error_as_observed_variance( + measurement_error_variance: f64, + observed_variance: f64, +) -> Result { + let _ = (measurement_error_variance, observed_variance); + Err(PsychometricError::MeasurementErrorIsNotObservedVariance) +} + +/// Refuse treating Driver Eq. 1 latent variance as `Var(y)`. +/// +/// `Var(η)` is the latent process variance. Equation 1 maps +/// `Var(y) = λ² Var(η) + θ`. +/// +/// # Errors +/// +/// Always returns [`PsychometricError::LatentVarianceIsNotObservedVariance`]. +pub fn refuse_latent_variance_as_observed_variance( + latent_variance: f64, + observed_variance: f64, +) -> Result { + let _ = (latent_variance, observed_variance); + Err(PsychometricError::LatentVarianceIsNotObservedVariance) +} + /// Refuse treating Driver Eq. 3 process noise as the unconditional variance. /// /// Driver, Oud, and Voelkle (2017, Eq. 3–4, pp. 4–5): @@ -1037,10 +1116,11 @@ mod tests { recover_discrete_process_noise, recover_discrete_time_varying_predictor_effect, recover_event_series_mean_log_rate, recover_event_time_discrete_lag_and_log_rate, recover_irregular_centered_residual_log_rate, recover_local_log_rate, - recover_stationary_latent_variance, recover_trait_plus_state_lagged_covariance, - recover_trait_plus_state_latent_variance, recover_within_residual_event_time_log_rate, - refuse_difference_quotient_as_local_rate, + recover_manifest_observed_variance, recover_stationary_latent_variance, + recover_trait_plus_state_lagged_covariance, recover_trait_plus_state_latent_variance, + recover_within_residual_event_time_log_rate, refuse_difference_quotient_as_local_rate, refuse_finite_interval_process_noise_as_stationary_variance, + refuse_latent_variance_as_observed_variance, refuse_measurement_error_as_observed_variance, refuse_pooled_discrete_lag_across_unequal_intervals, refuse_process_noise_as_unconditional_variance, refuse_trait_variance_as_process_noise, refuse_trait_variance_as_stationary_within_subject, @@ -2371,4 +2451,74 @@ mod tests { Err(PsychometricError::NonPositiveInterval) ); } + + #[test] + fn manifest_observed_variance_recovers_driver_equation_one() { + let loading = 2.0_f64; + let latent = 0.4_f64; + let measurement_error = 0.1_f64; + let recovered = + recover_manifest_observed_variance(loading, latent, measurement_error).expect("eq1"); + let expected = (loading * latent) * loading + measurement_error; + assert!((recovered - expected).abs() < 1e-15); + assert!((recovered - 1.7).abs() < 1e-15); + assert!((measurement_error - recovered).abs() > 1e-3); + assert!((latent - recovered).abs() > 1e-3); + assert_eq!( + refuse_measurement_error_as_observed_variance(measurement_error, recovered), + Err(PsychometricError::MeasurementErrorIsNotObservedVariance) + ); + assert_eq!( + refuse_latent_variance_as_observed_variance(latent, recovered), + Err(PsychometricError::LatentVarianceIsNotObservedVariance) + ); + assert_eq!( + recover_manifest_observed_variance(0.0, latent, measurement_error), + Ok(measurement_error) + ); + assert_eq!( + recover_manifest_observed_variance(loading, 0.0, measurement_error), + Ok(measurement_error) + ); + assert_eq!( + recover_manifest_observed_variance(loading, latent, 0.0), + Ok(1.6) + ); + // Do not form λ² first: (1e308)² overflows; (λ p) λ is 1e308. + let scaled = recover_manifest_observed_variance(1e308, 1e-308, 0.0).expect("scale"); + assert!((scaled - 1e308).abs() / 1e308 < 1e-15); + assert!(!(1e308_f64 * 1e308_f64).is_finite()); + } + + #[test] + fn manifest_observed_variance_invalid_inputs_fail_closed() { + assert_eq!( + recover_manifest_observed_variance(f64::NAN, 0.4, 0.1), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_manifest_observed_variance(2.0, -0.1, 0.1), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_manifest_observed_variance(2.0, 0.4, -0.1), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_manifest_observed_variance(2.0, f64::NAN, 0.1), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_manifest_observed_variance(2.0, 0.4, f64::NAN), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_manifest_observed_variance(1e308, 1.0, 0.0), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_manifest_observed_variance(1e308, 1.0, 1e308), + Err(PsychometricError::InvalidNumericInput) + ); + } } diff --git a/crates/psychometric_core/src/lib.rs b/crates/psychometric_core/src/lib.rs index 9c8e374e..b08323c1 100644 --- a/crates/psychometric_core/src/lib.rs +++ b/crates/psychometric_core/src/lib.rs @@ -21,7 +21,9 @@ //! variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 //! `asymDIFFUSION`), recovers the Driver §4.3 trait-plus-state //! variance and lagged covariance (`TRAITVAR` is not process noise -//! and not `asymDIFFUSION`), +//! and not `asymDIFFUSION`), recovers the Driver Eq. 1 scalar +//! observed-indicator variance (`λ² Var(η) + θ`; `MANIFESTVAR` is +//! not `Var(y)`), //! and refuses //! latent-mean comparison below strong invariance. @@ -92,6 +94,8 @@ pub use event_time::recover_event_time_discrete_lag_and_log_rate; pub use event_time::recover_irregular_centered_residual_log_rate; /// Exact scalar inverse `a = ln(φ) / Δt`. pub use event_time::recover_local_log_rate; +/// Exact scalar observed-indicator variance `λ² Var(η) + θ`. +pub use event_time::recover_manifest_observed_variance; /// Exact scalar stationary within-subject variance `-q / (2 a)`. pub use event_time::recover_stationary_latent_variance; /// Exact scalar trait-plus-state lagged covariance. @@ -104,6 +108,10 @@ pub use event_time::recover_within_residual_event_time_log_rate; pub use event_time::refuse_difference_quotient_as_local_rate; /// Refuse treating finite-interval `Q_Δt` as `asymDIFFUSION`. pub use event_time::refuse_finite_interval_process_noise_as_stationary_variance; +/// Refuse treating Driver Eq. 1 latent variance as `Var(y)`. +pub use event_time::refuse_latent_variance_as_observed_variance; +/// Refuse treating Driver Eq. 1 measurement error as `Var(y)`. +pub use event_time::refuse_measurement_error_as_observed_variance; /// Refuse pooling discrete lags from unequal event intervals. pub use event_time::refuse_pooled_discrete_lag_across_unequal_intervals; /// Refuse treating Driver Eq. 3 process noise as the unconditional variance. 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 ae711c0b..1a03c04f 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -10,10 +10,11 @@ use psychometric_core::{ recover_discrete_process_noise, recover_discrete_time_varying_predictor_effect, recover_event_series_mean_log_rate, recover_event_time_discrete_lag_and_log_rate, recover_irregular_centered_residual_log_rate, recover_kish_weighted_slope, - recover_stationary_latent_variance, recover_trait_plus_state_lagged_covariance, - recover_trait_plus_state_latent_variance, recover_within_residual_event_time_log_rate, - refuse_difference_quotient_as_local_rate, + recover_manifest_observed_variance, recover_stationary_latent_variance, + recover_trait_plus_state_lagged_covariance, recover_trait_plus_state_latent_variance, + recover_within_residual_event_time_log_rate, refuse_difference_quotient_as_local_rate, refuse_finite_interval_process_noise_as_stationary_variance, + refuse_latent_variance_as_observed_variance, refuse_measurement_error_as_observed_variance, refuse_pooled_discrete_lag_across_unequal_intervals, refuse_process_noise_as_unconditional_variance, refuse_trait_variance_as_process_noise, refuse_trait_variance_as_stationary_within_subject, @@ -783,6 +784,49 @@ fn trait_plus_state_recovers_driver_section_four_point_three() { ); } +#[test] +fn manifest_observed_variance_recovers_driver_equation_one() { + let loading = 2.0_f64; + let latent = 0.4_f64; + let measurement_error = 0.1_f64; + let observed = + recover_manifest_observed_variance(loading, latent, measurement_error).expect("eq1"); + let expected = (loading * latent) * loading + measurement_error; + let error = rmse(&[expected], &[observed]); + assert!(error < 1e-15, "Driver Eq. 1 Var(y) RMSE {error}"); + let collapsed_error = rmse(&[expected], &[measurement_error]); + let latent_error = rmse(&[expected], &[latent]); + assert!( + collapsed_error > error, + "MANIFESTVAR is not Var(y): collapsed RMSE {collapsed_error} must exceed {error}" + ); + assert!( + latent_error > error, + "Var(η) is not Var(y): latent RMSE {latent_error} must exceed {error}" + ); + assert_eq!( + refuse_measurement_error_as_observed_variance(measurement_error, observed), + Err(PsychometricError::MeasurementErrorIsNotObservedVariance) + ); + assert_eq!( + refuse_latent_variance_as_observed_variance(latent, observed), + Err(PsychometricError::LatentVarianceIsNotObservedVariance) + ); + assert_eq!( + recover_manifest_observed_variance(0.0, latent, measurement_error), + Ok(measurement_error) + ); + let scaled = recover_manifest_observed_variance(1e308, 1e-308, 0.0).expect("scale"); + assert!( + (scaled - 1e308).abs() / 1e308 < 1e-15, + "Driver Eq. 1 (λ p)λ must keep λ=1e308, p=1e-308: got {scaled}" + ); + assert_eq!( + recover_manifest_observed_variance(1e308, 1.0, 0.0), + Err(PsychometricError::InvalidNumericInput) + ); +} + #[test] fn admitted_coordinates_still_required_for_multilevel_weights() { assert_eq!( diff --git a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs index 95266239..7eaa7aa4 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -7,9 +7,10 @@ use psychometric_core::{ recover_discrete_lagged_latent_covariance, recover_discrete_latent_variance, recover_discrete_process_noise, recover_discrete_time_varying_predictor_effect, recover_irregular_centered_residual_log_rate, recover_loading_point_estimate_mean, - recover_stationary_latent_variance, recover_trait_plus_state_latent_variance, - recover_within_residual_event_time_log_rate, + recover_manifest_observed_variance, recover_stationary_latent_variance, + recover_trait_plus_state_latent_variance, recover_within_residual_event_time_log_rate, refuse_finite_interval_process_noise_as_stationary_variance, + refuse_latent_variance_as_observed_variance, refuse_measurement_error_as_observed_variance, refuse_process_noise_as_unconditional_variance, refuse_trait_variance_as_process_noise, refuse_trait_variance_as_stationary_within_subject, }; @@ -312,3 +313,28 @@ fn trait_variance_is_not_process_noise_or_stationary_within_subject() { Err(psychometric_core::PsychometricError::TraitVarianceIsNotStationaryWithinSubject) ); } + +#[test] +fn measurement_error_and_latent_variance_are_not_the_observed_variance() { + let loading = 2.0_f64; + let latent = 0.4_f64; + let measurement_error = 0.1_f64; + let observed = + recover_manifest_observed_variance(loading, latent, measurement_error).expect("eq1"); + assert!( + (measurement_error - observed).abs() > 1e-3, + "Driver et al. (2017, Eq. 1 / p. 16): MANIFESTVAR is not Var(y)" + ); + assert!( + (latent - observed).abs() > 1e-3, + "Driver et al. (2017, Eq. 1): Var(η) is not Var(y)" + ); + assert_eq!( + refuse_measurement_error_as_observed_variance(measurement_error, observed), + Err(psychometric_core::PsychometricError::MeasurementErrorIsNotObservedVariance) + ); + assert_eq!( + refuse_latent_variance_as_observed_variance(latent, observed), + Err(psychometric_core::PsychometricError::LatentVarianceIsNotObservedVariance) + ); +} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 1a87c80c..6a39ae70 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -28,7 +28,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | report template/section/copied/style/modality method effects | ADR 0004/0012; PRD/TRD | simulation truth factors implemented; estimator-side method model remains future | partial | | candidate K statistical/Pareto gates + blinded LLM review | ADR 0012; research | future `model_selection` | accepted-target | | compositional topic correlation / stable clustering | ADR 0005/0012; research | future `network_analysis` | accepted-target | -| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR; full ESEM/DSEM remaining | partial | +| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance (Driver et al., 2017, Eq. 1; `MANIFESTVAR` is not `Var(y)`), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR; full ESEM/DSEM remaining | partial | | CPU bounded multithreading + GPU/VRAM streaming/parity | ADR 0001/0006 | future `compute_backend` | accepted-target | | TDT detection/tracking vs CHRONOS schema/prediction/temporal consistency | ADR 0016; PRD/research | future `event_intelligence` | accepted-target | | evidence-bounded LLM interpretation | ADR 0010/0012; PRD | future `interpretation_gateway` | accepted-target | diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index 8c8d0c82..a38c2f5c 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -1,7 +1,7 @@ # ADR 0005 — Posterior-aware ESEM/DSEM and structural interpretation **Decision status:** Accepted -**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target +**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance (Driver et al., 2017, Eq. 1, p. 4; p. 16 `MANIFESTVAR`; `λ² Var(η) + θ`; not a Kalman filter), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target **Date:** 2026-08-05 **Supersedes:** None. ADR 0012 governs upstream topic measurement/network coordinates; this ADR governs higher-order psychometric structure and longitudinal interpretation. @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; form `(q / a) * -0.5`; do not form `2 a` first; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; form `(q / a) * -0.5`; do not form `2 a` first; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` (Driver et al., 2017, Eq. 1, p. 4; p. 16 `MANIFESTVAR`; form `(λ p) λ` then add `θ`; do not form `λ²` first; `MANIFESTVAR` is not `Var(y)`; `Var(η)` is not `Var(y)`), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. 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/adr/README.md b/docs/adr/README.md index f82d0b8e..0dd2b9be 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -10,7 +10,7 @@ Read [`ADR_POLICY.md`](ADR_POLICY.md) first. **Decision status and implementatio | [0002](0002-six-clock-temporal-semantics.md) | Six-clock temporal semantics and fail-closed historical leakage prevention | Accepted | active-PR | Unmerged PR #8 is the canonical Task 3 replacement implementing typed clocks/intervals against the current protected-main lineage; conflicted PR #5 is superseded lineage. Later graph/split enforcement remains target work. | | [0003](0003-relational-event-multiple-membership.md) | Relational event ontology and time-varying cross-classified multiple membership | Accepted | partial | Weighted time-varying membership network/roles are active-PR (PR #12); full multilevel estimators, graph ontology, and persistence remain accepted-target. ADR 0016 owns event-intelligence tasks. | | [0004](0004-shared-multilingual-latent-space.md) | One shared multilingual latent space with explicit invariance status | Accepted | accepted-target | ADR 0012 owns the full topic-estimator/backend/global-topic contract. | -| [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | partial | Input gates, posterior-draw point estimates, Rubin `T` on OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects, exact scalar discrete process noise, lagged latent covariance and unconditional latent variance, stationary within-subject variance (`asymDIFFUSION`), trait-plus-state variance (`TRAITVAR`; not process noise), irregular already-centered residual lag, and strong-gated latent means are on the stacked psychometric PR; full ESEM/DSEM estimator remains accepted-target. | +| [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | partial | Input gates, posterior-draw point estimates, Rubin `T` on OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects, exact scalar discrete process noise, lagged latent covariance and unconditional latent variance, stationary within-subject variance (`asymDIFFUSION`), trait-plus-state variance (`TRAITVAR`; not process noise), observed-indicator variance (Eq. 1; `MANIFESTVAR` is not `Var(y)`), irregular already-centered residual lag, and strong-gated latent means are on the stacked psychometric PR; full ESEM/DSEM estimator remains accepted-target. | | [0006](0006-vram-gpu-nvidia-orchestration.md) | VRAM-adaptive GPU compute and model-credential boundary | Accepted | accepted-target | LLM orchestration policy superseded by ADR 0010; autonomous development authority governed by ADR 0015. | | [0007](0007-rust-workspace-quality-gates.md) | Explicit Rust workspace, pinned toolchains, and exact quality gates | Accepted | implemented-main | ADR 0014 governs scientific/product claim promotion beyond repository-quality tooling. | | [0008](0008-immutable-evidence-identities-digests-and-spans.md) | Immutable evidence identities, `SHA-256` digests, exact spans, and strict wire reconstruction | Accepted | implemented-main | ADR 0013 governs future persistence/reproducibility/split authority. | diff --git a/docs/research/multilevel-event-time-recovery.md b/docs/research/multilevel-event-time-recovery.md index 4f2174cc..1eb3e9c8 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -17,15 +17,16 @@ This slice stays inside `psychometric_core`. It does not add a second invariance 11. recover the exact scalar discrete latent variance `A_Δt P A_Δt⊤ + Q_Δt` and refuse `Q_Δt` as that unconditional variance; 12. recover the exact scalar stationary within-subject variance as the `Δt → ∞` limit of Eq. 4 (`asymDIFFUSION`; §4.3 T0VAR stationarity) and refuse finite-interval `Q_Δt` as that limit; 13. recover the exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`) and refuse treating trait variance as process noise or as `asymDIFFUSION`; -14. refuse pooling discrete lags from unequal event intervals as one coefficient; -15. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); -16. refuse the difference quotient as a continuous-time rate; -17. apply the same event-time map to CWC residuals (still not DSEM); -18. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). +14. recover the exact scalar observed-indicator variance `λ² Var(η) + θ` (Driver et al., 2017, Eq. 1; p. 16 `MANIFESTVAR`) and refuse treating measurement error or latent variance as `Var(y)`; +15. refuse pooling discrete lags from unequal event intervals as one coefficient; +16. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); +17. refuse the difference quotient as a continuous-time rate; +18. apply the same event-time map to CWC residuals (still not DSEM); +19. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). ## Claim boundary -This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. +This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. ## Authoritative sources @@ -59,6 +60,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - **Discrete latent variance.** Equations 3–4 write \(Q_{\Delta t}\) as the covariance of the stochastic integral, so \(Q_{\Delta t}=\operatorname{cov}(\eta_{ti}\mid\eta_{t-1,i})\) when \(\xi\) and \(z\) are given. The law of total variance on that pair is \(\operatorname{Var}(\eta_{ti})=A_{\Delta t}\operatorname{Var}(\eta_{t-1,i})A_{\Delta t}^{\top}+Q_{\Delta t}\). The scalar map is \(\mathrm{e}^{2a\Delta t}p+Q_{\Delta t}\). This is not a Kalman measurement update. A zero prior variance is exactly \(Q_{\Delta t}\). Binary64 underflow of \(\mathrm{e}^{2a\Delta t}\) keeps \(Q_{\Delta t}\). Finite-\(z\) exponential overflow rewrites as \(\exp(\ln p+z)+Q_{\Delta t}\). A zero diffusion is exactly \(Q_{\Delta t}=0\); that skip does not license a non-finite carried term when \(2(a\Delta t)\) overflows to \(+\infty\). Treating \(Q_{\Delta t}\) as \(\operatorname{Var}(\eta_{t})\) fails closed. - **Stationary within-subject variance.** Driver et al. (2017, Eq. 4 as \(\Delta t\to\infty\); §4.3 pp. 9–10; p. 16 `asymDIFFUSION`; JSS PDF re-opened 2026-08-19T00:14Z, p. 5): for stable \(a<0\), \(\lim_{\Delta t\to\infty}Q_{\Delta t}=-q/(2a)\). Form the ratio \((q/a)\times-0.5\). Forming \(2a\) first overflows when \(|a|\) is at the binary64 extreme (`a=-1e308`, `q=1e308` → `0.5`). Forming \(0.5q\) first underflows at the minimum subnormal (`q=from_bits(1)`, `a=-from_bits(1)` → naive `+0`; representable solution `0.5`). Starting from that variance, \(\operatorname{Var}(\eta_{t})\) is invariant across finite event intervals. A zero diffusion is exactly zero. \(a\ge 0\) has no finite stationary variance (Brownian \(a=0\) grows as \(q\Delta t\)). An overflowing \((q/a)\times-0.5\) fails closed. Finite-interval \(Q_{\Delta t}\) is not that limit. This is not ctsem estimation and not a Kalman filter. - **Trait-plus-state variance.** Driver et al. (2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z): a stable trait process has `DRIFT` and `DIFFUSION` fixed to zero. The scalar maps are \(\operatorname{Var}=\mathrm{trait}+\mathrm{state}\) and \(\operatorname{cov}(t,t-1)=\mathrm{trait}+\mathrm{e}^{a\Delta t}p\). The ctsem `TRAITVAR` rewrite that adds the trait to `DIFFUSION` does not license treating trait variance as \(Q_{\Delta t}\). Trait variance is not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not this map. A zero trait is exactly the state. A zero state is exactly the trait. An overflowing sum fails closed. This is not RI-CLPM and not ctsem estimation. +- **Observed-indicator variance.** Driver et al. (2017, Eq. 1, p. 4; p. 16 `MANIFESTVAR`; JSS PDF re-opened 2026-08-19T00:14Z): \(y_i(t)=\Lambda\eta_i(t)+\tau+\varepsilon_i(t)\) with \(\varepsilon\sim N(0,\Theta)\). The scalar map is \(\operatorname{Var}(y)=\lambda^{2}\operatorname{Var}(\eta)+\theta\). Form \((\lambda p)\lambda\) then add \(\theta\). Forming \(\lambda^{2}\) first overflows at \(\lambda=10^{308}\), \(p=10^{-308}\). A zero loading or zero latent variance is exactly \(\theta\). A zero measurement error is exactly \(\lambda^{2}p\). `MANIFESTVAR` is not \(\operatorname{Var}(y)\). \(\operatorname{Var}(\eta)\) is not \(\operatorname{Var}(y)\). An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **CWC-then-lag.** Sample cluster means are removed first. Consecutive within residuals are then fitted by least squares to \(r_{t+\Delta t}\approx\exp(a\Delta t)\,r_{t}\) on event time. Same-sign pair-wise logs initialize the scalar Newton step. Sign-flipping \(T=2\) CWC pairs have no real logarithm and fail closed. Curran and Bauer (2011, pp. 607–608) show that this person-mean subtraction on a raw autoregressive series does **not** isolate the lagged within-person effect; the helper therefore does not claim to recover the raw-process drift. - **Already-centered irregular residual.** The caller supplies lagged within residuals. The mean of \(a=\ln(r_{t+\Delta t}/r_t)/\Delta t\) is the exact scalar map. Intervals may be irregular. The helper does not center again. This is not DSEM. @@ -76,6 +78,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - Driver et al. (2017, Eq. 3–4) recovers a known lagged covariance \(\mathrm{e}^{a\Delta t}p\) and a known latent variance \(\mathrm{e}^{2a\Delta t}p+Q_{\Delta t}\) at machine-scale RMSE, and that variance RMSE is smaller than treating \(Q_{\Delta t}\) as \(\operatorname{Var}(\eta_{t})\); a zero prior variance is exactly zero (lagged) or \(Q_{\Delta t}\) (unconditional); binary64 underflow of \(\mathrm{e}^{a\Delta t}\) keeps a vanishing lagged covariance; a finite \(\mathrm{e}^{a\Delta t}\) whose product with \(p\) overflows fails closed; a zero diffusion whose \(2(a\Delta t)\) overflows to \(+\infty\) fails closed; treating \(Q_{\Delta t}\) as the unconditional variance fails closed; - Driver et al. (2017, Eq. 4 as \(\Delta t\to\infty\); p. 16 `asymDIFFUSION`; §4.3) recovers a known stationary variance \(-q/(2a)\) at machine-scale RMSE, and that RMSE is smaller than treating finite-interval \(Q_{\Delta t}\) as the stationary variance; starting from that variance, \(\operatorname{Var}(\eta_{t})\) is invariant across finite \(\Delta t\); a zero diffusion is exactly zero; forming \(2a\) first overflows while \((q/a)\times-0.5\) stays finite; forming \(0.5q\) first underflows at the minimum subnormal while \((q/a)\times-0.5=0.5\); \(a\ge 0\), a non-event clock, a negative diffusion, and an overflowing \((q/a)\times-0.5\) fail closed; - Driver et al. (2017, §4.3, p. 9) recovers a known trait-plus-state variance and lagged covariance at machine-scale RMSE, and that RMSE is smaller than evolving the summed variance as if it were all state; a zero trait is the state; a zero state is the trait; treating trait variance as process noise or as `asymDIFFUSION` fails closed; an overflowing sum and a non-event clock fail closed; +- Driver et al. (2017, Eq. 1, p. 4; p. 16 `MANIFESTVAR`) recovers a known observed-indicator variance \(\lambda^{2}\operatorname{Var}(\eta)+\theta\) at machine-scale RMSE, and that RMSE is smaller than treating `MANIFESTVAR` or \(\operatorname{Var}(\eta)\) as \(\operatorname{Var}(y)\); a zero loading or zero latent variance is \(\theta\); forming \(\lambda^{2}\) first overflows while \((\lambda p)\lambda\) stays finite; an overflowing product or sum fails closed; - pooling discrete lags from unequal intervals fails closed; - CWC-then-lag on a two-cluster decaying series has smaller computed RMSE than a level-pooled series when the latter is identified; - already-centered irregular residuals recover a known drift at machine-scale RMSE, and that RMSE is smaller than CWC of the corresponding raw autoregressive series (Curran & Bauer, 2011, pp. 607–608); diff --git a/docs/validation/temporal-event-foundation.md b/docs/validation/temporal-event-foundation.md index 98c20753..e83f60bf 100644 --- a/docs/validation/temporal-event-foundation.md +++ b/docs/validation/temporal-event-foundation.md @@ -23,7 +23,7 @@ This report tracks exact-head scientific and engineering evidence required befor | Truth corpora / manifests | `tepp_simulation` | implemented-main | — | deterministic generator tests | Task 10 / PR #18 | | Recovery metrics | `validation_core` | implemented-main | — | RMSE/bias/coverage/MC gates | Task 11 / PR #19 | | Versioned API/export contracts | `tepp_api` | implemented-main | naruon HTTP interchange | unknown-field/version/limit + naruon HTTPS interchange tests | Task 12 / PR #21; live HTTP service remaining | -| Psychometric structural input gates | `psychometric_core` | partial | stacked psychometric PR | construct-class refusal + ALR/ILR boundary + true-loading RMSE + posterior-draw point-estimate mean + Rubin `T` + CWC within/between + CWC contextual effect + event-time log-rate + constant- and time-varying-predictor discrete effects + exact scalar discrete process noise + lagged latent covariance and unconditional latent variance + stationary within-subject variance + trait-plus-state variance + irregular already-centered residual lag + strong-gated latent means; full ESEM/DSEM remaining | ADR 0005; `docs/research/posterior-esem-input-gates.md`; `docs/research/multilevel-event-time-recovery.md`; `docs/research/rubin-total-variance.md`; `docs/research/strong-invariance-latent-means.md` | +| Psychometric structural input gates | `psychometric_core` | partial | stacked psychometric PR | construct-class refusal + ALR/ILR boundary + true-loading RMSE + posterior-draw point-estimate mean + Rubin `T` + CWC within/between + CWC contextual effect + event-time log-rate + constant- and time-varying-predictor discrete effects + exact scalar discrete process noise + lagged latent covariance and unconditional latent variance + stationary within-subject variance + trait-plus-state variance + observed-indicator variance + irregular already-centered residual lag + strong-gated latent means; full ESEM/DSEM remaining | ADR 0005; `docs/research/posterior-esem-input-gates.md`; `docs/research/multilevel-event-time-recovery.md`; `docs/research/rubin-total-variance.md`; `docs/research/strong-invariance-latent-means.md` | | CWL modular connectors | `docs/connectors/*` | implemented-main | — | contract docs + examples | PR #22; live HTTP ports remaining | | Release SBOM/provenance generator | `scripts/release_evidence.py` | partial | — | generate+validate in CI | Task 13 partial / PR #28 | From 2b834ff9103fb5f6caf780d63dc6c0f80ef46a95 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 04:10:29 +0000 Subject: [PATCH 27/62] feat(psychometric): divide Driver Eq. 4 by the finite Kronecker sum MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Driver, Oud, and Voelkle (2017, Eq. 4, p. 5; JSS PDF re-opened 2026-08-19T04:10Z). The Δt→∞ limit is -q/(2a). When 2a is finite, form q/-(2a). Forming q/a first overflows at q=MAX, a=-0.75 while MAX/1.5 is finite (CodeRabbit finding on 75ecdd3). When 2a overflows, form (q/a)*-0.5. Do not form 0.5q first. Still not a Kalman filter, not DSEM, and not a matrix expm. Meredith (1993) and Mislevy (1991) remain unread. --- CHANGELOG.md | 1 + CLAUDE.md | 2 +- crates/psychometric_core/src/event_time.rs | 45 ++++++++++++++----- ...multilevel_event_time_recovery_contract.rs | 7 +++ .../scientific_claim_boundary_contract.rs | 5 +++ docs/adr/0005-posterior-esem-dsem.md | 2 +- .../multilevel-event-time-recovery.md | 6 +-- 7 files changed, 53 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab732246..6b38907d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang ## [Unreleased] +- `psychometric_core` evaluates the Driver, Oud, and Voelkle (2017, Eq. 4, p. 5; JSS PDF re-opened 2026-08-19T04:10Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) stationary within-subject variance as `q / -(2 a)` when the scalar Kronecker sum `2 a` is finite. The paper limit is `-q / (2 a)` (`A# = A ⊗ I + I ⊗ A`; p. 16 `asymDIFFUSION`). Forming `q / a` first overflows at `q = MAX`, `a = -0.75` while `MAX / 1.5` is finite (CodeRabbit finding on `75ecdd3`). When `2 a` overflows (`a = -1e308`), form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). Still not a Kalman filter, not DSEM, not a matrix `expm`, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall/OpenAlex 2026-08-19T04:10Z: closed). Oud and Jansen (2000) remains unread. ZORA Anubis-blocked. - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 1, p. 4; p. 16 `MANIFESTVAR`; JSS PDF re-opened 2026-08-19T00:14Z) scalar observed-indicator variance. Equation 1 writes `y_i(t) = Λ η_i(t) + τ + ε_i(t)` with `ε ~ N(0, Θ)`. The scalar map is `Var(y) = λ² Var(η) + θ`. Form `(λ p) λ` then add `θ`. Do not form `λ²` first (`λ = 1e308`, `p = 1e-308` → `1e308`). `MANIFESTVAR` is not `Var(y)`. `Var(η)` is not `Var(y)`. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall/OpenAlex 2026-08-19T00:14Z: closed). Oud and Jansen (2000) remains unread. ZORA Anubis-blocked. - `psychometric_core` evaluates the Driver, Oud, and Voelkle (2017, Eq. 4, p. 5; JSS PDF re-opened 2026-08-19T00:14Z) stationary within-subject variance as `(q / a) * -0.5`. The paper limit is `-q / (2 a)`. Forming `2 a` first overflows at `a = -1e308`. Forming `0.5 q` first underflows at `q = from_bits(1)`, `a = -from_bits(1)` and returns `+0` (CodeRabbit finding on `556e23d`); the representable Lyapunov solution is `0.5`. Still not a Kalman filter, not DSEM, not a matrix `expm`, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall/OpenAlex 2026-08-19T00:14Z: closed; Springer `content/pdf` is HTML 200). Oud and Jansen (2000) remains unread. ZORA Anubis-blocked. - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z) scalar trait-plus-state latent variance and lagged covariance. A stable trait process has `DRIFT` and `DIFFUSION` fixed to zero, so `Var = trait + state` and `cov(t, t-1) = trait + exp(a Δt) p`. The ctsem `TRAITVAR` rewrite that adds the trait to `DIFFUSION` does not license treating trait variance as process noise. Trait variance is not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not this map. Still not RI-CLPM, not a Kalman filter, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall/OpenAlex 2026-08-18T21:07Z: closed). Oud and Jansen (2000) remains unread. ZORA accepted manuscript re-opened via bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`. diff --git a/CLAUDE.md b/CLAUDE.md index 1cab9917..10714ac0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. -- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). Form `(q / a) * -0.5`. Do not form `2 a` first. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` (Driver et al., 2017, Eq. 1, p. 4; p. 16 `MANIFESTVAR`). Form `(λ p) λ` then add `θ`. `MANIFESTVAR` is not `Var(y)`. `Var(η)` is not `Var(y)`. +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` (Driver et al., 2017, Eq. 1, p. 4; p. 16 `MANIFESTVAR`). Form `(λ p) λ` then add `θ`. `MANIFESTVAR` is not `Var(y)`. `Var(η)` is not `Var(y)`. - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index 52a173ac..c282a6ec 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -615,22 +615,25 @@ pub fn recover_discrete_latent_variance( /// Exact scalar stationary within-subject variance on event time. /// /// Driver, Oud, and Voelkle (2017, Eq. 4, p. 5; JSS PDF re-opened -/// 2026-08-19T00:14Z) write `Q_Δt` as +/// 2026-08-19T04:10Z) write `Q_Δt` as /// `irow(A#^{-1}[e^{A# Δt} − I] row(Q))` with `A# = A ⊗ I + I ⊗ A`. /// The scalar Kronecker sum is `2 a`. As `Δt → ∞` with stable /// `a < 0`, `e^{2 a Δt} → 0` and Eq. 4 becomes `-q / (2 a)`. The /// JSS summary names that limit `asymDIFFUSION` and takes it as the /// total within-subject variance (p. 16). Section 4.3 (pp. 9–10) /// constrains a stationary `T0VAR` to that same model-predicted -/// variance. Form `(q / a) * -0.5`. Do not form `2 a` first: at -/// `a = -1e308`, `q = 1e308`, `2 a` overflows and `-q / (2 a)` -/// collapses to `+0`, but `(q / a) * -0.5 = 0.5`. Do not form -/// `0.5 q` first: at `q = from_bits(1)`, `a = -from_bits(1)`, +/// variance. When `2 a` is finite, form `q / -(2 a)` so `q / a` +/// overflow does not lose a finite Lyapunov solution (`q = MAX`, +/// `a = -0.75` → `MAX / 1.5`; `CodeRabbit` on `75ecdd3`). When `2 a` +/// overflows, form `(q / a) * -0.5`. Do not form `2 a` as the only +/// path: at `a = -1e308`, `q = 1e308`, `2 a` overflows and +/// `-q / (2 a)` collapses to `+0`, but `(q / a) * -0.5 = 0.5`. Do +/// not form `0.5 q` first: at `q = from_bits(1)`, `a = -from_bits(1)`, /// `-0.5 * q` underflows to `-0` and the quotient is `+0`, but /// the representable Lyapunov solution is `0.5`. A zero diffusion is /// exactly zero. `a ≥ 0` has no finite stationary variance /// (including Brownian `a = 0`, whose variance grows as `q Δt`). -/// An overflowing `(q / a) * -0.5` fails closed. This is not a Kalman +/// An overflowing Lyapunov solution fails closed. This is not a Kalman /// filter, not DSEM, not a matrix `expm`, and not ctsem estimation. /// /// # Errors @@ -660,9 +663,18 @@ pub fn recover_stationary_latent_variance( if continuous_diffusion == 0.0 { return Ok(0.0); } - // −q / (2 a). Form the ratio first. Do not form 2 a (overflows - // at |a| = 1e308). Do not form 0.5 q (underflows at min subnormal). - require_finite((continuous_diffusion / log_rate) * -0.5) + // −q / (2 a). When 2 a is finite, divide by that Kronecker sum + // so q/a overflow does not lose a finite Lyapunov solution + // (q = MAX, a = −0.75 → MAX/1.5; CodeRabbit on 75ecdd3). + // When 2 a overflows (|a| = 1e308), form (q/a)*−0.5 instead. + // Do not form 0.5 q first (min-subnormal underflow). + let twice_rate = log_rate * 2.0; + let stationary = if twice_rate.is_finite() { + continuous_diffusion / -twice_rate + } else { + (continuous_diffusion / log_rate) * -0.5 + }; + require_finite(stationary) } /// Refuse treating finite-interval process noise as `asymDIFFUSION`. @@ -1846,6 +1858,18 @@ mod tests { .expect("subnormal ratio"); assert!((subnormal_ratio - 0.5).abs() < 1e-15); assert!(((min_subnormal / -min_subnormal) * -0.5 - 0.5).abs() < 1e-15); + // Do not form q/a first: MAX/-0.75 overflows; MAX/(2*0.75) is finite. + assert!(!(f64::MAX / -0.75_f64).is_finite()); + assert!(!((f64::MAX / -0.75_f64) * -0.5).is_finite()); + let twice = -0.75_f64 * 2.0; + assert!(twice.is_finite()); + let expected_max = f64::MAX / -twice; + assert!(expected_max.is_finite()); + assert_eq!(expected_max.to_bits(), (f64::MAX / 1.5).to_bits()); + let quotient_overflow = + recover_stationary_latent_variance(f64::MAX, -0.75, LagClock::EventTime) + .expect("q/a overflow"); + assert_eq!(quotient_overflow.to_bits(), expected_max.to_bits()); } #[test] @@ -1878,8 +1902,9 @@ mod tests { recover_stationary_latent_variance(0.4, f64::NAN, LagClock::EventTime), Err(PsychometricError::InvalidNumericInput) ); - // (q / a) * -0.5 overflows when q is huge relative to |a|. + // The Lyapunov solution overflows when |q| >> |a|. assert!(!((1e308_f64 / -1e-10_f64) * -0.5).is_finite()); + assert!(!(1e308_f64 / (2.0 * 1e-10_f64)).is_finite()); assert_eq!( recover_stationary_latent_variance(1e308, -1e-10, LagClock::EventTime), Err(PsychometricError::InvalidNumericInput) 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 1a03c04f..cf1d0f09 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -700,6 +700,13 @@ fn stationary_variance_recovers_driver_equation_four_asymptote() { recover_stationary_latent_variance(min_subnormal, -min_subnormal, LagClock::EventTime), Ok(0.5) ); + assert!(!(f64::MAX / -0.75_f64).is_finite()); + assert_eq!( + recover_stationary_latent_variance(f64::MAX, -0.75, LagClock::EventTime) + .expect("q/a overflow") + .to_bits(), + (f64::MAX / 1.5).to_bits() + ); assert_eq!( recover_stationary_latent_variance(diffusion, drift, LagClock::SystemTime), 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 7eaa7aa4..8c3fc517 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -276,6 +276,11 @@ fn finite_interval_process_noise_is_not_the_stationary_variance() { recover_stationary_latent_variance(min_subnormal, -min_subnormal, LagClock::EventTime) .expect("subnormal ratio"); assert!((subnormal_ratio - 0.5).abs() < 1e-15); + assert!(!(f64::MAX / -0.75_f64).is_finite()); + let quotient_overflow = + recover_stationary_latent_variance(f64::MAX, -0.75, LagClock::EventTime) + .expect("q/a overflow"); + assert_eq!(quotient_overflow.to_bits(), (f64::MAX / 1.5).to_bits()); } #[test] diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index a38c2f5c..bc10a9ce 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; form `(q / a) * -0.5`; do not form `2 a` first; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` (Driver et al., 2017, Eq. 1, p. 4; p. 16 `MANIFESTVAR`; form `(λ p) λ` then add `θ`; do not form `λ²` first; `MANIFESTVAR` is not `Var(y)`; `Var(η)` is not `Var(y)`), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` (Driver et al., 2017, Eq. 1, p. 4; p. 16 `MANIFESTVAR`; form `(λ p) λ` then add `θ`; do not form `λ²` first; `MANIFESTVAR` is not `Var(y)`; `Var(η)` is not `Var(y)`), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. 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 1eb3e9c8..c122677b 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -44,7 +44,7 @@ Kish, L. (1965). *Survey sampling*. John Wiley & Sons. Oud, J. H. L., & Jansen, R. A. R. G. (2000). Continuous time state space modeling of panel data by means of SEM. *Psychometrika, 65*(2), 199–215. https://doi.org/10.1007/BF02294374 (cited by Voelkle et al., 2012, Eq. 14 discussion; PDF not opened). -The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:07Z (https://www.zora.uzh.ch/handle/20.500.14742/72792; bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`). Page 16 writes discrete auto-effects as \(A^{*}(\Delta t)=\exp(A\Delta t)\) (Eq. 7). Introducing Intercepts (manuscript p. 20, Eq. 12) adds a continuous-time intercept \(b\) and writes the expected-value solution whose discrete increment is \(A^{-1}(\exp(A\Delta t)-I)b\); the scalar constant-predictor effect is \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). The next paragraph (manuscript p. 21, Eq. 14) writes the discrete effect of a **time-varying** predictor, when the sampling interval equals the constancy interval, as \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product does not depend on the predictor auto-effect. The manuscript calls Eq. 14 a first-order approximation that deteriorates as \(\Delta t\) grows, and defers unmatched sampling/constancy intervals to Oud and Jansen (2000), which is unread. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4; JSS PDF re-opened 2026-08-18T21:07Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\), the discrete intercept \(b_{\Delta t}=A^{-1}[A_{\Delta t}-I]b\), and the discrete process-noise covariance \(Q_{\Delta t}=\int_{0}^{\Delta t}\operatorname{expm}(A(\Delta t-\tau))LGG^{\top}L^{\top}\operatorname{expm}(A(\Delta t-\tau))^{\top}\,d\tau\). Equation 4 (p. 5) writes that the integral exhibits covariance \(Q_{\Delta t}=\operatorname{irow}(A^{\#^{-1}}[e^{A^{\#}\Delta t}-I]\operatorname{row}(Q))\) with \(A^{\#}=A\otimes I+I\otimes A\). The homogeneous-process consequence (`ξ`, `z` given) is \(Q_{\Delta t}=\operatorname{cov}(\eta_{ti}\mid\eta_{t-1,i})\) and \(\operatorname{cov}(\eta_{ti},\eta_{t-1,i})=A_{\Delta t}\operatorname{cov}(\eta_{t-1,i})\). The law of total variance on that pair is \(\operatorname{Var}(\eta_{ti})=A_{\Delta t}\operatorname{Var}(\eta_{t-1,i})A_{\Delta t}^{\top}+Q_{\Delta t}\). As \(\Delta t\to\infty\) with stable \(a<0\), Eq. 4 becomes \(-q/(2a)\). The JSS summary names that limit `asymDIFFUSION` and takes it as the total within-subject variance (p. 16). Section 4.3 (pp. 9–10) constrains a stationary `T0VAR` to that model-predicted variance and distinguishes it from a predetermined first occasion. The same section (p. 9) adds a stable trait process with `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is that time-invariant between-subject variance. The JSS article has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). This slice takes scalar \(L=1\) (every latent subject to system noise); it does not implement the 0/1 selector matrix and is not a Kalman filter. Discrete auto-effects are strictly positive for finite real drift and interval; the inverse \(a=\ln\varphi/\Delta t\) therefore requires \(\varphi>0\). Binary64 `exp` of a large negative argument is `+0` and is refused as a discrete lag; the same underflow is a vanishing lagged covariance and is kept. Equations 3–4 of Voelkle et al. (2012) are the discouraged difference-quotient approximation. Meredith (1993) remains unread (Unpaywall/OpenAlex 2026-08-19T00:14Z: closed; Springer `content/pdf` is HTML 200, not a PDF; archive.org title search empty). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex 2026-08-19T00:14Z: closed; Springer `content/pdf` is HTML 200; ETS landing page is HTML). ERIC ED334221 is Singer and Willett (1991), *From whether to when*, not the 1991 journal article. ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title is not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex 2026-08-18T21:07Z: closed; Radboud landing and bitstream 403). +The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:07Z (https://www.zora.uzh.ch/handle/20.500.14742/72792; bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`). Page 16 writes discrete auto-effects as \(A^{*}(\Delta t)=\exp(A\Delta t)\) (Eq. 7). Introducing Intercepts (manuscript p. 20, Eq. 12) adds a continuous-time intercept \(b\) and writes the expected-value solution whose discrete increment is \(A^{-1}(\exp(A\Delta t)-I)b\); the scalar constant-predictor effect is \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). The next paragraph (manuscript p. 21, Eq. 14) writes the discrete effect of a **time-varying** predictor, when the sampling interval equals the constancy interval, as \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product does not depend on the predictor auto-effect. The manuscript calls Eq. 14 a first-order approximation that deteriorates as \(\Delta t\) grows, and defers unmatched sampling/constancy intervals to Oud and Jansen (2000), which is unread. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4; JSS PDF re-opened 2026-08-19T04:10Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\), the discrete intercept \(b_{\Delta t}=A^{-1}[A_{\Delta t}-I]b\), and the discrete process-noise covariance \(Q_{\Delta t}=\int_{0}^{\Delta t}\operatorname{expm}(A(\Delta t-\tau))LGG^{\top}L^{\top}\operatorname{expm}(A(\Delta t-\tau))^{\top}\,d\tau\). Equation 4 (p. 5) writes that the integral exhibits covariance \(Q_{\Delta t}=\operatorname{irow}(A^{\#^{-1}}[e^{A^{\#}\Delta t}-I]\operatorname{row}(Q))\) with \(A^{\#}=A\otimes I+I\otimes A\). The homogeneous-process consequence (`ξ`, `z` given) is \(Q_{\Delta t}=\operatorname{cov}(\eta_{ti}\mid\eta_{t-1,i})\) and \(\operatorname{cov}(\eta_{ti},\eta_{t-1,i})=A_{\Delta t}\operatorname{cov}(\eta_{t-1,i})\). The law of total variance on that pair is \(\operatorname{Var}(\eta_{ti})=A_{\Delta t}\operatorname{Var}(\eta_{t-1,i})A_{\Delta t}^{\top}+Q_{\Delta t}\). As \(\Delta t\to\infty\) with stable \(a<0\), Eq. 4 becomes \(-q/(2a)\). The JSS summary names that limit `asymDIFFUSION` and takes it as the total within-subject variance (p. 16). Section 4.3 (pp. 9–10) constrains a stationary `T0VAR` to that model-predicted variance and distinguishes it from a predetermined first occasion. The same section (p. 9) adds a stable trait process with `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is that time-invariant between-subject variance. The JSS article has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). This slice takes scalar \(L=1\) (every latent subject to system noise); it does not implement the 0/1 selector matrix and is not a Kalman filter. Discrete auto-effects are strictly positive for finite real drift and interval; the inverse \(a=\ln\varphi/\Delta t\) therefore requires \(\varphi>0\). Binary64 `exp` of a large negative argument is `+0` and is refused as a discrete lag; the same underflow is a vanishing lagged covariance and is kept. Equations 3–4 of Voelkle et al. (2012) are the discouraged difference-quotient approximation. Meredith (1993) remains unread (Unpaywall/OpenAlex 2026-08-19T04:10Z: closed; Springer `content/pdf` is HTML 200, not a PDF; archive.org title search empty). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex 2026-08-19T04:10Z: closed; Springer `content/pdf` is HTML 200; ETS landing page is HTML). ERIC ED334221 is Singer and Willett (1991), *From whether to when*, not the 1991 journal article. ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title is not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex 2026-08-18T21:07Z: closed; Radboud landing and bitstream 403). ## Formula notes @@ -58,7 +58,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - **Discrete process noise.** Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-17T21:03Z, p. 4): \(Q_{\Delta t}=\int_{0}^{\Delta t}\operatorname{expm}(A(\Delta t-\tau))GG^{\top}\operatorname{expm}(A(\Delta t-\tau))^{\top}\,d\tau\). The scalar closed form with continuous diffusion \(q=GG^{\top}\ge 0\) is \(q(\mathrm{e}^{2a\Delta t}-1)/(2a)\) for \(a\neq 0\) and \(q\Delta t\) for \(a=0\). The algebraically identical finite-`expm1` evaluation is \(0.5 q(\operatorname{expm1}(z)/a)\) with \(z=2(a\Delta t)\). Form \(z\) as twice the product \(a\Delta t\). Forming \(2a\) first overflows when \(|a|\) is at the binary64 extreme even if \(a\Delta t\) and \(Q_{\Delta t}\) are finite. Binary64 underflow of \(z\) to `+0` recovers \(q\Delta t\). \(z\to-\infty\) keeps the equilibrium variance \(-q/(2a)=-0.5 q/a\); `expm1(−∞)` is \(-1\), so that path stays on the finite increment. When `expm1(z)` overflows at a finite \(z\), rewrite as \(\operatorname{sign}(q/a)\exp(\ln|q|+z-\ln|a|-\ln 2)-0.5 q/a\). An overflowing rewrite scale \(0.5 q/a\) is not a finite \(Q_{\Delta t}\) and fails closed (`q=1e308`, `a=0.1`, `Δt=4000` → `z=800`; JSS PDF re-opened 2026-08-18T03:07Z, p. 4). A zero diffusion is exactly zero even if `expm1` overflows. \(z\to+\infty\) fails closed unless \(q=0\). Negative \(q\) fails closed. This is not a Kalman filter and not a matrix `expm`. - **Lagged latent covariance.** Driver, Oud, and Voelkle (2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T11:20Z): \(\operatorname{cov}(\eta_{ti},\eta_{t-1,i})=A_{\Delta t}\operatorname{cov}(\eta_{t-1,i})\) for the homogeneous process. The scalar map is \(\mathrm{e}^{a\Delta t}p\) with prior variance \(p\ge 0\). This is not \(Q_{\Delta t}\). Binary64 underflow of \(\mathrm{e}^{a\Delta t}\) to `+0` is a vanishing covariance and is kept. A zero prior variance is exactly zero. Finite-\(a\Delta t\) exponential overflow rewrites as \(\exp(\ln p+a\Delta t)\). A finite \(\mathrm{e}^{a\Delta t}\) whose product with \(p\) overflows fails closed. The JSS article has no numbered §2.2. - **Discrete latent variance.** Equations 3–4 write \(Q_{\Delta t}\) as the covariance of the stochastic integral, so \(Q_{\Delta t}=\operatorname{cov}(\eta_{ti}\mid\eta_{t-1,i})\) when \(\xi\) and \(z\) are given. The law of total variance on that pair is \(\operatorname{Var}(\eta_{ti})=A_{\Delta t}\operatorname{Var}(\eta_{t-1,i})A_{\Delta t}^{\top}+Q_{\Delta t}\). The scalar map is \(\mathrm{e}^{2a\Delta t}p+Q_{\Delta t}\). This is not a Kalman measurement update. A zero prior variance is exactly \(Q_{\Delta t}\). Binary64 underflow of \(\mathrm{e}^{2a\Delta t}\) keeps \(Q_{\Delta t}\). Finite-\(z\) exponential overflow rewrites as \(\exp(\ln p+z)+Q_{\Delta t}\). A zero diffusion is exactly \(Q_{\Delta t}=0\); that skip does not license a non-finite carried term when \(2(a\Delta t)\) overflows to \(+\infty\). Treating \(Q_{\Delta t}\) as \(\operatorname{Var}(\eta_{t})\) fails closed. -- **Stationary within-subject variance.** Driver et al. (2017, Eq. 4 as \(\Delta t\to\infty\); §4.3 pp. 9–10; p. 16 `asymDIFFUSION`; JSS PDF re-opened 2026-08-19T00:14Z, p. 5): for stable \(a<0\), \(\lim_{\Delta t\to\infty}Q_{\Delta t}=-q/(2a)\). Form the ratio \((q/a)\times-0.5\). Forming \(2a\) first overflows when \(|a|\) is at the binary64 extreme (`a=-1e308`, `q=1e308` → `0.5`). Forming \(0.5q\) first underflows at the minimum subnormal (`q=from_bits(1)`, `a=-from_bits(1)` → naive `+0`; representable solution `0.5`). Starting from that variance, \(\operatorname{Var}(\eta_{t})\) is invariant across finite event intervals. A zero diffusion is exactly zero. \(a\ge 0\) has no finite stationary variance (Brownian \(a=0\) grows as \(q\Delta t\)). An overflowing \((q/a)\times-0.5\) fails closed. Finite-interval \(Q_{\Delta t}\) is not that limit. This is not ctsem estimation and not a Kalman filter. +- **Stationary within-subject variance.** Driver et al. (2017, Eq. 4 as \(\Delta t\to\infty\); §4.3 pp. 9–10; p. 16 `asymDIFFUSION`; JSS PDF re-opened 2026-08-19T04:10Z, p. 5): for stable \(a<0\), \(\lim_{\Delta t\to\infty}Q_{\Delta t}=-q/(2a)\). When \(2a\) is finite, form \(q/-(2a)\) so \(q/a\) overflow does not lose a finite Lyapunov solution (`q=MAX`, `a=-0.75` → `MAX/1.5`; CodeRabbit on `75ecdd3`). When \(2a\) overflows, form \((q/a)\times-0.5\). Forming \(2a\) as the only path overflows when \(|a|\) is at the binary64 extreme (`a=-1e308`, `q=1e308` → `0.5`). Forming \(0.5q\) first underflows at the minimum subnormal (`q=from_bits(1)`, `a=-from_bits(1)` → naive `+0`; representable solution `0.5`). Starting from that variance, \(\operatorname{Var}(\eta_{t})\) is invariant across finite event intervals. A zero diffusion is exactly zero. \(a\ge 0\) has no finite stationary variance (Brownian \(a=0\) grows as \(q\Delta t\)). An overflowing Lyapunov solution fails closed. Finite-interval \(Q_{\Delta t}\) is not that limit. This is not ctsem estimation and not a Kalman filter. - **Trait-plus-state variance.** Driver et al. (2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z): a stable trait process has `DRIFT` and `DIFFUSION` fixed to zero. The scalar maps are \(\operatorname{Var}=\mathrm{trait}+\mathrm{state}\) and \(\operatorname{cov}(t,t-1)=\mathrm{trait}+\mathrm{e}^{a\Delta t}p\). The ctsem `TRAITVAR` rewrite that adds the trait to `DIFFUSION` does not license treating trait variance as \(Q_{\Delta t}\). Trait variance is not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not this map. A zero trait is exactly the state. A zero state is exactly the trait. An overflowing sum fails closed. This is not RI-CLPM and not ctsem estimation. - **Observed-indicator variance.** Driver et al. (2017, Eq. 1, p. 4; p. 16 `MANIFESTVAR`; JSS PDF re-opened 2026-08-19T00:14Z): \(y_i(t)=\Lambda\eta_i(t)+\tau+\varepsilon_i(t)\) with \(\varepsilon\sim N(0,\Theta)\). The scalar map is \(\operatorname{Var}(y)=\lambda^{2}\operatorname{Var}(\eta)+\theta\). Form \((\lambda p)\lambda\) then add \(\theta\). Forming \(\lambda^{2}\) first overflows at \(\lambda=10^{308}\), \(p=10^{-308}\). A zero loading or zero latent variance is exactly \(\theta\). A zero measurement error is exactly \(\lambda^{2}p\). `MANIFESTVAR` is not \(\operatorname{Var}(y)\). \(\operatorname{Var}(\eta)\) is not \(\operatorname{Var}(y)\). An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **CWC-then-lag.** Sample cluster means are removed first. Consecutive within residuals are then fitted by least squares to \(r_{t+\Delta t}\approx\exp(a\Delta t)\,r_{t}\) on event time. Same-sign pair-wise logs initialize the scalar Newton step. Sign-flipping \(T=2\) CWC pairs have no real logarithm and fail closed. Curran and Bauer (2011, pp. 607–608) show that this person-mean subtraction on a raw autoregressive series does **not** isolate the lagged within-person effect; the helper therefore does not claim to recover the raw-process drift. @@ -76,7 +76,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - Voelkle et al. (2012, Eq. 14) recovers \(a_{yx}\Delta t\) at machine-scale RMSE when sampling, constancy, and event intervals match, and that value is not the Eq. 12 constant-predictor effect; unmatched intervals, a non-event clock, a non-finite continuous effect, and an overflowing product fail closed; - Driver et al. (2017, Eq. 3) recovers a known scalar \(Q_{\Delta t}\) at machine-scale RMSE, and that RMSE is smaller than treating the continuous diffusion as the discrete process noise; \(a=0\) and binary64 underflow of \(2a\Delta t\) recover \(q\Delta t\); \(z\to-\infty\) recovers \(-q/(2a)\); a finite result whose `expm1` overflows at a finite \(z\) is recovered in log space; a finite result whose \(2a\) overflows while \(a\Delta t\) stays finite is recovered as \(0.5 q(\operatorname{expm1}(z)/a)\); a zero diffusion is exactly zero; a negative diffusion, \(z\to+\infty\), and an overflowing rewrite scale \(0.5 q/a\) fail closed; non-finite event interval, non-positive sampling interval, and non-finite constancy interval fail closed on Eq. 14; non-finite / non-positive event interval, non-finite / negative diffusion, and non-finite log-rate fail closed on Eq. 3; - Driver et al. (2017, Eq. 3–4) recovers a known lagged covariance \(\mathrm{e}^{a\Delta t}p\) and a known latent variance \(\mathrm{e}^{2a\Delta t}p+Q_{\Delta t}\) at machine-scale RMSE, and that variance RMSE is smaller than treating \(Q_{\Delta t}\) as \(\operatorname{Var}(\eta_{t})\); a zero prior variance is exactly zero (lagged) or \(Q_{\Delta t}\) (unconditional); binary64 underflow of \(\mathrm{e}^{a\Delta t}\) keeps a vanishing lagged covariance; a finite \(\mathrm{e}^{a\Delta t}\) whose product with \(p\) overflows fails closed; a zero diffusion whose \(2(a\Delta t)\) overflows to \(+\infty\) fails closed; treating \(Q_{\Delta t}\) as the unconditional variance fails closed; -- Driver et al. (2017, Eq. 4 as \(\Delta t\to\infty\); p. 16 `asymDIFFUSION`; §4.3) recovers a known stationary variance \(-q/(2a)\) at machine-scale RMSE, and that RMSE is smaller than treating finite-interval \(Q_{\Delta t}\) as the stationary variance; starting from that variance, \(\operatorname{Var}(\eta_{t})\) is invariant across finite \(\Delta t\); a zero diffusion is exactly zero; forming \(2a\) first overflows while \((q/a)\times-0.5\) stays finite; forming \(0.5q\) first underflows at the minimum subnormal while \((q/a)\times-0.5=0.5\); \(a\ge 0\), a non-event clock, a negative diffusion, and an overflowing \((q/a)\times-0.5\) fail closed; +- Driver et al. (2017, Eq. 4 as \(\Delta t\to\infty\); p. 16 `asymDIFFUSION`; §4.3) recovers a known stationary variance \(-q/(2a)\) at machine-scale RMSE, and that RMSE is smaller than treating finite-interval \(Q_{\Delta t}\) as the stationary variance; starting from that variance, \(\operatorname{Var}(\eta_{t})\) is invariant across finite \(\Delta t\); a zero diffusion is exactly zero; forming \(2a\) first overflows while \((q/a)\times-0.5\) stays finite; forming \(0.5q\) first underflows at the minimum subnormal while \((q/a)\times-0.5=0.5\); forming \(q/a\) first overflows at `q=MAX`, `a=-0.75` while \(q/-(2a)=\mathrm{MAX}/1.5\) stays finite; \(a\ge 0\), a non-event clock, a negative diffusion, and an overflowing Lyapunov solution fail closed; - Driver et al. (2017, §4.3, p. 9) recovers a known trait-plus-state variance and lagged covariance at machine-scale RMSE, and that RMSE is smaller than evolving the summed variance as if it were all state; a zero trait is the state; a zero state is the trait; treating trait variance as process noise or as `asymDIFFUSION` fails closed; an overflowing sum and a non-event clock fail closed; - Driver et al. (2017, Eq. 1, p. 4; p. 16 `MANIFESTVAR`) recovers a known observed-indicator variance \(\lambda^{2}\operatorname{Var}(\eta)+\theta\) at machine-scale RMSE, and that RMSE is smaller than treating `MANIFESTVAR` or \(\operatorname{Var}(\eta)\) as \(\operatorname{Var}(y)\); a zero loading or zero latent variance is \(\theta\); forming \(\lambda^{2}\) first overflows while \((\lambda p)\lambda\) stays finite; an overflowing product or sum fails closed; - pooling discrete lags from unequal intervals fails closed; From 056abc3f84df03b0835d3bc0fa7cb0db592d2de3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 04:23:37 +0000 Subject: [PATCH 28/62] feat(psychometric): recover Driver Eq. 5 MANIFESTTRAITVAR observed variance Equation 1 is the latent SDE. Equation 5 (p. 5) is the measurement model. Table 2 (p. 12) names MANIFESTVAR as Theta and MANIFESTTRAITVAR as Psi_tau. Var(y) = lambda^2 p + theta + psi. TRAITVAR is latent and scaled by lambda^2. --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 1 + CLAUDE.md | 2 +- crates/psychometric_core/src/error.rs | 17 +- crates/psychometric_core/src/event_time.rs | 193 +++++++++++++++--- crates/psychometric_core/src/lib.rs | 16 +- ...multilevel_event_time_recovery_contract.rs | 82 +++++++- .../scientific_claim_boundary_contract.rs | 56 ++++- docs/TRACEABILITY.md | 2 +- docs/adr/0005-posterior-esem-dsem.md | 4 +- docs/adr/README.md | 2 +- .../multilevel-event-time-recovery.md | 6 +- 12 files changed, 324 insertions(+), 59 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index a087c12c..c9a235a1 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -61,7 +61,7 @@ boundaries above remain the target modular MSA architecture. | `tepp_simulation` | known-truth temporal/event data generation | | `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics | | `tepp_api` | versioned DTO, schema, and export contracts | -| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance (Driver et al., 2017, Eq. 1; `MANIFESTVAR` is not `Var(y)`), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means | +| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means | No crate exposes placeholder production behavior in Task 1. This prevents an empty façade from becoming a de facto public API before its invariants and tests diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b38907d..ea82aa6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar observed-indicator variance with `MANIFESTTRAITVAR`. Equation 5 writes `y_i(t) = τ_i + Λ η_i(t) + ε_i(t)` with `ε ~ N(0, Θ)` and `τ_i ~ N(μ_τ, Ψ_τ)`. Equation 1 (p. 4) is the latent SDE, not the measurement model. Table 2 names `Θ` `MANIFESTVAR` and `Ψ_τ` `MANIFESTTRAITVAR`; p. 16 restates those names. The scalar map is `Var(y) = λ² Var(η) + θ` when `Ψ_τ = 0` and `λ² Var(η) + θ + ψ` otherwise. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is not `Var(y)`. `MANIFESTTRAITVAR` is not `MANIFESTVAR`. `TRAITVAR` is latent additional variance and is scaled by `λ²`; `MANIFESTTRAITVAR` is not. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall/OpenAlex 2026-08-19T04:18Z: closed). Oud and Jansen (2000) remains unread. ZORA Anubis-blocked. - `psychometric_core` evaluates the Driver, Oud, and Voelkle (2017, Eq. 4, p. 5; JSS PDF re-opened 2026-08-19T04:10Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) stationary within-subject variance as `q / -(2 a)` when the scalar Kronecker sum `2 a` is finite. The paper limit is `-q / (2 a)` (`A# = A ⊗ I + I ⊗ A`; p. 16 `asymDIFFUSION`). Forming `q / a` first overflows at `q = MAX`, `a = -0.75` while `MAX / 1.5` is finite (CodeRabbit finding on `75ecdd3`). When `2 a` overflows (`a = -1e308`), form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). Still not a Kalman filter, not DSEM, not a matrix `expm`, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall/OpenAlex 2026-08-19T04:10Z: closed). Oud and Jansen (2000) remains unread. ZORA Anubis-blocked. - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 1, p. 4; p. 16 `MANIFESTVAR`; JSS PDF re-opened 2026-08-19T00:14Z) scalar observed-indicator variance. Equation 1 writes `y_i(t) = Λ η_i(t) + τ + ε_i(t)` with `ε ~ N(0, Θ)`. The scalar map is `Var(y) = λ² Var(η) + θ`. Form `(λ p) λ` then add `θ`. Do not form `λ²` first (`λ = 1e308`, `p = 1e-308` → `1e308`). `MANIFESTVAR` is not `Var(y)`. `Var(η)` is not `Var(y)`. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall/OpenAlex 2026-08-19T00:14Z: closed). Oud and Jansen (2000) remains unread. ZORA Anubis-blocked. - `psychometric_core` evaluates the Driver, Oud, and Voelkle (2017, Eq. 4, p. 5; JSS PDF re-opened 2026-08-19T00:14Z) stationary within-subject variance as `(q / a) * -0.5`. The paper limit is `-q / (2 a)`. Forming `2 a` first overflows at `a = -1e308`. Forming `0.5 q` first underflows at `q = from_bits(1)`, `a = -from_bits(1)` and returns `+0` (CodeRabbit finding on `556e23d`); the representable Lyapunov solution is `0.5`. Still not a Kalman filter, not DSEM, not a matrix `expm`, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall/OpenAlex 2026-08-19T00:14Z: closed; Springer `content/pdf` is HTML 200). Oud and Jansen (2000) remains unread. ZORA Anubis-blocked. diff --git a/CLAUDE.md b/CLAUDE.md index 10714ac0..2cc2a9d2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. -- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` (Driver et al., 2017, Eq. 1, p. 4; p. 16 `MANIFESTVAR`). Form `(λ p) λ` then add `θ`. `MANIFESTVAR` is not `Var(y)`. `Var(η)` is not `Var(y)`. +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. diff --git a/crates/psychometric_core/src/error.rs b/crates/psychometric_core/src/error.rs index 86ce9a6b..d29ccefe 100644 --- a/crates/psychometric_core/src/error.rs +++ b/crates/psychometric_core/src/error.rs @@ -62,12 +62,16 @@ pub enum PsychometricError { /// within-subject variance. `TRAITVAR` is between-subject and /// time-invariant; `asymDIFFUSION` is the `Δt → ∞` state variance. TraitVarianceIsNotStationaryWithinSubject, - /// Driver Eq. 1 measurement-error variance was treated as the - /// observed-indicator variance. `MANIFESTVAR` is `Θ`, not `Var(y)`. + /// Driver Eq. 5 measurement-error variance was treated as the + /// observed-indicator variance. Table 2 (p. 12) names + /// `MANIFESTVAR` as `Θ`, not `Var(y)`. MeasurementErrorIsNotObservedVariance, - /// Driver Eq. 1 latent variance was treated as the observed-indicator + /// Driver Eq. 5 latent variance was treated as the observed-indicator /// variance. `Var(η)` is not `Var(y)`. LatentVarianceIsNotObservedVariance, + /// Driver Eq. 5 `MANIFESTTRAITVAR` was treated as `MANIFESTVAR`. + /// Table 2 (p. 12) names `Ψ_τ` separately from `Θ`. + ManifestTraitVarianceIsNotMeasurementError, } impl fmt::Display for PsychometricError { @@ -126,6 +130,9 @@ impl fmt::Display for PsychometricError { Self::LatentVarianceIsNotObservedVariance => { "latent variance is not the observed-indicator variance" } + Self::ManifestTraitVarianceIsNotMeasurementError => { + "manifest-trait variance is not measurement-error variance" + } }; formatter.write_str(message) } @@ -231,5 +238,9 @@ mod tests { PsychometricError::LatentVarianceIsNotObservedVariance.to_string(), "latent variance is not the observed-indicator variance" ); + assert_eq!( + PsychometricError::ManifestTraitVarianceIsNotMeasurementError.to_string(), + "manifest-trait variance is not measurement-error variance" + ); } } diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index c282a6ec..0c77fbdb 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -24,11 +24,15 @@ //! `-q / (2 a)`. Section 4.3 (p. 9) then adds a stable trait process //! with `DRIFT` and `DIFFUSION` fixed to zero. That `TRAITVAR` is //! time-invariant between-subject variance; it is not process noise -//! and not `asymDIFFUSION`. Equation 1 (p. 4) writes -//! `y_i(t) = Λ η_i(t) + τ + ε_i(t)` with `ε ~ N(0, Θ)`. The JSS -//! summary (p. 16) names `Θ` `MANIFESTVAR`. The scalar observed -//! variance is `λ² Var(η) + θ`. `MANIFESTVAR` is not `Var(y)` and -//! `Var(η)` is not `Var(y)`. The JSS article +//! and not `asymDIFFUSION`. Equation 1 (p. 4) is the latent SDE. +//! Equation 5 (p. 5) writes `y_i(t) = τ_i + Λ η_i(t) + ε_i(t)` with +//! `ε ~ N(0, Θ)` and `τ_i ~ N(μ_τ, Ψ_τ)`. Table 2 (p. 12) names +//! `Θ` `MANIFESTVAR` and `Ψ_τ` `MANIFESTTRAITVAR`. The JSS summary +//! (p. 16) restates those names; it is not the measurement equation. +//! The scalar observed variance is `λ² Var(η) + θ` when `Ψ_τ = 0` +//! and `λ² Var(η) + θ + ψ` otherwise. `MANIFESTVAR` is not `Var(y)`, +//! `MANIFESTTRAITVAR` is not `MANIFESTVAR`, `TRAITVAR` is latent +//! (scaled by `λ²`), and `Var(η)` is not `Var(y)`. The JSS article //! has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). //! The difference quotient `(x(t+Δt) − x(t)) / Δt` (their //! Eqs. 3–4) is refused. This is not DSEM and not a matrix `expm`. @@ -799,15 +803,19 @@ pub fn refuse_trait_variance_as_stationary_within_subject( Err(PsychometricError::TraitVarianceIsNotStationaryWithinSubject) } -/// Exact scalar observed-indicator variance from Driver Equation 1. -/// -/// Driver, Oud, and Voelkle (2017, Eq. 1, p. 4; JSS PDF re-opened -/// 2026-08-19T00:14Z) write `y_i(t) = Λ η_i(t) + τ + ε_i(t)` with -/// `ε ~ N(0, Θ)`. The JSS summary (p. 16) names `Θ` `MANIFESTVAR`. -/// The scalar map is `Var(y) = λ² Var(η) + θ`. Form `(λ p) λ` then -/// add `θ`. Do not form `λ²` first: at `λ = 1e308`, `p = 1e-308`, -/// `λ²` overflows and `λ² p` is non-finite, but `(λ p) λ = 1e308`. -/// A zero loading or zero latent variance is exactly `θ`. A zero +/// Exact scalar observed-indicator variance from Driver Equation 5 +/// with `MANIFESTTRAITVAR = 0`. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Table 2, p. 12; JSS +/// PDF re-opened 2026-08-19T04:18Z) write `y_i(t) = τ_i + Λ η_i(t) + +/// ε_i(t)` with `ε ~ N(0, Θ)` and `τ_i ~ N(μ_τ, Ψ_τ)`. Equation 1 +/// (p. 4) is the latent SDE, not the measurement model. Table 2 names +/// `Θ` `MANIFESTVAR` and `Ψ_τ` `MANIFESTTRAITVAR`. The p. 16 summary +/// restates those names; it is not the equation. With `Ψ_τ = 0` the +/// scalar map is `Var(y) = λ² Var(η) + θ`. Form `(λ p) λ` then add +/// `θ`. Do not form `λ²` first: at `λ = 1e308`, `p = 1e-308`, `λ²` +/// overflows and `λ² p` is non-finite, but `(λ p) λ = 1e308`. A zero +/// loading or zero latent variance is exactly `θ`. A zero /// measurement error is exactly `λ² p`. Negative latent or /// measurement-error variance fails closed. An overflowing product /// or sum fails closed. This is not a Kalman filter, not ESEM @@ -841,10 +849,10 @@ pub fn recover_manifest_observed_variance( require_finite(explained + measurement_error_variance) } -/// Refuse treating Driver Eq. 1 measurement error as `Var(y)`. +/// Refuse treating Driver Eq. 5 measurement error as `Var(y)`. /// -/// `MANIFESTVAR` is `Θ`, the variance of `ε`. Equation 1 maps -/// `Var(y) = λ² Var(η) + θ`. +/// Table 2 (p. 12) names `MANIFESTVAR` as `Θ`, the variance of `ε`. +/// Equation 5 maps `Var(y) = λ² Var(η) + θ` when `Ψ_τ = 0`. /// /// # Errors /// @@ -858,10 +866,10 @@ pub fn refuse_measurement_error_as_observed_variance( Err(PsychometricError::MeasurementErrorIsNotObservedVariance) } -/// Refuse treating Driver Eq. 1 latent variance as `Var(y)`. +/// Refuse treating Driver Eq. 5 latent variance as `Var(y)`. /// -/// `Var(η)` is the latent process variance. Equation 1 maps -/// `Var(y) = λ² Var(η) + θ`. +/// `Var(η)` is the latent process variance. Equation 5 maps +/// `Var(y) = λ² Var(η) + θ` when `Ψ_τ = 0`. /// /// # Errors /// @@ -874,6 +882,63 @@ pub fn refuse_latent_variance_as_observed_variance( Err(PsychometricError::LatentVarianceIsNotObservedVariance) } +/// Exact scalar observed-indicator variance from Driver Equation 5 +/// with nonzero `MANIFESTTRAITVAR`. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Table 2, p. 12; JSS +/// PDF re-opened 2026-08-19T04:18Z) write `τ_i ~ N(μ_τ, Ψ_τ)` on the +/// indicator intercept. The scalar map is `Var(y) = λ² Var(η) + θ + +/// ψ`. Form the `Ψ_τ = 0` map first, then add `ψ`. Do not form +/// `λ²` first. A zero manifest trait is exactly `λ² p + θ`. A zero +/// loading or zero latent variance is exactly `θ + ψ`. `Ψ_τ` is not +/// `Θ`: Table 2 names `MANIFESTTRAITVAR` separately from +/// `MANIFESTVAR`. `TRAITVAR` is latent additional variance and is +/// scaled by `λ²`; `MANIFESTTRAITVAR` is not. Negative trait +/// variance fails closed. An overflowing sum fails closed. This is +/// not a Kalman filter, not ESEM estimation, and not ctsem +/// estimation. +/// +/// # Errors +/// +/// Propagates [`recover_manifest_observed_variance`]. Returns +/// [`PsychometricError::InvalidNumericInput`] when the manifest-trait +/// variance is negative or non-finite or the sum overflows. +pub fn recover_manifest_trait_plus_state_observed_variance( + loading: f64, + latent_variance: f64, + measurement_error_variance: f64, + manifest_trait_variance: f64, +) -> Result { + if !manifest_trait_variance.is_finite() || manifest_trait_variance < 0.0 { + return Err(PsychometricError::InvalidNumericInput); + } + let within = + recover_manifest_observed_variance(loading, latent_variance, measurement_error_variance)?; + if manifest_trait_variance == 0.0 { + return Ok(within); + } + require_finite(within + manifest_trait_variance) +} + +/// Refuse treating Driver Eq. 5 `MANIFESTTRAITVAR` as `MANIFESTVAR`. +/// +/// Table 2 (p. 12) names `MANIFESTTRAITVAR` as `Ψ_τ`, additional +/// intercept variance on the indicators, and `MANIFESTVAR` as `Θ`, +/// the variance of `ε`. Equation 5 maps `Var(y) = λ² Var(η) + θ + +/// ψ`. `Ψ_τ` is not `Θ`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::ManifestTraitVarianceIsNotMeasurementError`]. +pub fn refuse_manifest_trait_variance_as_measurement_error( + manifest_trait_variance: f64, + measurement_error_variance: f64, +) -> Result { + let _ = (manifest_trait_variance, measurement_error_variance); + Err(PsychometricError::ManifestTraitVarianceIsNotMeasurementError) +} + /// Refuse treating Driver Eq. 3 process noise as the unconditional variance. /// /// Driver, Oud, and Voelkle (2017, Eq. 3–4, pp. 4–5): @@ -1128,11 +1193,14 @@ mod tests { recover_discrete_process_noise, recover_discrete_time_varying_predictor_effect, recover_event_series_mean_log_rate, recover_event_time_discrete_lag_and_log_rate, recover_irregular_centered_residual_log_rate, recover_local_log_rate, - recover_manifest_observed_variance, recover_stationary_latent_variance, - recover_trait_plus_state_lagged_covariance, recover_trait_plus_state_latent_variance, - recover_within_residual_event_time_log_rate, refuse_difference_quotient_as_local_rate, + recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, + recover_stationary_latent_variance, recover_trait_plus_state_lagged_covariance, + recover_trait_plus_state_latent_variance, recover_within_residual_event_time_log_rate, + refuse_difference_quotient_as_local_rate, refuse_finite_interval_process_noise_as_stationary_variance, - refuse_latent_variance_as_observed_variance, refuse_measurement_error_as_observed_variance, + refuse_latent_variance_as_observed_variance, + refuse_manifest_trait_variance_as_measurement_error, + refuse_measurement_error_as_observed_variance, refuse_pooled_discrete_lag_across_unequal_intervals, refuse_process_noise_as_unconditional_variance, refuse_trait_variance_as_process_noise, refuse_trait_variance_as_stationary_within_subject, @@ -2478,12 +2546,12 @@ mod tests { } #[test] - fn manifest_observed_variance_recovers_driver_equation_one() { + fn manifest_observed_variance_recovers_driver_equation_five() { let loading = 2.0_f64; let latent = 0.4_f64; let measurement_error = 0.1_f64; let recovered = - recover_manifest_observed_variance(loading, latent, measurement_error).expect("eq1"); + recover_manifest_observed_variance(loading, latent, measurement_error).expect("eq5"); let expected = (loading * latent) * loading + measurement_error; assert!((recovered - expected).abs() < 1e-15); assert!((recovered - 1.7).abs() < 1e-15); @@ -2515,6 +2583,79 @@ mod tests { assert!(!(1e308_f64 * 1e308_f64).is_finite()); } + #[test] + fn manifest_trait_plus_state_observed_variance_recovers_driver_equation_five() { + let loading = 2.0_f64; + let latent = 0.4_f64; + let measurement_error = 0.1_f64; + let manifest_trait = 0.5_f64; + let recovered = recover_manifest_trait_plus_state_observed_variance( + loading, + latent, + measurement_error, + manifest_trait, + ) + .expect("eq5-trait"); + let expected = (loading * latent) * loading + measurement_error + manifest_trait; + assert!((recovered - expected).abs() < 1e-15); + assert!((recovered - 2.2).abs() < 1e-15); + let without_trait = + recover_manifest_observed_variance(loading, latent, measurement_error).expect("psi0"); + assert_eq!( + recover_manifest_trait_plus_state_observed_variance( + loading, + latent, + measurement_error, + 0.0 + ), + Ok(without_trait) + ); + assert!((without_trait - recovered).abs() > 1e-3); + assert_eq!( + refuse_manifest_trait_variance_as_measurement_error(manifest_trait, measurement_error), + Err(PsychometricError::ManifestTraitVarianceIsNotMeasurementError) + ); + // Zero loading: Var(y) = θ + ψ, not ψ stuffed as Θ. + assert_eq!( + recover_manifest_trait_plus_state_observed_variance( + 0.0, + latent, + measurement_error, + manifest_trait + ), + Ok(measurement_error + manifest_trait) + ); + // TRAITVAR is latent and scaled by λ²; MANIFESTTRAITVAR is not. + let latent_trait_as_state = + recover_manifest_observed_variance(loading, latent + manifest_trait, measurement_error) + .expect("traitvar"); + assert!((latent_trait_as_state - recovered).abs() > 1e-3); + // Do not form λ² first, then add ψ. + let scaled = recover_manifest_trait_plus_state_observed_variance(1e308, 1e-308, 0.0, 1.0) + .expect("scale-psi"); + assert!((scaled - 1e308).abs() / 1e308 < 1e-15); + } + + #[test] + fn manifest_trait_plus_state_observed_variance_invalid_inputs_fail_closed() { + assert_eq!( + recover_manifest_trait_plus_state_observed_variance(2.0, 0.4, 0.1, -0.1), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_manifest_trait_plus_state_observed_variance(2.0, 0.4, 0.1, f64::NAN), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_manifest_trait_plus_state_observed_variance(1e308, 1.0, 0.0, 0.3), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_manifest_trait_plus_state_observed_variance(1e308, 1e-308, 1e308, 1e308), + Err(PsychometricError::InvalidNumericInput) + ); + } + #[test] fn manifest_observed_variance_invalid_inputs_fail_closed() { assert_eq!( diff --git a/crates/psychometric_core/src/lib.rs b/crates/psychometric_core/src/lib.rs index b08323c1..05bdd7ba 100644 --- a/crates/psychometric_core/src/lib.rs +++ b/crates/psychometric_core/src/lib.rs @@ -21,9 +21,11 @@ //! variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 //! `asymDIFFUSION`), recovers the Driver §4.3 trait-plus-state //! variance and lagged covariance (`TRAITVAR` is not process noise -//! and not `asymDIFFUSION`), recovers the Driver Eq. 1 scalar -//! observed-indicator variance (`λ² Var(η) + θ`; `MANIFESTVAR` is -//! not `Var(y)`), +//! and not `asymDIFFUSION`), recovers the Driver Eq. 5 scalar +//! observed-indicator variance (`λ² Var(η) + θ` when +//! `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; Table 2, +//! p. 12: `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is +//! not `MANIFESTVAR`; Equation 1 is the SDE), //! and refuses //! latent-mean comparison below strong invariance. @@ -96,6 +98,8 @@ pub use event_time::recover_irregular_centered_residual_log_rate; pub use event_time::recover_local_log_rate; /// Exact scalar observed-indicator variance `λ² Var(η) + θ`. pub use event_time::recover_manifest_observed_variance; +/// Exact scalar observed-indicator variance `λ² Var(η) + θ + ψ`. +pub use event_time::recover_manifest_trait_plus_state_observed_variance; /// Exact scalar stationary within-subject variance `-q / (2 a)`. pub use event_time::recover_stationary_latent_variance; /// Exact scalar trait-plus-state lagged covariance. @@ -108,9 +112,11 @@ pub use event_time::recover_within_residual_event_time_log_rate; pub use event_time::refuse_difference_quotient_as_local_rate; /// Refuse treating finite-interval `Q_Δt` as `asymDIFFUSION`. pub use event_time::refuse_finite_interval_process_noise_as_stationary_variance; -/// Refuse treating Driver Eq. 1 latent variance as `Var(y)`. +/// Refuse treating Driver Eq. 5 latent variance as `Var(y)`. pub use event_time::refuse_latent_variance_as_observed_variance; -/// Refuse treating Driver Eq. 1 measurement error as `Var(y)`. +/// Refuse treating Driver Eq. 5 `MANIFESTTRAITVAR` as `MANIFESTVAR`. +pub use event_time::refuse_manifest_trait_variance_as_measurement_error; +/// Refuse treating Driver Eq. 5 measurement error as `Var(y)`. pub use event_time::refuse_measurement_error_as_observed_variance; /// Refuse pooling discrete lags from unequal event intervals. pub use event_time::refuse_pooled_discrete_lag_across_unequal_intervals; 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 cf1d0f09..b8eff0c0 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -10,11 +10,14 @@ use psychometric_core::{ recover_discrete_process_noise, recover_discrete_time_varying_predictor_effect, recover_event_series_mean_log_rate, recover_event_time_discrete_lag_and_log_rate, recover_irregular_centered_residual_log_rate, recover_kish_weighted_slope, - recover_manifest_observed_variance, recover_stationary_latent_variance, - recover_trait_plus_state_lagged_covariance, recover_trait_plus_state_latent_variance, - recover_within_residual_event_time_log_rate, refuse_difference_quotient_as_local_rate, + recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, + recover_stationary_latent_variance, recover_trait_plus_state_lagged_covariance, + recover_trait_plus_state_latent_variance, recover_within_residual_event_time_log_rate, + refuse_difference_quotient_as_local_rate, refuse_finite_interval_process_noise_as_stationary_variance, - refuse_latent_variance_as_observed_variance, refuse_measurement_error_as_observed_variance, + refuse_latent_variance_as_observed_variance, + refuse_manifest_trait_variance_as_measurement_error, + refuse_measurement_error_as_observed_variance, refuse_pooled_discrete_lag_across_unequal_intervals, refuse_process_noise_as_unconditional_variance, refuse_trait_variance_as_process_noise, refuse_trait_variance_as_stationary_within_subject, @@ -792,15 +795,15 @@ fn trait_plus_state_recovers_driver_section_four_point_three() { } #[test] -fn manifest_observed_variance_recovers_driver_equation_one() { +fn manifest_observed_variance_recovers_driver_equation_five() { let loading = 2.0_f64; let latent = 0.4_f64; let measurement_error = 0.1_f64; let observed = - recover_manifest_observed_variance(loading, latent, measurement_error).expect("eq1"); + recover_manifest_observed_variance(loading, latent, measurement_error).expect("eq5"); let expected = (loading * latent) * loading + measurement_error; let error = rmse(&[expected], &[observed]); - assert!(error < 1e-15, "Driver Eq. 1 Var(y) RMSE {error}"); + assert!(error < 1e-15, "Driver Eq. 5 Var(y) RMSE {error}"); let collapsed_error = rmse(&[expected], &[measurement_error]); let latent_error = rmse(&[expected], &[latent]); assert!( @@ -826,7 +829,7 @@ fn manifest_observed_variance_recovers_driver_equation_one() { let scaled = recover_manifest_observed_variance(1e308, 1e-308, 0.0).expect("scale"); assert!( (scaled - 1e308).abs() / 1e308 < 1e-15, - "Driver Eq. 1 (λ p)λ must keep λ=1e308, p=1e-308: got {scaled}" + "Driver Eq. 5 (λ p)λ must keep λ=1e308, p=1e-308: got {scaled}" ); assert_eq!( recover_manifest_observed_variance(1e308, 1.0, 0.0), @@ -834,6 +837,69 @@ fn manifest_observed_variance_recovers_driver_equation_one() { ); } +#[test] +fn manifest_trait_plus_state_observed_variance_recovers_driver_equation_five() { + let loading = 2.0_f64; + let latent = 0.4_f64; + let measurement_error = 0.1_f64; + let manifest_trait = 0.5_f64; + let observed = recover_manifest_trait_plus_state_observed_variance( + loading, + latent, + measurement_error, + manifest_trait, + ) + .expect("eq5-trait"); + let expected = (loading * latent) * loading + measurement_error + manifest_trait; + let error = rmse(&[expected], &[observed]); + assert!(error < 1e-15, "Driver Eq. 5 λ²p+θ+ψ RMSE {error}"); + let dropped_trait = + recover_manifest_observed_variance(loading, latent, measurement_error).expect("psi0"); + let dropped_error = rmse(&[expected], &[dropped_trait]); + assert!( + dropped_error > error, + "MANIFESTTRAITVAR is not dropped: RMSE {dropped_error} must exceed {error}" + ); + let stuffed = + recover_manifest_observed_variance(loading, latent, manifest_trait).expect("psi-as-theta"); + let stuffed_error = rmse(&[expected], &[stuffed]); + assert!( + stuffed_error > error, + "MANIFESTTRAITVAR is not MANIFESTVAR: stuffed RMSE {stuffed_error} must exceed {error}" + ); + let latent_trait = + recover_manifest_observed_variance(loading, latent + manifest_trait, measurement_error) + .expect("traitvar"); + let latent_trait_error = rmse(&[expected], &[latent_trait]); + assert!( + latent_trait_error > error, + "TRAITVAR is not MANIFESTTRAITVAR: scaled RMSE {latent_trait_error} must exceed {error}" + ); + assert_eq!( + refuse_manifest_trait_variance_as_measurement_error(manifest_trait, measurement_error), + Err(PsychometricError::ManifestTraitVarianceIsNotMeasurementError) + ); + assert_eq!( + recover_manifest_trait_plus_state_observed_variance( + 0.0, + latent, + measurement_error, + manifest_trait + ), + Ok(measurement_error + manifest_trait) + ); + let scaled = recover_manifest_trait_plus_state_observed_variance(1e308, 1e-308, 0.0, 1.0) + .expect("scale"); + assert!( + (scaled - 1e308).abs() / 1e308 < 1e-15, + "Driver Eq. 5 (λ p)λ + ψ must keep λ=1e308, p=1e-308: got {scaled}" + ); + assert_eq!( + recover_manifest_trait_plus_state_observed_variance(1e308, 1e-308, 1e308, 1e308), + Err(PsychometricError::InvalidNumericInput) + ); +} + #[test] fn admitted_coordinates_still_required_for_multilevel_weights() { assert_eq!( diff --git a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs index 8c3fc517..2f0b64ef 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -7,12 +7,14 @@ use psychometric_core::{ recover_discrete_lagged_latent_covariance, recover_discrete_latent_variance, recover_discrete_process_noise, recover_discrete_time_varying_predictor_effect, recover_irregular_centered_residual_log_rate, recover_loading_point_estimate_mean, - recover_manifest_observed_variance, recover_stationary_latent_variance, - recover_trait_plus_state_latent_variance, recover_within_residual_event_time_log_rate, + recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, + recover_stationary_latent_variance, recover_trait_plus_state_latent_variance, + recover_within_residual_event_time_log_rate, refuse_finite_interval_process_noise_as_stationary_variance, - refuse_latent_variance_as_observed_variance, refuse_measurement_error_as_observed_variance, - refuse_process_noise_as_unconditional_variance, refuse_trait_variance_as_process_noise, - refuse_trait_variance_as_stationary_within_subject, + refuse_latent_variance_as_observed_variance, + refuse_manifest_trait_variance_as_measurement_error, + refuse_measurement_error_as_observed_variance, refuse_process_noise_as_unconditional_variance, + refuse_trait_variance_as_process_noise, refuse_trait_variance_as_stationary_within_subject, }; #[test] @@ -325,14 +327,14 @@ fn measurement_error_and_latent_variance_are_not_the_observed_variance() { let latent = 0.4_f64; let measurement_error = 0.1_f64; let observed = - recover_manifest_observed_variance(loading, latent, measurement_error).expect("eq1"); + recover_manifest_observed_variance(loading, latent, measurement_error).expect("eq5"); assert!( (measurement_error - observed).abs() > 1e-3, - "Driver et al. (2017, Eq. 1 / p. 16): MANIFESTVAR is not Var(y)" + "Driver et al. (2017, Eq. 5 / Table 2 p. 12): MANIFESTVAR is not Var(y)" ); assert!( (latent - observed).abs() > 1e-3, - "Driver et al. (2017, Eq. 1): Var(η) is not Var(y)" + "Driver et al. (2017, Eq. 5): Var(η) is not Var(y)" ); assert_eq!( refuse_measurement_error_as_observed_variance(measurement_error, observed), @@ -343,3 +345,41 @@ fn measurement_error_and_latent_variance_are_not_the_observed_variance() { Err(psychometric_core::PsychometricError::LatentVarianceIsNotObservedVariance) ); } + +#[test] +fn manifest_trait_variance_is_not_measurement_error() { + let loading = 2.0_f64; + let latent = 0.4_f64; + let measurement_error = 0.1_f64; + let manifest_trait = 0.5_f64; + let observed = recover_manifest_trait_plus_state_observed_variance( + loading, + latent, + measurement_error, + manifest_trait, + ) + .expect("eq5-trait"); + let without_trait = + recover_manifest_observed_variance(loading, latent, measurement_error).expect("psi0"); + assert!( + (without_trait - observed).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 / Table 2 p. 12): MANIFESTTRAITVAR is not dropped" + ); + let stuffed = + recover_manifest_observed_variance(loading, latent, manifest_trait).expect("psi-as-theta"); + assert!( + (stuffed - observed).abs() > 1e-3, + "Driver et al. (2017, Table 2 p. 12): MANIFESTTRAITVAR is not MANIFESTVAR" + ); + let latent_trait = + recover_manifest_observed_variance(loading, latent + manifest_trait, measurement_error) + .expect("traitvar"); + assert!( + (latent_trait - observed).abs() > 1e-3, + "Driver et al. (2017, Table 2 p. 12): TRAITVAR is latent and scaled by λ²; MANIFESTTRAITVAR is not" + ); + assert_eq!( + refuse_manifest_trait_variance_as_measurement_error(manifest_trait, measurement_error), + Err(psychometric_core::PsychometricError::ManifestTraitVarianceIsNotMeasurementError) + ); +} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 6a39ae70..9dec2ded 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -28,7 +28,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | report template/section/copied/style/modality method effects | ADR 0004/0012; PRD/TRD | simulation truth factors implemented; estimator-side method model remains future | partial | | candidate K statistical/Pareto gates + blinded LLM review | ADR 0012; research | future `model_selection` | accepted-target | | compositional topic correlation / stable clustering | ADR 0005/0012; research | future `network_analysis` | accepted-target | -| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance (Driver et al., 2017, Eq. 1; `MANIFESTVAR` is not `Var(y)`), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR; full ESEM/DSEM remaining | partial | +| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR; full ESEM/DSEM remaining | partial | | CPU bounded multithreading + GPU/VRAM streaming/parity | ADR 0001/0006 | future `compute_backend` | accepted-target | | TDT detection/tracking vs CHRONOS schema/prediction/temporal consistency | ADR 0016; PRD/research | future `event_intelligence` | accepted-target | | evidence-bounded LLM interpretation | ADR 0010/0012; PRD | future `interpretation_gateway` | accepted-target | diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index bc10a9ce..cba3d73c 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -1,7 +1,7 @@ # ADR 0005 — Posterior-aware ESEM/DSEM and structural interpretation **Decision status:** Accepted -**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance (Driver et al., 2017, Eq. 1, p. 4; p. 16 `MANIFESTVAR`; `λ² Var(η) + θ`; not a Kalman filter), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target +**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; Equation 1 is the SDE; not a Kalman filter), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target **Date:** 2026-08-05 **Supersedes:** None. ADR 0012 governs upstream topic measurement/network coordinates; this ADR governs higher-order psychometric structure and longitudinal interpretation. @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` (Driver et al., 2017, Eq. 1, p. 4; p. 16 `MANIFESTVAR`; form `(λ p) λ` then add `θ`; do not form `λ²` first; `MANIFESTVAR` is not `Var(y)`; `Var(η)` is not `Var(y)`), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. 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/adr/README.md b/docs/adr/README.md index 0dd2b9be..7dd1030c 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -10,7 +10,7 @@ Read [`ADR_POLICY.md`](ADR_POLICY.md) first. **Decision status and implementatio | [0002](0002-six-clock-temporal-semantics.md) | Six-clock temporal semantics and fail-closed historical leakage prevention | Accepted | active-PR | Unmerged PR #8 is the canonical Task 3 replacement implementing typed clocks/intervals against the current protected-main lineage; conflicted PR #5 is superseded lineage. Later graph/split enforcement remains target work. | | [0003](0003-relational-event-multiple-membership.md) | Relational event ontology and time-varying cross-classified multiple membership | Accepted | partial | Weighted time-varying membership network/roles are active-PR (PR #12); full multilevel estimators, graph ontology, and persistence remain accepted-target. ADR 0016 owns event-intelligence tasks. | | [0004](0004-shared-multilingual-latent-space.md) | One shared multilingual latent space with explicit invariance status | Accepted | accepted-target | ADR 0012 owns the full topic-estimator/backend/global-topic contract. | -| [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | partial | Input gates, posterior-draw point estimates, Rubin `T` on OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects, exact scalar discrete process noise, lagged latent covariance and unconditional latent variance, stationary within-subject variance (`asymDIFFUSION`), trait-plus-state variance (`TRAITVAR`; not process noise), observed-indicator variance (Eq. 1; `MANIFESTVAR` is not `Var(y)`), irregular already-centered residual lag, and strong-gated latent means are on the stacked psychometric PR; full ESEM/DSEM estimator remains accepted-target. | +| [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | partial | Input gates, posterior-draw point estimates, Rubin `T` on OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects, exact scalar discrete process noise, lagged latent covariance and unconditional latent variance, stationary within-subject variance (`asymDIFFUSION`), trait-plus-state variance (`TRAITVAR`; not process noise), observed-indicator variance (Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`), irregular already-centered residual lag, and strong-gated latent means are on the stacked psychometric PR; full ESEM/DSEM estimator remains accepted-target. | | [0006](0006-vram-gpu-nvidia-orchestration.md) | VRAM-adaptive GPU compute and model-credential boundary | Accepted | accepted-target | LLM orchestration policy superseded by ADR 0010; autonomous development authority governed by ADR 0015. | | [0007](0007-rust-workspace-quality-gates.md) | Explicit Rust workspace, pinned toolchains, and exact quality gates | Accepted | implemented-main | ADR 0014 governs scientific/product claim promotion beyond repository-quality tooling. | | [0008](0008-immutable-evidence-identities-digests-and-spans.md) | Immutable evidence identities, `SHA-256` digests, exact spans, and strict wire reconstruction | Accepted | implemented-main | ADR 0013 governs future persistence/reproducibility/split authority. | diff --git a/docs/research/multilevel-event-time-recovery.md b/docs/research/multilevel-event-time-recovery.md index c122677b..ac03ab70 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -17,7 +17,7 @@ This slice stays inside `psychometric_core`. It does not add a second invariance 11. recover the exact scalar discrete latent variance `A_Δt P A_Δt⊤ + Q_Δt` and refuse `Q_Δt` as that unconditional variance; 12. recover the exact scalar stationary within-subject variance as the `Δt → ∞` limit of Eq. 4 (`asymDIFFUSION`; §4.3 T0VAR stationarity) and refuse finite-interval `Q_Δt` as that limit; 13. recover the exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`) and refuse treating trait variance as process noise or as `asymDIFFUSION`; -14. recover the exact scalar observed-indicator variance `λ² Var(η) + θ` (Driver et al., 2017, Eq. 1; p. 16 `MANIFESTVAR`) and refuse treating measurement error or latent variance as `Var(y)`; +14. recover the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`) and refuse treating measurement error, latent variance, or manifest-trait variance as `Var(y)` / `Θ`; 15. refuse pooling discrete lags from unequal event intervals as one coefficient; 16. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); 17. refuse the difference quotient as a continuous-time rate; @@ -60,7 +60,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - **Discrete latent variance.** Equations 3–4 write \(Q_{\Delta t}\) as the covariance of the stochastic integral, so \(Q_{\Delta t}=\operatorname{cov}(\eta_{ti}\mid\eta_{t-1,i})\) when \(\xi\) and \(z\) are given. The law of total variance on that pair is \(\operatorname{Var}(\eta_{ti})=A_{\Delta t}\operatorname{Var}(\eta_{t-1,i})A_{\Delta t}^{\top}+Q_{\Delta t}\). The scalar map is \(\mathrm{e}^{2a\Delta t}p+Q_{\Delta t}\). This is not a Kalman measurement update. A zero prior variance is exactly \(Q_{\Delta t}\). Binary64 underflow of \(\mathrm{e}^{2a\Delta t}\) keeps \(Q_{\Delta t}\). Finite-\(z\) exponential overflow rewrites as \(\exp(\ln p+z)+Q_{\Delta t}\). A zero diffusion is exactly \(Q_{\Delta t}=0\); that skip does not license a non-finite carried term when \(2(a\Delta t)\) overflows to \(+\infty\). Treating \(Q_{\Delta t}\) as \(\operatorname{Var}(\eta_{t})\) fails closed. - **Stationary within-subject variance.** Driver et al. (2017, Eq. 4 as \(\Delta t\to\infty\); §4.3 pp. 9–10; p. 16 `asymDIFFUSION`; JSS PDF re-opened 2026-08-19T04:10Z, p. 5): for stable \(a<0\), \(\lim_{\Delta t\to\infty}Q_{\Delta t}=-q/(2a)\). When \(2a\) is finite, form \(q/-(2a)\) so \(q/a\) overflow does not lose a finite Lyapunov solution (`q=MAX`, `a=-0.75` → `MAX/1.5`; CodeRabbit on `75ecdd3`). When \(2a\) overflows, form \((q/a)\times-0.5\). Forming \(2a\) as the only path overflows when \(|a|\) is at the binary64 extreme (`a=-1e308`, `q=1e308` → `0.5`). Forming \(0.5q\) first underflows at the minimum subnormal (`q=from_bits(1)`, `a=-from_bits(1)` → naive `+0`; representable solution `0.5`). Starting from that variance, \(\operatorname{Var}(\eta_{t})\) is invariant across finite event intervals. A zero diffusion is exactly zero. \(a\ge 0\) has no finite stationary variance (Brownian \(a=0\) grows as \(q\Delta t\)). An overflowing Lyapunov solution fails closed. Finite-interval \(Q_{\Delta t}\) is not that limit. This is not ctsem estimation and not a Kalman filter. - **Trait-plus-state variance.** Driver et al. (2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z): a stable trait process has `DRIFT` and `DIFFUSION` fixed to zero. The scalar maps are \(\operatorname{Var}=\mathrm{trait}+\mathrm{state}\) and \(\operatorname{cov}(t,t-1)=\mathrm{trait}+\mathrm{e}^{a\Delta t}p\). The ctsem `TRAITVAR` rewrite that adds the trait to `DIFFUSION` does not license treating trait variance as \(Q_{\Delta t}\). Trait variance is not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not this map. A zero trait is exactly the state. A zero state is exactly the trait. An overflowing sum fails closed. This is not RI-CLPM and not ctsem estimation. -- **Observed-indicator variance.** Driver et al. (2017, Eq. 1, p. 4; p. 16 `MANIFESTVAR`; JSS PDF re-opened 2026-08-19T00:14Z): \(y_i(t)=\Lambda\eta_i(t)+\tau+\varepsilon_i(t)\) with \(\varepsilon\sim N(0,\Theta)\). The scalar map is \(\operatorname{Var}(y)=\lambda^{2}\operatorname{Var}(\eta)+\theta\). Form \((\lambda p)\lambda\) then add \(\theta\). Forming \(\lambda^{2}\) first overflows at \(\lambda=10^{308}\), \(p=10^{-308}\). A zero loading or zero latent variance is exactly \(\theta\). A zero measurement error is exactly \(\lambda^{2}p\). `MANIFESTVAR` is not \(\operatorname{Var}(y)\). \(\operatorname{Var}(\eta)\) is not \(\operatorname{Var}(y)\). An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. +- **Observed-indicator variance.** Driver et al. (2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z): \(y_i(t)=\tau_i+\Lambda\eta_i(t)+\varepsilon_i(t)\) with \(\varepsilon\sim N(0,\Theta)\) and \(\tau_i\sim N(\mu_{\tau},\Psi_{\tau})\). Equation 1 (p. 4) is the latent SDE. Table 2 names \(\Theta\) `MANIFESTVAR` and \(\Psi_{\tau}\) `MANIFESTTRAITVAR`; p. 16 restates those names. The scalar map is \(\operatorname{Var}(y)=\lambda^{2}\operatorname{Var}(\eta)+\theta\) when \(\Psi_{\tau}=0\) and \(\lambda^{2}\operatorname{Var}(\eta)+\theta+\psi\) otherwise. Form \((\lambda p)\lambda\) then add \(\theta\), then add \(\psi\). Forming \(\lambda^{2}\) first overflows at \(\lambda=10^{308}\), \(p=10^{-308}\). A zero loading or zero latent variance is exactly \(\theta\) (\(\Psi_{\tau}=0\)) or \(\theta+\psi\). A zero measurement error is exactly \(\lambda^{2}p+\psi\). A zero manifest trait is exactly \(\lambda^{2}p+\theta\). `MANIFESTVAR` is not \(\operatorname{Var}(y)\). `MANIFESTTRAITVAR` is not `MANIFESTVAR`. `TRAITVAR` is latent additional variance and is scaled by \(\lambda^{2}\); `MANIFESTTRAITVAR` is not. \(\operatorname{Var}(\eta)\) is not \(\operatorname{Var}(y)\). An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **CWC-then-lag.** Sample cluster means are removed first. Consecutive within residuals are then fitted by least squares to \(r_{t+\Delta t}\approx\exp(a\Delta t)\,r_{t}\) on event time. Same-sign pair-wise logs initialize the scalar Newton step. Sign-flipping \(T=2\) CWC pairs have no real logarithm and fail closed. Curran and Bauer (2011, pp. 607–608) show that this person-mean subtraction on a raw autoregressive series does **not** isolate the lagged within-person effect; the helper therefore does not claim to recover the raw-process drift. - **Already-centered irregular residual.** The caller supplies lagged within residuals. The mean of \(a=\ln(r_{t+\Delta t}/r_t)/\Delta t\) is the exact scalar map. Intervals may be irregular. The helper does not center again. This is not DSEM. @@ -78,7 +78,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - Driver et al. (2017, Eq. 3–4) recovers a known lagged covariance \(\mathrm{e}^{a\Delta t}p\) and a known latent variance \(\mathrm{e}^{2a\Delta t}p+Q_{\Delta t}\) at machine-scale RMSE, and that variance RMSE is smaller than treating \(Q_{\Delta t}\) as \(\operatorname{Var}(\eta_{t})\); a zero prior variance is exactly zero (lagged) or \(Q_{\Delta t}\) (unconditional); binary64 underflow of \(\mathrm{e}^{a\Delta t}\) keeps a vanishing lagged covariance; a finite \(\mathrm{e}^{a\Delta t}\) whose product with \(p\) overflows fails closed; a zero diffusion whose \(2(a\Delta t)\) overflows to \(+\infty\) fails closed; treating \(Q_{\Delta t}\) as the unconditional variance fails closed; - Driver et al. (2017, Eq. 4 as \(\Delta t\to\infty\); p. 16 `asymDIFFUSION`; §4.3) recovers a known stationary variance \(-q/(2a)\) at machine-scale RMSE, and that RMSE is smaller than treating finite-interval \(Q_{\Delta t}\) as the stationary variance; starting from that variance, \(\operatorname{Var}(\eta_{t})\) is invariant across finite \(\Delta t\); a zero diffusion is exactly zero; forming \(2a\) first overflows while \((q/a)\times-0.5\) stays finite; forming \(0.5q\) first underflows at the minimum subnormal while \((q/a)\times-0.5=0.5\); forming \(q/a\) first overflows at `q=MAX`, `a=-0.75` while \(q/-(2a)=\mathrm{MAX}/1.5\) stays finite; \(a\ge 0\), a non-event clock, a negative diffusion, and an overflowing Lyapunov solution fail closed; - Driver et al. (2017, §4.3, p. 9) recovers a known trait-plus-state variance and lagged covariance at machine-scale RMSE, and that RMSE is smaller than evolving the summed variance as if it were all state; a zero trait is the state; a zero state is the trait; treating trait variance as process noise or as `asymDIFFUSION` fails closed; an overflowing sum and a non-event clock fail closed; -- Driver et al. (2017, Eq. 1, p. 4; p. 16 `MANIFESTVAR`) recovers a known observed-indicator variance \(\lambda^{2}\operatorname{Var}(\eta)+\theta\) at machine-scale RMSE, and that RMSE is smaller than treating `MANIFESTVAR` or \(\operatorname{Var}(\eta)\) as \(\operatorname{Var}(y)\); a zero loading or zero latent variance is \(\theta\); forming \(\lambda^{2}\) first overflows while \((\lambda p)\lambda\) stays finite; an overflowing product or sum fails closed; +- Driver et al. (2017, Eq. 5, p. 5; Table 2, p. 12) recovers a known observed-indicator variance \(\lambda^{2}\operatorname{Var}(\eta)+\theta\) at machine-scale RMSE when `MANIFESTTRAITVAR` is zero, and \(\lambda^{2}\operatorname{Var}(\eta)+\theta+\psi\) otherwise; those RMSEs are smaller than treating `MANIFESTVAR`, \(\operatorname{Var}(\eta)\), or `MANIFESTTRAITVAR` as \(\operatorname{Var}(y)\) / \(\Theta\); a zero loading or zero latent variance is \(\theta\) or \(\theta+\psi\); `TRAITVAR` is latent and scaled by \(\lambda^{2}\); forming \(\lambda^{2}\) first overflows while \((\lambda p)\lambda\) stays finite; an overflowing product or sum fails closed; - pooling discrete lags from unequal intervals fails closed; - CWC-then-lag on a two-cluster decaying series has smaller computed RMSE than a level-pooled series when the latter is identified; - already-centered irregular residuals recover a known drift at machine-scale RMSE, and that RMSE is smaller than CWC of the corresponding raw autoregressive series (Curran & Bauer, 2011, pp. 607–608); From 7c0978826e8f2b1ddd82bc33c5bb543915c490cb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 04:30:32 +0000 Subject: [PATCH 29/62] feat(psychometric): recover Driver Eq. 5 lagged observed covariance Independent measurement error does not enter cov(y_t, y_{t-1}). The scalar map is lambda^2 cov(eta_t, eta_{t-1}) + psi. Form (lambda c) lambda then add psi. Do not form lambda^2 first. --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 1 + CLAUDE.md | 2 +- crates/psychometric_core/src/error.rs | 26 +++ crates/psychometric_core/src/event_time.rs | 157 +++++++++++++++++- crates/psychometric_core/src/lib.rs | 10 +- ...multilevel_event_time_recovery_contract.rs | 51 +++++- .../scientific_claim_boundary_contract.rs | 34 +++- docs/TRACEABILITY.md | 2 +- docs/adr/0005-posterior-esem-dsem.md | 4 +- docs/adr/README.md | 2 +- .../multilevel-event-time-recovery.md | 13 +- 12 files changed, 280 insertions(+), 24 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index c9a235a1..9264d514 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -61,7 +61,7 @@ boundaries above remain the target modular MSA architecture. | `tepp_simulation` | known-truth temporal/event data generation | | `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics | | `tepp_api` | versioned DTO, schema, and export contracts | -| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means | +| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means | No crate exposes placeholder production behavior in Task 1. This prevents an empty façade from becoming a de facto public API before its invariants and tests diff --git a/CHANGELOG.md b/CHANGELOG.md index ea82aa6f..4979c936 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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, Eq. 5, p. 5; Eq. 3–4, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z) scalar lagged observed-indicator covariance. Independent measurement error does not enter `cov(y_t, y_{t-1})`. The scalar map is `λ² cov(η_t, η_{t-1}) + ψ`. Form `(λ c) λ` then add `ψ`. Do not form `λ²` first (`λ = 1e308`, `c = 1e-308` → `1e308`). `MANIFESTVAR` is not lagged observed covariance. Lagged `Var(η)` path is not `cov(y)`. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-19T04:25Z: closed). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar observed-indicator variance with `MANIFESTTRAITVAR`. Equation 5 writes `y_i(t) = τ_i + Λ η_i(t) + ε_i(t)` with `ε ~ N(0, Θ)` and `τ_i ~ N(μ_τ, Ψ_τ)`. Equation 1 (p. 4) is the latent SDE, not the measurement model. Table 2 names `Θ` `MANIFESTVAR` and `Ψ_τ` `MANIFESTTRAITVAR`; p. 16 restates those names. The scalar map is `Var(y) = λ² Var(η) + θ` when `Ψ_τ = 0` and `λ² Var(η) + θ + ψ` otherwise. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is not `Var(y)`. `MANIFESTTRAITVAR` is not `MANIFESTVAR`. `TRAITVAR` is latent additional variance and is scaled by `λ²`; `MANIFESTTRAITVAR` is not. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall/OpenAlex 2026-08-19T04:18Z: closed). Oud and Jansen (2000) remains unread. ZORA Anubis-blocked. - `psychometric_core` evaluates the Driver, Oud, and Voelkle (2017, Eq. 4, p. 5; JSS PDF re-opened 2026-08-19T04:10Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) stationary within-subject variance as `q / -(2 a)` when the scalar Kronecker sum `2 a` is finite. The paper limit is `-q / (2 a)` (`A# = A ⊗ I + I ⊗ A`; p. 16 `asymDIFFUSION`). Forming `q / a` first overflows at `q = MAX`, `a = -0.75` while `MAX / 1.5` is finite (CodeRabbit finding on `75ecdd3`). When `2 a` overflows (`a = -1e308`), form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). Still not a Kalman filter, not DSEM, not a matrix `expm`, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall/OpenAlex 2026-08-19T04:10Z: closed). Oud and Jansen (2000) remains unread. ZORA Anubis-blocked. - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 1, p. 4; p. 16 `MANIFESTVAR`; JSS PDF re-opened 2026-08-19T00:14Z) scalar observed-indicator variance. Equation 1 writes `y_i(t) = Λ η_i(t) + τ + ε_i(t)` with `ε ~ N(0, Θ)`. The scalar map is `Var(y) = λ² Var(η) + θ`. Form `(λ p) λ` then add `θ`. Do not form `λ²` first (`λ = 1e308`, `p = 1e-308` → `1e308`). `MANIFESTVAR` is not `Var(y)`. `Var(η)` is not `Var(y)`. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall/OpenAlex 2026-08-19T00:14Z: closed). Oud and Jansen (2000) remains unread. ZORA Anubis-blocked. diff --git a/CLAUDE.md b/CLAUDE.md index 2cc2a9d2..9f923bab 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. -- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. diff --git a/crates/psychometric_core/src/error.rs b/crates/psychometric_core/src/error.rs index d29ccefe..29ad2019 100644 --- a/crates/psychometric_core/src/error.rs +++ b/crates/psychometric_core/src/error.rs @@ -72,6 +72,14 @@ pub enum PsychometricError { /// Driver Eq. 5 `MANIFESTTRAITVAR` was treated as `MANIFESTVAR`. /// Table 2 (p. 12) names `Ψ_τ` separately from `Θ`. ManifestTraitVarianceIsNotMeasurementError, + /// Driver Eq. 3–4 lagged latent covariance was treated as the + /// lagged observed-indicator covariance. Equation 5 maps + /// `cov(y_t, y_{t-1}) = λ² cov(η_t, η_{t-1}) + ψ`. + LatentLaggedCovarianceIsNotObservedCovariance, + /// Driver Eq. 5 measurement-error variance was treated as the + /// lagged observed-indicator covariance. Independent `ε` does + /// not enter `cov(y_t, y_{t-1})`. + MeasurementErrorIsNotLaggedObservedCovariance, } impl fmt::Display for PsychometricError { @@ -133,6 +141,12 @@ impl fmt::Display for PsychometricError { Self::ManifestTraitVarianceIsNotMeasurementError => { "manifest-trait variance is not measurement-error variance" } + Self::LatentLaggedCovarianceIsNotObservedCovariance => { + "lagged latent covariance is not the lagged observed-indicator covariance" + } + Self::MeasurementErrorIsNotLaggedObservedCovariance => { + "measurement-error variance is not the lagged observed-indicator covariance" + } }; formatter.write_str(message) } @@ -230,6 +244,10 @@ mod tests { PsychometricError::TraitVarianceIsNotStationaryWithinSubject.to_string(), "trait variance is not the stationary within-subject variance" ); + } + + #[test] + fn observed_indicator_boundary_messages_are_stable() { assert_eq!( PsychometricError::MeasurementErrorIsNotObservedVariance.to_string(), "measurement-error variance is not the observed-indicator variance" @@ -242,5 +260,13 @@ mod tests { PsychometricError::ManifestTraitVarianceIsNotMeasurementError.to_string(), "manifest-trait variance is not measurement-error variance" ); + assert_eq!( + PsychometricError::LatentLaggedCovarianceIsNotObservedCovariance.to_string(), + "lagged latent covariance is not the lagged observed-indicator covariance" + ); + assert_eq!( + PsychometricError::MeasurementErrorIsNotLaggedObservedCovariance.to_string(), + "measurement-error variance is not the lagged observed-indicator covariance" + ); } } diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index 0c77fbdb..a3501c49 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -32,7 +32,9 @@ //! The scalar observed variance is `λ² Var(η) + θ` when `Ψ_τ = 0` //! and `λ² Var(η) + θ + ψ` otherwise. `MANIFESTVAR` is not `Var(y)`, //! `MANIFESTTRAITVAR` is not `MANIFESTVAR`, `TRAITVAR` is latent -//! (scaled by `λ²`), and `Var(η)` is not `Var(y)`. The JSS article +//! (scaled by `λ²`), and `Var(η)` is not `Var(y)`. The lagged +//! observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `Θ` does not +//! enter. The JSS article //! has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). //! The difference quotient `(x(t+Δt) − x(t)) / Δt` (their //! Eqs. 3–4) is refused. This is not DSEM and not a matrix `expm`. @@ -939,6 +941,88 @@ pub fn refuse_manifest_trait_variance_as_measurement_error( Err(PsychometricError::ManifestTraitVarianceIsNotMeasurementError) } +/// Exact scalar lagged observed-indicator covariance from Driver +/// Equation 5. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Eq. 3–4, pp. 4–5; +/// Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z) write +/// `y_i(t) = τ_i + Λ η_i(t) + ε_i(t)` with independent measurement +/// error and a person-level intercept `τ_i ~ N(μ_τ, Ψ_τ)`. The +/// scalar lagged covariance is `cov(y_t, y_{t-1}) = λ² cov(η_t, +/// η_{t-1}) + ψ`. `Θ` does not enter: `ε_t` and `ε_{t-1}` are +/// independent. Form `(λ c) λ` then add `ψ`. Do not form `λ²` +/// first. A zero loading or zero latent lagged covariance is +/// exactly `ψ`. A zero manifest trait is exactly `λ² c`. Negative +/// latent lagged covariance or trait variance fails closed. An +/// overflowing product or sum fails closed. This is not a Kalman +/// filter and not ctsem estimation. +/// +/// # Errors +/// +/// Returns [`PsychometricError::InvalidNumericInput`] when the loading +/// is non-finite, the latent lagged covariance is negative or +/// non-finite, the manifest-trait variance is negative or +/// non-finite, or the mapped covariance is non-finite. +pub fn recover_manifest_lagged_observed_covariance( + loading: f64, + lagged_latent_covariance: f64, + manifest_trait_variance: f64, +) -> Result { + if !loading.is_finite() + || !lagged_latent_covariance.is_finite() + || lagged_latent_covariance < 0.0 + || !manifest_trait_variance.is_finite() + || manifest_trait_variance < 0.0 + { + return Err(PsychometricError::InvalidNumericInput); + } + if loading == 0.0 || lagged_latent_covariance == 0.0 { + return Ok(manifest_trait_variance); + } + let explained = require_finite((loading * lagged_latent_covariance) * loading)?; + if manifest_trait_variance == 0.0 { + return Ok(explained); + } + require_finite(explained + manifest_trait_variance) +} + +/// Refuse treating Driver Eq. 3–4 lagged latent covariance as +/// `cov(y_t, y_{t-1})`. +/// +/// Equation 5 maps `cov(y_t, y_{t-1}) = λ² cov(η_t, η_{t-1}) + ψ`. +/// The latent lagged covariance is not the observed lagged +/// covariance. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::LatentLaggedCovarianceIsNotObservedCovariance`]. +pub fn refuse_latent_lagged_covariance_as_observed_covariance( + lagged_latent_covariance: f64, + observed_lagged_covariance: f64, +) -> Result { + let _ = (lagged_latent_covariance, observed_lagged_covariance); + Err(PsychometricError::LatentLaggedCovarianceIsNotObservedCovariance) +} + +/// Refuse treating Driver Eq. 5 measurement error as lagged observed +/// covariance. +/// +/// `MANIFESTVAR` is `Θ`. Independent `ε_t` does not enter +/// `cov(y_t, y_{t-1})`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::MeasurementErrorIsNotLaggedObservedCovariance`]. +pub fn refuse_measurement_error_as_lagged_observed_covariance( + measurement_error_variance: f64, + observed_lagged_covariance: f64, +) -> Result { + let _ = (measurement_error_variance, observed_lagged_covariance); + Err(PsychometricError::MeasurementErrorIsNotLaggedObservedCovariance) +} + /// Refuse treating Driver Eq. 3 process noise as the unconditional variance. /// /// Driver, Oud, and Voelkle (2017, Eq. 3–4, pp. 4–5): @@ -1193,13 +1277,15 @@ mod tests { recover_discrete_process_noise, recover_discrete_time_varying_predictor_effect, recover_event_series_mean_log_rate, recover_event_time_discrete_lag_and_log_rate, recover_irregular_centered_residual_log_rate, recover_local_log_rate, - recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, - recover_stationary_latent_variance, recover_trait_plus_state_lagged_covariance, - recover_trait_plus_state_latent_variance, recover_within_residual_event_time_log_rate, - refuse_difference_quotient_as_local_rate, + recover_manifest_lagged_observed_covariance, recover_manifest_observed_variance, + recover_manifest_trait_plus_state_observed_variance, recover_stationary_latent_variance, + recover_trait_plus_state_lagged_covariance, recover_trait_plus_state_latent_variance, + recover_within_residual_event_time_log_rate, refuse_difference_quotient_as_local_rate, refuse_finite_interval_process_noise_as_stationary_variance, + refuse_latent_lagged_covariance_as_observed_covariance, refuse_latent_variance_as_observed_variance, refuse_manifest_trait_variance_as_measurement_error, + refuse_measurement_error_as_lagged_observed_covariance, refuse_measurement_error_as_observed_variance, refuse_pooled_discrete_lag_across_unequal_intervals, refuse_process_noise_as_unconditional_variance, refuse_trait_variance_as_process_noise, @@ -2687,4 +2773,65 @@ mod tests { Err(PsychometricError::InvalidNumericInput) ); } + + #[test] + fn manifest_lagged_observed_covariance_recovers_driver_equation_five() { + let loading = 2.0_f64; + let lagged = 0.4_f64; + let manifest_trait = 0.5_f64; + let recovered = + recover_manifest_lagged_observed_covariance(loading, lagged, manifest_trait) + .expect("eq5-lag"); + let expected = (loading * lagged) * loading + manifest_trait; + assert!((recovered - expected).abs() < 1e-15); + assert!((recovered - 2.1).abs() < 1e-15); + assert_eq!( + recover_manifest_lagged_observed_covariance(loading, lagged, 0.0), + Ok(1.6) + ); + assert_eq!( + recover_manifest_lagged_observed_covariance(0.0, lagged, manifest_trait), + Ok(manifest_trait) + ); + assert_eq!( + recover_manifest_lagged_observed_covariance(loading, 0.0, manifest_trait), + Ok(manifest_trait) + ); + assert_eq!( + refuse_latent_lagged_covariance_as_observed_covariance(lagged, recovered), + Err(PsychometricError::LatentLaggedCovarianceIsNotObservedCovariance) + ); + assert_eq!( + refuse_measurement_error_as_lagged_observed_covariance(0.1, recovered), + Err(PsychometricError::MeasurementErrorIsNotLaggedObservedCovariance) + ); + let scaled = + recover_manifest_lagged_observed_covariance(1e308, 1e-308, 0.0).expect("scale"); + assert!((scaled - 1e308).abs() / 1e308 < 1e-15); + assert!(!(1e308_f64 * 1e308_f64).is_finite()); + } + + #[test] + fn manifest_lagged_observed_covariance_invalid_inputs_fail_closed() { + assert_eq!( + recover_manifest_lagged_observed_covariance(f64::NAN, 0.4, 0.0), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_manifest_lagged_observed_covariance(2.0, -0.1, 0.0), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_manifest_lagged_observed_covariance(2.0, 0.4, -0.1), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_manifest_lagged_observed_covariance(1e308, 1.0, 0.0), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_manifest_lagged_observed_covariance(1e308, 1e-308, 1e308), + Err(PsychometricError::InvalidNumericInput) + ); + } } diff --git a/crates/psychometric_core/src/lib.rs b/crates/psychometric_core/src/lib.rs index 05bdd7ba..e846e823 100644 --- a/crates/psychometric_core/src/lib.rs +++ b/crates/psychometric_core/src/lib.rs @@ -25,7 +25,9 @@ //! observed-indicator variance (`λ² Var(η) + θ` when //! `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; Table 2, //! p. 12: `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is -//! not `MANIFESTVAR`; Equation 1 is the SDE), +//! not `MANIFESTVAR`; lagged observed covariance is +//! `λ² cov(η_t, η_{t-1}) + ψ` and does not include `Θ`; Equation 1 +//! is the SDE), //! and refuses //! latent-mean comparison below strong invariance. @@ -96,6 +98,8 @@ pub use event_time::recover_event_time_discrete_lag_and_log_rate; pub use event_time::recover_irregular_centered_residual_log_rate; /// Exact scalar inverse `a = ln(φ) / Δt`. pub use event_time::recover_local_log_rate; +/// Exact scalar lagged observed-indicator covariance `λ² cov(η) + ψ`. +pub use event_time::recover_manifest_lagged_observed_covariance; /// Exact scalar observed-indicator variance `λ² Var(η) + θ`. pub use event_time::recover_manifest_observed_variance; /// Exact scalar observed-indicator variance `λ² Var(η) + θ + ψ`. @@ -112,10 +116,14 @@ pub use event_time::recover_within_residual_event_time_log_rate; pub use event_time::refuse_difference_quotient_as_local_rate; /// Refuse treating finite-interval `Q_Δt` as `asymDIFFUSION`. pub use event_time::refuse_finite_interval_process_noise_as_stationary_variance; +/// Refuse treating Driver Eq. 3–4 lagged latent covariance as `cov(y_t, y_{t-1})`. +pub use event_time::refuse_latent_lagged_covariance_as_observed_covariance; /// Refuse treating Driver Eq. 5 latent variance as `Var(y)`. pub use event_time::refuse_latent_variance_as_observed_variance; /// Refuse treating Driver Eq. 5 `MANIFESTTRAITVAR` as `MANIFESTVAR`. pub use event_time::refuse_manifest_trait_variance_as_measurement_error; +/// Refuse treating Driver Eq. 5 measurement error as lagged observed covariance. +pub use event_time::refuse_measurement_error_as_lagged_observed_covariance; /// Refuse treating Driver Eq. 5 measurement error as `Var(y)`. pub use event_time::refuse_measurement_error_as_observed_variance; /// Refuse pooling discrete lags from unequal event intervals. 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 b8eff0c0..1b6f5198 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -10,13 +10,15 @@ use psychometric_core::{ recover_discrete_process_noise, recover_discrete_time_varying_predictor_effect, recover_event_series_mean_log_rate, recover_event_time_discrete_lag_and_log_rate, recover_irregular_centered_residual_log_rate, recover_kish_weighted_slope, - recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, - recover_stationary_latent_variance, recover_trait_plus_state_lagged_covariance, - recover_trait_plus_state_latent_variance, recover_within_residual_event_time_log_rate, - refuse_difference_quotient_as_local_rate, + recover_manifest_lagged_observed_covariance, recover_manifest_observed_variance, + recover_manifest_trait_plus_state_observed_variance, recover_stationary_latent_variance, + recover_trait_plus_state_lagged_covariance, recover_trait_plus_state_latent_variance, + recover_within_residual_event_time_log_rate, refuse_difference_quotient_as_local_rate, refuse_finite_interval_process_noise_as_stationary_variance, + refuse_latent_lagged_covariance_as_observed_covariance, refuse_latent_variance_as_observed_variance, refuse_manifest_trait_variance_as_measurement_error, + refuse_measurement_error_as_lagged_observed_covariance, refuse_measurement_error_as_observed_variance, refuse_pooled_discrete_lag_across_unequal_intervals, refuse_process_noise_as_unconditional_variance, refuse_trait_variance_as_process_noise, @@ -900,6 +902,47 @@ fn manifest_trait_plus_state_observed_variance_recovers_driver_equation_five() { ); } +#[test] +fn manifest_lagged_observed_covariance_recovers_driver_equation_five() { + let loading = 2.0_f64; + let lagged = 0.4_f64; + let manifest_trait = 0.5_f64; + let observed = recover_manifest_lagged_observed_covariance(loading, lagged, manifest_trait) + .expect("eq5-lag"); + let expected = (loading * lagged) * loading + manifest_trait; + let error = rmse(&[expected], &[observed]); + assert!(error < 1e-15, "Driver Eq. 5 lagged cov RMSE {error}"); + let latent_error = rmse(&[expected], &[lagged]); + assert!( + latent_error > error, + "lagged Var(η) path is not cov(y): RMSE {latent_error} must exceed {error}" + ); + let without_trait = + recover_manifest_lagged_observed_covariance(loading, lagged, 0.0).expect("psi0"); + let dropped_error = rmse(&[expected], &[without_trait]); + assert!( + dropped_error > error, + "MANIFESTTRAITVAR is not dropped from lagged cov: RMSE {dropped_error} must exceed {error}" + ); + assert_eq!( + refuse_latent_lagged_covariance_as_observed_covariance(lagged, observed), + Err(PsychometricError::LatentLaggedCovarianceIsNotObservedCovariance) + ); + assert_eq!( + refuse_measurement_error_as_lagged_observed_covariance(0.1, observed), + Err(PsychometricError::MeasurementErrorIsNotLaggedObservedCovariance) + ); + let scaled = recover_manifest_lagged_observed_covariance(1e308, 1e-308, 0.0).expect("scale"); + assert!( + (scaled - 1e308).abs() / 1e308 < 1e-15, + "Driver Eq. 5 (λ c)λ must keep λ=1e308, c=1e-308: got {scaled}" + ); + assert_eq!( + recover_manifest_lagged_observed_covariance(1e308, 1.0, 0.0), + Err(PsychometricError::InvalidNumericInput) + ); +} + #[test] fn admitted_coordinates_still_required_for_multilevel_weights() { assert_eq!( diff --git a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs index 2f0b64ef..c59f5fb1 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -7,12 +7,14 @@ use psychometric_core::{ recover_discrete_lagged_latent_covariance, recover_discrete_latent_variance, recover_discrete_process_noise, recover_discrete_time_varying_predictor_effect, recover_irregular_centered_residual_log_rate, recover_loading_point_estimate_mean, - recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, - recover_stationary_latent_variance, recover_trait_plus_state_latent_variance, - recover_within_residual_event_time_log_rate, + recover_manifest_lagged_observed_covariance, recover_manifest_observed_variance, + recover_manifest_trait_plus_state_observed_variance, recover_stationary_latent_variance, + recover_trait_plus_state_latent_variance, recover_within_residual_event_time_log_rate, refuse_finite_interval_process_noise_as_stationary_variance, + refuse_latent_lagged_covariance_as_observed_covariance, refuse_latent_variance_as_observed_variance, refuse_manifest_trait_variance_as_measurement_error, + refuse_measurement_error_as_lagged_observed_covariance, refuse_measurement_error_as_observed_variance, refuse_process_noise_as_unconditional_variance, refuse_trait_variance_as_process_noise, refuse_trait_variance_as_stationary_within_subject, }; @@ -383,3 +385,29 @@ fn manifest_trait_variance_is_not_measurement_error() { Err(psychometric_core::PsychometricError::ManifestTraitVarianceIsNotMeasurementError) ); } + +#[test] +fn lagged_latent_covariance_and_measurement_error_are_not_lagged_observed_covariance() { + let loading = 2.0_f64; + let lagged = 0.4_f64; + let manifest_trait = 0.5_f64; + let measurement_error = 0.1_f64; + let observed = recover_manifest_lagged_observed_covariance(loading, lagged, manifest_trait) + .expect("eq5-lag"); + assert!( + (lagged - observed).abs() > 1e-3, + "Driver et al. (2017, Eq. 5): cov(η_t, η_{{t-1}}) is not cov(y_t, y_{{t-1}})" + ); + assert!( + (measurement_error - observed).abs() > 1e-3, + "Driver et al. (2017, Eq. 5): MANIFESTVAR does not enter lagged observed covariance" + ); + assert_eq!( + refuse_latent_lagged_covariance_as_observed_covariance(lagged, observed), + Err(psychometric_core::PsychometricError::LatentLaggedCovarianceIsNotObservedCovariance) + ); + assert_eq!( + refuse_measurement_error_as_lagged_observed_covariance(measurement_error, observed), + Err(psychometric_core::PsychometricError::MeasurementErrorIsNotLaggedObservedCovariance) + ); +} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 9dec2ded..b1cf0934 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -28,7 +28,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | report template/section/copied/style/modality method effects | ADR 0004/0012; PRD/TRD | simulation truth factors implemented; estimator-side method model remains future | partial | | candidate K statistical/Pareto gates + blinded LLM review | ADR 0012; research | future `model_selection` | accepted-target | | compositional topic correlation / stable clustering | ADR 0005/0012; research | future `network_analysis` | accepted-target | -| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR; full ESEM/DSEM remaining | partial | +| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR; full ESEM/DSEM remaining | partial | | CPU bounded multithreading + GPU/VRAM streaming/parity | ADR 0001/0006 | future `compute_backend` | accepted-target | | TDT detection/tracking vs CHRONOS schema/prediction/temporal consistency | ADR 0016; PRD/research | future `event_intelligence` | accepted-target | | evidence-bounded LLM interpretation | ADR 0010/0012; PRD | future `interpretation_gateway` | accepted-target | diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index cba3d73c..43bfad29 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -1,7 +1,7 @@ # ADR 0005 — Posterior-aware ESEM/DSEM and structural interpretation **Decision status:** Accepted -**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; Equation 1 is the SDE; not a Kalman filter), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target +**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; Equation 1 is the SDE; not a Kalman filter), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target **Date:** 2026-08-05 **Supersedes:** None. ADR 0012 governs upstream topic measurement/network coordinates; this ADR governs higher-order psychometric structure and longitudinal interpretation. @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. 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/adr/README.md b/docs/adr/README.md index 7dd1030c..02ca6d7c 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -10,7 +10,7 @@ Read [`ADR_POLICY.md`](ADR_POLICY.md) first. **Decision status and implementatio | [0002](0002-six-clock-temporal-semantics.md) | Six-clock temporal semantics and fail-closed historical leakage prevention | Accepted | active-PR | Unmerged PR #8 is the canonical Task 3 replacement implementing typed clocks/intervals against the current protected-main lineage; conflicted PR #5 is superseded lineage. Later graph/split enforcement remains target work. | | [0003](0003-relational-event-multiple-membership.md) | Relational event ontology and time-varying cross-classified multiple membership | Accepted | partial | Weighted time-varying membership network/roles are active-PR (PR #12); full multilevel estimators, graph ontology, and persistence remain accepted-target. ADR 0016 owns event-intelligence tasks. | | [0004](0004-shared-multilingual-latent-space.md) | One shared multilingual latent space with explicit invariance status | Accepted | accepted-target | ADR 0012 owns the full topic-estimator/backend/global-topic contract. | -| [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | partial | Input gates, posterior-draw point estimates, Rubin `T` on OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects, exact scalar discrete process noise, lagged latent covariance and unconditional latent variance, stationary within-subject variance (`asymDIFFUSION`), trait-plus-state variance (`TRAITVAR`; not process noise), observed-indicator variance (Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`), irregular already-centered residual lag, and strong-gated latent means are on the stacked psychometric PR; full ESEM/DSEM estimator remains accepted-target. | +| [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | partial | Input gates, posterior-draw point estimates, Rubin `T` on OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects, exact scalar discrete process noise, lagged latent covariance and unconditional latent variance, stationary within-subject variance (`asymDIFFUSION`), trait-plus-state variance (`TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance), irregular already-centered residual lag, and strong-gated latent means are on the stacked psychometric PR; full ESEM/DSEM estimator remains accepted-target. | | [0006](0006-vram-gpu-nvidia-orchestration.md) | VRAM-adaptive GPU compute and model-credential boundary | Accepted | accepted-target | LLM orchestration policy superseded by ADR 0010; autonomous development authority governed by ADR 0015. | | [0007](0007-rust-workspace-quality-gates.md) | Explicit Rust workspace, pinned toolchains, and exact quality gates | Accepted | implemented-main | ADR 0014 governs scientific/product claim promotion beyond repository-quality tooling. | | [0008](0008-immutable-evidence-identities-digests-and-spans.md) | Immutable evidence identities, `SHA-256` digests, exact spans, and strict wire reconstruction | Accepted | implemented-main | ADR 0013 governs future persistence/reproducibility/split authority. | diff --git a/docs/research/multilevel-event-time-recovery.md b/docs/research/multilevel-event-time-recovery.md index ac03ab70..901093e7 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -18,11 +18,12 @@ This slice stays inside `psychometric_core`. It does not add a second invariance 12. recover the exact scalar stationary within-subject variance as the `Δt → ∞` limit of Eq. 4 (`asymDIFFUSION`; §4.3 T0VAR stationarity) and refuse finite-interval `Q_Δt` as that limit; 13. recover the exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`) and refuse treating trait variance as process noise or as `asymDIFFUSION`; 14. recover the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`) and refuse treating measurement error, latent variance, or manifest-trait variance as `Var(y)` / `Θ`; -15. refuse pooling discrete lags from unequal event intervals as one coefficient; -16. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); -17. refuse the difference quotient as a continuous-time rate; -18. apply the same event-time map to CWC residuals (still not DSEM); -19. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). +15. recover the exact scalar lagged observed-indicator covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5; `Θ` does not enter) and refuse treating lagged latent covariance or `MANIFESTVAR` as `cov(y_t, y_{t-1})`; +16. refuse pooling discrete lags from unequal event intervals as one coefficient; +17. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); +18. refuse the difference quotient as a continuous-time rate; +19. apply the same event-time map to CWC residuals (still not DSEM); +20. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). ## Claim boundary @@ -61,6 +62,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - **Stationary within-subject variance.** Driver et al. (2017, Eq. 4 as \(\Delta t\to\infty\); §4.3 pp. 9–10; p. 16 `asymDIFFUSION`; JSS PDF re-opened 2026-08-19T04:10Z, p. 5): for stable \(a<0\), \(\lim_{\Delta t\to\infty}Q_{\Delta t}=-q/(2a)\). When \(2a\) is finite, form \(q/-(2a)\) so \(q/a\) overflow does not lose a finite Lyapunov solution (`q=MAX`, `a=-0.75` → `MAX/1.5`; CodeRabbit on `75ecdd3`). When \(2a\) overflows, form \((q/a)\times-0.5\). Forming \(2a\) as the only path overflows when \(|a|\) is at the binary64 extreme (`a=-1e308`, `q=1e308` → `0.5`). Forming \(0.5q\) first underflows at the minimum subnormal (`q=from_bits(1)`, `a=-from_bits(1)` → naive `+0`; representable solution `0.5`). Starting from that variance, \(\operatorname{Var}(\eta_{t})\) is invariant across finite event intervals. A zero diffusion is exactly zero. \(a\ge 0\) has no finite stationary variance (Brownian \(a=0\) grows as \(q\Delta t\)). An overflowing Lyapunov solution fails closed. Finite-interval \(Q_{\Delta t}\) is not that limit. This is not ctsem estimation and not a Kalman filter. - **Trait-plus-state variance.** Driver et al. (2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z): a stable trait process has `DRIFT` and `DIFFUSION` fixed to zero. The scalar maps are \(\operatorname{Var}=\mathrm{trait}+\mathrm{state}\) and \(\operatorname{cov}(t,t-1)=\mathrm{trait}+\mathrm{e}^{a\Delta t}p\). The ctsem `TRAITVAR` rewrite that adds the trait to `DIFFUSION` does not license treating trait variance as \(Q_{\Delta t}\). Trait variance is not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not this map. A zero trait is exactly the state. A zero state is exactly the trait. An overflowing sum fails closed. This is not RI-CLPM and not ctsem estimation. - **Observed-indicator variance.** Driver et al. (2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z): \(y_i(t)=\tau_i+\Lambda\eta_i(t)+\varepsilon_i(t)\) with \(\varepsilon\sim N(0,\Theta)\) and \(\tau_i\sim N(\mu_{\tau},\Psi_{\tau})\). Equation 1 (p. 4) is the latent SDE. Table 2 names \(\Theta\) `MANIFESTVAR` and \(\Psi_{\tau}\) `MANIFESTTRAITVAR`; p. 16 restates those names. The scalar map is \(\operatorname{Var}(y)=\lambda^{2}\operatorname{Var}(\eta)+\theta\) when \(\Psi_{\tau}=0\) and \(\lambda^{2}\operatorname{Var}(\eta)+\theta+\psi\) otherwise. Form \((\lambda p)\lambda\) then add \(\theta\), then add \(\psi\). Forming \(\lambda^{2}\) first overflows at \(\lambda=10^{308}\), \(p=10^{-308}\). A zero loading or zero latent variance is exactly \(\theta\) (\(\Psi_{\tau}=0\)) or \(\theta+\psi\). A zero measurement error is exactly \(\lambda^{2}p+\psi\). A zero manifest trait is exactly \(\lambda^{2}p+\theta\). `MANIFESTVAR` is not \(\operatorname{Var}(y)\). `MANIFESTTRAITVAR` is not `MANIFESTVAR`. `TRAITVAR` is latent additional variance and is scaled by \(\lambda^{2}\); `MANIFESTTRAITVAR` is not. \(\operatorname{Var}(\eta)\) is not \(\operatorname{Var}(y)\). An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. +- **Lagged observed-indicator covariance.** Driver et al. (2017, Eq. 5 with Eq. 3–4; JSS PDF re-opened 2026-08-19T04:18Z): independent \(\varepsilon_t\) does not enter \(\operatorname{cov}(y_t,y_{t-1})\). The scalar map is \(\lambda^{2}\operatorname{cov}(\eta_t,\eta_{t-1})+\psi\). Form \((\lambda c)\lambda\) then add \(\psi\). A zero loading or zero latent lagged covariance is exactly \(\psi\). A zero manifest trait is exactly \(\lambda^{2}c\). `MANIFESTVAR` is not lagged observed covariance. Lagged latent covariance is not lagged observed covariance. An overflowing product or sum fails closed. - **CWC-then-lag.** Sample cluster means are removed first. Consecutive within residuals are then fitted by least squares to \(r_{t+\Delta t}\approx\exp(a\Delta t)\,r_{t}\) on event time. Same-sign pair-wise logs initialize the scalar Newton step. Sign-flipping \(T=2\) CWC pairs have no real logarithm and fail closed. Curran and Bauer (2011, pp. 607–608) show that this person-mean subtraction on a raw autoregressive series does **not** isolate the lagged within-person effect; the helper therefore does not claim to recover the raw-process drift. - **Already-centered irregular residual.** The caller supplies lagged within residuals. The mean of \(a=\ln(r_{t+\Delta t}/r_t)/\Delta t\) is the exact scalar map. Intervals may be irregular. The helper does not center again. This is not DSEM. @@ -79,6 +81,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - Driver et al. (2017, Eq. 4 as \(\Delta t\to\infty\); p. 16 `asymDIFFUSION`; §4.3) recovers a known stationary variance \(-q/(2a)\) at machine-scale RMSE, and that RMSE is smaller than treating finite-interval \(Q_{\Delta t}\) as the stationary variance; starting from that variance, \(\operatorname{Var}(\eta_{t})\) is invariant across finite \(\Delta t\); a zero diffusion is exactly zero; forming \(2a\) first overflows while \((q/a)\times-0.5\) stays finite; forming \(0.5q\) first underflows at the minimum subnormal while \((q/a)\times-0.5=0.5\); forming \(q/a\) first overflows at `q=MAX`, `a=-0.75` while \(q/-(2a)=\mathrm{MAX}/1.5\) stays finite; \(a\ge 0\), a non-event clock, a negative diffusion, and an overflowing Lyapunov solution fail closed; - Driver et al. (2017, §4.3, p. 9) recovers a known trait-plus-state variance and lagged covariance at machine-scale RMSE, and that RMSE is smaller than evolving the summed variance as if it were all state; a zero trait is the state; a zero state is the trait; treating trait variance as process noise or as `asymDIFFUSION` fails closed; an overflowing sum and a non-event clock fail closed; - Driver et al. (2017, Eq. 5, p. 5; Table 2, p. 12) recovers a known observed-indicator variance \(\lambda^{2}\operatorname{Var}(\eta)+\theta\) at machine-scale RMSE when `MANIFESTTRAITVAR` is zero, and \(\lambda^{2}\operatorname{Var}(\eta)+\theta+\psi\) otherwise; those RMSEs are smaller than treating `MANIFESTVAR`, \(\operatorname{Var}(\eta)\), or `MANIFESTTRAITVAR` as \(\operatorname{Var}(y)\) / \(\Theta\); a zero loading or zero latent variance is \(\theta\) or \(\theta+\psi\); `TRAITVAR` is latent and scaled by \(\lambda^{2}\); forming \(\lambda^{2}\) first overflows while \((\lambda p)\lambda\) stays finite; an overflowing product or sum fails closed; +- Driver et al. (2017, Eq. 5 lagged covariance) recovers a known \(\lambda^{2}\operatorname{cov}(\eta_t,\eta_{t-1})+\psi\) at machine-scale RMSE, and that RMSE is smaller than treating lagged latent covariance or `MANIFESTVAR` as \(\operatorname{cov}(y_t,y_{t-1})\); forming \(\lambda^{2}\) first overflows while \((\lambda c)\lambda\) stays finite; an overflowing product or sum fails closed; - pooling discrete lags from unequal intervals fails closed; - CWC-then-lag on a two-cluster decaying series has smaller computed RMSE than a level-pooled series when the latter is identified; - already-centered irregular residuals recover a known drift at machine-scale RMSE, and that RMSE is smaller than CWC of the corresponding raw autoregressive series (Curran & Bauer, 2011, pp. 607–608); From 5a8416f14bbb58dea5962948d937ae28655ff5fa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 14:17:13 +0000 Subject: [PATCH 30/62] feat(psychometric): recover Driver Eq. 5 observed-indicator mean Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z). y = Gamma + Lambda eta + zeta with Gamma ~ N(tau, Psi). MANIFESTMEANS is tau. E(y) = tau + lambda mu. Form lambda mu then add tau. MANIFESTMEANS is not E(y). E(eta) is not E(y). CINT is not MANIFESTMEANS. T0MEANS is not E(y). The mean path is lambda mu, not lambda^2. Still not a Kalman filter, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991) remain unread (Unpaywall 2026-08-19T14:08Z: closed). --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 1 + CLAUDE.md | 2 +- crates/psychometric_core/src/error.rs | 28 +++ crates/psychometric_core/src/event_time.rs | 171 +++++++++++++++++- crates/psychometric_core/src/lib.rs | 12 +- ...multilevel_event_time_recovery_contract.rs | 68 ++++++- .../scientific_claim_boundary_contract.rs | 45 ++++- docs/TRACEABILITY.md | 2 +- docs/adr/0005-posterior-esem-dsem.md | 4 +- docs/adr/README.md | 2 +- .../multilevel-event-time-recovery.md | 15 +- 12 files changed, 322 insertions(+), 30 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 9264d514..8af17871 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -61,7 +61,7 @@ boundaries above remain the target modular MSA architecture. | `tepp_simulation` | known-truth temporal/event data generation | | `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics | | `tepp_api` | versioned DTO, schema, and export contracts | -| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means | +| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means | No crate exposes placeholder production behavior in Task 1. This prevents an empty façade from becoming a de facto public API before its invariants and tests diff --git a/CHANGELOG.md b/CHANGELOG.md index 4979c936..b160e35d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar observed-indicator mean. Equation 5 writes `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and `Γ ~ N(τ, Ψ)`. Table 2 names `τ` `MANIFESTMEANS`, `κ` `CINT`, and the first-occasion latent mean `T0MEANS`. The scalar map is `E(y) = τ + λ μ`. Form `λ μ` then add `τ`. Do not treat `MANIFESTMEANS` as `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. Forming `λ²` is the variance path, not this mean. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-19T14:08Z: closed). Oud and Jansen (2000) remains unread. ZORA Anubis-blocked. - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Eq. 3–4, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z) scalar lagged observed-indicator covariance. Independent measurement error does not enter `cov(y_t, y_{t-1})`. The scalar map is `λ² cov(η_t, η_{t-1}) + ψ`. Form `(λ c) λ` then add `ψ`. Do not form `λ²` first (`λ = 1e308`, `c = 1e-308` → `1e308`). `MANIFESTVAR` is not lagged observed covariance. Lagged `Var(η)` path is not `cov(y)`. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-19T04:25Z: closed). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar observed-indicator variance with `MANIFESTTRAITVAR`. Equation 5 writes `y_i(t) = τ_i + Λ η_i(t) + ε_i(t)` with `ε ~ N(0, Θ)` and `τ_i ~ N(μ_τ, Ψ_τ)`. Equation 1 (p. 4) is the latent SDE, not the measurement model. Table 2 names `Θ` `MANIFESTVAR` and `Ψ_τ` `MANIFESTTRAITVAR`; p. 16 restates those names. The scalar map is `Var(y) = λ² Var(η) + θ` when `Ψ_τ = 0` and `λ² Var(η) + θ + ψ` otherwise. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is not `Var(y)`. `MANIFESTTRAITVAR` is not `MANIFESTVAR`. `TRAITVAR` is latent additional variance and is scaled by `λ²`; `MANIFESTTRAITVAR` is not. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall/OpenAlex 2026-08-19T04:18Z: closed). Oud and Jansen (2000) remains unread. ZORA Anubis-blocked. - `psychometric_core` evaluates the Driver, Oud, and Voelkle (2017, Eq. 4, p. 5; JSS PDF re-opened 2026-08-19T04:10Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) stationary within-subject variance as `q / -(2 a)` when the scalar Kronecker sum `2 a` is finite. The paper limit is `-q / (2 a)` (`A# = A ⊗ I + I ⊗ A`; p. 16 `asymDIFFUSION`). Forming `q / a` first overflows at `q = MAX`, `a = -0.75` while `MAX / 1.5` is finite (CodeRabbit finding on `75ecdd3`). When `2 a` overflows (`a = -1e308`), form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). Still not a Kalman filter, not DSEM, not a matrix `expm`, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall/OpenAlex 2026-08-19T04:10Z: closed). Oud and Jansen (2000) remains unread. ZORA Anubis-blocked. diff --git a/CLAUDE.md b/CLAUDE.md index 9f923bab..07b45c6c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. -- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. diff --git a/crates/psychometric_core/src/error.rs b/crates/psychometric_core/src/error.rs index 29ad2019..16c092a9 100644 --- a/crates/psychometric_core/src/error.rs +++ b/crates/psychometric_core/src/error.rs @@ -80,6 +80,15 @@ pub enum PsychometricError { /// lagged observed-indicator covariance. Independent `ε` does /// not enter `cov(y_t, y_{t-1})`. MeasurementErrorIsNotLaggedObservedCovariance, + /// Driver Eq. 5 `MANIFESTMEANS` was treated as `E(y)`. Table 2 + /// (p. 12) names `τ` the expected intercept `Γ`, not `τ + λ μ`. + ManifestMeansIsNotObservedMean, + /// Driver Eq. 5 latent mean was treated as `E(y)`. `E(η)` is + /// not `τ + λ E(η)`. + LatentMeanIsNotObservedMean, + /// Driver Table 2 `CINT` was treated as `MANIFESTMEANS`. `κ` is + /// the latent continuous intercept, not the expected `Γ`. + ContinuousInterceptIsNotManifestMeans, } impl fmt::Display for PsychometricError { @@ -147,6 +156,13 @@ impl fmt::Display for PsychometricError { Self::MeasurementErrorIsNotLaggedObservedCovariance => { "measurement-error variance is not the lagged observed-indicator covariance" } + Self::ManifestMeansIsNotObservedMean => { + "manifest means are not the observed-indicator mean" + } + Self::LatentMeanIsNotObservedMean => "latent mean is not the observed-indicator mean", + Self::ContinuousInterceptIsNotManifestMeans => { + "continuous intercept is not the manifest mean" + } }; formatter.write_str(message) } @@ -268,5 +284,17 @@ mod tests { PsychometricError::MeasurementErrorIsNotLaggedObservedCovariance.to_string(), "measurement-error variance is not the lagged observed-indicator covariance" ); + assert_eq!( + PsychometricError::ManifestMeansIsNotObservedMean.to_string(), + "manifest means are not the observed-indicator mean" + ); + assert_eq!( + PsychometricError::LatentMeanIsNotObservedMean.to_string(), + "latent mean is not the observed-indicator mean" + ); + assert_eq!( + PsychometricError::ContinuousInterceptIsNotManifestMeans.to_string(), + "continuous intercept is not the manifest mean" + ); } } diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index a3501c49..717812a0 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -34,7 +34,9 @@ //! `MANIFESTTRAITVAR` is not `MANIFESTVAR`, `TRAITVAR` is latent //! (scaled by `λ²`), and `Var(η)` is not `Var(y)`. The lagged //! observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `Θ` does not -//! enter. The JSS article +//! enter. The scalar observed mean is `τ + λ μ` (Table 2, p. 12: +//! `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is `κ`, not `τ`; +//! `T0MEANS` is the initial latent mean, not `E(y)`). The JSS article //! has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). //! The difference quotient `(x(t+Δt) − x(t)) / Δt` (their //! Eqs. 3–4) is refused. This is not DSEM and not a matrix `expm`. @@ -1023,6 +1025,93 @@ pub fn refuse_measurement_error_as_lagged_observed_covariance( Err(PsychometricError::MeasurementErrorIsNotLaggedObservedCovariance) } +/// Exact scalar observed-indicator mean from Driver Equation 5. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Table 2, p. 12; JSS +/// PDF re-opened 2026-08-19T14:08Z) write `y_i(t) = Γ + Λ η_i(t) + +/// ζ_i(t)` with `ζ ~ N(0, Θ)` and `Γ ~ N(τ, Ψ)`. The expected +/// intercept is `τ`. Table 2 names `τ` `MANIFESTMEANS`. The scalar +/// map is `E(y) = τ + λ μ`. Form `λ μ` then add `τ`. A zero loading +/// or zero latent mean is exactly `τ`. A zero intercept is exactly +/// `λ μ`. `MANIFESTMEANS` is not `E(y)`. `E(η)` is not `E(y)`. +/// `CINT` `κ` is the latent continuous intercept from Equation 1, +/// not `τ`. `T0MEANS` is the initial latent mean, not `E(y)`. An +/// overflowing product or sum fails closed. This is not a Kalman +/// filter and not ctsem estimation. +/// +/// # Errors +/// +/// Returns [`PsychometricError::InvalidNumericInput`] when the loading, +/// latent mean, or intercept is non-finite, or the mapped mean is +/// non-finite. +pub fn recover_manifest_observed_mean( + loading: f64, + latent_mean: f64, + manifest_mean: f64, +) -> Result { + if !loading.is_finite() || !latent_mean.is_finite() || !manifest_mean.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + if loading == 0.0 || latent_mean == 0.0 { + return Ok(manifest_mean); + } + let explained = require_finite(loading * latent_mean)?; + if manifest_mean == 0.0 { + return Ok(explained); + } + require_finite(explained + manifest_mean) +} + +/// Refuse treating Driver Eq. 5 `MANIFESTMEANS` as `E(y)`. +/// +/// Table 2 (p. 12) names `τ` the expected intercept `Γ`. Equation 5 +/// maps `E(y) = τ + λ μ`. +/// +/// # Errors +/// +/// Always returns [`PsychometricError::ManifestMeansIsNotObservedMean`]. +pub fn refuse_manifest_means_as_observed_mean( + manifest_mean: f64, + observed_mean: f64, +) -> Result { + let _ = (manifest_mean, observed_mean); + Err(PsychometricError::ManifestMeansIsNotObservedMean) +} + +/// Refuse treating Driver Eq. 5 latent mean as `E(y)`. +/// +/// `E(η)` is the latent process mean. Equation 5 maps `E(y) = τ + λ μ`. +/// `T0MEANS` is that latent mean at the first occasion, not `E(y)`. +/// +/// # Errors +/// +/// Always returns [`PsychometricError::LatentMeanIsNotObservedMean`]. +pub fn refuse_latent_mean_as_observed_mean( + latent_mean: f64, + observed_mean: f64, +) -> Result { + let _ = (latent_mean, observed_mean); + Err(PsychometricError::LatentMeanIsNotObservedMean) +} + +/// Refuse treating Driver Table 2 `CINT` as `MANIFESTMEANS`. +/// +/// Table 2 (p. 12) names `κ` `CINT`, the latent continuous intercept +/// from Equation 1, and `τ` `MANIFESTMEANS`, the expected `Γ` from +/// Equation 5. `κ` is not `τ`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::ContinuousInterceptIsNotManifestMeans`]. +pub fn refuse_continuous_intercept_as_manifest_means( + continuous_intercept: f64, + manifest_mean: f64, +) -> Result { + let _ = (continuous_intercept, manifest_mean); + Err(PsychometricError::ContinuousInterceptIsNotManifestMeans) +} + /// Refuse treating Driver Eq. 3 process noise as the unconditional variance. /// /// Driver, Oud, and Voelkle (2017, Eq. 3–4, pp. 4–5): @@ -1277,13 +1366,15 @@ mod tests { recover_discrete_process_noise, recover_discrete_time_varying_predictor_effect, recover_event_series_mean_log_rate, recover_event_time_discrete_lag_and_log_rate, recover_irregular_centered_residual_log_rate, recover_local_log_rate, - recover_manifest_lagged_observed_covariance, recover_manifest_observed_variance, - recover_manifest_trait_plus_state_observed_variance, recover_stationary_latent_variance, - recover_trait_plus_state_lagged_covariance, recover_trait_plus_state_latent_variance, - recover_within_residual_event_time_log_rate, refuse_difference_quotient_as_local_rate, + recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, + recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, + recover_stationary_latent_variance, recover_trait_plus_state_lagged_covariance, + recover_trait_plus_state_latent_variance, recover_within_residual_event_time_log_rate, + refuse_continuous_intercept_as_manifest_means, refuse_difference_quotient_as_local_rate, refuse_finite_interval_process_noise_as_stationary_variance, refuse_latent_lagged_covariance_as_observed_covariance, - refuse_latent_variance_as_observed_variance, + refuse_latent_mean_as_observed_mean, refuse_latent_variance_as_observed_variance, + refuse_manifest_means_as_observed_mean, refuse_manifest_trait_variance_as_measurement_error, refuse_measurement_error_as_lagged_observed_covariance, refuse_measurement_error_as_observed_variance, @@ -2834,4 +2925,72 @@ mod tests { Err(PsychometricError::InvalidNumericInput) ); } + + #[test] + fn manifest_observed_mean_recovers_driver_equation_five() { + let loading = 2.0_f64; + let latent_mean = 0.4_f64; + let manifest_mean = 0.5_f64; + let recovered = + recover_manifest_observed_mean(loading, latent_mean, manifest_mean).expect("eq5-mean"); + let expected = loading * latent_mean + manifest_mean; + assert!((recovered - expected).abs() < 1e-15); + assert!((recovered - 1.3).abs() < 1e-15); + assert_eq!( + recover_manifest_observed_mean(loading, latent_mean, 0.0), + Ok(0.8) + ); + assert_eq!( + recover_manifest_observed_mean(0.0, latent_mean, manifest_mean), + Ok(manifest_mean) + ); + assert_eq!( + recover_manifest_observed_mean(loading, 0.0, manifest_mean), + Ok(manifest_mean) + ); + assert_eq!(recover_manifest_observed_mean(-2.0, 0.5, 1.0), Ok(0.0)); + assert_eq!( + refuse_manifest_means_as_observed_mean(manifest_mean, recovered), + Err(PsychometricError::ManifestMeansIsNotObservedMean) + ); + assert_eq!( + refuse_latent_mean_as_observed_mean(latent_mean, recovered), + Err(PsychometricError::LatentMeanIsNotObservedMean) + ); + assert_eq!( + refuse_continuous_intercept_as_manifest_means(0.3, manifest_mean), + Err(PsychometricError::ContinuousInterceptIsNotManifestMeans) + ); + let scaled = recover_manifest_observed_mean(1e308, 1e-308, 0.0).expect("scale"); + assert!((scaled - 1.0).abs() < 1e-15); + let finite_loaded = recover_manifest_observed_mean(1e308, 1.0, 0.0).expect("lambda-mu"); + assert!((finite_loaded - 1e308).abs() / 1e308 < 1e-15); + assert!(!(1e308_f64 * 1e308_f64).is_finite()); + } + + #[test] + fn manifest_observed_mean_invalid_inputs_fail_closed() { + assert_eq!( + recover_manifest_observed_mean(f64::NAN, 0.4, 0.0), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_manifest_observed_mean(2.0, f64::NAN, 0.0), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_manifest_observed_mean(2.0, 0.4, f64::NAN), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_manifest_observed_mean(1e308, 2.0, 0.0), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_manifest_observed_mean(1.0, 1e308, 1e308), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!(recover_manifest_observed_mean(0.0, 1e308, 0.5), Ok(0.5)); + assert_eq!(recover_manifest_observed_mean(1e308, 0.0, 0.5), Ok(0.5)); + } } diff --git a/crates/psychometric_core/src/lib.rs b/crates/psychometric_core/src/lib.rs index e846e823..4d311fac 100644 --- a/crates/psychometric_core/src/lib.rs +++ b/crates/psychometric_core/src/lib.rs @@ -26,7 +26,9 @@ //! `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; Table 2, //! p. 12: `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is //! not `MANIFESTVAR`; lagged observed covariance is -//! `λ² cov(η_t, η_{t-1}) + ψ` and does not include `Θ`; Equation 1 +//! `λ² cov(η_t, η_{t-1}) + ψ` and does not include `Θ`; the +//! observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, +//! not `E(y)`; `CINT` is not `MANIFESTMEANS`; Equation 1 //! is the SDE), //! and refuses //! latent-mean comparison below strong invariance. @@ -100,6 +102,8 @@ pub use event_time::recover_irregular_centered_residual_log_rate; pub use event_time::recover_local_log_rate; /// Exact scalar lagged observed-indicator covariance `λ² cov(η) + ψ`. pub use event_time::recover_manifest_lagged_observed_covariance; +/// Exact scalar observed-indicator mean `τ + λ μ`. +pub use event_time::recover_manifest_observed_mean; /// Exact scalar observed-indicator variance `λ² Var(η) + θ`. pub use event_time::recover_manifest_observed_variance; /// Exact scalar observed-indicator variance `λ² Var(η) + θ + ψ`. @@ -112,14 +116,20 @@ pub use event_time::recover_trait_plus_state_lagged_covariance; pub use event_time::recover_trait_plus_state_latent_variance; /// CWC-then-event-time local log-rate (not DSEM; not raw-process AR drift). pub use event_time::recover_within_residual_event_time_log_rate; +/// Refuse treating Driver Table 2 `CINT` as `MANIFESTMEANS`. +pub use event_time::refuse_continuous_intercept_as_manifest_means; /// Refuse the difference quotient as a continuous-time rate. pub use event_time::refuse_difference_quotient_as_local_rate; /// Refuse treating finite-interval `Q_Δt` as `asymDIFFUSION`. pub use event_time::refuse_finite_interval_process_noise_as_stationary_variance; /// Refuse treating Driver Eq. 3–4 lagged latent covariance as `cov(y_t, y_{t-1})`. pub use event_time::refuse_latent_lagged_covariance_as_observed_covariance; +/// Refuse treating Driver Eq. 5 latent mean as `E(y)`. +pub use event_time::refuse_latent_mean_as_observed_mean; /// Refuse treating Driver Eq. 5 latent variance as `Var(y)`. pub use event_time::refuse_latent_variance_as_observed_variance; +/// Refuse treating Driver Eq. 5 `MANIFESTMEANS` as `E(y)`. +pub use event_time::refuse_manifest_means_as_observed_mean; /// Refuse treating Driver Eq. 5 `MANIFESTTRAITVAR` as `MANIFESTVAR`. pub use event_time::refuse_manifest_trait_variance_as_measurement_error; /// Refuse treating Driver Eq. 5 measurement error as lagged observed covariance. 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 1b6f5198..e80a50d7 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -10,13 +10,14 @@ use psychometric_core::{ recover_discrete_process_noise, recover_discrete_time_varying_predictor_effect, recover_event_series_mean_log_rate, recover_event_time_discrete_lag_and_log_rate, recover_irregular_centered_residual_log_rate, recover_kish_weighted_slope, - recover_manifest_lagged_observed_covariance, recover_manifest_observed_variance, - recover_manifest_trait_plus_state_observed_variance, recover_stationary_latent_variance, - recover_trait_plus_state_lagged_covariance, recover_trait_plus_state_latent_variance, - recover_within_residual_event_time_log_rate, refuse_difference_quotient_as_local_rate, + recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, + recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, + recover_stationary_latent_variance, recover_trait_plus_state_lagged_covariance, + recover_trait_plus_state_latent_variance, recover_within_residual_event_time_log_rate, + refuse_continuous_intercept_as_manifest_means, refuse_difference_quotient_as_local_rate, refuse_finite_interval_process_noise_as_stationary_variance, - refuse_latent_lagged_covariance_as_observed_covariance, - refuse_latent_variance_as_observed_variance, + refuse_latent_lagged_covariance_as_observed_covariance, refuse_latent_mean_as_observed_mean, + refuse_latent_variance_as_observed_variance, refuse_manifest_means_as_observed_mean, refuse_manifest_trait_variance_as_measurement_error, refuse_measurement_error_as_lagged_observed_covariance, refuse_measurement_error_as_observed_variance, @@ -943,6 +944,61 @@ fn manifest_lagged_observed_covariance_recovers_driver_equation_five() { ); } +#[test] +fn manifest_observed_mean_recovers_driver_equation_five() { + let loading = 2.0_f64; + let latent_mean = 0.4_f64; + let manifest_mean = 0.5_f64; + let observed = + recover_manifest_observed_mean(loading, latent_mean, manifest_mean).expect("eq5-mean"); + let expected = loading * latent_mean + manifest_mean; + let error = rmse(&[expected], &[observed]); + assert!(error < 1e-15, "Driver Eq. 5 observed mean RMSE {error}"); + let intercept_error = rmse(&[expected], &[manifest_mean]); + assert!( + intercept_error > error, + "MANIFESTMEANS is not E(y): RMSE {intercept_error} must exceed {error}" + ); + let latent_error = rmse(&[expected], &[latent_mean]); + assert!( + latent_error > error, + "E(η) is not E(y): RMSE {latent_error} must exceed {error}" + ); + let without_loading = + recover_manifest_observed_mean(0.0, latent_mean, manifest_mean).expect("lambda0"); + let dropped_error = rmse(&[expected], &[without_loading]); + assert!( + dropped_error > error, + "zero loading is τ, not τ + λμ: RMSE {dropped_error} must exceed {error}" + ); + assert_eq!( + refuse_manifest_means_as_observed_mean(manifest_mean, observed), + Err(PsychometricError::ManifestMeansIsNotObservedMean) + ); + assert_eq!( + refuse_latent_mean_as_observed_mean(latent_mean, observed), + Err(PsychometricError::LatentMeanIsNotObservedMean) + ); + assert_eq!( + refuse_continuous_intercept_as_manifest_means(0.3, manifest_mean), + Err(PsychometricError::ContinuousInterceptIsNotManifestMeans) + ); + let scaled = recover_manifest_observed_mean(1e308, 1e-308, 0.0).expect("scale"); + assert!( + (scaled - 1.0).abs() < 1e-15, + "Driver Eq. 5 λμ must keep λ=1e308, μ=1e-308: got {scaled}" + ); + let finite_loaded = recover_manifest_observed_mean(1e308, 1.0, 0.0).expect("lambda-mu"); + assert!( + (finite_loaded - 1e308).abs() / 1e308 < 1e-15, + "Driver Eq. 5 mean is λμ, not λ²: got {finite_loaded}" + ); + assert_eq!( + recover_manifest_observed_mean(1e308, 2.0, 0.0), + Err(PsychometricError::InvalidNumericInput) + ); +} + #[test] fn admitted_coordinates_still_required_for_multilevel_weights() { assert_eq!( diff --git a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs index c59f5fb1..b2673c38 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -7,12 +7,13 @@ use psychometric_core::{ recover_discrete_lagged_latent_covariance, recover_discrete_latent_variance, recover_discrete_process_noise, recover_discrete_time_varying_predictor_effect, recover_irregular_centered_residual_log_rate, recover_loading_point_estimate_mean, - recover_manifest_lagged_observed_covariance, recover_manifest_observed_variance, - recover_manifest_trait_plus_state_observed_variance, recover_stationary_latent_variance, - recover_trait_plus_state_latent_variance, recover_within_residual_event_time_log_rate, + recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, + recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, + recover_stationary_latent_variance, recover_trait_plus_state_latent_variance, + recover_within_residual_event_time_log_rate, refuse_continuous_intercept_as_manifest_means, refuse_finite_interval_process_noise_as_stationary_variance, - refuse_latent_lagged_covariance_as_observed_covariance, - refuse_latent_variance_as_observed_variance, + refuse_latent_lagged_covariance_as_observed_covariance, refuse_latent_mean_as_observed_mean, + refuse_latent_variance_as_observed_variance, refuse_manifest_means_as_observed_mean, refuse_manifest_trait_variance_as_measurement_error, refuse_measurement_error_as_lagged_observed_covariance, refuse_measurement_error_as_observed_variance, refuse_process_noise_as_unconditional_variance, @@ -411,3 +412,37 @@ fn lagged_latent_covariance_and_measurement_error_are_not_lagged_observed_covari Err(psychometric_core::PsychometricError::MeasurementErrorIsNotLaggedObservedCovariance) ); } + +#[test] +fn manifest_means_and_latent_mean_are_not_observed_mean() { + let loading = 2.0_f64; + let latent_mean = 0.4_f64; + let manifest_mean = 0.5_f64; + let continuous_intercept = 0.3_f64; + let observed = + recover_manifest_observed_mean(loading, latent_mean, manifest_mean).expect("eq5-mean"); + assert!( + (manifest_mean - observed).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 / Table 2 p. 12): MANIFESTMEANS is not E(y)" + ); + assert!( + (latent_mean - observed).abs() > 1e-3, + "Driver et al. (2017, Eq. 5): E(η) is not E(y)" + ); + assert!( + (continuous_intercept - manifest_mean).abs() > 1e-3, + "Driver et al. (2017, Table 2 p. 12): CINT is not MANIFESTMEANS" + ); + assert_eq!( + refuse_manifest_means_as_observed_mean(manifest_mean, observed), + Err(psychometric_core::PsychometricError::ManifestMeansIsNotObservedMean) + ); + assert_eq!( + refuse_latent_mean_as_observed_mean(latent_mean, observed), + Err(psychometric_core::PsychometricError::LatentMeanIsNotObservedMean) + ); + assert_eq!( + refuse_continuous_intercept_as_manifest_means(continuous_intercept, manifest_mean), + Err(psychometric_core::PsychometricError::ContinuousInterceptIsNotManifestMeans) + ); +} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index b1cf0934..a3ad2ec2 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -28,7 +28,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | report template/section/copied/style/modality method effects | ADR 0004/0012; PRD/TRD | simulation truth factors implemented; estimator-side method model remains future | partial | | candidate K statistical/Pareto gates + blinded LLM review | ADR 0012; research | future `model_selection` | accepted-target | | compositional topic correlation / stable clustering | ADR 0005/0012; research | future `network_analysis` | accepted-target | -| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR; full ESEM/DSEM remaining | partial | +| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR; full ESEM/DSEM remaining | partial | | CPU bounded multithreading + GPU/VRAM streaming/parity | ADR 0001/0006 | future `compute_backend` | accepted-target | | TDT detection/tracking vs CHRONOS schema/prediction/temporal consistency | ADR 0016; PRD/research | future `event_intelligence` | accepted-target | | evidence-bounded LLM interpretation | ADR 0010/0012; PRD | future `interpretation_gateway` | accepted-target | diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index 43bfad29..f758241e 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -1,7 +1,7 @@ # ADR 0005 — Posterior-aware ESEM/DSEM and structural interpretation **Decision status:** Accepted -**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; Equation 1 is the SDE; not a Kalman filter), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target +**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target **Date:** 2026-08-05 **Supersedes:** None. ADR 0012 governs upstream topic measurement/network coordinates; this ADR governs higher-order psychometric structure and longitudinal interpretation. @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. 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/adr/README.md b/docs/adr/README.md index 02ca6d7c..1064a8a7 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -10,7 +10,7 @@ Read [`ADR_POLICY.md`](ADR_POLICY.md) first. **Decision status and implementatio | [0002](0002-six-clock-temporal-semantics.md) | Six-clock temporal semantics and fail-closed historical leakage prevention | Accepted | active-PR | Unmerged PR #8 is the canonical Task 3 replacement implementing typed clocks/intervals against the current protected-main lineage; conflicted PR #5 is superseded lineage. Later graph/split enforcement remains target work. | | [0003](0003-relational-event-multiple-membership.md) | Relational event ontology and time-varying cross-classified multiple membership | Accepted | partial | Weighted time-varying membership network/roles are active-PR (PR #12); full multilevel estimators, graph ontology, and persistence remain accepted-target. ADR 0016 owns event-intelligence tasks. | | [0004](0004-shared-multilingual-latent-space.md) | One shared multilingual latent space with explicit invariance status | Accepted | accepted-target | ADR 0012 owns the full topic-estimator/backend/global-topic contract. | -| [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | partial | Input gates, posterior-draw point estimates, Rubin `T` on OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects, exact scalar discrete process noise, lagged latent covariance and unconditional latent variance, stationary within-subject variance (`asymDIFFUSION`), trait-plus-state variance (`TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance), irregular already-centered residual lag, and strong-gated latent means are on the stacked psychometric PR; full ESEM/DSEM estimator remains accepted-target. | +| [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | partial | Input gates, posterior-draw point estimates, Rubin `T` on OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects, exact scalar discrete process noise, lagged latent covariance and unconditional latent variance, stationary within-subject variance (`asymDIFFUSION`), trait-plus-state variance (`TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`), irregular already-centered residual lag, and strong-gated latent means are on the stacked psychometric PR; full ESEM/DSEM estimator remains accepted-target. | | [0006](0006-vram-gpu-nvidia-orchestration.md) | VRAM-adaptive GPU compute and model-credential boundary | Accepted | accepted-target | LLM orchestration policy superseded by ADR 0010; autonomous development authority governed by ADR 0015. | | [0007](0007-rust-workspace-quality-gates.md) | Explicit Rust workspace, pinned toolchains, and exact quality gates | Accepted | implemented-main | ADR 0014 governs scientific/product claim promotion beyond repository-quality tooling. | | [0008](0008-immutable-evidence-identities-digests-and-spans.md) | Immutable evidence identities, `SHA-256` digests, exact spans, and strict wire reconstruction | Accepted | implemented-main | ADR 0013 governs future persistence/reproducibility/split authority. | diff --git a/docs/research/multilevel-event-time-recovery.md b/docs/research/multilevel-event-time-recovery.md index 901093e7..8a28d4a5 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -19,15 +19,16 @@ This slice stays inside `psychometric_core`. It does not add a second invariance 13. recover the exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`) and refuse treating trait variance as process noise or as `asymDIFFUSION`; 14. recover the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`) and refuse treating measurement error, latent variance, or manifest-trait variance as `Var(y)` / `Θ`; 15. recover the exact scalar lagged observed-indicator covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5; `Θ` does not enter) and refuse treating lagged latent covariance or `MANIFESTVAR` as `cov(y_t, y_{t-1})`; -16. refuse pooling discrete lags from unequal event intervals as one coefficient; -17. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); -18. refuse the difference quotient as a continuous-time rate; -19. apply the same event-time map to CWC residuals (still not DSEM); -20. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). +16. recover the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`) and refuse treating the intercept, latent mean, or continuous intercept as `E(y)` / `τ`; +17. refuse pooling discrete lags from unequal event intervals as one coefficient; +18. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); +19. refuse the difference quotient as a continuous-time rate; +20. apply the same event-time map to CWC residuals (still not DSEM); +21. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). ## Claim boundary -This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. +This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. ## Authoritative sources @@ -63,6 +64,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - **Trait-plus-state variance.** Driver et al. (2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z): a stable trait process has `DRIFT` and `DIFFUSION` fixed to zero. The scalar maps are \(\operatorname{Var}=\mathrm{trait}+\mathrm{state}\) and \(\operatorname{cov}(t,t-1)=\mathrm{trait}+\mathrm{e}^{a\Delta t}p\). The ctsem `TRAITVAR` rewrite that adds the trait to `DIFFUSION` does not license treating trait variance as \(Q_{\Delta t}\). Trait variance is not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not this map. A zero trait is exactly the state. A zero state is exactly the trait. An overflowing sum fails closed. This is not RI-CLPM and not ctsem estimation. - **Observed-indicator variance.** Driver et al. (2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z): \(y_i(t)=\tau_i+\Lambda\eta_i(t)+\varepsilon_i(t)\) with \(\varepsilon\sim N(0,\Theta)\) and \(\tau_i\sim N(\mu_{\tau},\Psi_{\tau})\). Equation 1 (p. 4) is the latent SDE. Table 2 names \(\Theta\) `MANIFESTVAR` and \(\Psi_{\tau}\) `MANIFESTTRAITVAR`; p. 16 restates those names. The scalar map is \(\operatorname{Var}(y)=\lambda^{2}\operatorname{Var}(\eta)+\theta\) when \(\Psi_{\tau}=0\) and \(\lambda^{2}\operatorname{Var}(\eta)+\theta+\psi\) otherwise. Form \((\lambda p)\lambda\) then add \(\theta\), then add \(\psi\). Forming \(\lambda^{2}\) first overflows at \(\lambda=10^{308}\), \(p=10^{-308}\). A zero loading or zero latent variance is exactly \(\theta\) (\(\Psi_{\tau}=0\)) or \(\theta+\psi\). A zero measurement error is exactly \(\lambda^{2}p+\psi\). A zero manifest trait is exactly \(\lambda^{2}p+\theta\). `MANIFESTVAR` is not \(\operatorname{Var}(y)\). `MANIFESTTRAITVAR` is not `MANIFESTVAR`. `TRAITVAR` is latent additional variance and is scaled by \(\lambda^{2}\); `MANIFESTTRAITVAR` is not. \(\operatorname{Var}(\eta)\) is not \(\operatorname{Var}(y)\). An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **Lagged observed-indicator covariance.** Driver et al. (2017, Eq. 5 with Eq. 3–4; JSS PDF re-opened 2026-08-19T04:18Z): independent \(\varepsilon_t\) does not enter \(\operatorname{cov}(y_t,y_{t-1})\). The scalar map is \(\lambda^{2}\operatorname{cov}(\eta_t,\eta_{t-1})+\psi\). Form \((\lambda c)\lambda\) then add \(\psi\). A zero loading or zero latent lagged covariance is exactly \(\psi\). A zero manifest trait is exactly \(\lambda^{2}c\). `MANIFESTVAR` is not lagged observed covariance. Lagged latent covariance is not lagged observed covariance. An overflowing product or sum fails closed. +- **Observed-indicator mean.** Driver et al. (2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z): \(y_i(t)=\Gamma+\Lambda\eta_i(t)+\zeta_i(t)\) with \(\zeta\sim N(0,\Theta)\) and \(\Gamma\sim N(\tau,\Psi)\). Table 2 names \(\tau\) `MANIFESTMEANS`, \(\kappa\) `CINT`, and the first-occasion latent mean `T0MEANS`. The scalar map is \(E(y)=\tau+\lambda\mu\). Form \(\lambda\mu\) then add \(\tau\). A zero loading or zero latent mean is exactly \(\tau\). A zero intercept is exactly \(\lambda\mu\). `MANIFESTMEANS` is not \(E(y)\). \(E(\eta)\) is not \(E(y)\). `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not \(E(y)\). An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **CWC-then-lag.** Sample cluster means are removed first. Consecutive within residuals are then fitted by least squares to \(r_{t+\Delta t}\approx\exp(a\Delta t)\,r_{t}\) on event time. Same-sign pair-wise logs initialize the scalar Newton step. Sign-flipping \(T=2\) CWC pairs have no real logarithm and fail closed. Curran and Bauer (2011, pp. 607–608) show that this person-mean subtraction on a raw autoregressive series does **not** isolate the lagged within-person effect; the helper therefore does not claim to recover the raw-process drift. - **Already-centered irregular residual.** The caller supplies lagged within residuals. The mean of \(a=\ln(r_{t+\Delta t}/r_t)/\Delta t\) is the exact scalar map. Intervals may be irregular. The helper does not center again. This is not DSEM. @@ -82,6 +84,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - Driver et al. (2017, §4.3, p. 9) recovers a known trait-plus-state variance and lagged covariance at machine-scale RMSE, and that RMSE is smaller than evolving the summed variance as if it were all state; a zero trait is the state; a zero state is the trait; treating trait variance as process noise or as `asymDIFFUSION` fails closed; an overflowing sum and a non-event clock fail closed; - Driver et al. (2017, Eq. 5, p. 5; Table 2, p. 12) recovers a known observed-indicator variance \(\lambda^{2}\operatorname{Var}(\eta)+\theta\) at machine-scale RMSE when `MANIFESTTRAITVAR` is zero, and \(\lambda^{2}\operatorname{Var}(\eta)+\theta+\psi\) otherwise; those RMSEs are smaller than treating `MANIFESTVAR`, \(\operatorname{Var}(\eta)\), or `MANIFESTTRAITVAR` as \(\operatorname{Var}(y)\) / \(\Theta\); a zero loading or zero latent variance is \(\theta\) or \(\theta+\psi\); `TRAITVAR` is latent and scaled by \(\lambda^{2}\); forming \(\lambda^{2}\) first overflows while \((\lambda p)\lambda\) stays finite; an overflowing product or sum fails closed; - Driver et al. (2017, Eq. 5 lagged covariance) recovers a known \(\lambda^{2}\operatorname{cov}(\eta_t,\eta_{t-1})+\psi\) at machine-scale RMSE, and that RMSE is smaller than treating lagged latent covariance or `MANIFESTVAR` as \(\operatorname{cov}(y_t,y_{t-1})\); forming \(\lambda^{2}\) first overflows while \((\lambda c)\lambda\) stays finite; an overflowing product or sum fails closed; +- Driver et al. (2017, Eq. 5 observed mean; Table 2, p. 12) recovers a known \(E(y)=\tau+\lambda\mu\) at machine-scale RMSE, and that RMSE is smaller than treating `MANIFESTMEANS` or \(E(\eta)\) as \(E(y)\); a zero loading or zero latent mean is \(\tau\); `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not \(E(y)\); an overflowing product or sum fails closed; - pooling discrete lags from unequal intervals fails closed; - CWC-then-lag on a two-cluster decaying series has smaller computed RMSE than a level-pooled series when the latter is identified; - already-centered irregular residuals recover a known drift at machine-scale RMSE, and that RMSE is smaller than CWC of the corresponding raw autoregressive series (Curran & Bauer, 2011, pp. 607–608); From b0b288b497f621a154abf5c03f31a4786a6851eb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 18:25:09 +0000 Subject: [PATCH 31/62] feat(psychometric): recover Driver Eq. 3 expected-value latent mean Driver, Oud, and Voelkle (2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z). mu_t = exp(a dt) mu_0 + (exp(a dt)-1)/a kappa. T0MEANS is not mu_t. CINT is not the discrete increment. A zero drift is the Eq. 3 integral kappa dt. Underflow of exp(a dt) to +0 drops T0MEANS and keeps -kappa/a. Still not a Kalman filter, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991) remain unread (OpenAlex 2026-08-19T18:10Z: closed). --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 1 + CLAUDE.md | 2 +- crates/psychometric_core/src/error.rs | 30 ++ crates/psychometric_core/src/event_time.rs | 293 +++++++++++++++++- crates/psychometric_core/src/lib.rs | 14 +- ...multilevel_event_time_recovery_contract.rs | 84 ++++- .../scientific_claim_boundary_contract.rs | 56 +++- docs/TRACEABILITY.md | 2 +- docs/adr/0005-posterior-esem-dsem.md | 4 +- docs/adr/README.md | 2 +- .../multilevel-event-time-recovery.md | 15 +- docs/validation/temporal-event-foundation.md | 2 +- 13 files changed, 474 insertions(+), 33 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 8af17871..63b135ab 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -61,7 +61,7 @@ boundaries above remain the target modular MSA architecture. | `tepp_simulation` | known-truth temporal/event data generation | | `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics | | `tepp_api` | versioned DTO, schema, and export contracts | -| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means | +| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means | No crate exposes placeholder production behavior in Task 1. This prevents an empty façade from becoming a de facto public API before its invariants and tests diff --git a/CHANGELOG.md b/CHANGELOG.md index b160e35d..c812b434 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar expected-value latent mean. Equation 3 writes `η(t) = exp(A Δt) η(t0) + ∫ exp(A(t−s)) (b + …) ds` plus a stochastic integral of mean zero. Table 2 names the first-occasion latent mean `T0MEANS` and `κ` `CINT`. The scalar map is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`. Form the CINT increment first, then add the carried `T0MEANS` term. A zero drift is the Eq. 3 integral `κ Δt` (`A = 0` has no inverse). As `Δt → ∞` with stable `a < 0`, `μ_t → −κ / a`. Binary64 underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps that equilibrium increment. `T0MEANS` is not `μ_t`. `CINT` is not the discrete increment. `CINT` is not `T0MEANS`. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (OpenAlex 2026-08-19T18:10Z: closed; Springer `content/pdf` is HTML 200). Oud and Jansen (2000) remains unread. ZORA Anubis-blocked. - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar observed-indicator mean. Equation 5 writes `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and `Γ ~ N(τ, Ψ)`. Table 2 names `τ` `MANIFESTMEANS`, `κ` `CINT`, and the first-occasion latent mean `T0MEANS`. The scalar map is `E(y) = τ + λ μ`. Form `λ μ` then add `τ`. Do not treat `MANIFESTMEANS` as `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. Forming `λ²` is the variance path, not this mean. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-19T14:08Z: closed). Oud and Jansen (2000) remains unread. ZORA Anubis-blocked. - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Eq. 3–4, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z) scalar lagged observed-indicator covariance. Independent measurement error does not enter `cov(y_t, y_{t-1})`. The scalar map is `λ² cov(η_t, η_{t-1}) + ψ`. Form `(λ c) λ` then add `ψ`. Do not form `λ²` first (`λ = 1e308`, `c = 1e-308` → `1e308`). `MANIFESTVAR` is not lagged observed covariance. Lagged `Var(η)` path is not `cov(y)`. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-19T04:25Z: closed). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar observed-indicator variance with `MANIFESTTRAITVAR`. Equation 5 writes `y_i(t) = τ_i + Λ η_i(t) + ε_i(t)` with `ε ~ N(0, Θ)` and `τ_i ~ N(μ_τ, Ψ_τ)`. Equation 1 (p. 4) is the latent SDE, not the measurement model. Table 2 names `Θ` `MANIFESTVAR` and `Ψ_τ` `MANIFESTTRAITVAR`; p. 16 restates those names. The scalar map is `Var(y) = λ² Var(η) + θ` when `Ψ_τ = 0` and `λ² Var(η) + θ + ψ` otherwise. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is not `Var(y)`. `MANIFESTTRAITVAR` is not `MANIFESTVAR`. `TRAITVAR` is latent additional variance and is scaled by `λ²`; `MANIFESTTRAITVAR` is not. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall/OpenAlex 2026-08-19T04:18Z: closed). Oud and Jansen (2000) remains unread. ZORA Anubis-blocked. diff --git a/CLAUDE.md b/CLAUDE.md index 07b45c6c..335cf896 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. -- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. diff --git a/crates/psychometric_core/src/error.rs b/crates/psychometric_core/src/error.rs index 16c092a9..5e48313e 100644 --- a/crates/psychometric_core/src/error.rs +++ b/crates/psychometric_core/src/error.rs @@ -89,6 +89,15 @@ pub enum PsychometricError { /// Driver Table 2 `CINT` was treated as `MANIFESTMEANS`. `κ` is /// the latent continuous intercept, not the expected `Γ`. ContinuousInterceptIsNotManifestMeans, + /// Driver Table 2 `T0MEANS` was treated as the evolved latent mean. + /// Equation 3 maps `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`. + InitialLatentMeanIsNotEvolvedMean, + /// Driver Table 2 `CINT` was treated as the discrete mean increment. + /// `κ` is not `A^{-1}[e^{A Δt} − I] κ`. + ContinuousInterceptIsNotDiscreteMeanIncrement, + /// Driver Table 2 `CINT` was treated as `T0MEANS`. `κ` is not + /// the first-occasion latent mean. + ContinuousInterceptIsNotInitialLatentMean, } impl fmt::Display for PsychometricError { @@ -163,6 +172,15 @@ impl fmt::Display for PsychometricError { Self::ContinuousInterceptIsNotManifestMeans => { "continuous intercept is not the manifest mean" } + Self::InitialLatentMeanIsNotEvolvedMean => { + "initial latent mean is not the evolved latent mean" + } + Self::ContinuousInterceptIsNotDiscreteMeanIncrement => { + "continuous intercept is not the discrete mean increment" + } + Self::ContinuousInterceptIsNotInitialLatentMean => { + "continuous intercept is not the initial latent mean" + } }; formatter.write_str(message) } @@ -296,5 +314,17 @@ mod tests { PsychometricError::ContinuousInterceptIsNotManifestMeans.to_string(), "continuous intercept is not the manifest mean" ); + assert_eq!( + PsychometricError::InitialLatentMeanIsNotEvolvedMean.to_string(), + "initial latent mean is not the evolved latent mean" + ); + assert_eq!( + PsychometricError::ContinuousInterceptIsNotDiscreteMeanIncrement.to_string(), + "continuous intercept is not the discrete mean increment" + ); + assert_eq!( + PsychometricError::ContinuousInterceptIsNotInitialLatentMean.to_string(), + "continuous intercept is not the initial latent mean" + ); } } diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index 717812a0..e5244c0b 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -36,7 +36,9 @@ //! observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `Θ` does not //! enter. The scalar observed mean is `τ + λ μ` (Table 2, p. 12: //! `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is `κ`, not `τ`; -//! `T0MEANS` is the initial latent mean, not `E(y)`). The JSS article +//! `T0MEANS` is the initial latent mean, not `E(y)`). Equation 3's +//! expected-value map is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` +//! (`T0MEANS` is not `μ_t`; `CINT` is not that discrete increment). The JSS article //! has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). //! The difference quotient `(x(t+Δt) − x(t)) / Δt` (their //! Eqs. 3–4) is refused. This is not DSEM and not a matrix `expm`. @@ -1112,6 +1114,170 @@ pub fn refuse_continuous_intercept_as_manifest_means( Err(PsychometricError::ContinuousInterceptIsNotManifestMeans) } +/// Exact scalar discrete intercept increment from Driver Equation 3. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 3, p. 4; Table 2, p. 12; JSS +/// PDF re-opened 2026-08-19T18:10Z) write the expected-value term +/// `A^{-1}[e^{A Δt} − I] b` after the stochastic integral is taken +/// to have mean zero. Table 2 names `κ` `CINT`. The scalar map is +/// `κ (expm1(a Δt) / a)` for `a ≠ 0`. A zero drift is the Eq. 3 +/// integral with `A = 0`: `κ Δt`. That path has no matrix inverse. +/// A zero intercept is exactly zero. `CINT` is not this discrete +/// increment. The `a ≠ 0` evaluation is +/// [`recover_discrete_constant_predictor_effect`]. This is not a +/// Kalman filter and not ctsem estimation. +/// +/// # Errors +/// +/// Returns [`PsychometricError::EventTimeRequired`] for any non-event +/// clock, [`PsychometricError::NonPositiveInterval`] when +/// `event_delta` is not strictly positive, and +/// [`PsychometricError::InvalidNumericInput`] when the intercept or +/// drift is non-finite or the mapped increment is non-finite. +pub fn recover_discrete_continuous_intercept_effect( + continuous_intercept: f64, + log_rate: f64, + event_delta: f64, + clock: LagClock, +) -> Result { + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + if !event_delta.is_finite() || event_delta <= 0.0 { + return Err(PsychometricError::NonPositiveInterval); + } + if !continuous_intercept.is_finite() || !log_rate.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + if log_rate == 0.0 { + if continuous_intercept == 0.0 { + return Ok(0.0); + } + return require_finite(continuous_intercept * event_delta); + } + recover_discrete_constant_predictor_effect(continuous_intercept, log_rate, event_delta, clock) +} + +/// Exact scalar discrete latent mean from Driver Equation 3. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 3, p. 4; Table 2, p. 12; JSS +/// PDF re-opened 2026-08-19T18:10Z) write +/// `η(t) = exp(A Δt) η(t0) + ∫ exp(A(t−s)) (b + …) ds` plus a +/// stochastic integral of mean zero. With no time-varying covariates +/// the scalar expected-value map is +/// `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`. Table 2 names `μ_0` +/// at the first occasion `T0MEANS` and `κ` `CINT`. Form the CINT +/// increment first, then add the carried `T0MEANS` term. A zero +/// initial mean is exactly the increment. A zero intercept is exactly +/// `exp(a Δt) μ_0`. A zero drift carries `T0MEANS` unchanged and adds +/// `κ Δt`. As `Δt → ∞` with stable `a < 0`, `μ_t → −κ / a`. Binary64 +/// underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and +/// keeps the equilibrium increment. `T0MEANS` is not `μ_t`. `CINT` is +/// not the discrete increment. `CINT` is not `T0MEANS`. This is not a +/// Kalman filter and not ctsem estimation. +/// +/// # Errors +/// +/// Propagates [`recover_discrete_continuous_intercept_effect`] and +/// returns [`PsychometricError::InvalidNumericInput`] when the initial +/// mean is non-finite, the carried exponential overflows, or the +/// mapped mean is non-finite. +pub fn recover_discrete_latent_mean( + initial_latent_mean: f64, + log_rate: f64, + continuous_intercept: f64, + event_delta: f64, + clock: LagClock, +) -> Result { + let intercept_effect = recover_discrete_continuous_intercept_effect( + continuous_intercept, + log_rate, + event_delta, + clock, + )?; + if !initial_latent_mean.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + if initial_latent_mean == 0.0 { + return Ok(intercept_effect); + } + let carried = if log_rate == 0.0 { + initial_latent_mean + } else { + let increment_argument = log_rate * event_delta; + if increment_argument == 0.0 { + initial_latent_mean + } else { + let discrete_lag = increment_argument.exp(); + if discrete_lag == 0.0 { + 0.0 + } else if !discrete_lag.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } else { + require_finite(discrete_lag * initial_latent_mean)? + } + } + }; + if intercept_effect == 0.0 { + return Ok(carried); + } + if carried == 0.0 { + return Ok(intercept_effect); + } + require_finite(carried + intercept_effect) +} + +/// Refuse treating Driver Table 2 `T0MEANS` as the evolved latent mean. +/// +/// Equation 3 maps `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`. +/// `T0MEANS` is `μ_0`, not `μ_t`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::InitialLatentMeanIsNotEvolvedMean`]. +pub fn refuse_initial_latent_mean_as_evolved_mean( + initial_latent_mean: f64, + evolved_latent_mean: f64, +) -> Result { + let _ = (initial_latent_mean, evolved_latent_mean); + Err(PsychometricError::InitialLatentMeanIsNotEvolvedMean) +} + +/// Refuse treating Driver Table 2 `CINT` as the discrete mean increment. +/// +/// `κ` is the continuous intercept. Equation 3 maps it through +/// `A^{-1}[e^{A Δt} − I]`. `κ` is not that increment. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::ContinuousInterceptIsNotDiscreteMeanIncrement`]. +pub fn refuse_continuous_intercept_as_discrete_mean_increment( + continuous_intercept: f64, + discrete_mean_increment: f64, +) -> Result { + let _ = (continuous_intercept, discrete_mean_increment); + Err(PsychometricError::ContinuousInterceptIsNotDiscreteMeanIncrement) +} + +/// Refuse treating Driver Table 2 `CINT` as `T0MEANS`. +/// +/// Table 2 (p. 12) names `κ` `CINT` and the first-occasion latent +/// mean `T0MEANS`. `κ` is not `E(η_{i1})`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::ContinuousInterceptIsNotInitialLatentMean`]. +pub fn refuse_continuous_intercept_as_initial_latent_mean( + continuous_intercept: f64, + initial_latent_mean: f64, +) -> Result { + let _ = (continuous_intercept, initial_latent_mean); + Err(PsychometricError::ContinuousInterceptIsNotInitialLatentMean) +} + /// Refuse treating Driver Eq. 3 process noise as the unconditional variance. /// /// Driver, Oud, and Voelkle (2017, Eq. 3–4, pp. 4–5): @@ -1361,8 +1527,9 @@ mod tests { use super::{ ClusteredEventScore, EventOccasion, LagClock, LaggedWithinResidual, fit_scalar_log_rate, map_discrete_lag_across_event_intervals, recover_discrete_constant_predictor_effect, - recover_discrete_lag_from_log_rate, recover_discrete_lag_one, - recover_discrete_lagged_latent_covariance, recover_discrete_latent_variance, + recover_discrete_continuous_intercept_effect, recover_discrete_lag_from_log_rate, + recover_discrete_lag_one, recover_discrete_lagged_latent_covariance, + recover_discrete_latent_mean, recover_discrete_latent_variance, recover_discrete_process_noise, recover_discrete_time_varying_predictor_effect, recover_event_series_mean_log_rate, recover_event_time_discrete_lag_and_log_rate, recover_irregular_centered_residual_log_rate, recover_local_log_rate, @@ -1370,8 +1537,11 @@ mod tests { recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, recover_stationary_latent_variance, recover_trait_plus_state_lagged_covariance, recover_trait_plus_state_latent_variance, recover_within_residual_event_time_log_rate, + refuse_continuous_intercept_as_discrete_mean_increment, + refuse_continuous_intercept_as_initial_latent_mean, refuse_continuous_intercept_as_manifest_means, refuse_difference_quotient_as_local_rate, refuse_finite_interval_process_noise_as_stationary_variance, + refuse_initial_latent_mean_as_evolved_mean, refuse_latent_lagged_covariance_as_observed_covariance, refuse_latent_mean_as_observed_mean, refuse_latent_variance_as_observed_variance, refuse_manifest_means_as_observed_mean, @@ -2993,4 +3163,121 @@ mod tests { assert_eq!(recover_manifest_observed_mean(0.0, 1e308, 0.5), Ok(0.5)); assert_eq!(recover_manifest_observed_mean(1e308, 0.0, 0.5), Ok(0.5)); } + + #[test] + fn discrete_latent_mean_recovers_driver_equation_three() { + let drift = -0.5_f64; + let delta = 2.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let recovered = + recover_discrete_latent_mean(initial, drift, intercept, delta, LagClock::EventTime) + .expect("eq3-mean"); + let expected = + (drift * delta).exp() * initial + intercept * ((drift * delta).exp_m1() / drift); + assert!((recovered - expected).abs() < 1e-15); + let increment = recover_discrete_continuous_intercept_effect( + intercept, + drift, + delta, + LagClock::EventTime, + ) + .expect("cint"); + assert!((increment - intercept * ((drift * delta).exp_m1() / drift)).abs() < 1e-15); + assert_eq!( + recover_discrete_latent_mean(0.0, drift, intercept, delta, LagClock::EventTime), + Ok(increment) + ); + assert_eq!( + recover_discrete_latent_mean(initial, drift, 0.0, delta, LagClock::EventTime), + Ok((drift * delta).exp() * initial) + ); + assert_eq!( + recover_discrete_latent_mean(initial, 0.0, intercept, delta, LagClock::EventTime), + Ok(initial + intercept * delta) + ); + assert_eq!( + recover_discrete_continuous_intercept_effect( + intercept, + 0.0, + delta, + LagClock::EventTime + ), + Ok(intercept * delta) + ); + assert_eq!( + recover_discrete_continuous_intercept_effect(0.0, 0.0, delta, LagClock::EventTime), + Ok(0.0) + ); + assert_eq!( + refuse_initial_latent_mean_as_evolved_mean(initial, recovered), + Err(PsychometricError::InitialLatentMeanIsNotEvolvedMean) + ); + assert_eq!( + refuse_continuous_intercept_as_discrete_mean_increment(intercept, increment), + Err(PsychometricError::ContinuousInterceptIsNotDiscreteMeanIncrement) + ); + assert_eq!( + refuse_continuous_intercept_as_initial_latent_mean(intercept, initial), + Err(PsychometricError::ContinuousInterceptIsNotInitialLatentMean) + ); + let equilibrium = + recover_discrete_latent_mean(initial, -1e308, 1.0, 2.0, LagClock::EventTime) + .expect("eq3-equilibrium"); + let equilibrium_expected = -(1.0 / -1e308); + assert!((equilibrium - equilibrium_expected).abs() / 1e-308 < 1e-12); + assert_eq!( + recover_discrete_latent_mean(1e308, 1.0, 0.0, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_latent_mean(0.0, 1e308, 0.0, 2.0, LagClock::EventTime), + Ok(0.0) + ); + } + + #[test] + fn discrete_latent_mean_invalid_inputs_fail_closed() { + assert_eq!( + recover_discrete_latent_mean(f64::NAN, -0.5, 0.3, 2.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_latent_mean(1.0, f64::NAN, 0.3, 2.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_latent_mean(1.0, -0.5, f64::NAN, 2.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_latent_mean(1.0, -0.5, 0.3, 0.0, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_latent_mean(1.0, -0.5, 0.3, 2.0, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_discrete_continuous_intercept_effect(1.0, 1e308, 2.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_latent_mean(1.0, 1e308, 1.0, 2.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_latent_mean(1e308, 0.0, 1e308, 2.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_latent_mean(1e308, 0.0, 1e308, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + let underflow_argument = 1e-308_f64 * 1e-308_f64; + assert_eq!(underflow_argument.to_bits(), 0.0_f64.to_bits()); + let underflow = recover_discrete_latent_mean(2.0, 1e-308, 4.0, 1e-308, LagClock::EventTime) + .expect("a-delta-underflow"); + assert!((underflow - 2.0).abs() < 1e-15); + } } diff --git a/crates/psychometric_core/src/lib.rs b/crates/psychometric_core/src/lib.rs index 4d311fac..74348e10 100644 --- a/crates/psychometric_core/src/lib.rs +++ b/crates/psychometric_core/src/lib.rs @@ -29,7 +29,9 @@ //! `λ² cov(η_t, η_{t-1}) + ψ` and does not include `Θ`; the //! observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, //! not `E(y)`; `CINT` is not `MANIFESTMEANS`; Equation 1 -//! is the SDE), +//! is the SDE), recovers the Driver Eq. 3 expected-value latent +//! mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (`T0MEANS` is not +//! `μ_t`; `CINT` is not the discrete increment), //! and refuses //! latent-mean comparison below strong invariance. @@ -80,12 +82,16 @@ pub use event_time::LaggedWithinResidual; pub use event_time::map_discrete_lag_across_event_intervals; /// Exact scalar discrete effect of a constant event-time predictor. pub use event_time::recover_discrete_constant_predictor_effect; +/// Exact scalar discrete intercept increment `A^{-1}[e^{A Δt} − I] κ`. +pub use event_time::recover_discrete_continuous_intercept_effect; /// Exact scalar forward map `φ = exp(a Δt)`. pub use event_time::recover_discrete_lag_from_log_rate; /// Noiseless scalar discrete lag `later / earlier`. pub use event_time::recover_discrete_lag_one; /// Exact scalar lagged latent covariance `A_Δt cov(η_{t-1})`. pub use event_time::recover_discrete_lagged_latent_covariance; +/// Exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`. +pub use event_time::recover_discrete_latent_mean; /// Exact scalar discrete latent variance `A_Δt P A_Δt⊤ + Q_Δt`. pub use event_time::recover_discrete_latent_variance; /// Exact scalar discrete process noise `Q_Δt` on event time. @@ -116,12 +122,18 @@ pub use event_time::recover_trait_plus_state_lagged_covariance; pub use event_time::recover_trait_plus_state_latent_variance; /// CWC-then-event-time local log-rate (not DSEM; not raw-process AR drift). pub use event_time::recover_within_residual_event_time_log_rate; +/// Refuse treating Driver Table 2 `CINT` as the discrete mean increment. +pub use event_time::refuse_continuous_intercept_as_discrete_mean_increment; +/// Refuse treating Driver Table 2 `CINT` as `T0MEANS`. +pub use event_time::refuse_continuous_intercept_as_initial_latent_mean; /// Refuse treating Driver Table 2 `CINT` as `MANIFESTMEANS`. pub use event_time::refuse_continuous_intercept_as_manifest_means; /// Refuse the difference quotient as a continuous-time rate. pub use event_time::refuse_difference_quotient_as_local_rate; /// Refuse treating finite-interval `Q_Δt` as `asymDIFFUSION`. pub use event_time::refuse_finite_interval_process_noise_as_stationary_variance; +/// Refuse treating Driver Table 2 `T0MEANS` as the evolved latent mean. +pub use event_time::refuse_initial_latent_mean_as_evolved_mean; /// Refuse treating Driver Eq. 3–4 lagged latent covariance as `cov(y_t, y_{t-1})`. pub use event_time::refuse_latent_lagged_covariance_as_observed_covariance; /// Refuse treating Driver Eq. 5 latent mean as `E(y)`. 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 e80a50d7..d7052ff6 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -5,17 +5,21 @@ use psychometric_core::{ ClusteredEventScore, ClusteredScore, EventOccasion, IndicatorKind, LagClock, LaggedWithinResidual, PsychometricError, map_discrete_lag_across_event_intervals, ordinary_least_squares_slope, recover_cluster_mean_within_between_slopes, - recover_discrete_constant_predictor_effect, recover_discrete_lag_from_log_rate, - recover_discrete_lagged_latent_covariance, recover_discrete_latent_variance, - recover_discrete_process_noise, recover_discrete_time_varying_predictor_effect, - recover_event_series_mean_log_rate, recover_event_time_discrete_lag_and_log_rate, - recover_irregular_centered_residual_log_rate, recover_kish_weighted_slope, - recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, - recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, - recover_stationary_latent_variance, recover_trait_plus_state_lagged_covariance, - recover_trait_plus_state_latent_variance, recover_within_residual_event_time_log_rate, + recover_discrete_constant_predictor_effect, recover_discrete_continuous_intercept_effect, + recover_discrete_lag_from_log_rate, recover_discrete_lagged_latent_covariance, + recover_discrete_latent_mean, recover_discrete_latent_variance, recover_discrete_process_noise, + recover_discrete_time_varying_predictor_effect, recover_event_series_mean_log_rate, + recover_event_time_discrete_lag_and_log_rate, recover_irregular_centered_residual_log_rate, + recover_kish_weighted_slope, recover_manifest_lagged_observed_covariance, + recover_manifest_observed_mean, recover_manifest_observed_variance, + recover_manifest_trait_plus_state_observed_variance, recover_stationary_latent_variance, + recover_trait_plus_state_lagged_covariance, recover_trait_plus_state_latent_variance, + recover_within_residual_event_time_log_rate, + refuse_continuous_intercept_as_discrete_mean_increment, + refuse_continuous_intercept_as_initial_latent_mean, refuse_continuous_intercept_as_manifest_means, refuse_difference_quotient_as_local_rate, refuse_finite_interval_process_noise_as_stationary_variance, + refuse_initial_latent_mean_as_evolved_mean, refuse_latent_lagged_covariance_as_observed_covariance, refuse_latent_mean_as_observed_mean, refuse_latent_variance_as_observed_variance, refuse_manifest_means_as_observed_mean, refuse_manifest_trait_variance_as_measurement_error, @@ -999,6 +1003,68 @@ fn manifest_observed_mean_recovers_driver_equation_five() { ); } +#[test] +fn discrete_latent_mean_recovers_driver_equation_three() { + let drift = -0.5_f64; + let delta = 2.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let observed = + recover_discrete_latent_mean(initial, drift, intercept, delta, LagClock::EventTime) + .expect("eq3-mean"); + let expected = (drift * delta).exp() * initial + intercept * ((drift * delta).exp_m1() / drift); + let error = rmse(&[expected], &[observed]); + assert!(error < 1e-15, "Driver Eq. 3 latent mean RMSE {error}"); + let initial_error = rmse(&[expected], &[initial]); + assert!( + initial_error > error, + "T0MEANS is not μ_t: RMSE {initial_error} must exceed {error}" + ); + let intercept_error = rmse(&[expected], &[intercept]); + assert!( + intercept_error > error, + "CINT is not μ_t: RMSE {intercept_error} must exceed {error}" + ); + let increment = + recover_discrete_continuous_intercept_effect(intercept, drift, delta, LagClock::EventTime) + .expect("cint"); + let increment_error = rmse(&[increment], &[intercept]); + assert!( + increment_error > 1e-3, + "CINT is not the discrete increment: RMSE {increment_error}" + ); + assert_eq!( + refuse_initial_latent_mean_as_evolved_mean(initial, observed), + Err(PsychometricError::InitialLatentMeanIsNotEvolvedMean) + ); + assert_eq!( + refuse_continuous_intercept_as_discrete_mean_increment(intercept, increment), + Err(PsychometricError::ContinuousInterceptIsNotDiscreteMeanIncrement) + ); + assert_eq!( + refuse_continuous_intercept_as_initial_latent_mean(intercept, initial), + Err(PsychometricError::ContinuousInterceptIsNotInitialLatentMean) + ); + let integrator = + recover_discrete_latent_mean(initial, 0.0, intercept, delta, LagClock::EventTime) + .expect("a0"); + assert!( + (integrator - (initial + intercept * delta)).abs() < 1e-15, + "Driver Eq. 3 A=0 integral is κ Δt: got {integrator}" + ); + let equilibrium = recover_discrete_latent_mean(initial, -1e308, 1.0, 2.0, LagClock::EventTime) + .expect("eq3-equilibrium"); + let equilibrium_expected = -(1.0 / -1e308); + assert!( + (equilibrium - equilibrium_expected).abs() / 1e-308 < 1e-12, + "Driver Eq. 3 z→-∞ drops T0MEANS and keeps -κ/a: got {equilibrium}" + ); + assert_eq!( + recover_discrete_latent_mean(1e308, 1.0, 0.0, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); +} + #[test] fn admitted_coordinates_still_required_for_multilevel_weights() { assert_eq!( diff --git a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs index b2673c38..f2d80af6 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -4,14 +4,18 @@ use psychometric_core::{ ClusteredEventScore, ClusteredScore, IndicatorKind, LagClock, LaggedWithinResidual, ordinary_least_squares_slope, posterior_draw_point_estimate_mean, recover_cluster_mean_within_between_slopes, recover_discrete_constant_predictor_effect, - recover_discrete_lagged_latent_covariance, recover_discrete_latent_variance, - recover_discrete_process_noise, recover_discrete_time_varying_predictor_effect, - recover_irregular_centered_residual_log_rate, recover_loading_point_estimate_mean, - recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, - recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, - recover_stationary_latent_variance, recover_trait_plus_state_latent_variance, - recover_within_residual_event_time_log_rate, refuse_continuous_intercept_as_manifest_means, + recover_discrete_continuous_intercept_effect, recover_discrete_lagged_latent_covariance, + recover_discrete_latent_mean, recover_discrete_latent_variance, recover_discrete_process_noise, + recover_discrete_time_varying_predictor_effect, recover_irregular_centered_residual_log_rate, + recover_loading_point_estimate_mean, recover_manifest_lagged_observed_covariance, + recover_manifest_observed_mean, recover_manifest_observed_variance, + recover_manifest_trait_plus_state_observed_variance, recover_stationary_latent_variance, + recover_trait_plus_state_latent_variance, recover_within_residual_event_time_log_rate, + refuse_continuous_intercept_as_discrete_mean_increment, + refuse_continuous_intercept_as_initial_latent_mean, + refuse_continuous_intercept_as_manifest_means, refuse_finite_interval_process_noise_as_stationary_variance, + refuse_initial_latent_mean_as_evolved_mean, refuse_latent_lagged_covariance_as_observed_covariance, refuse_latent_mean_as_observed_mean, refuse_latent_variance_as_observed_variance, refuse_manifest_means_as_observed_mean, refuse_manifest_trait_variance_as_measurement_error, @@ -446,3 +450,41 @@ fn manifest_means_and_latent_mean_are_not_observed_mean() { Err(psychometric_core::PsychometricError::ContinuousInterceptIsNotManifestMeans) ); } + +#[test] +fn initial_latent_mean_and_continuous_intercept_are_not_evolved_mean() { + let drift = -0.5_f64; + let delta = 2.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let evolved = + recover_discrete_latent_mean(initial, drift, intercept, delta, LagClock::EventTime) + .expect("eq3-mean"); + let increment = + recover_discrete_continuous_intercept_effect(intercept, drift, delta, LagClock::EventTime) + .expect("cint"); + assert!( + (initial - evolved).abs() > 1e-3, + "Driver et al. (2017, Eq. 3 / Table 2 p. 12): T0MEANS is not μ_t" + ); + assert!( + (intercept - increment).abs() > 1e-3, + "Driver et al. (2017, Eq. 3 / Table 2 p. 12): CINT is not the discrete mean increment" + ); + assert!( + (intercept - initial).abs() > 1e-3, + "Driver et al. (2017, Table 2 p. 12): CINT is not T0MEANS" + ); + assert_eq!( + refuse_initial_latent_mean_as_evolved_mean(initial, evolved), + Err(psychometric_core::PsychometricError::InitialLatentMeanIsNotEvolvedMean) + ); + assert_eq!( + refuse_continuous_intercept_as_discrete_mean_increment(intercept, increment), + Err(psychometric_core::PsychometricError::ContinuousInterceptIsNotDiscreteMeanIncrement) + ); + assert_eq!( + refuse_continuous_intercept_as_initial_latent_mean(intercept, initial), + Err(psychometric_core::PsychometricError::ContinuousInterceptIsNotInitialLatentMean) + ); +} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index a3ad2ec2..bbcfc0da 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -28,7 +28,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | report template/section/copied/style/modality method effects | ADR 0004/0012; PRD/TRD | simulation truth factors implemented; estimator-side method model remains future | partial | | candidate K statistical/Pareto gates + blinded LLM review | ADR 0012; research | future `model_selection` | accepted-target | | compositional topic correlation / stable clustering | ADR 0005/0012; research | future `network_analysis` | accepted-target | -| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR; full ESEM/DSEM remaining | partial | +| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR; full ESEM/DSEM remaining | partial | | CPU bounded multithreading + GPU/VRAM streaming/parity | ADR 0001/0006 | future `compute_backend` | accepted-target | | TDT detection/tracking vs CHRONOS schema/prediction/temporal consistency | ADR 0016; PRD/research | future `event_intelligence` | accepted-target | | evidence-bounded LLM interpretation | ADR 0010/0012; PRD | future `interpretation_gateway` | accepted-target | diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index f758241e..6bf14913 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -1,7 +1,7 @@ # ADR 0005 — Posterior-aware ESEM/DSEM and structural interpretation **Decision status:** Accepted -**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target +**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target **Date:** 2026-08-05 **Supersedes:** None. ADR 0012 governs upstream topic measurement/network coordinates; this ADR governs higher-order psychometric structure and longitudinal interpretation. @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. 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/adr/README.md b/docs/adr/README.md index 1064a8a7..668465fe 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -10,7 +10,7 @@ Read [`ADR_POLICY.md`](ADR_POLICY.md) first. **Decision status and implementatio | [0002](0002-six-clock-temporal-semantics.md) | Six-clock temporal semantics and fail-closed historical leakage prevention | Accepted | active-PR | Unmerged PR #8 is the canonical Task 3 replacement implementing typed clocks/intervals against the current protected-main lineage; conflicted PR #5 is superseded lineage. Later graph/split enforcement remains target work. | | [0003](0003-relational-event-multiple-membership.md) | Relational event ontology and time-varying cross-classified multiple membership | Accepted | partial | Weighted time-varying membership network/roles are active-PR (PR #12); full multilevel estimators, graph ontology, and persistence remain accepted-target. ADR 0016 owns event-intelligence tasks. | | [0004](0004-shared-multilingual-latent-space.md) | One shared multilingual latent space with explicit invariance status | Accepted | accepted-target | ADR 0012 owns the full topic-estimator/backend/global-topic contract. | -| [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | partial | Input gates, posterior-draw point estimates, Rubin `T` on OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects, exact scalar discrete process noise, lagged latent covariance and unconditional latent variance, stationary within-subject variance (`asymDIFFUSION`), trait-plus-state variance (`TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`), irregular already-centered residual lag, and strong-gated latent means are on the stacked psychometric PR; full ESEM/DSEM estimator remains accepted-target. | +| [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | partial | Input gates, posterior-draw point estimates, Rubin `T` on OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects, exact scalar discrete process noise, lagged latent covariance and unconditional latent variance, stationary within-subject variance (`asymDIFFUSION`), trait-plus-state variance (`TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`), irregular already-centered residual lag, and strong-gated latent means are on the stacked psychometric PR; full ESEM/DSEM estimator remains accepted-target. | | [0006](0006-vram-gpu-nvidia-orchestration.md) | VRAM-adaptive GPU compute and model-credential boundary | Accepted | accepted-target | LLM orchestration policy superseded by ADR 0010; autonomous development authority governed by ADR 0015. | | [0007](0007-rust-workspace-quality-gates.md) | Explicit Rust workspace, pinned toolchains, and exact quality gates | Accepted | implemented-main | ADR 0014 governs scientific/product claim promotion beyond repository-quality tooling. | | [0008](0008-immutable-evidence-identities-digests-and-spans.md) | Immutable evidence identities, `SHA-256` digests, exact spans, and strict wire reconstruction | Accepted | implemented-main | ADR 0013 governs future persistence/reproducibility/split authority. | diff --git a/docs/research/multilevel-event-time-recovery.md b/docs/research/multilevel-event-time-recovery.md index 8a28d4a5..fd1ca608 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -20,15 +20,16 @@ This slice stays inside `psychometric_core`. It does not add a second invariance 14. recover the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`) and refuse treating measurement error, latent variance, or manifest-trait variance as `Var(y)` / `Θ`; 15. recover the exact scalar lagged observed-indicator covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5; `Θ` does not enter) and refuse treating lagged latent covariance or `MANIFESTVAR` as `cov(y_t, y_{t-1})`; 16. recover the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`) and refuse treating the intercept, latent mean, or continuous intercept as `E(y)` / `τ`; -17. refuse pooling discrete lags from unequal event intervals as one coefficient; -18. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); -19. refuse the difference quotient as a continuous-time rate; -20. apply the same event-time map to CWC residuals (still not DSEM); -21. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). +17. recover the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; Table 2 `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment) and refuse treating `T0MEANS` or `CINT` as `μ_t`; +18. refuse pooling discrete lags from unequal event intervals as one coefficient; +19. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); +20. refuse the difference quotient as a continuous-time rate; +21. apply the same event-time map to CWC residuals (still not DSEM); +22. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). ## Claim boundary -This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. +This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. ## Authoritative sources @@ -65,6 +66,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - **Observed-indicator variance.** Driver et al. (2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z): \(y_i(t)=\tau_i+\Lambda\eta_i(t)+\varepsilon_i(t)\) with \(\varepsilon\sim N(0,\Theta)\) and \(\tau_i\sim N(\mu_{\tau},\Psi_{\tau})\). Equation 1 (p. 4) is the latent SDE. Table 2 names \(\Theta\) `MANIFESTVAR` and \(\Psi_{\tau}\) `MANIFESTTRAITVAR`; p. 16 restates those names. The scalar map is \(\operatorname{Var}(y)=\lambda^{2}\operatorname{Var}(\eta)+\theta\) when \(\Psi_{\tau}=0\) and \(\lambda^{2}\operatorname{Var}(\eta)+\theta+\psi\) otherwise. Form \((\lambda p)\lambda\) then add \(\theta\), then add \(\psi\). Forming \(\lambda^{2}\) first overflows at \(\lambda=10^{308}\), \(p=10^{-308}\). A zero loading or zero latent variance is exactly \(\theta\) (\(\Psi_{\tau}=0\)) or \(\theta+\psi\). A zero measurement error is exactly \(\lambda^{2}p+\psi\). A zero manifest trait is exactly \(\lambda^{2}p+\theta\). `MANIFESTVAR` is not \(\operatorname{Var}(y)\). `MANIFESTTRAITVAR` is not `MANIFESTVAR`. `TRAITVAR` is latent additional variance and is scaled by \(\lambda^{2}\); `MANIFESTTRAITVAR` is not. \(\operatorname{Var}(\eta)\) is not \(\operatorname{Var}(y)\). An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **Lagged observed-indicator covariance.** Driver et al. (2017, Eq. 5 with Eq. 3–4; JSS PDF re-opened 2026-08-19T04:18Z): independent \(\varepsilon_t\) does not enter \(\operatorname{cov}(y_t,y_{t-1})\). The scalar map is \(\lambda^{2}\operatorname{cov}(\eta_t,\eta_{t-1})+\psi\). Form \((\lambda c)\lambda\) then add \(\psi\). A zero loading or zero latent lagged covariance is exactly \(\psi\). A zero manifest trait is exactly \(\lambda^{2}c\). `MANIFESTVAR` is not lagged observed covariance. Lagged latent covariance is not lagged observed covariance. An overflowing product or sum fails closed. - **Observed-indicator mean.** Driver et al. (2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z): \(y_i(t)=\Gamma+\Lambda\eta_i(t)+\zeta_i(t)\) with \(\zeta\sim N(0,\Theta)\) and \(\Gamma\sim N(\tau,\Psi)\). Table 2 names \(\tau\) `MANIFESTMEANS`, \(\kappa\) `CINT`, and the first-occasion latent mean `T0MEANS`. The scalar map is \(E(y)=\tau+\lambda\mu\). Form \(\lambda\mu\) then add \(\tau\). A zero loading or zero latent mean is exactly \(\tau\). A zero intercept is exactly \(\lambda\mu\). `MANIFESTMEANS` is not \(E(y)\). \(E(\eta)\) is not \(E(y)\). `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not \(E(y)\). An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. +- **Discrete latent mean.** Driver et al. (2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z): \(\eta(t)=\exp(A\Delta t)\eta(t_0)+\int\exp(A(t-s))(b+\cdots)\,ds\) plus a stochastic integral of mean zero. Table 2 names the first-occasion latent mean `T0MEANS` and \(\kappa\) `CINT`. The scalar map is \(\mu_t=\exp(a\Delta t)\mu_0+(\exp(a\Delta t)-1)/a\,\kappa\). Form the `CINT` increment first, then add the carried `T0MEANS` term. A zero drift is the Eq. 3 integral \(\kappa\Delta t\) (\(A=0\) has no inverse). A zero intercept is exactly \(\exp(a\Delta t)\mu_0\). A zero initial mean is exactly the increment. As \(\Delta t\to\infty\) with stable \(a<0\), \(\mu_t\to-\kappa/a\). Binary64 underflow of \(\exp(a\Delta t)\) to `+0` drops the carried `T0MEANS` and keeps that equilibrium increment. `T0MEANS` is not \(\mu_t\). `CINT` is not the discrete increment. `CINT` is not `T0MEANS`. An overflowing exponential, product, or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **CWC-then-lag.** Sample cluster means are removed first. Consecutive within residuals are then fitted by least squares to \(r_{t+\Delta t}\approx\exp(a\Delta t)\,r_{t}\) on event time. Same-sign pair-wise logs initialize the scalar Newton step. Sign-flipping \(T=2\) CWC pairs have no real logarithm and fail closed. Curran and Bauer (2011, pp. 607–608) show that this person-mean subtraction on a raw autoregressive series does **not** isolate the lagged within-person effect; the helper therefore does not claim to recover the raw-process drift. - **Already-centered irregular residual.** The caller supplies lagged within residuals. The mean of \(a=\ln(r_{t+\Delta t}/r_t)/\Delta t\) is the exact scalar map. Intervals may be irregular. The helper does not center again. This is not DSEM. @@ -85,6 +87,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - Driver et al. (2017, Eq. 5, p. 5; Table 2, p. 12) recovers a known observed-indicator variance \(\lambda^{2}\operatorname{Var}(\eta)+\theta\) at machine-scale RMSE when `MANIFESTTRAITVAR` is zero, and \(\lambda^{2}\operatorname{Var}(\eta)+\theta+\psi\) otherwise; those RMSEs are smaller than treating `MANIFESTVAR`, \(\operatorname{Var}(\eta)\), or `MANIFESTTRAITVAR` as \(\operatorname{Var}(y)\) / \(\Theta\); a zero loading or zero latent variance is \(\theta\) or \(\theta+\psi\); `TRAITVAR` is latent and scaled by \(\lambda^{2}\); forming \(\lambda^{2}\) first overflows while \((\lambda p)\lambda\) stays finite; an overflowing product or sum fails closed; - Driver et al. (2017, Eq. 5 lagged covariance) recovers a known \(\lambda^{2}\operatorname{cov}(\eta_t,\eta_{t-1})+\psi\) at machine-scale RMSE, and that RMSE is smaller than treating lagged latent covariance or `MANIFESTVAR` as \(\operatorname{cov}(y_t,y_{t-1})\); forming \(\lambda^{2}\) first overflows while \((\lambda c)\lambda\) stays finite; an overflowing product or sum fails closed; - Driver et al. (2017, Eq. 5 observed mean; Table 2, p. 12) recovers a known \(E(y)=\tau+\lambda\mu\) at machine-scale RMSE, and that RMSE is smaller than treating `MANIFESTMEANS` or \(E(\eta)\) as \(E(y)\); a zero loading or zero latent mean is \(\tau\); `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not \(E(y)\); an overflowing product or sum fails closed; +- Driver et al. (2017, Eq. 3 expected-value latent mean; Table 2, p. 12) recovers a known \(\mu_t=\exp(a\Delta t)\mu_0+(\exp(a\Delta t)-1)/a\,\kappa\) at machine-scale RMSE, and that RMSE is smaller than treating `T0MEANS` or `CINT` as \(\mu_t\); a zero drift is \(\mu_0+\kappa\Delta t\); underflow of \(\exp(a\Delta t)\) to `+0` drops `T0MEANS` and keeps \(-\kappa/a\); `CINT` is not the discrete increment; an overflowing exponential, product, or sum fails closed; - pooling discrete lags from unequal intervals fails closed; - CWC-then-lag on a two-cluster decaying series has smaller computed RMSE than a level-pooled series when the latter is identified; - already-centered irregular residuals recover a known drift at machine-scale RMSE, and that RMSE is smaller than CWC of the corresponding raw autoregressive series (Curran & Bauer, 2011, pp. 607–608); diff --git a/docs/validation/temporal-event-foundation.md b/docs/validation/temporal-event-foundation.md index e83f60bf..8db76a1f 100644 --- a/docs/validation/temporal-event-foundation.md +++ b/docs/validation/temporal-event-foundation.md @@ -23,7 +23,7 @@ This report tracks exact-head scientific and engineering evidence required befor | Truth corpora / manifests | `tepp_simulation` | implemented-main | — | deterministic generator tests | Task 10 / PR #18 | | Recovery metrics | `validation_core` | implemented-main | — | RMSE/bias/coverage/MC gates | Task 11 / PR #19 | | Versioned API/export contracts | `tepp_api` | implemented-main | naruon HTTP interchange | unknown-field/version/limit + naruon HTTPS interchange tests | Task 12 / PR #21; live HTTP service remaining | -| Psychometric structural input gates | `psychometric_core` | partial | stacked psychometric PR | construct-class refusal + ALR/ILR boundary + true-loading RMSE + posterior-draw point-estimate mean + Rubin `T` + CWC within/between + CWC contextual effect + event-time log-rate + constant- and time-varying-predictor discrete effects + exact scalar discrete process noise + lagged latent covariance and unconditional latent variance + stationary within-subject variance + trait-plus-state variance + observed-indicator variance + irregular already-centered residual lag + strong-gated latent means; full ESEM/DSEM remaining | ADR 0005; `docs/research/posterior-esem-input-gates.md`; `docs/research/multilevel-event-time-recovery.md`; `docs/research/rubin-total-variance.md`; `docs/research/strong-invariance-latent-means.md` | +| Psychometric structural input gates | `psychometric_core` | partial | stacked psychometric PR | construct-class refusal + ALR/ILR boundary + true-loading RMSE + posterior-draw point-estimate mean + Rubin `T` + CWC within/between + CWC contextual effect + event-time log-rate + constant- and time-varying-predictor discrete effects + exact scalar discrete process noise + lagged latent covariance and unconditional latent variance + stationary within-subject variance + trait-plus-state variance + observed-indicator variance + discrete latent mean (`T0MEANS`/`CINT`) + irregular already-centered residual lag + strong-gated latent means; full ESEM/DSEM remaining | ADR 0005; `docs/research/posterior-esem-input-gates.md`; `docs/research/multilevel-event-time-recovery.md`; `docs/research/rubin-total-variance.md`; `docs/research/strong-invariance-latent-means.md` | | CWL modular connectors | `docs/connectors/*` | implemented-main | — | contract docs + examples | PR #22; live HTTP ports remaining | | Release SBOM/provenance generator | `scripts/release_evidence.py` | partial | — | generate+validate in CI | Task 13 partial / PR #28 | From 7f29e390f7acc7c9260dd75026083b3c0fa83cd6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 22:14:46 +0000 Subject: [PATCH 32/62] feat(psychometric): recover Driver Eq. 5 of the Eq. 3 evolved mean MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compose E(y_t) = τ + λ μ_t from the re-opened Driver, Oud, and Voelkle (2017) JSS PDF (Eq. 3, p. 5; Eq. 5, p. 5; Table 2, p. 12). Form μ_t first. τ + λ μ_0 is not E(y_t). Close the zero-CINT exp(a Δt) overflow on the carried T0MEANS term. --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 1 + CLAUDE.md | 2 +- crates/psychometric_core/src/error.rs | 10 + crates/psychometric_core/src/event_time.rs | 227 +++++++++++++++++- crates/psychometric_core/src/lib.rs | 8 +- ...multilevel_event_time_recovery_contract.rs | 155 +++++++++++- .../scientific_claim_boundary_contract.rs | 63 ++++- docs/TRACEABILITY.md | 2 +- docs/adr/0005-posterior-esem-dsem.md | 4 +- docs/adr/README.md | 2 +- .../multilevel-event-time-recovery.md | 15 +- docs/research/posterior-esem-input-gates.md | 2 +- docs/validation/temporal-event-foundation.md | 2 +- 14 files changed, 459 insertions(+), 36 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 63b135ab..e0ffa94c 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -61,7 +61,7 @@ boundaries above remain the target modular MSA architecture. | `tepp_simulation` | known-truth temporal/event data generation | | `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics | | `tepp_api` | versioned DTO, schema, and export contracts | -| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means | +| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means | No crate exposes placeholder production behavior in Task 1. This prevents an empty façade from becoming a de facto public API before its invariants and tests diff --git a/CHANGELOG.md b/CHANGELOG.md index c812b434..09c92b01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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, Eq. 3, p. 5; Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T22:10Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar evolved observed-indicator mean. Equation 3 writes `η_i(t) = exp(A Δt) η_i(t0) + A^{-1}[exp(A Δt) − I] ξ_i + …` with `ξ_i ~ N(κ, φ_ξ)` (p. 4) and a stochastic integral of mean zero. Equation 5 writes `y_i(t) = Γ_i + Λ η_i(t) + ζ_i(t)` with `Γ ~ N(τ, Ψ)` and `ζ ~ N(0, Θ)`. The scalar composition is `E(y_t) = τ + λ μ_t` with `μ_t` the Eq. 3 expected-value map. Form `μ_t` first, then `τ + λ μ_t`. The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `MANIFESTMEANS` is not `E(y_t)`. `T0MEANS` is not `E(y_t)`. `μ_t` is not `E(y_t)`. A zero-CINT overflow of `exp(a Δt)` fails the carried `T0MEANS` term closed (`a = 710`, `Δt = 1`). Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) remains unread (OpenAlex 2026-08-19T22:10Z: closed; Springer `content/pdf` is HTML 200). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (OpenAlex/Semantic Scholar 2026-08-19T22:10Z: closed). Putnick and Bornstein (2016) PMC PDF was HTML/500 on this cycle. Oud and Jansen (2000) remains unread. ZORA Anubis-blocked. Asparouhov and Muthén (2009) statmodel PDF re-opened 2026-08-19T22:10Z; it cites Meredith (1993) and discusses multiple-group intercept/mean structures but does not license this map. - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar expected-value latent mean. Equation 3 writes `η(t) = exp(A Δt) η(t0) + ∫ exp(A(t−s)) (b + …) ds` plus a stochastic integral of mean zero. Table 2 names the first-occasion latent mean `T0MEANS` and `κ` `CINT`. The scalar map is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`. Form the CINT increment first, then add the carried `T0MEANS` term. A zero drift is the Eq. 3 integral `κ Δt` (`A = 0` has no inverse). As `Δt → ∞` with stable `a < 0`, `μ_t → −κ / a`. Binary64 underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps that equilibrium increment. `T0MEANS` is not `μ_t`. `CINT` is not the discrete increment. `CINT` is not `T0MEANS`. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (OpenAlex 2026-08-19T18:10Z: closed; Springer `content/pdf` is HTML 200). Oud and Jansen (2000) remains unread. ZORA Anubis-blocked. - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar observed-indicator mean. Equation 5 writes `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and `Γ ~ N(τ, Ψ)`. Table 2 names `τ` `MANIFESTMEANS`, `κ` `CINT`, and the first-occasion latent mean `T0MEANS`. The scalar map is `E(y) = τ + λ μ`. Form `λ μ` then add `τ`. Do not treat `MANIFESTMEANS` as `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. Forming `λ²` is the variance path, not this mean. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-19T14:08Z: closed). Oud and Jansen (2000) remains unread. ZORA Anubis-blocked. - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Eq. 3–4, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z) scalar lagged observed-indicator covariance. Independent measurement error does not enter `cov(y_t, y_{t-1})`. The scalar map is `λ² cov(η_t, η_{t-1}) + ψ`. Form `(λ c) λ` then add `ψ`. Do not form `λ²` first (`λ = 1e308`, `c = 1e-308` → `1e308`). `MANIFESTVAR` is not lagged observed covariance. Lagged `Var(η)` path is not `cov(y)`. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-19T04:25Z: closed). diff --git a/CLAUDE.md b/CLAUDE.md index 335cf896..caf671a4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. -- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. diff --git a/crates/psychometric_core/src/error.rs b/crates/psychometric_core/src/error.rs index 5e48313e..20822ebd 100644 --- a/crates/psychometric_core/src/error.rs +++ b/crates/psychometric_core/src/error.rs @@ -98,6 +98,9 @@ pub enum PsychometricError { /// Driver Table 2 `CINT` was treated as `T0MEANS`. `κ` is not /// the first-occasion latent mean. ContinuousInterceptIsNotInitialLatentMean, + /// Driver Eq. 5 of the first-occasion mean was treated as + /// `E(y_t)`. `τ + λ μ_0` is not `τ + λ μ_t`. + InitialObservedMeanIsNotEvolvedObservedMean, } impl fmt::Display for PsychometricError { @@ -181,6 +184,9 @@ impl fmt::Display for PsychometricError { Self::ContinuousInterceptIsNotInitialLatentMean => { "continuous intercept is not the initial latent mean" } + Self::InitialObservedMeanIsNotEvolvedObservedMean => { + "first-occasion observed mean is not the evolved observed mean" + } }; formatter.write_str(message) } @@ -326,5 +332,9 @@ mod tests { PsychometricError::ContinuousInterceptIsNotInitialLatentMean.to_string(), "continuous intercept is not the initial latent mean" ); + assert_eq!( + PsychometricError::InitialObservedMeanIsNotEvolvedObservedMean.to_string(), + "first-occasion observed mean is not the evolved observed mean" + ); } } diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index e5244c0b..6e1be3b2 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -1227,6 +1227,67 @@ pub fn recover_discrete_latent_mean( require_finite(carried + intercept_effect) } +/// Exact scalar discrete observed-indicator mean from Driver +/// Equations 3 and 5. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 3, p. 5; Eq. 5, p. 5; +/// Table 2, p. 12; JSS PDF re-opened 2026-08-19T22:10Z) write +/// `η_i(t) = exp(A Δt) η_i(t0) + A^{-1}[exp(A Δt) − I] ξ_i + …` +/// with `ξ_i ~ N(κ, φ_ξ)` (p. 4) and a stochastic integral of +/// mean zero, then `y_i(t) = Γ_i + Λ η_i(t) + ζ_i(t)` with +/// `Γ ~ N(τ, Ψ)` and `ζ ~ N(0, Θ)`. The scalar expected-value +/// composition is `E(y_t) = τ + λ μ_t` with +/// `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`. Form `μ_t` +/// first, then `τ + λ μ_t`. Table 2 names `μ_0` `T0MEANS`, `κ` +/// `CINT`, and `τ` `MANIFESTMEANS`. A zero loading is exactly +/// `τ`. A zero evolved latent mean is exactly `τ`. A zero +/// intercept is exactly `λ μ_t`. The first-occasion map +/// `τ + λ μ_0` is not `E(y_t)`. `MANIFESTMEANS` is not +/// `E(y_t)`. `T0MEANS` is not `E(y_t)`. `μ_t` is not `E(y_t)`. +/// `CINT` is not `E(y_t)`. This is not a Kalman filter and not +/// ctsem estimation. +/// +/// # Errors +/// +/// Propagates [`recover_discrete_latent_mean`] and +/// [`recover_manifest_observed_mean`]. +pub fn recover_discrete_observed_mean( + loading: f64, + initial_latent_mean: f64, + log_rate: f64, + continuous_intercept: f64, + manifest_mean: f64, + event_delta: f64, + clock: LagClock, +) -> Result { + let evolved_latent_mean = recover_discrete_latent_mean( + initial_latent_mean, + log_rate, + continuous_intercept, + event_delta, + clock, + )?; + recover_manifest_observed_mean(loading, evolved_latent_mean, manifest_mean) +} + +/// Refuse treating the first-occasion observed mean as `E(y_t)`. +/// +/// Equation 5 of `T0MEANS` is `τ + λ μ_0`. Equation 5 of the +/// Eq. 3 evolved mean is `τ + λ μ_t`. Those are not the same +/// map. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::InitialObservedMeanIsNotEvolvedObservedMean`]. +pub fn refuse_initial_observed_mean_as_evolved_observed_mean( + initial_observed_mean: f64, + evolved_observed_mean: f64, +) -> Result { + let _ = (initial_observed_mean, evolved_observed_mean); + Err(PsychometricError::InitialObservedMeanIsNotEvolvedObservedMean) +} + /// Refuse treating Driver Table 2 `T0MEANS` as the evolved latent mean. /// /// Equation 3 maps `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`. @@ -1530,18 +1591,20 @@ mod tests { recover_discrete_continuous_intercept_effect, recover_discrete_lag_from_log_rate, recover_discrete_lag_one, recover_discrete_lagged_latent_covariance, recover_discrete_latent_mean, recover_discrete_latent_variance, - recover_discrete_process_noise, recover_discrete_time_varying_predictor_effect, - recover_event_series_mean_log_rate, recover_event_time_discrete_lag_and_log_rate, - recover_irregular_centered_residual_log_rate, recover_local_log_rate, - recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, - recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, - recover_stationary_latent_variance, recover_trait_plus_state_lagged_covariance, - recover_trait_plus_state_latent_variance, recover_within_residual_event_time_log_rate, + recover_discrete_observed_mean, recover_discrete_process_noise, + recover_discrete_time_varying_predictor_effect, recover_event_series_mean_log_rate, + recover_event_time_discrete_lag_and_log_rate, recover_irregular_centered_residual_log_rate, + recover_local_log_rate, recover_manifest_lagged_observed_covariance, + recover_manifest_observed_mean, recover_manifest_observed_variance, + recover_manifest_trait_plus_state_observed_variance, recover_stationary_latent_variance, + recover_trait_plus_state_lagged_covariance, recover_trait_plus_state_latent_variance, + recover_within_residual_event_time_log_rate, refuse_continuous_intercept_as_discrete_mean_increment, refuse_continuous_intercept_as_initial_latent_mean, refuse_continuous_intercept_as_manifest_means, refuse_difference_quotient_as_local_rate, refuse_finite_interval_process_noise_as_stationary_variance, refuse_initial_latent_mean_as_evolved_mean, + refuse_initial_observed_mean_as_evolved_observed_mean, refuse_latent_lagged_covariance_as_observed_covariance, refuse_latent_mean_as_observed_mean, refuse_latent_variance_as_observed_variance, refuse_manifest_means_as_observed_mean, @@ -3234,6 +3297,13 @@ mod tests { recover_discrete_latent_mean(0.0, 1e308, 0.0, 2.0, LagClock::EventTime), Ok(0.0) ); + // CINT = 0 so the increment path stays finite; exp(a Δt) then + // overflows and the carried T0MEANS term fails closed. + assert_eq!( + recover_discrete_latent_mean(1.0, 710.0, 0.0, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert!(!(710.0_f64.exp()).is_finite()); } #[test] @@ -3280,4 +3350,147 @@ mod tests { .expect("a-delta-underflow"); assert!((underflow - 2.0).abs() < 1e-15); } + + #[test] + fn discrete_observed_mean_recovers_driver_equations_three_and_five() { + let loading = 2.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let recovered = recover_discrete_observed_mean( + loading, + initial, + drift, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq3-eq5-mean"); + let evolved = + recover_discrete_latent_mean(initial, drift, intercept, delta, LagClock::EventTime) + .expect("mu-t"); + let expected = manifest_mean + loading * evolved; + assert!((recovered - expected).abs() < 1e-15); + let first_occasion = + recover_manifest_observed_mean(loading, initial, manifest_mean).expect("t0"); + assert!((first_occasion - recovered).abs() > 1e-3); + assert_eq!( + recover_discrete_observed_mean( + 0.0, + initial, + drift, + intercept, + manifest_mean, + delta, + LagClock::EventTime + ), + Ok(manifest_mean) + ); + assert_eq!( + recover_discrete_observed_mean( + loading, + initial, + drift, + intercept, + 0.0, + delta, + LagClock::EventTime + ), + Ok(loading * evolved) + ); + let zero_evolved = recover_discrete_observed_mean( + loading, + 0.0, + 0.0, + 0.0, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("zero-mu"); + assert!((zero_evolved - manifest_mean).abs() < 1e-15); + let integrator = recover_discrete_observed_mean( + loading, + initial, + 0.0, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("a0"); + assert!( + (integrator - (manifest_mean + loading * (initial + intercept * delta))).abs() < 1e-15 + ); + let equilibrium = recover_discrete_observed_mean( + loading, + initial, + -1e308, + 1.0, + manifest_mean, + 2.0, + LagClock::EventTime, + ) + .expect("eq3-eq5-equilibrium"); + let equilibrium_latent = -(1.0 / -1e308); + assert!((equilibrium - (manifest_mean + loading * equilibrium_latent)).abs() < 1e-15); + } + + #[test] + fn discrete_observed_mean_refuses_first_occasion_and_overflow() { + let loading = 2.0_f64; + let recovered = + recover_discrete_observed_mean(loading, 1.0, -0.5, 0.3, 0.5, 2.0, LagClock::EventTime) + .expect("eq3-eq5-mean"); + let evolved = + recover_discrete_latent_mean(1.0, -0.5, 0.3, 2.0, LagClock::EventTime).expect("mu-t"); + let first_occasion = recover_manifest_observed_mean(loading, 1.0, 0.5).expect("t0"); + assert_eq!( + refuse_initial_observed_mean_as_evolved_observed_mean(first_occasion, recovered), + Err(PsychometricError::InitialObservedMeanIsNotEvolvedObservedMean) + ); + assert_eq!( + refuse_latent_mean_as_observed_mean(evolved, recovered), + Err(PsychometricError::LatentMeanIsNotObservedMean) + ); + assert_eq!( + refuse_manifest_means_as_observed_mean(0.5, recovered), + Err(PsychometricError::ManifestMeansIsNotObservedMean) + ); + let scaled = + recover_discrete_observed_mean(1e308, 1e-308, 0.0, 0.0, 0.0, 1.0, LagClock::EventTime) + .expect("scale"); + assert!((scaled - 1.0).abs() < 1e-15); + let finite_loaded = + recover_discrete_observed_mean(1e308, 1.0, 0.0, 0.0, 0.0, 1.0, LagClock::EventTime) + .expect("lambda-mu"); + assert!((finite_loaded - 1e308).abs() / 1e308 < 1e-15); + } + + #[test] + fn discrete_observed_mean_invalid_inputs_fail_closed() { + assert_eq!( + recover_discrete_observed_mean(f64::NAN, 1.0, -0.5, 0.3, 0.5, 2.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_observed_mean(2.0, 1.0, -0.5, 0.3, 0.5, 0.0, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_observed_mean(2.0, 1.0, -0.5, 0.3, 0.5, 2.0, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_discrete_observed_mean(1e308, 2.0, 0.0, 0.0, 0.0, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_observed_mean(1.0, 1.0, 710.0, 0.0, 0.5, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + } } diff --git a/crates/psychometric_core/src/lib.rs b/crates/psychometric_core/src/lib.rs index 74348e10..b0fb5597 100644 --- a/crates/psychometric_core/src/lib.rs +++ b/crates/psychometric_core/src/lib.rs @@ -31,7 +31,9 @@ //! not `E(y)`; `CINT` is not `MANIFESTMEANS`; Equation 1 //! is the SDE), recovers the Driver Eq. 3 expected-value latent //! mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (`T0MEANS` is not -//! `μ_t`; `CINT` is not the discrete increment), +//! `μ_t`; `CINT` is not the discrete increment), recovers the +//! Driver Eq. 5 of that evolved mean as `τ + λ μ_t` (the +//! first-occasion map `τ + λ μ_0` is not `E(y_t)`), //! and refuses //! latent-mean comparison below strong invariance. @@ -94,6 +96,8 @@ pub use event_time::recover_discrete_lagged_latent_covariance; pub use event_time::recover_discrete_latent_mean; /// Exact scalar discrete latent variance `A_Δt P A_Δt⊤ + Q_Δt`. pub use event_time::recover_discrete_latent_variance; +/// Exact scalar discrete observed mean `τ + λ μ_t` from Eq. 3 then Eq. 5. +pub use event_time::recover_discrete_observed_mean; /// Exact scalar discrete process noise `Q_Δt` on event time. pub use event_time::recover_discrete_process_noise; /// First-order discrete effect of a time-varying event-time predictor. @@ -134,6 +138,8 @@ pub use event_time::refuse_difference_quotient_as_local_rate; pub use event_time::refuse_finite_interval_process_noise_as_stationary_variance; /// Refuse treating Driver Table 2 `T0MEANS` as the evolved latent mean. pub use event_time::refuse_initial_latent_mean_as_evolved_mean; +/// Refuse treating first-occasion `τ + λ μ_0` as `E(y_t)`. +pub use event_time::refuse_initial_observed_mean_as_evolved_observed_mean; /// Refuse treating Driver Eq. 3–4 lagged latent covariance as `cov(y_t, y_{t-1})`. pub use event_time::refuse_latent_lagged_covariance_as_observed_covariance; /// Refuse treating Driver Eq. 5 latent mean as `E(y)`. 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 d7052ff6..451c515c 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -7,19 +7,20 @@ use psychometric_core::{ ordinary_least_squares_slope, recover_cluster_mean_within_between_slopes, recover_discrete_constant_predictor_effect, recover_discrete_continuous_intercept_effect, recover_discrete_lag_from_log_rate, recover_discrete_lagged_latent_covariance, - recover_discrete_latent_mean, recover_discrete_latent_variance, recover_discrete_process_noise, - recover_discrete_time_varying_predictor_effect, recover_event_series_mean_log_rate, - recover_event_time_discrete_lag_and_log_rate, recover_irregular_centered_residual_log_rate, - recover_kish_weighted_slope, recover_manifest_lagged_observed_covariance, - recover_manifest_observed_mean, recover_manifest_observed_variance, - recover_manifest_trait_plus_state_observed_variance, recover_stationary_latent_variance, - recover_trait_plus_state_lagged_covariance, recover_trait_plus_state_latent_variance, - recover_within_residual_event_time_log_rate, + recover_discrete_latent_mean, recover_discrete_latent_variance, recover_discrete_observed_mean, + recover_discrete_process_noise, recover_discrete_time_varying_predictor_effect, + recover_event_series_mean_log_rate, recover_event_time_discrete_lag_and_log_rate, + recover_irregular_centered_residual_log_rate, recover_kish_weighted_slope, + recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, + recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, + recover_stationary_latent_variance, recover_trait_plus_state_lagged_covariance, + recover_trait_plus_state_latent_variance, recover_within_residual_event_time_log_rate, refuse_continuous_intercept_as_discrete_mean_increment, refuse_continuous_intercept_as_initial_latent_mean, refuse_continuous_intercept_as_manifest_means, refuse_difference_quotient_as_local_rate, refuse_finite_interval_process_noise_as_stationary_variance, refuse_initial_latent_mean_as_evolved_mean, + refuse_initial_observed_mean_as_evolved_observed_mean, refuse_latent_lagged_covariance_as_observed_covariance, refuse_latent_mean_as_observed_mean, refuse_latent_variance_as_observed_variance, refuse_manifest_means_as_observed_mean, refuse_manifest_trait_variance_as_measurement_error, @@ -1065,6 +1066,144 @@ fn discrete_latent_mean_recovers_driver_equation_three() { ); } +#[test] +fn discrete_observed_mean_recovers_driver_equations_three_and_five() { + let loading = 2.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let observed = recover_discrete_observed_mean( + loading, + initial, + drift, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq3-eq5-mean"); + let evolved = + recover_discrete_latent_mean(initial, drift, intercept, delta, LagClock::EventTime) + .expect("mu-t"); + let expected = manifest_mean + loading * evolved; + let error = rmse(&[expected], &[observed]); + assert!( + error < 1e-15, + "Driver Eq. 5 of Eq. 3 evolved mean RMSE {error}" + ); + let first_occasion = + recover_manifest_observed_mean(loading, initial, manifest_mean).expect("t0"); + let first_error = rmse(&[expected], &[first_occasion]); + assert!( + first_error > error, + "τ + λ μ_0 is not E(y_t): RMSE {first_error} must exceed {error}" + ); + let intercept_error = rmse(&[expected], &[manifest_mean]); + assert!( + intercept_error > error, + "MANIFESTMEANS is not E(y_t): RMSE {intercept_error} must exceed {error}" + ); + let latent_error = rmse(&[expected], &[evolved]); + assert!( + latent_error > error, + "μ_t is not E(y_t): RMSE {latent_error} must exceed {error}" + ); + let zero_loading = recover_discrete_observed_mean( + 0.0, + initial, + drift, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("lambda0"); + let dropped_error = rmse(&[expected], &[zero_loading]); + assert!( + dropped_error > error, + "zero loading is τ, not τ + λ μ_t: RMSE {dropped_error} must exceed {error}" + ); +} + +#[test] +fn discrete_observed_mean_refuses_first_occasion_and_overflow() { + let loading = 2.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let observed = recover_discrete_observed_mean( + loading, + initial, + drift, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq3-eq5-mean"); + let evolved = + recover_discrete_latent_mean(initial, drift, intercept, delta, LagClock::EventTime) + .expect("mu-t"); + let first_occasion = + recover_manifest_observed_mean(loading, initial, manifest_mean).expect("t0"); + assert_eq!( + refuse_initial_observed_mean_as_evolved_observed_mean(first_occasion, observed), + Err(PsychometricError::InitialObservedMeanIsNotEvolvedObservedMean) + ); + assert_eq!( + refuse_latent_mean_as_observed_mean(evolved, observed), + Err(PsychometricError::LatentMeanIsNotObservedMean) + ); + assert_eq!( + refuse_manifest_means_as_observed_mean(manifest_mean, observed), + Err(PsychometricError::ManifestMeansIsNotObservedMean) + ); + let integrator = recover_discrete_observed_mean( + loading, + initial, + 0.0, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("a0"); + assert!( + (integrator - (manifest_mean + loading * (initial + intercept * delta))).abs() < 1e-15, + "Driver Eq. 5 of A=0 mean is τ + λ(μ_0 + κ Δt): got {integrator}" + ); + let equilibrium = recover_discrete_observed_mean( + loading, + initial, + -1e308, + 1.0, + manifest_mean, + 2.0, + LagClock::EventTime, + ) + .expect("eq3-eq5-equilibrium"); + let equilibrium_expected = manifest_mean + loading * (-(1.0 / -1e308)); + assert!( + (equilibrium - equilibrium_expected).abs() < 1e-15, + "Driver Eq. 5 of z→-∞ mean keeps τ + λ(-κ/a): got {equilibrium}" + ); + let scaled = + recover_discrete_observed_mean(1e308, 1e-308, 0.0, 0.0, 0.0, 1.0, LagClock::EventTime) + .expect("scale"); + assert!( + (scaled - 1.0).abs() < 1e-15, + "Driver Eq. 5 of Eq. 3 mean must keep λ=1e308, μ_t=1e-308: got {scaled}" + ); + assert_eq!( + recover_discrete_observed_mean(1e308, 2.0, 0.0, 0.0, 0.0, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); +} + #[test] fn admitted_coordinates_still_required_for_multilevel_weights() { assert_eq!( diff --git a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs index f2d80af6..51061533 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -5,17 +5,19 @@ use psychometric_core::{ ordinary_least_squares_slope, posterior_draw_point_estimate_mean, recover_cluster_mean_within_between_slopes, recover_discrete_constant_predictor_effect, recover_discrete_continuous_intercept_effect, recover_discrete_lagged_latent_covariance, - recover_discrete_latent_mean, recover_discrete_latent_variance, recover_discrete_process_noise, - recover_discrete_time_varying_predictor_effect, recover_irregular_centered_residual_log_rate, - recover_loading_point_estimate_mean, recover_manifest_lagged_observed_covariance, - recover_manifest_observed_mean, recover_manifest_observed_variance, - recover_manifest_trait_plus_state_observed_variance, recover_stationary_latent_variance, - recover_trait_plus_state_latent_variance, recover_within_residual_event_time_log_rate, + recover_discrete_latent_mean, recover_discrete_latent_variance, recover_discrete_observed_mean, + recover_discrete_process_noise, recover_discrete_time_varying_predictor_effect, + recover_irregular_centered_residual_log_rate, recover_loading_point_estimate_mean, + recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, + recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, + recover_stationary_latent_variance, recover_trait_plus_state_latent_variance, + recover_within_residual_event_time_log_rate, refuse_continuous_intercept_as_discrete_mean_increment, refuse_continuous_intercept_as_initial_latent_mean, refuse_continuous_intercept_as_manifest_means, refuse_finite_interval_process_noise_as_stationary_variance, refuse_initial_latent_mean_as_evolved_mean, + refuse_initial_observed_mean_as_evolved_observed_mean, refuse_latent_lagged_covariance_as_observed_covariance, refuse_latent_mean_as_observed_mean, refuse_latent_variance_as_observed_variance, refuse_manifest_means_as_observed_mean, refuse_manifest_trait_variance_as_measurement_error, @@ -488,3 +490,52 @@ fn initial_latent_mean_and_continuous_intercept_are_not_evolved_mean() { Err(psychometric_core::PsychometricError::ContinuousInterceptIsNotInitialLatentMean) ); } + +#[test] +fn first_occasion_observed_mean_is_not_evolved_observed_mean() { + let loading = 2.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let evolved_observed = recover_discrete_observed_mean( + loading, + initial, + drift, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq3-eq5-mean"); + let first_occasion = + recover_manifest_observed_mean(loading, initial, manifest_mean).expect("t0"); + let evolved_latent = + recover_discrete_latent_mean(initial, drift, intercept, delta, LagClock::EventTime) + .expect("mu-t"); + assert!( + (first_occasion - evolved_observed).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 of Eq. 3): τ + λ μ_0 is not E(y_t)" + ); + assert!( + (manifest_mean - evolved_observed).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 / Table 2 p. 12): MANIFESTMEANS is not E(y_t)" + ); + assert!( + (evolved_latent - evolved_observed).abs() > 1e-3, + "Driver et al. (2017, Eq. 5): μ_t is not E(y_t)" + ); + assert_eq!( + refuse_initial_observed_mean_as_evolved_observed_mean(first_occasion, evolved_observed), + Err(psychometric_core::PsychometricError::InitialObservedMeanIsNotEvolvedObservedMean) + ); + assert_eq!( + refuse_latent_mean_as_observed_mean(evolved_latent, evolved_observed), + Err(psychometric_core::PsychometricError::LatentMeanIsNotObservedMean) + ); + assert_eq!( + refuse_manifest_means_as_observed_mean(manifest_mean, evolved_observed), + Err(psychometric_core::PsychometricError::ManifestMeansIsNotObservedMean) + ); +} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index bbcfc0da..7da05a0e 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -28,7 +28,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | report template/section/copied/style/modality method effects | ADR 0004/0012; PRD/TRD | simulation truth factors implemented; estimator-side method model remains future | partial | | candidate K statistical/Pareto gates + blinded LLM review | ADR 0012; research | future `model_selection` | accepted-target | | compositional topic correlation / stable clustering | ADR 0005/0012; research | future `network_analysis` | accepted-target | -| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR; full ESEM/DSEM remaining | partial | +| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR; full ESEM/DSEM remaining | partial | | CPU bounded multithreading + GPU/VRAM streaming/parity | ADR 0001/0006 | future `compute_backend` | accepted-target | | TDT detection/tracking vs CHRONOS schema/prediction/temporal consistency | ADR 0016; PRD/research | future `event_intelligence` | accepted-target | | evidence-bounded LLM interpretation | ADR 0010/0012; PRD | future `interpretation_gateway` | accepted-target | diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index 6bf14913..bd708cc4 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -1,7 +1,7 @@ # ADR 0005 — Posterior-aware ESEM/DSEM and structural interpretation **Decision status:** Accepted -**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target +**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target **Date:** 2026-08-05 **Supersedes:** None. ADR 0012 governs upstream topic measurement/network coordinates; this ADR governs higher-order psychometric structure and longitudinal interpretation. @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. 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/adr/README.md b/docs/adr/README.md index 668465fe..b02fed4a 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -10,7 +10,7 @@ Read [`ADR_POLICY.md`](ADR_POLICY.md) first. **Decision status and implementatio | [0002](0002-six-clock-temporal-semantics.md) | Six-clock temporal semantics and fail-closed historical leakage prevention | Accepted | active-PR | Unmerged PR #8 is the canonical Task 3 replacement implementing typed clocks/intervals against the current protected-main lineage; conflicted PR #5 is superseded lineage. Later graph/split enforcement remains target work. | | [0003](0003-relational-event-multiple-membership.md) | Relational event ontology and time-varying cross-classified multiple membership | Accepted | partial | Weighted time-varying membership network/roles are active-PR (PR #12); full multilevel estimators, graph ontology, and persistence remain accepted-target. ADR 0016 owns event-intelligence tasks. | | [0004](0004-shared-multilingual-latent-space.md) | One shared multilingual latent space with explicit invariance status | Accepted | accepted-target | ADR 0012 owns the full topic-estimator/backend/global-topic contract. | -| [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | partial | Input gates, posterior-draw point estimates, Rubin `T` on OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects, exact scalar discrete process noise, lagged latent covariance and unconditional latent variance, stationary within-subject variance (`asymDIFFUSION`), trait-plus-state variance (`TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`), irregular already-centered residual lag, and strong-gated latent means are on the stacked psychometric PR; full ESEM/DSEM estimator remains accepted-target. | +| [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | partial | Input gates, posterior-draw point estimates, Rubin `T` on OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects, exact scalar discrete process noise, lagged latent covariance and unconditional latent variance, stationary within-subject variance (`asymDIFFUSION`), trait-plus-state variance (`TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`), irregular already-centered residual lag, and strong-gated latent means are on the stacked psychometric PR; full ESEM/DSEM estimator remains accepted-target. | | [0006](0006-vram-gpu-nvidia-orchestration.md) | VRAM-adaptive GPU compute and model-credential boundary | Accepted | accepted-target | LLM orchestration policy superseded by ADR 0010; autonomous development authority governed by ADR 0015. | | [0007](0007-rust-workspace-quality-gates.md) | Explicit Rust workspace, pinned toolchains, and exact quality gates | Accepted | implemented-main | ADR 0014 governs scientific/product claim promotion beyond repository-quality tooling. | | [0008](0008-immutable-evidence-identities-digests-and-spans.md) | Immutable evidence identities, `SHA-256` digests, exact spans, and strict wire reconstruction | Accepted | implemented-main | ADR 0013 governs future persistence/reproducibility/split authority. | diff --git a/docs/research/multilevel-event-time-recovery.md b/docs/research/multilevel-event-time-recovery.md index fd1ca608..1dd3de89 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -21,15 +21,16 @@ This slice stays inside `psychometric_core`. It does not add a second invariance 15. recover the exact scalar lagged observed-indicator covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5; `Θ` does not enter) and refuse treating lagged latent covariance or `MANIFESTVAR` as `cov(y_t, y_{t-1})`; 16. recover the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`) and refuse treating the intercept, latent mean, or continuous intercept as `E(y)` / `τ`; 17. recover the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; Table 2 `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment) and refuse treating `T0MEANS` or `CINT` as `μ_t`; -18. refuse pooling discrete lags from unequal event intervals as one coefficient; -19. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); -20. refuse the difference quotient as a continuous-time rate; -21. apply the same event-time map to CWC residuals (still not DSEM); -22. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). +18. recover the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`) and refuse treating that first-occasion mean as `E(y_t)`; +19. refuse pooling discrete lags from unequal event intervals as one coefficient; +20. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); +21. refuse the difference quotient as a continuous-time rate; +22. apply the same event-time map to CWC residuals (still not DSEM); +23. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). ## Claim boundary -This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. +This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. ## Authoritative sources @@ -67,6 +68,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - **Lagged observed-indicator covariance.** Driver et al. (2017, Eq. 5 with Eq. 3–4; JSS PDF re-opened 2026-08-19T04:18Z): independent \(\varepsilon_t\) does not enter \(\operatorname{cov}(y_t,y_{t-1})\). The scalar map is \(\lambda^{2}\operatorname{cov}(\eta_t,\eta_{t-1})+\psi\). Form \((\lambda c)\lambda\) then add \(\psi\). A zero loading or zero latent lagged covariance is exactly \(\psi\). A zero manifest trait is exactly \(\lambda^{2}c\). `MANIFESTVAR` is not lagged observed covariance. Lagged latent covariance is not lagged observed covariance. An overflowing product or sum fails closed. - **Observed-indicator mean.** Driver et al. (2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z): \(y_i(t)=\Gamma+\Lambda\eta_i(t)+\zeta_i(t)\) with \(\zeta\sim N(0,\Theta)\) and \(\Gamma\sim N(\tau,\Psi)\). Table 2 names \(\tau\) `MANIFESTMEANS`, \(\kappa\) `CINT`, and the first-occasion latent mean `T0MEANS`. The scalar map is \(E(y)=\tau+\lambda\mu\). Form \(\lambda\mu\) then add \(\tau\). A zero loading or zero latent mean is exactly \(\tau\). A zero intercept is exactly \(\lambda\mu\). `MANIFESTMEANS` is not \(E(y)\). \(E(\eta)\) is not \(E(y)\). `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not \(E(y)\). An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **Discrete latent mean.** Driver et al. (2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z): \(\eta(t)=\exp(A\Delta t)\eta(t_0)+\int\exp(A(t-s))(b+\cdots)\,ds\) plus a stochastic integral of mean zero. Table 2 names the first-occasion latent mean `T0MEANS` and \(\kappa\) `CINT`. The scalar map is \(\mu_t=\exp(a\Delta t)\mu_0+(\exp(a\Delta t)-1)/a\,\kappa\). Form the `CINT` increment first, then add the carried `T0MEANS` term. A zero drift is the Eq. 3 integral \(\kappa\Delta t\) (\(A=0\) has no inverse). A zero intercept is exactly \(\exp(a\Delta t)\mu_0\). A zero initial mean is exactly the increment. As \(\Delta t\to\infty\) with stable \(a<0\), \(\mu_t\to-\kappa/a\). Binary64 underflow of \(\exp(a\Delta t)\) to `+0` drops the carried `T0MEANS` and keeps that equilibrium increment. `T0MEANS` is not \(\mu_t\). `CINT` is not the discrete increment. `CINT` is not `T0MEANS`. An overflowing exponential, product, or sum fails closed. This is not a Kalman filter and not ctsem estimation. +- **Discrete observed-indicator mean.** Driver et al. (2017, Eq. 3, p. 5; Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T22:10Z): `η_i(t)=exp(AΔt)η_i(t0)+A^{-1}[exp(AΔt)−I]ξ_i+…` with `ξ_i∼N(κ,φ_ξ)` and a stochastic integral of mean zero, then `y_i(t)=Γ_i+Λη_i(t)+ζ_i(t)` with `Γ∼N(τ,Ψ)`. The scalar composition is `E(y_t)=τ+λμ_t`. Form `μ_t` first, then `τ+λμ_t`. A zero loading or zero evolved latent mean is exactly `τ`. A zero intercept is exactly `λμ_t`. A zero drift is `τ+λ(μ_0+κΔt)`. Underflow of `exp(aΔt)` to `+0` keeps `τ+λ(−κ/a)`. The first-occasion map `τ+λμ_0` is not `E(y_t)`. `MANIFESTMEANS` is not `E(y_t)`. `T0MEANS` is not `E(y_t)`. `μ_t` is not `E(y_t)`. An overflowing exponential, product, or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **CWC-then-lag.** Sample cluster means are removed first. Consecutive within residuals are then fitted by least squares to \(r_{t+\Delta t}\approx\exp(a\Delta t)\,r_{t}\) on event time. Same-sign pair-wise logs initialize the scalar Newton step. Sign-flipping \(T=2\) CWC pairs have no real logarithm and fail closed. Curran and Bauer (2011, pp. 607–608) show that this person-mean subtraction on a raw autoregressive series does **not** isolate the lagged within-person effect; the helper therefore does not claim to recover the raw-process drift. - **Already-centered irregular residual.** The caller supplies lagged within residuals. The mean of \(a=\ln(r_{t+\Delta t}/r_t)/\Delta t\) is the exact scalar map. Intervals may be irregular. The helper does not center again. This is not DSEM. @@ -88,6 +90,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - Driver et al. (2017, Eq. 5 lagged covariance) recovers a known \(\lambda^{2}\operatorname{cov}(\eta_t,\eta_{t-1})+\psi\) at machine-scale RMSE, and that RMSE is smaller than treating lagged latent covariance or `MANIFESTVAR` as \(\operatorname{cov}(y_t,y_{t-1})\); forming \(\lambda^{2}\) first overflows while \((\lambda c)\lambda\) stays finite; an overflowing product or sum fails closed; - Driver et al. (2017, Eq. 5 observed mean; Table 2, p. 12) recovers a known \(E(y)=\tau+\lambda\mu\) at machine-scale RMSE, and that RMSE is smaller than treating `MANIFESTMEANS` or \(E(\eta)\) as \(E(y)\); a zero loading or zero latent mean is \(\tau\); `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not \(E(y)\); an overflowing product or sum fails closed; - Driver et al. (2017, Eq. 3 expected-value latent mean; Table 2, p. 12) recovers a known \(\mu_t=\exp(a\Delta t)\mu_0+(\exp(a\Delta t)-1)/a\,\kappa\) at machine-scale RMSE, and that RMSE is smaller than treating `T0MEANS` or `CINT` as \(\mu_t\); a zero drift is \(\mu_0+\kappa\Delta t\); underflow of \(\exp(a\Delta t)\) to `+0` drops `T0MEANS` and keeps \(-\kappa/a\); `CINT` is not the discrete increment; an overflowing exponential, product, or sum fails closed; +- Driver et al. (2017, Eq. 5 of the Eq. 3 evolved mean; Table 2, p. 12) recovers a known \(E(y_t)=\tau+\lambda\mu_t\) at machine-scale RMSE, and that RMSE is smaller than treating \(\tau+\lambda\mu_0\), `MANIFESTMEANS`, or \(\mu_t\) as \(E(y_t)\); a zero loading is \(\tau\); a zero drift is \(\tau+\lambda(\mu_0+\kappa\Delta t)\); underflow of \(\exp(a\Delta t)\) keeps \(\tau+\lambda(-\kappa/a)\); a zero-CINT overflow of \(\exp(a\Delta t)\) fails closed; an overflowing product or sum fails closed; - pooling discrete lags from unequal intervals fails closed; - CWC-then-lag on a two-cluster decaying series has smaller computed RMSE than a level-pooled series when the latter is identified; - already-centered irregular residuals recover a known drift at machine-scale RMSE, and that RMSE is smaller than CWC of the corresponding raw autoregressive series (Curran & Bauer, 2011, pp. 607–608); diff --git a/docs/research/posterior-esem-input-gates.md b/docs/research/posterior-esem-input-gates.md index 502f0fce..46b3e26a 100644 --- a/docs/research/posterior-esem-input-gates.md +++ b/docs/research/posterior-esem-input-gates.md @@ -12,7 +12,7 @@ This slice delivers the first executable ADR 0005 contract in `psychometric_core 6. refuse latent-mean comparison without invariance evidence, and recover a mean difference only under strong or strict two-group OLS status; 7. refuse causal language that rests only on temporal precedence, document linkage, event tracking, or model prediction. -Cluster-mean CWC, the CWC contextual effect, Kish WLS, event-time log-rate, CWC-then-lag, and irregular already-centered residual log-rate live in the same crate and are doctored in `docs/research/multilevel-event-time-recovery.md`. Full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target. +Cluster-mean CWC, the CWC contextual effect, Kish WLS, event-time log-rate, CWC-then-lag, irregular already-centered residual log-rate, and the Driver Eq. 5 of the Eq. 3 evolved mean live in the same crate and are documented in `docs/research/multilevel-event-time-recovery.md`. Full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target. ## Authoritative sources diff --git a/docs/validation/temporal-event-foundation.md b/docs/validation/temporal-event-foundation.md index 8db76a1f..a8644286 100644 --- a/docs/validation/temporal-event-foundation.md +++ b/docs/validation/temporal-event-foundation.md @@ -23,7 +23,7 @@ This report tracks exact-head scientific and engineering evidence required befor | Truth corpora / manifests | `tepp_simulation` | implemented-main | — | deterministic generator tests | Task 10 / PR #18 | | Recovery metrics | `validation_core` | implemented-main | — | RMSE/bias/coverage/MC gates | Task 11 / PR #19 | | Versioned API/export contracts | `tepp_api` | implemented-main | naruon HTTP interchange | unknown-field/version/limit + naruon HTTPS interchange tests | Task 12 / PR #21; live HTTP service remaining | -| Psychometric structural input gates | `psychometric_core` | partial | stacked psychometric PR | construct-class refusal + ALR/ILR boundary + true-loading RMSE + posterior-draw point-estimate mean + Rubin `T` + CWC within/between + CWC contextual effect + event-time log-rate + constant- and time-varying-predictor discrete effects + exact scalar discrete process noise + lagged latent covariance and unconditional latent variance + stationary within-subject variance + trait-plus-state variance + observed-indicator variance + discrete latent mean (`T0MEANS`/`CINT`) + irregular already-centered residual lag + strong-gated latent means; full ESEM/DSEM remaining | ADR 0005; `docs/research/posterior-esem-input-gates.md`; `docs/research/multilevel-event-time-recovery.md`; `docs/research/rubin-total-variance.md`; `docs/research/strong-invariance-latent-means.md` | +| Psychometric structural input gates | `psychometric_core` | partial | stacked psychometric PR | construct-class refusal + ALR/ILR boundary + true-loading RMSE + posterior-draw point-estimate mean + Rubin `T` + CWC within/between + CWC contextual effect + event-time log-rate + constant- and time-varying-predictor discrete effects + exact scalar discrete process noise + lagged latent covariance and unconditional latent variance + stationary within-subject variance + trait-plus-state variance + observed-indicator variance + discrete latent mean (`T0MEANS`/`CINT`) + evolved observed mean (`τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`) + irregular already-centered residual lag + strong-gated latent means; full ESEM/DSEM remaining | ADR 0005; `docs/research/posterior-esem-input-gates.md`; `docs/research/multilevel-event-time-recovery.md`; `docs/research/rubin-total-variance.md`; `docs/research/strong-invariance-latent-means.md` | | CWL modular connectors | `docs/connectors/*` | implemented-main | — | contract docs + examples | PR #22; live HTTP ports remaining | | Release SBOM/provenance generator | `scripts/release_evidence.py` | partial | — | generate+validate in CI | Task 13 partial / PR #28 | From 2fcb9e5ed87ff1b9cebdf76688511d2f527d988b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 22:21:03 +0000 Subject: [PATCH 33/62] feat(psychometric): cap two-observation residual invariance at strong MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Putnick and Bornstein (2016, PMC5145197 opened 2026-08-19T22:15Z) require scalar invariance before latent-mean comparison; residual invariance is not a prerequisite. Two-observation OLS residual variance is identically 0 and is not an estimated residual, so those series cap at strong/scalar and still license (ȳ_c − ȳ_r)/λ. --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 1 + CLAUDE.md | 2 +- crates/psychometric_core/src/latent_mean.rs | 47 +++++++++++++++++-- .../tests/rubin_and_mean_gate_contract.rs | 46 ++++++++++++++++++ docs/TRACEABILITY.md | 2 +- docs/adr/0005-posterior-esem-dsem.md | 2 +- docs/research/posterior-esem-input-gates.md | 2 +- docs/research/standards-and-literature.md | 4 +- .../strong-invariance-latent-means.md | 20 ++++++-- docs/validation/temporal-event-foundation.md | 2 +- 11 files changed, 115 insertions(+), 15 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index e0ffa94c..63239d5b 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -61,7 +61,7 @@ boundaries above remain the target modular MSA architecture. | `tepp_simulation` | known-truth temporal/event data generation | | `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics | | `tepp_api` | versioned DTO, schema, and export contracts | -| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means | +| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | No crate exposes placeholder production behavior in Task 1. This prevents an empty façade from becoming a de facto public API before its invariants and tests diff --git a/CHANGELOG.md b/CHANGELOG.md index 09c92b01..ddc9ae38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang ## [Unreleased] +- `psychometric_core` caps two-observation two-group OLS residual invariance at strong/scalar. `ordinary_least_squares_fit` returns residual variance `0` when `n ≤ 2`; that identity is not an estimated residual and is not strict. Putnick and Bornstein (2016, PMC author manuscript PMC5145197 opened 2026-08-19T22:15Z from https://pmc.ncbi.nlm.nih.gov/articles/PMC5145197/) require scalar invariance before latent-mean comparison; residual invariance is not a prerequisite because residuals are not part of the latent factor. Matching loading and intercept with `n = 2` therefore stay strong/scalar and still license `(ȳ_c − ȳ_r)/λ`. This is still two-group OLS, not MGCFA. Meredith (1993) remains unread (OpenAlex/Semantic Scholar 2026-08-19T22:15Z: closed; Springer `content/pdf` is HTML 200). Vandenberg and Lance (2000) remains unread (cited by Putnick for the residual-not-required claim). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread. - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 3, p. 5; Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T22:10Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar evolved observed-indicator mean. Equation 3 writes `η_i(t) = exp(A Δt) η_i(t0) + A^{-1}[exp(A Δt) − I] ξ_i + …` with `ξ_i ~ N(κ, φ_ξ)` (p. 4) and a stochastic integral of mean zero. Equation 5 writes `y_i(t) = Γ_i + Λ η_i(t) + ζ_i(t)` with `Γ ~ N(τ, Ψ)` and `ζ ~ N(0, Θ)`. The scalar composition is `E(y_t) = τ + λ μ_t` with `μ_t` the Eq. 3 expected-value map. Form `μ_t` first, then `τ + λ μ_t`. The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `MANIFESTMEANS` is not `E(y_t)`. `T0MEANS` is not `E(y_t)`. `μ_t` is not `E(y_t)`. A zero-CINT overflow of `exp(a Δt)` fails the carried `T0MEANS` term closed (`a = 710`, `Δt = 1`). Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) remains unread (OpenAlex 2026-08-19T22:10Z: closed; Springer `content/pdf` is HTML 200). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (OpenAlex/Semantic Scholar 2026-08-19T22:10Z: closed). Putnick and Bornstein (2016) PMC PDF was HTML/500 on this cycle. Oud and Jansen (2000) remains unread. ZORA Anubis-blocked. Asparouhov and Muthén (2009) statmodel PDF re-opened 2026-08-19T22:10Z; it cites Meredith (1993) and discusses multiple-group intercept/mean structures but does not license this map. - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar expected-value latent mean. Equation 3 writes `η(t) = exp(A Δt) η(t0) + ∫ exp(A(t−s)) (b + …) ds` plus a stochastic integral of mean zero. Table 2 names the first-occasion latent mean `T0MEANS` and `κ` `CINT`. The scalar map is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`. Form the CINT increment first, then add the carried `T0MEANS` term. A zero drift is the Eq. 3 integral `κ Δt` (`A = 0` has no inverse). As `Δt → ∞` with stable `a < 0`, `μ_t → −κ / a`. Binary64 underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps that equilibrium increment. `T0MEANS` is not `μ_t`. `CINT` is not the discrete increment. `CINT` is not `T0MEANS`. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (OpenAlex 2026-08-19T18:10Z: closed; Springer `content/pdf` is HTML 200). Oud and Jansen (2000) remains unread. ZORA Anubis-blocked. - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar observed-indicator mean. Equation 5 writes `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and `Γ ~ N(τ, Ψ)`. Table 2 names `τ` `MANIFESTMEANS`, `κ` `CINT`, and the first-occasion latent mean `T0MEANS`. The scalar map is `E(y) = τ + λ μ`. Form `λ μ` then add `τ`. Do not treat `MANIFESTMEANS` as `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. Forming `λ²` is the variance path, not this mean. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-19T14:08Z: closed). Oud and Jansen (2000) remains unread. ZORA Anubis-blocked. diff --git a/CLAUDE.md b/CLAUDE.md index caf671a4..751c01e8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,7 +14,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not convert association, temporal precedence, or document links into causal language without identification evidence. - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. -- Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. +- Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. Putnick and Bornstein (2016, PMC5145197 opened 2026-08-19T22:15Z) require scalar invariance before latent-mean comparison; residual invariance is not a prerequisite. Two-observation series have no residual degrees of freedom (`ordinary_least_squares_fit` returns residual variance `0`) and cap at strong/scalar; they still license means. This is two-group OLS, not MGCFA. Meredith (1993) names remain unread labels. - Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). diff --git a/crates/psychometric_core/src/latent_mean.rs b/crates/psychometric_core/src/latent_mean.rs index 933edb08..41501a15 100644 --- a/crates/psychometric_core/src/latent_mean.rs +++ b/crates/psychometric_core/src/latent_mean.rs @@ -8,8 +8,13 @@ //! Wire names `configural` / `metric` / `scalar` match the unpublished //! `measurement_invariance` crate (#84) without importing it. That crate's //! `Metric` gate is not used here for latent means. `#84` `scalar` is the -//! strong/scalar status. Meredith (1993) names weak/strong/strict are used -//! only as conventional labels; that PDF was not opened. +//! strong/scalar status. Putnick and Bornstein (2016, PMC author manuscript +//! opened 2026-08-19T22:15Z) require scalar invariance before latent-mean +//! comparison; residual invariance is not a prerequisite for those means. +//! Two-observation series have no residual degrees of freedom +//! (`ordinary_least_squares_fit` returns residual variance `0`) and +//! therefore cannot be classified as strict. Meredith (1993) names +//! weak/strong/strict remain unread labels. use crate::error::PsychometricError; use crate::indicator::{IndicatorKind, require_finite, require_valid_indicator}; @@ -125,11 +130,13 @@ pub fn classify_two_group_ols_invariance( let loading_gap = (reference_fit.slope - comparison_fit.slope).abs(); let intercept_gap = (reference_fit.intercept - comparison_fit.intercept).abs(); let residual_gap = (reference_fit.residual_variance - comparison_fit.residual_variance).abs(); + let residual_degrees_of_freedom = + reference.factor_scores.len() > 2 && comparison.factor_scores.len() > 2; let status = if loading_gap > loading_tolerance { MeanInvarianceStatus::Configural } else if intercept_gap > intercept_tolerance { MeanInvarianceStatus::Metric - } else if residual_gap > residual_tolerance { + } else if !residual_degrees_of_freedom || residual_gap > residual_tolerance { MeanInvarianceStatus::Strong } else { MeanInvarianceStatus::Strict @@ -324,6 +331,40 @@ mod tests { assert_eq!(classified.status, MeanInvarianceStatus::Configural); } + #[test] + fn two_observation_series_cap_at_strong_and_still_license_means() { + let reference = series(&[-1.0, 1.0], 0.5, 1.2); + let comparison = series(&[0.0, 2.0], 0.5, 1.2); + let classified = classify_two_group_ols_invariance( + &reference, + &comparison, + IndicatorKind::AdditiveLogRatio, + 1e-9, + 1e-9, + 1e-9, + ) + .expect("two-obs"); + assert_eq!(classified.status, MeanInvarianceStatus::Strong); + assert_eq!( + classified.reference_residual_variance.to_bits(), + 0.0_f64.to_bits() + ); + assert_eq!( + classified.comparison_residual_variance.to_bits(), + 0.0_f64.to_bits() + ); + let difference = recover_strong_gated_latent_mean_difference( + &reference, + &comparison, + IndicatorKind::AdditiveLogRatio, + 1e-9, + 1e-9, + 1e-9, + ) + .expect("licensed"); + assert!((difference - 1.0).abs() < 1e-12); + } + #[test] fn strong_but_not_strict_still_licenses_means() { let reference = GroupIndicatorSeries { diff --git a/crates/psychometric_core/tests/rubin_and_mean_gate_contract.rs b/crates/psychometric_core/tests/rubin_and_mean_gate_contract.rs index 2fb644fb..d69460f6 100644 --- a/crates/psychometric_core/tests/rubin_and_mean_gate_contract.rs +++ b/crates/psychometric_core/tests/rubin_and_mean_gate_contract.rs @@ -120,3 +120,49 @@ fn strong_status_matches_hash84_scalar_and_recovers_mean_difference() { let error = rmse(&[2.0], &[difference]); assert!(error < 1e-12, "latent-mean RMSE {error}"); } + +#[test] +fn two_observation_series_cap_at_strong_scalar_and_still_license_means() { + let reference = GroupIndicatorSeries { + factor_scores: vec![-1.0, 1.0], + indicators: vec![-0.7, 1.7], + }; + let comparison = GroupIndicatorSeries { + factor_scores: vec![0.0, 2.0], + indicators: vec![0.5, 2.9], + }; + let classified = classify_two_group_ols_invariance( + &reference, + &comparison, + IndicatorKind::AdditiveLogRatio, + 1e-9, + 1e-9, + 1e-9, + ) + .expect("two-obs"); + assert_eq!(classified.status, MeanInvarianceStatus::Strong); + assert_eq!( + classified.status.as_measurement_invariance_wire_name(), + "scalar" + ); + assert_eq!( + classified.reference_residual_variance.to_bits(), + 0.0_f64.to_bits() + ); + assert_eq!( + classified.comparison_residual_variance.to_bits(), + 0.0_f64.to_bits() + ); + assert!(classified.status.licenses_latent_mean_comparison()); + let difference = recover_strong_gated_latent_mean_difference( + &reference, + &comparison, + IndicatorKind::AdditiveLogRatio, + 1e-9, + 1e-9, + 1e-9, + ) + .expect("licensed"); + let error = rmse(&[1.0], &[difference]); + assert!(error < 1e-12, "two-obs latent-mean RMSE {error}"); +} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 7da05a0e..839f2dbd 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -28,7 +28,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | report template/section/copied/style/modality method effects | ADR 0004/0012; PRD/TRD | simulation truth factors implemented; estimator-side method model remains future | partial | | candidate K statistical/Pareto gates + blinded LLM review | ADR 0012; research | future `model_selection` | accepted-target | | compositional topic correlation / stable clustering | ADR 0005/0012; research | future `network_analysis` | accepted-target | -| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR; full ESEM/DSEM remaining | partial | +| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z); full ESEM/DSEM remaining | partial | | CPU bounded multithreading + GPU/VRAM streaming/parity | ADR 0001/0006 | future `compute_backend` | accepted-target | | TDT detection/tracking vs CHRONOS schema/prediction/temporal consistency | ADR 0016; PRD/research | future `event_intelligence` | accepted-target | | evidence-bounded LLM interpretation | ADR 0010/0012; PRD | future `interpretation_gateway` | accepted-target | diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index bd708cc4..a942a221 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. 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/posterior-esem-input-gates.md b/docs/research/posterior-esem-input-gates.md index 46b3e26a..d39eac03 100644 --- a/docs/research/posterior-esem-input-gates.md +++ b/docs/research/posterior-esem-input-gates.md @@ -9,7 +9,7 @@ This slice delivers the first executable ADR 0005 contract in `psychometric_core 3. admit ALR, ILR, or logistic-normal coordinates as unconstrained structural inputs while reserving orthonormal Aitchison-distance claims for ILR; 4. recover a reflective loading point estimate by ordinary least squares on a CPU `f64` path; 5. average recovered loading point estimates across posterior indicator draws without claiming Rubin within/between uncertainty pooling (the Rubin `T` helper is a separate API; see `docs/research/rubin-total-variance.md`); -6. refuse latent-mean comparison without invariance evidence, and recover a mean difference only under strong or strict two-group OLS status; +6. refuse latent-mean comparison without invariance evidence, and recover a mean difference only under strong or strict two-group OLS status (Putnick & Bornstein, 2016: scalar licenses means; residual invariance is not required; two-observation series cap at strong because residual variance is identically `0`); 7. refuse causal language that rests only on temporal precedence, document linkage, event tracking, or model prediction. Cluster-mean CWC, the CWC contextual effect, Kish WLS, event-time log-rate, CWC-then-lag, irregular already-centered residual log-rate, and the Driver Eq. 5 of the Eq. 3 evolved mean live in the same crate and are documented in `docs/research/multilevel-event-time-recovery.md`. Full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target. diff --git a/docs/research/standards-and-literature.md b/docs/research/standards-and-literature.md index ad713f92..3e1abd8c 100644 --- a/docs/research/standards-and-literature.md +++ b/docs/research/standards-and-literature.md @@ -32,9 +32,11 @@ Mislevy, R. J. (1991). Randomization-based inference about latent variables from Meredith, W. (1993). Measurement invariance, factor analysis and factorial invariance. *Psychometrika, 58*(4), 525–543. https://doi.org/10.1007/BF02294825 +Putnick, D. L., & Bornstein, M. H. (2016). Measurement invariance conventions and reporting: The state of the art and future directions for psychological research. *Developmental Review, 41*, 71–90. https://doi.org/10.1016/j.dr.2016.06.004 + Holland, P. W. (1986). Statistics and causal inference. *Journal of the American Statistical Association, 81*(396), 945–960. https://doi.org/10.1080/01621459.1986.10478354 -TEPP applies these sources to construct definition, score interpretation, reliability, validity evidence, uncertainty, consequences, longitudinal invariance, ESEM cross-loadings, and DSEM. Topic outputs are treated as fallible indicators or components only after their construct role is evaluated. Reflective, formative, and network classes remain distinct (Bollen & Lennox, 1991). Complete-data OLS loadings across posterior indicator draws are combined with Rubin (1996) \(T_m\); the arithmetic-mean helper remains a point estimate. Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-18T03:07Z: closed). The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title was opened 2026-08-17T12:04Z from archive.org; it is not the 1991 journal article and is not used as Mislevy plausible-value authority. Temporal precedence is not causal identification (Holland, 1986). Within/between OLS follows Enders and Tofighi (2007), Curran and Bauer (2011), and Hamaker et al. (2015). Enders and Tofighi (2007, Table 2, pp. 124–127; PDF opened 2026-08-17) show that the CWC cluster-mean coefficient is the contextual effect (`between − within`), not the between-cluster effect. Curran and Bauer (2011, pp. 607–608) reject person-mean subtraction on a raw autoregressive series as the lagged within-person residual; already-centered irregular residuals use the Voelkle et al. (2012, Eq. 7) / Driver et al. (2017, Eq. 3) scalar map. Discrete lags from unequal event intervals are remapped through that log-rate (Voelkle et al., 2012, ZORA accepted manuscript re-opened 2026-08-17T13:13Z) and are not pooled. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\) and restate the discrete intercept as a function of \(A\) and \(\Delta t\). A binary64 underflow of \(\exp(a\Delta t)\) to `+0` is refused because discrete auto-effects are strictly positive. The discrete effect of a constant predictor is Voelkle et al. (2012, Eq. 12; ZORA accepted manuscript re-opened 2026-08-17T14:20Z, Introducing Intercepts, manuscript p. 20), evaluated as \(a_{yx}(\operatorname{expm1}(z)/a_{xx})\) with \(z=a_{xx}\Delta t\) so a finite result is not lost when \(z\) overflows to \(-\infty\) or when \(a_{yx}\Delta t\) overflows, and in log space when `expm1(z)` overflows at a finite \(z\); a zero continuous effect is exactly zero; an overflowing \(a_{yx}/a_{xx}\) rewrite term fails closed; the first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. The discrete effect of a time-varying predictor with matched sampling and constancy intervals is Voelkle et al. (2012, Eq. 14; manuscript p. 21): \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product is not Eq. 12. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). The exact scalar discrete process noise is Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-18T14:04Z, p. 4): \(Q_{\Delta t}=0.5 q(\operatorname{expm1}(z)/a)\) with \(z=2(a\Delta t)\) for \(a\neq 0\) and \(q=GG^{\top}\ge 0\); do not form \(2a\) first; \(a=0\) recovers \(q\Delta t\); an overflowing rewrite scale \(0.5 q/a\) fails closed. This is not a Kalman filter. Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-18T14:04Z) write the same discrete intercept as \(A^{-1}[e^{A\Delta t}-I]\xi\). The lagged covariance is \(\mathrm{e}^{a\Delta t}p\) and the unconditional variance is \(\mathrm{e}^{2a\Delta t}p+Q_{\Delta t}\) (Driver et al., 2017, Eq. 3–4, pp. 4–5); a zero diffusion whose \(2(a\Delta t)\) overflows to \(+\infty\) fails closed. The stationary within-subject variance is the \(\Delta t\to\infty\) limit of Eq. 4: \(-q/(2a)\) for stable \(a<0\) (JSS p. 16 `asymDIFFUSION`; §4.3; PDF re-opened 2026-08-18T18:03Z). Finite-interval \(Q_{\Delta t}\) is not that limit. Trait-plus-state variance is \(\mathrm{trait}+\mathrm{state}\) and lagged covariance is \(\mathrm{trait}+\mathrm{e}^{a\Delta t}p\) (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z). Trait variance is not process noise and not `asymDIFFUSION`. Metric/weak invariance does not license latent-mean comparison. Meredith (1993) remains unread (Unpaywall/OpenAlex 2026-08-18T21:07Z: closed; Springer `content/pdf` is HTML 200). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex 2026-08-18T21:07Z: closed). ERIC ED334221 is Singer and Willett (1991), not the 1991 journal article. ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex 2026-08-18T21:07Z: closed). +TEPP applies these sources to construct definition, score interpretation, reliability, validity evidence, uncertainty, consequences, longitudinal invariance, ESEM cross-loadings, and DSEM. Topic outputs are treated as fallible indicators or components only after their construct role is evaluated. Reflective, formative, and network classes remain distinct (Bollen & Lennox, 1991). Complete-data OLS loadings across posterior indicator draws are combined with Rubin (1996) \(T_m\); the arithmetic-mean helper remains a point estimate. Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-18T03:07Z: closed). The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title was opened 2026-08-17T12:04Z from archive.org; it is not the 1991 journal article and is not used as Mislevy plausible-value authority. Temporal precedence is not causal identification (Holland, 1986). Within/between OLS follows Enders and Tofighi (2007), Curran and Bauer (2011), and Hamaker et al. (2015). Enders and Tofighi (2007, Table 2, pp. 124–127; PDF opened 2026-08-17) show that the CWC cluster-mean coefficient is the contextual effect (`between − within`), not the between-cluster effect. Curran and Bauer (2011, pp. 607–608) reject person-mean subtraction on a raw autoregressive series as the lagged within-person residual; already-centered irregular residuals use the Voelkle et al. (2012, Eq. 7) / Driver et al. (2017, Eq. 3) scalar map. Discrete lags from unequal event intervals are remapped through that log-rate (Voelkle et al., 2012, ZORA accepted manuscript re-opened 2026-08-17T13:13Z) and are not pooled. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\) and restate the discrete intercept as a function of \(A\) and \(\Delta t\). A binary64 underflow of \(\exp(a\Delta t)\) to `+0` is refused because discrete auto-effects are strictly positive. The discrete effect of a constant predictor is Voelkle et al. (2012, Eq. 12; ZORA accepted manuscript re-opened 2026-08-17T14:20Z, Introducing Intercepts, manuscript p. 20), evaluated as \(a_{yx}(\operatorname{expm1}(z)/a_{xx})\) with \(z=a_{xx}\Delta t\) so a finite result is not lost when \(z\) overflows to \(-\infty\) or when \(a_{yx}\Delta t\) overflows, and in log space when `expm1(z)` overflows at a finite \(z\); a zero continuous effect is exactly zero; an overflowing \(a_{yx}/a_{xx}\) rewrite term fails closed; the first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. The discrete effect of a time-varying predictor with matched sampling and constancy intervals is Voelkle et al. (2012, Eq. 14; manuscript p. 21): \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product is not Eq. 12. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). The exact scalar discrete process noise is Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-18T14:04Z, p. 4): \(Q_{\Delta t}=0.5 q(\operatorname{expm1}(z)/a)\) with \(z=2(a\Delta t)\) for \(a\neq 0\) and \(q=GG^{\top}\ge 0\); do not form \(2a\) first; \(a=0\) recovers \(q\Delta t\); an overflowing rewrite scale \(0.5 q/a\) fails closed. This is not a Kalman filter. Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-18T14:04Z) write the same discrete intercept as \(A^{-1}[e^{A\Delta t}-I]\xi\). The lagged covariance is \(\mathrm{e}^{a\Delta t}p\) and the unconditional variance is \(\mathrm{e}^{2a\Delta t}p+Q_{\Delta t}\) (Driver et al., 2017, Eq. 3–4, pp. 4–5); a zero diffusion whose \(2(a\Delta t)\) overflows to \(+\infty\) fails closed. The stationary within-subject variance is the \(\Delta t\to\infty\) limit of Eq. 4: \(-q/(2a)\) for stable \(a<0\) (JSS p. 16 `asymDIFFUSION`; §4.3; PDF re-opened 2026-08-18T18:03Z). Finite-interval \(Q_{\Delta t}\) is not that limit. Trait-plus-state variance is \(\mathrm{trait}+\mathrm{state}\) and lagged covariance is \(\mathrm{trait}+\mathrm{e}^{a\Delta t}p\) (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z). Trait variance is not process noise and not `asymDIFFUSION`. Metric/weak invariance does not license latent-mean comparison. Putnick and Bornstein (2016, PMC author manuscript PMC5145197 opened 2026-08-19T22:15Z) require scalar invariance before latent-mean comparison and state that residual invariance is not a prerequisite. Two-observation OLS residual variance is identically `0` and is not strict. Meredith (1993) remains unread (Unpaywall/OpenAlex 2026-08-19T22:15Z: closed; Springer `content/pdf` is HTML 200). Vandenberg and Lance (2000) remains unread. Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex 2026-08-18T21:07Z: closed). ERIC ED334221 is Singer and Willett (1991), not the 1991 journal article. ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex 2026-08-18T21:07Z: closed). ## Structural, correlated, dynamic, relational, and multilingual topic models diff --git a/docs/research/strong-invariance-latent-means.md b/docs/research/strong-invariance-latent-means.md index 327fa7a8..101089b9 100644 --- a/docs/research/strong-invariance-latent-means.md +++ b/docs/research/strong-invariance-latent-means.md @@ -10,26 +10,35 @@ This slice does **not** import the unpublished `measurement_invariance` crate on - `#84` `metric` licenses shared **metric** meaning. It does **not** license latent means. - `#84` `scalar` is the strong/scalar status (equal loading and intercept). That status licenses latent means. -- Strict (also equal residual variance) also licenses latent means. +- Strict (also equal residual variance) also licenses latent means. Residual invariance is **not** required for those means. +- Two-observation series have no residual degrees of freedom. OLS residual variance is then identically `0` and is not an estimated residual. Those series cap at strong/scalar and still license means. - This is two-group OLS, not MGCFA, not partial invariance, and not alignment optimization. -- Meredith (1993) names weak/strong/strict are used only as conventional labels. That PDF was not opened (Unpaywall/OpenAlex/Semantic Scholar/CORE/OpenAIRE/archive.org 2026-08-18T03:07Z: closed; Springer remains an HTML stub). Do not cite Meredith equations as having been read. +- Meredith (1993) names weak/strong/strict are used only as conventional labels. That PDF was not opened (Unpaywall/OpenAlex/Semantic Scholar 2026-08-19T22:15Z: closed; Springer `content/pdf` is HTML 200). Do not cite Meredith equations as having been read. Putnick and Bornstein (2016) cite Meredith for residual invariance as part of *full factorial invariance*; that citation is not a reading of Meredith. ## Authoritative sources used for the mean gate -The executable gate follows the ADR 0005 rule that mean comparison requires the invariance level needed for that claim, and the `#84` terminology split between metric meaning and scalar/strong means. Opened sources that constrain the surrounding longitudinal/invariance stance: +Putnick, D. L., & Bornstein, M. H. (2016). Measurement invariance conventions and reporting: The state of the art and future directions for psychological research. *Developmental Review, 41*, 71–90. https://doi.org/10.1016/j.dr.2016.06.004 + +PMC author manuscript (PMC5145197) opened 2026-08-19T22:15Z from https://pmc.ncbi.nlm.nih.gov/articles/PMC5145197/. The NIHMS PDF endpoints returned HTML/500 on this cycle; the PMC HTML full text is the opened copy. + +Putnick and Bornstein write that measurement invariance is a prerequisite to comparing group means. Metric invariance is equivalence of item loadings: each item contributes to the latent construct to a similar degree across groups. Scalar invariance is equivalence of item intercepts after metric: “mean differences in the latent construct capture all mean differences in the shared variance of the items.” After those steps, “the researcher is free to compare group means on the latent factors.” Residual invariance “is not a prerequisite for testing mean differences because the residuals are not part of the latent factor” (they cite Vandenberg & Lance, 2000, unread). Configural, metric, and scalar “are required prior to group mean comparisons.” This crate’s `#84` `metric` / `scalar` split follows that terminology. The executable map remains two-group OLS, not their multiple-group CFA. + +Opened sources that constrain the surrounding longitudinal/invariance stance: Asparouhov, T., & Muthén, B. (2009). Exploratory structural equation modeling. *Structural Equation Modeling: A Multidisciplinary Journal, 16*(3), 397–438. https://doi.org/10.1080/10705510903008204 Hamaker, E. L., Kuiper, R. M., & Grasman, R. P. P. P. (2015). A critique of the cross-lagged panel model. *Psychological Methods, 20*(1), 102–116. https://doi.org/10.1037/a0038889 +Vandenberg, R. J., & Lance, C. E. (2000). A review and synthesis of the measurement invariance literature: Suggestions, practices, and recommendations for organizational research. *Organizational Research Methods, 3*(1), 4–70. https://doi.org/10.1177/109442810031002 (cited by Putnick & Bornstein, 2016, for residual invariance not being required for latent means; PDF not opened). + ## Formula notes Per group, \(y=\nu+\lambda f+e\) is fit by OLS. Status is: - configural when \(|\lambda_r-\lambda_c|\) exceeds tolerance; - metric when loadings match and intercepts differ; -- strong when loadings and intercepts match and residual variances differ; -- strict when loadings, intercepts, and residual variances match. +- strong when loadings and intercepts match and residual variances differ, or when residual degrees of freedom are absent; +- strict when both groups have residual degrees of freedom and loadings, intercepts, and residual variances match. The latent-mean difference is \((\bar y_c-\bar y_r)/\lambda\) with \(\lambda\) the midpoint of the two loadings, and only after strong or strict. @@ -37,4 +46,5 @@ The latent-mean difference is \((\bar y_c-\bar y_r)/\lambda\) with \(\lambda\) t - strong/strict series recover a known mean difference with computed RMSE; - metric-only (equal loading, shifted intercept) and configural series return `StrongInvarianceRequired`; +- two-observation series with matching loading and intercept classify as strong, not strict, and still recover the known mean difference; - `#84` wire-name tests: `metric` licenses shared metric meaning and refuses means; `scalar` is strong and licenses means. diff --git a/docs/validation/temporal-event-foundation.md b/docs/validation/temporal-event-foundation.md index a8644286..d734ed47 100644 --- a/docs/validation/temporal-event-foundation.md +++ b/docs/validation/temporal-event-foundation.md @@ -23,7 +23,7 @@ This report tracks exact-head scientific and engineering evidence required befor | Truth corpora / manifests | `tepp_simulation` | implemented-main | — | deterministic generator tests | Task 10 / PR #18 | | Recovery metrics | `validation_core` | implemented-main | — | RMSE/bias/coverage/MC gates | Task 11 / PR #19 | | Versioned API/export contracts | `tepp_api` | implemented-main | naruon HTTP interchange | unknown-field/version/limit + naruon HTTPS interchange tests | Task 12 / PR #21; live HTTP service remaining | -| Psychometric structural input gates | `psychometric_core` | partial | stacked psychometric PR | construct-class refusal + ALR/ILR boundary + true-loading RMSE + posterior-draw point-estimate mean + Rubin `T` + CWC within/between + CWC contextual effect + event-time log-rate + constant- and time-varying-predictor discrete effects + exact scalar discrete process noise + lagged latent covariance and unconditional latent variance + stationary within-subject variance + trait-plus-state variance + observed-indicator variance + discrete latent mean (`T0MEANS`/`CINT`) + evolved observed mean (`τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`) + irregular already-centered residual lag + strong-gated latent means; full ESEM/DSEM remaining | ADR 0005; `docs/research/posterior-esem-input-gates.md`; `docs/research/multilevel-event-time-recovery.md`; `docs/research/rubin-total-variance.md`; `docs/research/strong-invariance-latent-means.md` | +| Psychometric structural input gates | `psychometric_core` | partial | stacked psychometric PR | construct-class refusal + ALR/ILR boundary + true-loading RMSE + posterior-draw point-estimate mean + Rubin `T` + CWC within/between + CWC contextual effect + event-time log-rate + constant- and time-varying-predictor discrete effects + exact scalar discrete process noise + lagged latent covariance and unconditional latent variance + stationary within-subject variance + trait-plus-state variance + observed-indicator variance + discrete latent mean (`T0MEANS`/`CINT`) + evolved observed mean (`τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`) + irregular already-centered residual lag + strong-gated latent means (n=2 residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016); full ESEM/DSEM remaining | ADR 0005; `docs/research/posterior-esem-input-gates.md`; `docs/research/multilevel-event-time-recovery.md`; `docs/research/rubin-total-variance.md`; `docs/research/strong-invariance-latent-means.md` | | CWL modular connectors | `docs/connectors/*` | implemented-main | — | contract docs + examples | PR #22; live HTTP ports remaining | | Release SBOM/provenance generator | `scripts/release_evidence.py` | partial | — | generate+validate in CI | Task 13 partial / PR #28 | From b519fe40df236b023d8eba073cf39f1e38a23d6e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 22:27:27 +0000 Subject: [PATCH 34/62] feat(psychometric): recover Driver Eq. 3 TDPREDEFFECT impulse MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Driver, Oud, and Voelkle (2017, Eq. 1–3; Table 2; §7.2; JSS PDF re-opened 2026-08-20T07:10Z) write the time-dependent predictor as the Dirac impulse χ(t)=Σ x δ(t−u). The fourth summand is M x. Table 2 names M TDPREDEFFECT. Form μ_t first, then add m x. TDPREDEFFECT is not CINT, not TIPREDEFFECT, and not Voelkle Eq. 14. The §7.2 level-change form is a different specification. --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 1 + CLAUDE.md | 2 +- crates/psychometric_core/src/error.rs | 35 ++ crates/psychometric_core/src/event_time.rs | 302 +++++++++++++++++- crates/psychometric_core/src/lib.rs | 14 +- ...multilevel_event_time_recovery_contract.rs | 106 +++++- .../scientific_claim_boundary_contract.rs | 70 +++- docs/TRACEABILITY.md | 2 +- docs/adr/0005-posterior-esem-dsem.md | 4 +- .../multilevel-event-time-recovery.md | 15 +- docs/research/posterior-esem-input-gates.md | 2 +- docs/research/standards-and-literature.md | 2 +- docs/validation/temporal-event-foundation.md | 2 +- 14 files changed, 526 insertions(+), 33 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 63239d5b..7ee00335 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -61,7 +61,7 @@ boundaries above remain the target modular MSA architecture. | `tepp_simulation` | known-truth temporal/event data generation | | `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics | | `tepp_api` | versioned DTO, schema, and export contracts | -| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | +| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | No crate exposes placeholder production behavior in Task 1. This prevents an empty façade from becoming a de facto public API before its invariants and tests diff --git a/CHANGELOG.md b/CHANGELOG.md index ddc9ae38..9982b58b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar contemporaneous time-dependent predictor impulse. Equation 2 writes `χ_i(t) = Σ x_{i,u} δ(t − u)`. Equation 3's fourth summand is `M Σ x_{i,u} δ(t − u)`. Table 2 names `M` `TDPREDEFFECT`. Section 7.2 calls this a sudden impulse that dissipates back to the process mean and reports `TDPREDEFFECT` as the initial impact. The scalar jump is `m x`. Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` (`TIPREDEFFECT`). `M x` is not Voelkle et al. (2012, Eq. 14) `a_{yx} Δt`. The §7.2 level-change form is a different specification and is not this map. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993), Vandenberg and Lance (2000), and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T07:10Z: `is_oa: false`). - `psychometric_core` caps two-observation two-group OLS residual invariance at strong/scalar. `ordinary_least_squares_fit` returns residual variance `0` when `n ≤ 2`; that identity is not an estimated residual and is not strict. Putnick and Bornstein (2016, PMC author manuscript PMC5145197 opened 2026-08-19T22:15Z from https://pmc.ncbi.nlm.nih.gov/articles/PMC5145197/) require scalar invariance before latent-mean comparison; residual invariance is not a prerequisite because residuals are not part of the latent factor. Matching loading and intercept with `n = 2` therefore stay strong/scalar and still license `(ȳ_c − ȳ_r)/λ`. This is still two-group OLS, not MGCFA. Meredith (1993) remains unread (OpenAlex/Semantic Scholar 2026-08-19T22:15Z: closed; Springer `content/pdf` is HTML 200). Vandenberg and Lance (2000) remains unread (cited by Putnick for the residual-not-required claim). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread. - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 3, p. 5; Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T22:10Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar evolved observed-indicator mean. Equation 3 writes `η_i(t) = exp(A Δt) η_i(t0) + A^{-1}[exp(A Δt) − I] ξ_i + …` with `ξ_i ~ N(κ, φ_ξ)` (p. 4) and a stochastic integral of mean zero. Equation 5 writes `y_i(t) = Γ_i + Λ η_i(t) + ζ_i(t)` with `Γ ~ N(τ, Ψ)` and `ζ ~ N(0, Θ)`. The scalar composition is `E(y_t) = τ + λ μ_t` with `μ_t` the Eq. 3 expected-value map. Form `μ_t` first, then `τ + λ μ_t`. The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `MANIFESTMEANS` is not `E(y_t)`. `T0MEANS` is not `E(y_t)`. `μ_t` is not `E(y_t)`. A zero-CINT overflow of `exp(a Δt)` fails the carried `T0MEANS` term closed (`a = 710`, `Δt = 1`). Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) remains unread (OpenAlex 2026-08-19T22:10Z: closed; Springer `content/pdf` is HTML 200). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (OpenAlex/Semantic Scholar 2026-08-19T22:10Z: closed). Putnick and Bornstein (2016) PMC PDF was HTML/500 on this cycle. Oud and Jansen (2000) remains unread. ZORA Anubis-blocked. Asparouhov and Muthén (2009) statmodel PDF re-opened 2026-08-19T22:10Z; it cites Meredith (1993) and discusses multiple-group intercept/mean structures but does not license this map. - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar expected-value latent mean. Equation 3 writes `η(t) = exp(A Δt) η(t0) + ∫ exp(A(t−s)) (b + …) ds` plus a stochastic integral of mean zero. Table 2 names the first-occasion latent mean `T0MEANS` and `κ` `CINT`. The scalar map is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`. Form the CINT increment first, then add the carried `T0MEANS` term. A zero drift is the Eq. 3 integral `κ Δt` (`A = 0` has no inverse). As `Δt → ∞` with stable `a < 0`, `μ_t → −κ / a`. Binary64 underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps that equilibrium increment. `T0MEANS` is not `μ_t`. `CINT` is not the discrete increment. `CINT` is not `T0MEANS`. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (OpenAlex 2026-08-19T18:10Z: closed; Springer `content/pdf` is HTML 200). Oud and Jansen (2000) remains unread. ZORA Anubis-blocked. diff --git a/CLAUDE.md b/CLAUDE.md index 751c01e8..351beabf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. Putnick and Bornstein (2016, PMC5145197 opened 2026-08-19T22:15Z) require scalar invariance before latent-mean comparison; residual invariance is not a prerequisite. Two-observation series have no residual degrees of freedom (`ordinary_least_squares_fit` returns residual variance `0`) and cap at strong/scalar; they still license means. This is two-group OLS, not MGCFA. Meredith (1993) names remain unread labels. -- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. The contemporaneous time-dependent predictor impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`). Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). The §7.2 level-change form is not that impulse. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. diff --git a/crates/psychometric_core/src/error.rs b/crates/psychometric_core/src/error.rs index 20822ebd..2f9d9f60 100644 --- a/crates/psychometric_core/src/error.rs +++ b/crates/psychometric_core/src/error.rs @@ -101,6 +101,16 @@ pub enum PsychometricError { /// Driver Eq. 5 of the first-occasion mean was treated as /// `E(y_t)`. `τ + λ μ_0` is not `τ + λ μ_t`. InitialObservedMeanIsNotEvolvedObservedMean, + /// Driver Eq. 3 fourth-summand impulse was treated as `CINT`. + /// Table 2 names `M` `TDPREDEFFECT`, not `κ`. + TimeDependentImpulseIsNotContinuousIntercept, + /// Driver Eq. 3 fourth-summand impulse was treated as the + /// time-independent discrete effect. `M x` is not + /// `A^{-1}[e^{A Δt} − I] B z`. + TimeDependentImpulseIsNotTimeIndependentEffect, + /// Driver Eq. 3 fourth-summand impulse was treated as Voelkle + /// et al. (2012, Eq. 14). `M x` is not `a_{yx} Δt`. + TimeDependentImpulseIsNotTimeVaryingDiscreteEffect, } impl fmt::Display for PsychometricError { @@ -187,6 +197,15 @@ impl fmt::Display for PsychometricError { Self::InitialObservedMeanIsNotEvolvedObservedMean => { "first-occasion observed mean is not the evolved observed mean" } + Self::TimeDependentImpulseIsNotContinuousIntercept => { + "time-dependent predictor impulse is not the continuous intercept" + } + Self::TimeDependentImpulseIsNotTimeIndependentEffect => { + "time-dependent predictor impulse is not the time-independent discrete effect" + } + Self::TimeDependentImpulseIsNotTimeVaryingDiscreteEffect => { + "time-dependent predictor impulse is not the time-varying discrete effect" + } }; formatter.write_str(message) } @@ -337,4 +356,20 @@ mod tests { "first-occasion observed mean is not the evolved observed mean" ); } + + #[test] + fn time_dependent_impulse_boundary_messages_are_stable() { + assert_eq!( + PsychometricError::TimeDependentImpulseIsNotContinuousIntercept.to_string(), + "time-dependent predictor impulse is not the continuous intercept" + ); + assert_eq!( + PsychometricError::TimeDependentImpulseIsNotTimeIndependentEffect.to_string(), + "time-dependent predictor impulse is not the time-independent discrete effect" + ); + assert_eq!( + PsychometricError::TimeDependentImpulseIsNotTimeVaryingDiscreteEffect.to_string(), + "time-dependent predictor impulse is not the time-varying discrete effect" + ); + } } diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index 6e1be3b2..4c5b553a 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -38,7 +38,10 @@ //! `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is `κ`, not `τ`; //! `T0MEANS` is the initial latent mean, not `E(y)`). Equation 3's //! expected-value map is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` -//! (`T0MEANS` is not `μ_t`; `CINT` is not that discrete increment). The JSS article +//! (`T0MEANS` is not `μ_t`; `CINT` is not that discrete increment). Equation 3's +//! fourth summand is the contemporaneous Dirac impulse `M x` (Table 2 +//! `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not +//! Voelkle et al., 2012, Eq. 14). The JSS article //! has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). //! The difference quotient `(x(t+Δt) − x(t)) / Δt` (their //! Eqs. 3–4) is refused. This is not DSEM and not a matrix `expm`. @@ -1288,6 +1291,137 @@ pub fn refuse_initial_observed_mean_as_evolved_observed_mean( Err(PsychometricError::InitialObservedMeanIsNotEvolvedObservedMean) } +/// Exact scalar contemporaneous impulse from Driver Equation 3. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; +/// §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z from +/// ) +/// write the time-dependent predictor as the Dirac impulse +/// `χ_i(t) = Σ_{u ∈ U_i} x_{i,u} δ(t − u)` (Eq. 2). Equation 3's +/// fourth summand is `M Σ x_{i,u} δ(t − u)`. Table 2 names `M` +/// `TDPREDEFFECT`. Section 7.2 calls this "a sudden impulse to the +/// system which then dissipates back to the process mean" and reports +/// `TDPREDEFFECT` as "the initial impact of the predictor on the +/// processes." The scalar contemporaneous jump is `m x`. It is not +/// the second-summand `CINT` map `A^{-1}[e^{A Δt} − I] κ`, not the +/// third-summand time-independent map `A^{-1}[e^{A Δt} − I] B z`, +/// and not Voelkle et al. (2012, Eq. 14) `a_{yx} Δt`. The §7.2 +/// level-change form is a different specification (an extra latent +/// process with near-zero drift) and is not this map. A zero effect +/// or zero predictor is exactly zero. This is not a Kalman filter +/// and not ctsem estimation. +/// +/// # Errors +/// +/// Returns [`PsychometricError::InvalidNumericInput`] when the effect +/// or predictor is non-finite or the product overflows. +pub fn recover_time_dependent_predictor_impulse( + time_dependent_effect: f64, + time_dependent_predictor: f64, +) -> Result { + if !time_dependent_effect.is_finite() || !time_dependent_predictor.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + if time_dependent_effect == 0.0 || time_dependent_predictor == 0.0 { + return Ok(0.0); + } + require_finite(time_dependent_effect * time_dependent_predictor) +} + +/// Exact scalar evolved latent mean plus a contemporaneous impulse. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 3, p. 5) write the first two +/// summands as the carried `T0MEANS` and `CINT` increment, then add +/// the fourth-summand impulse at the observation instant. Form `μ_t` +/// first, then add `m x`. A zero impulse is exactly `μ_t`. A zero +/// evolved mean is exactly the impulse. The first-occasion map +/// `μ_0 + m x` is not this composition when the process has already +/// evolved. The level-change form is not this map. +/// +/// # Errors +/// +/// Propagates [`recover_discrete_latent_mean`] and +/// [`recover_time_dependent_predictor_impulse`], and returns +/// [`PsychometricError::InvalidNumericInput`] when the sum overflows. +pub fn recover_discrete_latent_mean_with_impulse( + initial_latent_mean: f64, + log_rate: f64, + continuous_intercept: f64, + time_dependent_effect: f64, + time_dependent_predictor: f64, + event_delta: f64, + clock: LagClock, +) -> Result { + let evolved_latent_mean = recover_discrete_latent_mean( + initial_latent_mean, + log_rate, + continuous_intercept, + event_delta, + clock, + )?; + let impulse = + recover_time_dependent_predictor_impulse(time_dependent_effect, time_dependent_predictor)?; + if impulse == 0.0 { + return Ok(evolved_latent_mean); + } + if evolved_latent_mean == 0.0 { + return Ok(impulse); + } + require_finite(evolved_latent_mean + impulse) +} + +/// Refuse treating the Eq. 3 impulse as `CINT`. +/// +/// Table 2 names `M` `TDPREDEFFECT` and `κ` `CINT`. The impulse is +/// `m x`. The continuous intercept is not that jump. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::TimeDependentImpulseIsNotContinuousIntercept`]. +pub fn refuse_time_dependent_impulse_as_continuous_intercept( + time_dependent_impulse: f64, + continuous_intercept: f64, +) -> Result { + let _ = (time_dependent_impulse, continuous_intercept); + Err(PsychometricError::TimeDependentImpulseIsNotContinuousIntercept) +} + +/// Refuse treating the Eq. 3 impulse as the time-independent effect. +/// +/// The third summand is `A^{-1}[e^{A Δt} − I] B z`. Table 2 names +/// `B` `TIPREDEFFECT`. The fourth-summand impulse is `M x`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::TimeDependentImpulseIsNotTimeIndependentEffect`]. +pub fn refuse_time_dependent_impulse_as_time_independent_effect( + time_dependent_impulse: f64, + time_independent_effect: f64, +) -> Result { + let _ = (time_dependent_impulse, time_independent_effect); + Err(PsychometricError::TimeDependentImpulseIsNotTimeIndependentEffect) +} + +/// Refuse treating the Eq. 3 impulse as Voelkle et al. (2012, Eq. 14). +/// +/// Equation 14 is `a_{yx} Δt` for a piecewise-constant time-varying +/// predictor whose sampling interval equals its constancy interval. +/// The Dirac impulse is `m x`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::TimeDependentImpulseIsNotTimeVaryingDiscreteEffect`]. +pub fn refuse_time_dependent_impulse_as_time_varying_discrete_effect( + time_dependent_impulse: f64, + time_varying_discrete_effect: f64, +) -> Result { + let _ = (time_dependent_impulse, time_varying_discrete_effect); + Err(PsychometricError::TimeDependentImpulseIsNotTimeVaryingDiscreteEffect) +} + /// Refuse treating Driver Table 2 `T0MEANS` as the evolved latent mean. /// /// Equation 3 maps `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`. @@ -1590,13 +1724,14 @@ mod tests { map_discrete_lag_across_event_intervals, recover_discrete_constant_predictor_effect, recover_discrete_continuous_intercept_effect, recover_discrete_lag_from_log_rate, recover_discrete_lag_one, recover_discrete_lagged_latent_covariance, - recover_discrete_latent_mean, recover_discrete_latent_variance, - recover_discrete_observed_mean, recover_discrete_process_noise, - recover_discrete_time_varying_predictor_effect, recover_event_series_mean_log_rate, - recover_event_time_discrete_lag_and_log_rate, recover_irregular_centered_residual_log_rate, - recover_local_log_rate, recover_manifest_lagged_observed_covariance, - recover_manifest_observed_mean, recover_manifest_observed_variance, - recover_manifest_trait_plus_state_observed_variance, recover_stationary_latent_variance, + recover_discrete_latent_mean, recover_discrete_latent_mean_with_impulse, + recover_discrete_latent_variance, recover_discrete_observed_mean, + recover_discrete_process_noise, recover_discrete_time_varying_predictor_effect, + recover_event_series_mean_log_rate, recover_event_time_discrete_lag_and_log_rate, + recover_irregular_centered_residual_log_rate, recover_local_log_rate, + recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, + recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, + recover_stationary_latent_variance, recover_time_dependent_predictor_impulse, recover_trait_plus_state_lagged_covariance, recover_trait_plus_state_latent_variance, recover_within_residual_event_time_log_rate, refuse_continuous_intercept_as_discrete_mean_increment, @@ -1612,8 +1747,11 @@ mod tests { refuse_measurement_error_as_lagged_observed_covariance, refuse_measurement_error_as_observed_variance, refuse_pooled_discrete_lag_across_unequal_intervals, - refuse_process_noise_as_unconditional_variance, refuse_trait_variance_as_process_noise, - refuse_trait_variance_as_stationary_within_subject, + refuse_process_noise_as_unconditional_variance, + refuse_time_dependent_impulse_as_continuous_intercept, + refuse_time_dependent_impulse_as_time_independent_effect, + refuse_time_dependent_impulse_as_time_varying_discrete_effect, + refuse_trait_variance_as_process_noise, refuse_trait_variance_as_stationary_within_subject, refuse_unmatched_time_varying_predictor_interval, }; use crate::error::PsychometricError; @@ -3493,4 +3631,148 @@ mod tests { Err(PsychometricError::InvalidNumericInput) ); } + + #[test] + fn time_dependent_impulse_recovers_driver_equation_three_fourth_summand() { + let effect = 0.4_f64; + let predictor = 3.0_f64; + let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("tdpred"); + assert!((impulse - 1.2).abs() < 1e-15); + assert_eq!( + recover_time_dependent_predictor_impulse(0.0, predictor), + Ok(0.0) + ); + assert_eq!( + recover_time_dependent_predictor_impulse(effect, 0.0), + Ok(0.0) + ); + let drift = -0.5_f64; + let delta = 2.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let composed = recover_discrete_latent_mean_with_impulse( + initial, + drift, + intercept, + effect, + predictor, + delta, + LagClock::EventTime, + ) + .expect("eq3-impulse"); + let evolved = + recover_discrete_latent_mean(initial, drift, intercept, delta, LagClock::EventTime) + .expect("mu-t"); + assert!((composed - (evolved + impulse)).abs() < 1e-15); + assert_eq!( + recover_discrete_latent_mean_with_impulse( + initial, + drift, + intercept, + 0.0, + predictor, + delta, + LagClock::EventTime + ), + Ok(evolved) + ); + let intercept_effect = + recover_discrete_continuous_intercept_effect(effect, drift, delta, LagClock::EventTime) + .expect("cint"); + assert!((impulse - intercept_effect).abs() > 1e-3); + let equation_fourteen = recover_discrete_time_varying_predictor_effect( + effect, + delta, + delta, + delta, + LagClock::EventTime, + ) + .expect("eq14"); + assert!((impulse - equation_fourteen).abs() > 1e-3); + } + + #[test] + fn time_dependent_impulse_refuses_cint_tipred_and_equation_fourteen() { + let effect = 0.4_f64; + let predictor = 2.0_f64; + let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("tdpred"); + let intercept_effect = + recover_discrete_continuous_intercept_effect(effect, -0.5, 2.0, LagClock::EventTime) + .expect("cint"); + let equation_fourteen = recover_discrete_time_varying_predictor_effect( + effect, + 2.0, + 2.0, + 2.0, + LagClock::EventTime, + ) + .expect("eq14"); + assert_eq!( + refuse_time_dependent_impulse_as_continuous_intercept(impulse, effect), + Err(PsychometricError::TimeDependentImpulseIsNotContinuousIntercept) + ); + assert_eq!( + refuse_time_dependent_impulse_as_time_independent_effect(impulse, intercept_effect), + Err(PsychometricError::TimeDependentImpulseIsNotTimeIndependentEffect) + ); + assert_eq!( + refuse_time_dependent_impulse_as_time_varying_discrete_effect( + impulse, + equation_fourteen + ), + Err(PsychometricError::TimeDependentImpulseIsNotTimeVaryingDiscreteEffect) + ); + } + + #[test] + fn time_dependent_impulse_invalid_inputs_fail_closed() { + assert_eq!( + recover_time_dependent_predictor_impulse(f64::NAN, 1.0), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_time_dependent_predictor_impulse(1.0, f64::INFINITY), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_time_dependent_predictor_impulse(1e308, 2.0), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_latent_mean_with_impulse( + 1.0, + -0.5, + 0.3, + 0.4, + 2.0, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_latent_mean_with_impulse( + 1.0, + -0.5, + 0.3, + 0.4, + 2.0, + 2.0, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_discrete_latent_mean_with_impulse( + 1e308, + 0.0, + 0.0, + 1e308, + 1.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + } } diff --git a/crates/psychometric_core/src/lib.rs b/crates/psychometric_core/src/lib.rs index b0fb5597..6727764a 100644 --- a/crates/psychometric_core/src/lib.rs +++ b/crates/psychometric_core/src/lib.rs @@ -33,7 +33,9 @@ //! mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (`T0MEANS` is not //! `μ_t`; `CINT` is not the discrete increment), recovers the //! Driver Eq. 5 of that evolved mean as `τ + λ μ_t` (the -//! first-occasion map `τ + λ μ_0` is not `E(y_t)`), +//! first-occasion map `τ + λ μ_0` is not `E(y_t)`), recovers the +//! Driver Eq. 3 fourth-summand impulse `m x` (Table 2 `TDPREDEFFECT` +//! is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), //! and refuses //! latent-mean comparison below strong invariance. @@ -94,6 +96,8 @@ pub use event_time::recover_discrete_lag_one; pub use event_time::recover_discrete_lagged_latent_covariance; /// Exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`. pub use event_time::recover_discrete_latent_mean; +/// Exact scalar evolved latent mean plus a contemporaneous impulse. +pub use event_time::recover_discrete_latent_mean_with_impulse; /// Exact scalar discrete latent variance `A_Δt P A_Δt⊤ + Q_Δt`. pub use event_time::recover_discrete_latent_variance; /// Exact scalar discrete observed mean `τ + λ μ_t` from Eq. 3 then Eq. 5. @@ -120,6 +124,8 @@ pub use event_time::recover_manifest_observed_variance; pub use event_time::recover_manifest_trait_plus_state_observed_variance; /// Exact scalar stationary within-subject variance `-q / (2 a)`. pub use event_time::recover_stationary_latent_variance; +/// Exact scalar contemporaneous `TDPREDEFFECT` impulse `m x`. +pub use event_time::recover_time_dependent_predictor_impulse; /// Exact scalar trait-plus-state lagged covariance. pub use event_time::recover_trait_plus_state_lagged_covariance; /// Exact scalar trait-plus-state latent variance. @@ -158,6 +164,12 @@ pub use event_time::refuse_measurement_error_as_observed_variance; pub use event_time::refuse_pooled_discrete_lag_across_unequal_intervals; /// Refuse treating Driver Eq. 3 process noise as the unconditional variance. pub use event_time::refuse_process_noise_as_unconditional_variance; +/// Refuse treating Driver Eq. 3 `TDPREDEFFECT` impulse as `CINT`. +pub use event_time::refuse_time_dependent_impulse_as_continuous_intercept; +/// Refuse treating Driver Eq. 3 impulse as `TIPREDEFFECT`. +pub use event_time::refuse_time_dependent_impulse_as_time_independent_effect; +/// Refuse treating Driver Eq. 3 impulse as Voelkle Eq. 14. +pub use event_time::refuse_time_dependent_impulse_as_time_varying_discrete_effect; /// Refuse treating Driver §4.3 trait variance as process noise. pub use event_time::refuse_trait_variance_as_process_noise; /// Refuse treating Driver §4.3 trait variance as `asymDIFFUSION`. 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 451c515c..d0c0ff51 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -7,14 +7,16 @@ use psychometric_core::{ ordinary_least_squares_slope, recover_cluster_mean_within_between_slopes, recover_discrete_constant_predictor_effect, recover_discrete_continuous_intercept_effect, recover_discrete_lag_from_log_rate, recover_discrete_lagged_latent_covariance, - recover_discrete_latent_mean, recover_discrete_latent_variance, recover_discrete_observed_mean, + recover_discrete_latent_mean, recover_discrete_latent_mean_with_impulse, + recover_discrete_latent_variance, recover_discrete_observed_mean, recover_discrete_process_noise, recover_discrete_time_varying_predictor_effect, recover_event_series_mean_log_rate, recover_event_time_discrete_lag_and_log_rate, recover_irregular_centered_residual_log_rate, recover_kish_weighted_slope, recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, - recover_stationary_latent_variance, recover_trait_plus_state_lagged_covariance, - recover_trait_plus_state_latent_variance, recover_within_residual_event_time_log_rate, + recover_stationary_latent_variance, recover_time_dependent_predictor_impulse, + recover_trait_plus_state_lagged_covariance, recover_trait_plus_state_latent_variance, + recover_within_residual_event_time_log_rate, refuse_continuous_intercept_as_discrete_mean_increment, refuse_continuous_intercept_as_initial_latent_mean, refuse_continuous_intercept_as_manifest_means, refuse_difference_quotient_as_local_rate, @@ -27,8 +29,11 @@ use psychometric_core::{ refuse_measurement_error_as_lagged_observed_covariance, refuse_measurement_error_as_observed_variance, refuse_pooled_discrete_lag_across_unequal_intervals, - refuse_process_noise_as_unconditional_variance, refuse_trait_variance_as_process_noise, - refuse_trait_variance_as_stationary_within_subject, + refuse_process_noise_as_unconditional_variance, + refuse_time_dependent_impulse_as_continuous_intercept, + refuse_time_dependent_impulse_as_time_independent_effect, + refuse_time_dependent_impulse_as_time_varying_discrete_effect, + refuse_trait_variance_as_process_noise, refuse_trait_variance_as_stationary_within_subject, refuse_unmatched_time_varying_predictor_interval, }; @@ -1204,6 +1209,97 @@ fn discrete_observed_mean_refuses_first_occasion_and_overflow() { ); } +#[test] +fn time_dependent_impulse_recovers_driver_equation_three_fourth_summand() { + let effect = 0.4_f64; + let predictor = 3.0_f64; + let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("tdpred"); + let error = rmse(&[1.2], &[impulse]); + assert!( + error < 1e-15, + "Driver Eq. 3 fourth summand RMSE {error}: got {impulse}" + ); + let drift = -0.5_f64; + let delta = 2.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let composed = recover_discrete_latent_mean_with_impulse( + initial, + drift, + intercept, + effect, + predictor, + delta, + LagClock::EventTime, + ) + .expect("eq3-impulse"); + let evolved = + recover_discrete_latent_mean(initial, drift, intercept, delta, LagClock::EventTime) + .expect("mu-t"); + let composed_error = rmse(&[evolved + impulse], &[composed]); + assert!( + composed_error < 1e-15, + "Driver Eq. 3 μ_t + m x RMSE {composed_error}: got {composed}" + ); + let intercept_effect = + recover_discrete_continuous_intercept_effect(effect, drift, delta, LagClock::EventTime) + .expect("cint"); + let equation_fourteen = recover_discrete_time_varying_predictor_effect( + effect, + delta, + delta, + delta, + LagClock::EventTime, + ) + .expect("eq14"); + assert!((impulse - intercept_effect).abs() > 1e-3); + assert!((impulse - equation_fourteen).abs() > 1e-3); + assert_eq!( + refuse_time_dependent_impulse_as_continuous_intercept(impulse, effect), + Err(PsychometricError::TimeDependentImpulseIsNotContinuousIntercept) + ); + assert_eq!( + refuse_time_dependent_impulse_as_time_independent_effect(impulse, intercept_effect), + Err(PsychometricError::TimeDependentImpulseIsNotTimeIndependentEffect) + ); + assert_eq!( + refuse_time_dependent_impulse_as_time_varying_discrete_effect(impulse, equation_fourteen), + Err(PsychometricError::TimeDependentImpulseIsNotTimeVaryingDiscreteEffect) + ); +} + +#[test] +fn time_dependent_impulse_refuses_overflow_and_non_event_clocks() { + assert_eq!( + recover_time_dependent_predictor_impulse(1e308, 2.0), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_latent_mean_with_impulse( + 1.0, + -0.5, + 0.3, + 0.4, + 2.0, + 2.0, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_discrete_latent_mean_with_impulse( + 1e308, + 0.0, + 0.0, + 1e308, + 1.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); +} + #[test] fn admitted_coordinates_still_required_for_multilevel_weights() { assert_eq!( diff --git a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs index 51061533..446bad25 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -5,13 +5,14 @@ use psychometric_core::{ ordinary_least_squares_slope, posterior_draw_point_estimate_mean, recover_cluster_mean_within_between_slopes, recover_discrete_constant_predictor_effect, recover_discrete_continuous_intercept_effect, recover_discrete_lagged_latent_covariance, - recover_discrete_latent_mean, recover_discrete_latent_variance, recover_discrete_observed_mean, + recover_discrete_latent_mean, recover_discrete_latent_mean_with_impulse, + recover_discrete_latent_variance, recover_discrete_observed_mean, recover_discrete_process_noise, recover_discrete_time_varying_predictor_effect, recover_irregular_centered_residual_log_rate, recover_loading_point_estimate_mean, recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, - recover_stationary_latent_variance, recover_trait_plus_state_latent_variance, - recover_within_residual_event_time_log_rate, + recover_stationary_latent_variance, recover_time_dependent_predictor_impulse, + recover_trait_plus_state_latent_variance, recover_within_residual_event_time_log_rate, refuse_continuous_intercept_as_discrete_mean_increment, refuse_continuous_intercept_as_initial_latent_mean, refuse_continuous_intercept_as_manifest_means, @@ -23,6 +24,9 @@ use psychometric_core::{ refuse_manifest_trait_variance_as_measurement_error, refuse_measurement_error_as_lagged_observed_covariance, refuse_measurement_error_as_observed_variance, refuse_process_noise_as_unconditional_variance, + refuse_time_dependent_impulse_as_continuous_intercept, + refuse_time_dependent_impulse_as_time_independent_effect, + refuse_time_dependent_impulse_as_time_varying_discrete_effect, refuse_trait_variance_as_process_noise, refuse_trait_variance_as_stationary_within_subject, }; @@ -539,3 +543,63 @@ fn first_occasion_observed_mean_is_not_evolved_observed_mean() { Err(psychometric_core::PsychometricError::ManifestMeansIsNotObservedMean) ); } + +#[test] +fn time_dependent_impulse_is_not_cint_tipred_or_equation_fourteen() { + let effect = 0.4_f64; + let predictor = 3.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("tdpred"); + let intercept_effect = + recover_discrete_continuous_intercept_effect(effect, drift, delta, LagClock::EventTime) + .expect("cint"); + let equation_fourteen = recover_discrete_time_varying_predictor_effect( + effect, + delta, + delta, + delta, + LagClock::EventTime, + ) + .expect("eq14"); + let evolved = + recover_discrete_latent_mean(1.0, drift, 0.3, delta, LagClock::EventTime).expect("mu-t"); + let composed = recover_discrete_latent_mean_with_impulse( + 1.0, + drift, + 0.3, + effect, + predictor, + delta, + LagClock::EventTime, + ) + .expect("eq3-impulse"); + assert!( + (impulse - effect).abs() > 1e-3, + "Driver et al. (2017, Eq. 3 / Table 2 p. 12): TDPREDEFFECT is not CINT" + ); + assert!( + (impulse - intercept_effect).abs() > 1e-3, + "Driver et al. (2017, Eq. 3): M x is not the time-independent discrete effect" + ); + assert!( + (impulse - equation_fourteen).abs() > 1e-3, + "Driver et al. (2017, Eq. 3): M x is not Voelkle Eq. 14" + ); + assert!( + (composed - evolved).abs() > 1e-3, + "Driver et al. (2017, Eq. 3): μ_t is not μ_t + M x" + ); + assert_eq!( + refuse_time_dependent_impulse_as_continuous_intercept(impulse, effect), + Err(psychometric_core::PsychometricError::TimeDependentImpulseIsNotContinuousIntercept) + ); + assert_eq!( + refuse_time_dependent_impulse_as_time_independent_effect(impulse, intercept_effect), + Err(psychometric_core::PsychometricError::TimeDependentImpulseIsNotTimeIndependentEffect) + ); + assert_eq!( + refuse_time_dependent_impulse_as_time_varying_discrete_effect(impulse, equation_fourteen), + Err(psychometric_core::PsychometricError::TimeDependentImpulseIsNotTimeVaryingDiscreteEffect) + ); +} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 839f2dbd..091f12c6 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -28,7 +28,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | report template/section/copied/style/modality method effects | ADR 0004/0012; PRD/TRD | simulation truth factors implemented; estimator-side method model remains future | partial | | candidate K statistical/Pareto gates + blinded LLM review | ADR 0012; research | future `model_selection` | accepted-target | | compositional topic correlation / stable clustering | ADR 0005/0012; research | future `network_analysis` | accepted-target | -| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z); full ESEM/DSEM remaining | partial | +| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z); full ESEM/DSEM remaining | partial | | CPU bounded multithreading + GPU/VRAM streaming/parity | ADR 0001/0006 | future `compute_backend` | accepted-target | | TDT detection/tracking vs CHRONOS schema/prediction/temporal consistency | ADR 0016; PRD/research | future `event_intelligence` | accepted-target | | evidence-bounded LLM interpretation | ADR 0010/0012; PRD | future `interpretation_gateway` | accepted-target | diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index a942a221..1ddd14bf 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -1,7 +1,7 @@ # ADR 0005 — Posterior-aware ESEM/DSEM and structural interpretation **Decision status:** Accepted -**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target +**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), exact scalar contemporaneous `TDPREDEFFECT` impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; the §7.2 level-change form is not that impulse), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target **Date:** 2026-08-05 **Supersedes:** None. ADR 0012 governs upstream topic measurement/network coordinates; this ADR governs higher-order psychometric structure and longitudinal interpretation. @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), recovers the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z; `TDPREDEFFECT` is `M`, not `CINT`; `M x` is not `A^{-1}[e^{A Δt} − I] B z`; `M x` is not Voelkle et al., 2012, Eq. 14; the level-change form is not that impulse), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. 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 1dd3de89..86fdc35f 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -22,15 +22,16 @@ This slice stays inside `psychometric_core`. It does not add a second invariance 16. recover the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`) and refuse treating the intercept, latent mean, or continuous intercept as `E(y)` / `τ`; 17. recover the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; Table 2 `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment) and refuse treating `T0MEANS` or `CINT` as `μ_t`; 18. recover the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`) and refuse treating that first-occasion mean as `E(y_t)`; -19. refuse pooling discrete lags from unequal event intervals as one coefficient; -20. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); -21. refuse the difference quotient as a continuous-time rate; -22. apply the same event-time map to CWC residuals (still not DSEM); -23. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). +19. recover the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`) and refuse treating that impulse as `CINT`, `TIPREDEFFECT`, or Voelkle et al. (2012, Eq. 14); +20. refuse pooling discrete lags from unequal event intervals as one coefficient; +21. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); +22. refuse the difference quotient as a continuous-time rate; +23. apply the same event-time map to CWC residuals (still not DSEM); +24. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). ## Claim boundary -This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. +This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. The contemporaneous `TDPREDEFFECT` impulse is not the continuous intercept, not the time-independent discrete effect, and not Voelkle et al. (2012, Eq. 14). ## Authoritative sources @@ -69,6 +70,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - **Observed-indicator mean.** Driver et al. (2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z): \(y_i(t)=\Gamma+\Lambda\eta_i(t)+\zeta_i(t)\) with \(\zeta\sim N(0,\Theta)\) and \(\Gamma\sim N(\tau,\Psi)\). Table 2 names \(\tau\) `MANIFESTMEANS`, \(\kappa\) `CINT`, and the first-occasion latent mean `T0MEANS`. The scalar map is \(E(y)=\tau+\lambda\mu\). Form \(\lambda\mu\) then add \(\tau\). A zero loading or zero latent mean is exactly \(\tau\). A zero intercept is exactly \(\lambda\mu\). `MANIFESTMEANS` is not \(E(y)\). \(E(\eta)\) is not \(E(y)\). `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not \(E(y)\). An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **Discrete latent mean.** Driver et al. (2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z): \(\eta(t)=\exp(A\Delta t)\eta(t_0)+\int\exp(A(t-s))(b+\cdots)\,ds\) plus a stochastic integral of mean zero. Table 2 names the first-occasion latent mean `T0MEANS` and \(\kappa\) `CINT`. The scalar map is \(\mu_t=\exp(a\Delta t)\mu_0+(\exp(a\Delta t)-1)/a\,\kappa\). Form the `CINT` increment first, then add the carried `T0MEANS` term. A zero drift is the Eq. 3 integral \(\kappa\Delta t\) (\(A=0\) has no inverse). A zero intercept is exactly \(\exp(a\Delta t)\mu_0\). A zero initial mean is exactly the increment. As \(\Delta t\to\infty\) with stable \(a<0\), \(\mu_t\to-\kappa/a\). Binary64 underflow of \(\exp(a\Delta t)\) to `+0` drops the carried `T0MEANS` and keeps that equilibrium increment. `T0MEANS` is not \(\mu_t\). `CINT` is not the discrete increment. `CINT` is not `T0MEANS`. An overflowing exponential, product, or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **Discrete observed-indicator mean.** Driver et al. (2017, Eq. 3, p. 5; Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T22:10Z): `η_i(t)=exp(AΔt)η_i(t0)+A^{-1}[exp(AΔt)−I]ξ_i+…` with `ξ_i∼N(κ,φ_ξ)` and a stochastic integral of mean zero, then `y_i(t)=Γ_i+Λη_i(t)+ζ_i(t)` with `Γ∼N(τ,Ψ)`. The scalar composition is `E(y_t)=τ+λμ_t`. Form `μ_t` first, then `τ+λμ_t`. A zero loading or zero evolved latent mean is exactly `τ`. A zero intercept is exactly `λμ_t`. A zero drift is `τ+λ(μ_0+κΔt)`. Underflow of `exp(aΔt)` to `+0` keeps `τ+λ(−κ/a)`. The first-occasion map `τ+λμ_0` is not `E(y_t)`. `MANIFESTMEANS` is not `E(y_t)`. `T0MEANS` is not `E(y_t)`. `μ_t` is not `E(y_t)`. An overflowing exponential, product, or sum fails closed. This is not a Kalman filter and not ctsem estimation. +- **Time-dependent predictor impulse.** Driver et al. (2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z): `χ_i(t)=Σ x_{i,u} δ(t−u)` and the fourth summand is `M Σ x_{i,u} δ(t−u)`. Table 2 names `M` `TDPREDEFFECT`. Section 7.2 calls this a sudden impulse that dissipates back to the process mean. The scalar contemporaneous jump is `m x`. Form `μ_t` first, then add `m x`. A zero effect or zero predictor is exactly zero. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{AΔt}−I] B z` (`TIPREDEFFECT`). `M x` is not Voelkle et al. (2012, Eq. 14) `a_{yx}Δt`. The §7.2 level-change form is an extra latent process with near-zero drift and is not this map. An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **CWC-then-lag.** Sample cluster means are removed first. Consecutive within residuals are then fitted by least squares to \(r_{t+\Delta t}\approx\exp(a\Delta t)\,r_{t}\) on event time. Same-sign pair-wise logs initialize the scalar Newton step. Sign-flipping \(T=2\) CWC pairs have no real logarithm and fail closed. Curran and Bauer (2011, pp. 607–608) show that this person-mean subtraction on a raw autoregressive series does **not** isolate the lagged within-person effect; the helper therefore does not claim to recover the raw-process drift. - **Already-centered irregular residual.** The caller supplies lagged within residuals. The mean of \(a=\ln(r_{t+\Delta t}/r_t)/\Delta t\) is the exact scalar map. Intervals may be irregular. The helper does not center again. This is not DSEM. @@ -91,6 +93,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - Driver et al. (2017, Eq. 5 observed mean; Table 2, p. 12) recovers a known \(E(y)=\tau+\lambda\mu\) at machine-scale RMSE, and that RMSE is smaller than treating `MANIFESTMEANS` or \(E(\eta)\) as \(E(y)\); a zero loading or zero latent mean is \(\tau\); `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not \(E(y)\); an overflowing product or sum fails closed; - Driver et al. (2017, Eq. 3 expected-value latent mean; Table 2, p. 12) recovers a known \(\mu_t=\exp(a\Delta t)\mu_0+(\exp(a\Delta t)-1)/a\,\kappa\) at machine-scale RMSE, and that RMSE is smaller than treating `T0MEANS` or `CINT` as \(\mu_t\); a zero drift is \(\mu_0+\kappa\Delta t\); underflow of \(\exp(a\Delta t)\) to `+0` drops `T0MEANS` and keeps \(-\kappa/a\); `CINT` is not the discrete increment; an overflowing exponential, product, or sum fails closed; - Driver et al. (2017, Eq. 5 of the Eq. 3 evolved mean; Table 2, p. 12) recovers a known \(E(y_t)=\tau+\lambda\mu_t\) at machine-scale RMSE, and that RMSE is smaller than treating \(\tau+\lambda\mu_0\), `MANIFESTMEANS`, or \(\mu_t\) as \(E(y_t)\); a zero loading is \(\tau\); a zero drift is \(\tau+\lambda(\mu_0+\kappa\Delta t)\); underflow of \(\exp(a\Delta t)\) keeps \(\tau+\lambda(-\kappa/a)\); a zero-CINT overflow of \(\exp(a\Delta t)\) fails closed; an overflowing product or sum fails closed; +- Driver et al. (2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT`; §7.2) recovers a known contemporaneous impulse \(m x\) at machine-scale RMSE, and that RMSE is smaller than treating `CINT`, the time-independent discrete effect, or Voelkle et al. (2012, Eq. 14) as the impulse; composing \(\mu_t+m x\) recovers the known sum; a zero effect or zero predictor is exactly zero; an overflowing product or sum, a non-event clock, and a non-positive interval fail closed; - pooling discrete lags from unequal intervals fails closed; - CWC-then-lag on a two-cluster decaying series has smaller computed RMSE than a level-pooled series when the latter is identified; - already-centered irregular residuals recover a known drift at machine-scale RMSE, and that RMSE is smaller than CWC of the corresponding raw autoregressive series (Curran & Bauer, 2011, pp. 607–608); diff --git a/docs/research/posterior-esem-input-gates.md b/docs/research/posterior-esem-input-gates.md index d39eac03..f9bb4aa9 100644 --- a/docs/research/posterior-esem-input-gates.md +++ b/docs/research/posterior-esem-input-gates.md @@ -12,7 +12,7 @@ This slice delivers the first executable ADR 0005 contract in `psychometric_core 6. refuse latent-mean comparison without invariance evidence, and recover a mean difference only under strong or strict two-group OLS status (Putnick & Bornstein, 2016: scalar licenses means; residual invariance is not required; two-observation series cap at strong because residual variance is identically `0`); 7. refuse causal language that rests only on temporal precedence, document linkage, event tracking, or model prediction. -Cluster-mean CWC, the CWC contextual effect, Kish WLS, event-time log-rate, CWC-then-lag, irregular already-centered residual log-rate, and the Driver Eq. 5 of the Eq. 3 evolved mean live in the same crate and are documented in `docs/research/multilevel-event-time-recovery.md`. Full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target. +Cluster-mean CWC, the CWC contextual effect, Kish WLS, event-time log-rate, CWC-then-lag, irregular already-centered residual log-rate, the Driver Eq. 5 of the Eq. 3 evolved mean, and the Driver Eq. 3 contemporaneous `TDPREDEFFECT` impulse live in the same crate and are documented in `docs/research/multilevel-event-time-recovery.md`. Full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target. ## Authoritative sources diff --git a/docs/research/standards-and-literature.md b/docs/research/standards-and-literature.md index 3e1abd8c..ed28f8e4 100644 --- a/docs/research/standards-and-literature.md +++ b/docs/research/standards-and-literature.md @@ -36,7 +36,7 @@ Putnick, D. L., & Bornstein, M. H. (2016). Measurement invariance conventions an Holland, P. W. (1986). Statistics and causal inference. *Journal of the American Statistical Association, 81*(396), 945–960. https://doi.org/10.1080/01621459.1986.10478354 -TEPP applies these sources to construct definition, score interpretation, reliability, validity evidence, uncertainty, consequences, longitudinal invariance, ESEM cross-loadings, and DSEM. Topic outputs are treated as fallible indicators or components only after their construct role is evaluated. Reflective, formative, and network classes remain distinct (Bollen & Lennox, 1991). Complete-data OLS loadings across posterior indicator draws are combined with Rubin (1996) \(T_m\); the arithmetic-mean helper remains a point estimate. Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-18T03:07Z: closed). The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title was opened 2026-08-17T12:04Z from archive.org; it is not the 1991 journal article and is not used as Mislevy plausible-value authority. Temporal precedence is not causal identification (Holland, 1986). Within/between OLS follows Enders and Tofighi (2007), Curran and Bauer (2011), and Hamaker et al. (2015). Enders and Tofighi (2007, Table 2, pp. 124–127; PDF opened 2026-08-17) show that the CWC cluster-mean coefficient is the contextual effect (`between − within`), not the between-cluster effect. Curran and Bauer (2011, pp. 607–608) reject person-mean subtraction on a raw autoregressive series as the lagged within-person residual; already-centered irregular residuals use the Voelkle et al. (2012, Eq. 7) / Driver et al. (2017, Eq. 3) scalar map. Discrete lags from unequal event intervals are remapped through that log-rate (Voelkle et al., 2012, ZORA accepted manuscript re-opened 2026-08-17T13:13Z) and are not pooled. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\) and restate the discrete intercept as a function of \(A\) and \(\Delta t\). A binary64 underflow of \(\exp(a\Delta t)\) to `+0` is refused because discrete auto-effects are strictly positive. The discrete effect of a constant predictor is Voelkle et al. (2012, Eq. 12; ZORA accepted manuscript re-opened 2026-08-17T14:20Z, Introducing Intercepts, manuscript p. 20), evaluated as \(a_{yx}(\operatorname{expm1}(z)/a_{xx})\) with \(z=a_{xx}\Delta t\) so a finite result is not lost when \(z\) overflows to \(-\infty\) or when \(a_{yx}\Delta t\) overflows, and in log space when `expm1(z)` overflows at a finite \(z\); a zero continuous effect is exactly zero; an overflowing \(a_{yx}/a_{xx}\) rewrite term fails closed; the first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. The discrete effect of a time-varying predictor with matched sampling and constancy intervals is Voelkle et al. (2012, Eq. 14; manuscript p. 21): \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product is not Eq. 12. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). The exact scalar discrete process noise is Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-18T14:04Z, p. 4): \(Q_{\Delta t}=0.5 q(\operatorname{expm1}(z)/a)\) with \(z=2(a\Delta t)\) for \(a\neq 0\) and \(q=GG^{\top}\ge 0\); do not form \(2a\) first; \(a=0\) recovers \(q\Delta t\); an overflowing rewrite scale \(0.5 q/a\) fails closed. This is not a Kalman filter. Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-18T14:04Z) write the same discrete intercept as \(A^{-1}[e^{A\Delta t}-I]\xi\). The lagged covariance is \(\mathrm{e}^{a\Delta t}p\) and the unconditional variance is \(\mathrm{e}^{2a\Delta t}p+Q_{\Delta t}\) (Driver et al., 2017, Eq. 3–4, pp. 4–5); a zero diffusion whose \(2(a\Delta t)\) overflows to \(+\infty\) fails closed. The stationary within-subject variance is the \(\Delta t\to\infty\) limit of Eq. 4: \(-q/(2a)\) for stable \(a<0\) (JSS p. 16 `asymDIFFUSION`; §4.3; PDF re-opened 2026-08-18T18:03Z). Finite-interval \(Q_{\Delta t}\) is not that limit. Trait-plus-state variance is \(\mathrm{trait}+\mathrm{state}\) and lagged covariance is \(\mathrm{trait}+\mathrm{e}^{a\Delta t}p\) (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z). Trait variance is not process noise and not `asymDIFFUSION`. Metric/weak invariance does not license latent-mean comparison. Putnick and Bornstein (2016, PMC author manuscript PMC5145197 opened 2026-08-19T22:15Z) require scalar invariance before latent-mean comparison and state that residual invariance is not a prerequisite. Two-observation OLS residual variance is identically `0` and is not strict. Meredith (1993) remains unread (Unpaywall/OpenAlex 2026-08-19T22:15Z: closed; Springer `content/pdf` is HTML 200). Vandenberg and Lance (2000) remains unread. Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex 2026-08-18T21:07Z: closed). ERIC ED334221 is Singer and Willett (1991), not the 1991 journal article. ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex 2026-08-18T21:07Z: closed). +TEPP applies these sources to construct definition, score interpretation, reliability, validity evidence, uncertainty, consequences, longitudinal invariance, ESEM cross-loadings, and DSEM. Topic outputs are treated as fallible indicators or components only after their construct role is evaluated. Reflective, formative, and network classes remain distinct (Bollen & Lennox, 1991). Complete-data OLS loadings across posterior indicator draws are combined with Rubin (1996) \(T_m\); the arithmetic-mean helper remains a point estimate. Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-18T03:07Z: closed). The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title was opened 2026-08-17T12:04Z from archive.org; it is not the 1991 journal article and is not used as Mislevy plausible-value authority. Temporal precedence is not causal identification (Holland, 1986). Within/between OLS follows Enders and Tofighi (2007), Curran and Bauer (2011), and Hamaker et al. (2015). Enders and Tofighi (2007, Table 2, pp. 124–127; PDF opened 2026-08-17) show that the CWC cluster-mean coefficient is the contextual effect (`between − within`), not the between-cluster effect. Curran and Bauer (2011, pp. 607–608) reject person-mean subtraction on a raw autoregressive series as the lagged within-person residual; already-centered irregular residuals use the Voelkle et al. (2012, Eq. 7) / Driver et al. (2017, Eq. 3) scalar map. Discrete lags from unequal event intervals are remapped through that log-rate (Voelkle et al., 2012, ZORA accepted manuscript re-opened 2026-08-17T13:13Z) and are not pooled. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\) and restate the discrete intercept as a function of \(A\) and \(\Delta t\). A binary64 underflow of \(\exp(a\Delta t)\) to `+0` is refused because discrete auto-effects are strictly positive. The discrete effect of a constant predictor is Voelkle et al. (2012, Eq. 12; ZORA accepted manuscript re-opened 2026-08-17T14:20Z, Introducing Intercepts, manuscript p. 20), evaluated as \(a_{yx}(\operatorname{expm1}(z)/a_{xx})\) with \(z=a_{xx}\Delta t\) so a finite result is not lost when \(z\) overflows to \(-\infty\) or when \(a_{yx}\Delta t\) overflows, and in log space when `expm1(z)` overflows at a finite \(z\); a zero continuous effect is exactly zero; an overflowing \(a_{yx}/a_{xx}\) rewrite term fails closed; the first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. The discrete effect of a time-varying predictor with matched sampling and constancy intervals is Voelkle et al. (2012, Eq. 14; manuscript p. 21): \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product is not Eq. 12. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). The exact scalar discrete process noise is Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-18T14:04Z, p. 4): \(Q_{\Delta t}=0.5 q(\operatorname{expm1}(z)/a)\) with \(z=2(a\Delta t)\) for \(a\neq 0\) and \(q=GG^{\top}\ge 0\); do not form \(2a\) first; \(a=0\) recovers \(q\Delta t\); an overflowing rewrite scale \(0.5 q/a\) fails closed. This is not a Kalman filter. Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-18T14:04Z) write the same discrete intercept as \(A^{-1}[e^{A\Delta t}-I]\xi\). The lagged covariance is \(\mathrm{e}^{a\Delta t}p\) and the unconditional variance is \(\mathrm{e}^{2a\Delta t}p+Q_{\Delta t}\) (Driver et al., 2017, Eq. 3–4, pp. 4–5); a zero diffusion whose \(2(a\Delta t)\) overflows to \(+\infty\) fails closed. The stationary within-subject variance is the \(\Delta t\to\infty\) limit of Eq. 4: \(-q/(2a)\) for stable \(a<0\) (JSS p. 16 `asymDIFFUSION`; §4.3; PDF re-opened 2026-08-18T18:03Z). Finite-interval \(Q_{\Delta t}\) is not that limit. Trait-plus-state variance is \(\mathrm{trait}+\mathrm{state}\) and lagged covariance is \(\mathrm{trait}+\mathrm{e}^{a\Delta t}p\) (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z). Trait variance is not process noise and not `asymDIFFUSION`. The first-occasion map `τ + λ μ_0` is not `E(y_t)`. The contemporaneous `TDPREDEFFECT` impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2; §7.2; JSS PDF re-opened 2026-08-20T07:10Z). `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). Metric/weak invariance does not license latent-mean comparison. Putnick and Bornstein (2016, PMC author manuscript PMC5145197 opened 2026-08-19T22:15Z) require scalar invariance before latent-mean comparison and state that residual invariance is not a prerequisite. Two-observation OLS residual variance is identically `0` and is not strict. Meredith (1993) remains unread (Unpaywall/OpenAlex 2026-08-19T22:15Z: closed; Springer `content/pdf` is HTML 200). Vandenberg and Lance (2000) remains unread. Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex 2026-08-18T21:07Z: closed). ERIC ED334221 is Singer and Willett (1991), not the 1991 journal article. ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex 2026-08-18T21:07Z: closed). ## Structural, correlated, dynamic, relational, and multilingual topic models diff --git a/docs/validation/temporal-event-foundation.md b/docs/validation/temporal-event-foundation.md index d734ed47..a598cb80 100644 --- a/docs/validation/temporal-event-foundation.md +++ b/docs/validation/temporal-event-foundation.md @@ -23,7 +23,7 @@ This report tracks exact-head scientific and engineering evidence required befor | Truth corpora / manifests | `tepp_simulation` | implemented-main | — | deterministic generator tests | Task 10 / PR #18 | | Recovery metrics | `validation_core` | implemented-main | — | RMSE/bias/coverage/MC gates | Task 11 / PR #19 | | Versioned API/export contracts | `tepp_api` | implemented-main | naruon HTTP interchange | unknown-field/version/limit + naruon HTTPS interchange tests | Task 12 / PR #21; live HTTP service remaining | -| Psychometric structural input gates | `psychometric_core` | partial | stacked psychometric PR | construct-class refusal + ALR/ILR boundary + true-loading RMSE + posterior-draw point-estimate mean + Rubin `T` + CWC within/between + CWC contextual effect + event-time log-rate + constant- and time-varying-predictor discrete effects + exact scalar discrete process noise + lagged latent covariance and unconditional latent variance + stationary within-subject variance + trait-plus-state variance + observed-indicator variance + discrete latent mean (`T0MEANS`/`CINT`) + evolved observed mean (`τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`) + irregular already-centered residual lag + strong-gated latent means (n=2 residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016); full ESEM/DSEM remaining | ADR 0005; `docs/research/posterior-esem-input-gates.md`; `docs/research/multilevel-event-time-recovery.md`; `docs/research/rubin-total-variance.md`; `docs/research/strong-invariance-latent-means.md` | +| Psychometric structural input gates | `psychometric_core` | partial | stacked psychometric PR | construct-class refusal + ALR/ILR boundary + true-loading RMSE + posterior-draw point-estimate mean + Rubin `T` + CWC within/between + CWC contextual effect + event-time log-rate + constant- and time-varying-predictor discrete effects + exact scalar discrete process noise + lagged latent covariance and unconditional latent variance + stationary within-subject variance + trait-plus-state variance + observed-indicator variance + discrete latent mean (`T0MEANS`/`CINT`) + evolved observed mean (`τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`) + contemporaneous `TDPREDEFFECT` impulse (`m x`; not `CINT`, not `TIPREDEFFECT`, not Voelkle Eq. 14) + irregular already-centered residual lag + strong-gated latent means (n=2 residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016); full ESEM/DSEM remaining | ADR 0005; `docs/research/posterior-esem-input-gates.md`; `docs/research/multilevel-event-time-recovery.md`; `docs/research/rubin-total-variance.md`; `docs/research/strong-invariance-latent-means.md` | | CWL modular connectors | `docs/connectors/*` | implemented-main | — | contract docs + examples | PR #22; live HTTP ports remaining | | Release SBOM/provenance generator | `scripts/release_evidence.py` | partial | — | generate+validate in CI | Task 13 partial / PR #28 | From f11ab00c481675fd4b1f8d78108e761734736dc3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 01:27:32 +0000 Subject: [PATCH 35/62] feat(psychometric): recover Driver Eq. 3 TIPREDEFFECT discrete map MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Driver, Oud, and Voelkle (2017, Eq. 1–3; Table 2; JSS PDF re-opened 2026-08-20T10:13Z) write the time-independent predictor in the second summand as A^{-1}[e^{A Δt} − I] B z. Table 2 names B TIPREDEFFECT. Form B z first, then the discrete intercept map. A zero drift is B z Δt. TIPREDEFFECT is not CINT, not TDPREDEFFECT, and not Voelkle Eq. 14. B is not the discrete increment. --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 1 + CLAUDE.md | 2 +- README.md | 2 +- crates/psychometric_core/Cargo.toml | 2 +- crates/psychometric_core/src/error.rs | 43 ++ crates/psychometric_core/src/event_time.rs | 449 +++++++++++++++++- crates/psychometric_core/src/lib.rs | 16 + ...multilevel_event_time_recovery_contract.rs | 137 +++++- .../scientific_claim_boundary_contract.rs | 96 +++- docs/TRACEABILITY.md | 4 +- docs/adr/0005-posterior-esem-dsem.md | 4 +- .../multilevel-event-time-recovery.md | 15 +- docs/research/posterior-esem-input-gates.md | 2 +- docs/validation/temporal-event-foundation.md | 4 +- 15 files changed, 737 insertions(+), 42 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 7ee00335..3f3abb4e 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -61,7 +61,7 @@ boundaries above remain the target modular MSA architecture. | `tepp_simulation` | known-truth temporal/event data generation | | `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics | | `tepp_api` | versioned DTO, schema, and export contracts | -| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | +| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | No crate exposes placeholder production behavior in Task 1. This prevents an empty façade from becoming a de facto public API before its invariants and tests diff --git a/CHANGELOG.md b/CHANGELOG.md index 9982b58b..3cd33950 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar time-independent predictor increment. Equation 1 writes `dη = (A η + b + A_{ηξ} ξ + B z) dt + G dW + M dχ`. Equation 3's second summand is `A^{-1}[e^{A Δt} − I](b + A_{ηξ} ξ + B z)`. Table 2 names `B` `TIPREDEFFECT`. Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. Form `μ_t` first, then add that increment. `TIPREDEFFECT` is `B`, not the discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x` (`TDPREDEFFECT`), and not Voelkle et al. (2012, Eq. 14) `a_{yx} Δt`. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993), Vandenberg and Lance (2000), and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T10:13Z: `is_oa: false`). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar contemporaneous time-dependent predictor impulse. Equation 2 writes `χ_i(t) = Σ x_{i,u} δ(t − u)`. Equation 3's fourth summand is `M Σ x_{i,u} δ(t − u)`. Table 2 names `M` `TDPREDEFFECT`. Section 7.2 calls this a sudden impulse that dissipates back to the process mean and reports `TDPREDEFFECT` as the initial impact. The scalar jump is `m x`. Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` (`TIPREDEFFECT`). `M x` is not Voelkle et al. (2012, Eq. 14) `a_{yx} Δt`. The §7.2 level-change form is a different specification and is not this map. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993), Vandenberg and Lance (2000), and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T07:10Z: `is_oa: false`). - `psychometric_core` caps two-observation two-group OLS residual invariance at strong/scalar. `ordinary_least_squares_fit` returns residual variance `0` when `n ≤ 2`; that identity is not an estimated residual and is not strict. Putnick and Bornstein (2016, PMC author manuscript PMC5145197 opened 2026-08-19T22:15Z from https://pmc.ncbi.nlm.nih.gov/articles/PMC5145197/) require scalar invariance before latent-mean comparison; residual invariance is not a prerequisite because residuals are not part of the latent factor. Matching loading and intercept with `n = 2` therefore stay strong/scalar and still license `(ȳ_c − ȳ_r)/λ`. This is still two-group OLS, not MGCFA. Meredith (1993) remains unread (OpenAlex/Semantic Scholar 2026-08-19T22:15Z: closed; Springer `content/pdf` is HTML 200). Vandenberg and Lance (2000) remains unread (cited by Putnick for the residual-not-required claim). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread. - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 3, p. 5; Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T22:10Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar evolved observed-indicator mean. Equation 3 writes `η_i(t) = exp(A Δt) η_i(t0) + A^{-1}[exp(A Δt) − I] ξ_i + …` with `ξ_i ~ N(κ, φ_ξ)` (p. 4) and a stochastic integral of mean zero. Equation 5 writes `y_i(t) = Γ_i + Λ η_i(t) + ζ_i(t)` with `Γ ~ N(τ, Ψ)` and `ζ ~ N(0, Θ)`. The scalar composition is `E(y_t) = τ + λ μ_t` with `μ_t` the Eq. 3 expected-value map. Form `μ_t` first, then `τ + λ μ_t`. The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `MANIFESTMEANS` is not `E(y_t)`. `T0MEANS` is not `E(y_t)`. `μ_t` is not `E(y_t)`. A zero-CINT overflow of `exp(a Δt)` fails the carried `T0MEANS` term closed (`a = 710`, `Δt = 1`). Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) remains unread (OpenAlex 2026-08-19T22:10Z: closed; Springer `content/pdf` is HTML 200). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (OpenAlex/Semantic Scholar 2026-08-19T22:10Z: closed). Putnick and Bornstein (2016) PMC PDF was HTML/500 on this cycle. Oud and Jansen (2000) remains unread. ZORA Anubis-blocked. Asparouhov and Muthén (2009) statmodel PDF re-opened 2026-08-19T22:10Z; it cites Meredith (1993) and discusses multiple-group intercept/mean structures but does not license this map. diff --git a/CLAUDE.md b/CLAUDE.md index 351beabf..bb198893 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. Putnick and Bornstein (2016, PMC5145197 opened 2026-08-19T22:15Z) require scalar invariance before latent-mean comparison; residual invariance is not a prerequisite. Two-observation series have no residual degrees of freedom (`ordinary_least_squares_fit` returns residual variance `0`) and cap at strong/scalar; they still license means. This is two-group OLS, not MGCFA. Meredith (1993) names remain unread labels. -- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. The contemporaneous time-dependent predictor impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`). Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). The §7.2 level-change form is not that impulse. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. The contemporaneous time-dependent predictor impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`). Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). The §7.2 level-change form is not that impulse. The time-independent predictor increment is `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`). Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. `TIPREDEFFECT` is `B`, not that discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle et al. (2012, Eq. 14). Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. diff --git a/README.md b/README.md index 0d00ceed..1486b42f 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ crates/corpus_split crates/tepp_simulation crates/validation_core crates/tepp_api -crates/psychometric_core # input gates, CWC/event-time/contextual, irregular residual lag, Rubin T, strong means; not a full ESEM/DSEM estimator +crates/psychometric_core # input gates, CWC/event-time/contextual, irregular residual lag, Rubin T, strong means, Driver Eq. 3 TDPRED/TIPRED maps; not a full ESEM/DSEM estimator ``` ## Local verification diff --git a/crates/psychometric_core/Cargo.toml b/crates/psychometric_core/Cargo.toml index 03915bab..d33ec543 100644 --- a/crates/psychometric_core/Cargo.toml +++ b/crates/psychometric_core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "psychometric_core" -description = "Posterior-aware ESEM/DSEM input gates, multilevel/event-time recovery, CWC contextual effect, Voelkle Eqs. 12 and 14, Rubin T, and strong-invariance latent means." +description = "Posterior-aware ESEM/DSEM input gates, multilevel/event-time recovery, CWC contextual effect, Voelkle Eqs. 12 and 14, Driver Eq. 3 TDPRED/TIPRED maps, Rubin T, and strong-invariance latent means." version.workspace = true edition.workspace = true rust-version.workspace = true diff --git a/crates/psychometric_core/src/error.rs b/crates/psychometric_core/src/error.rs index 2f9d9f60..08ab3a1d 100644 --- a/crates/psychometric_core/src/error.rs +++ b/crates/psychometric_core/src/error.rs @@ -111,9 +111,24 @@ pub enum PsychometricError { /// Driver Eq. 3 fourth-summand impulse was treated as Voelkle /// et al. (2012, Eq. 14). `M x` is not `a_{yx} Δt`. TimeDependentImpulseIsNotTimeVaryingDiscreteEffect, + /// Driver Eq. 3 time-independent discrete effect was treated as + /// `CINT`. `A^{-1}[e^{A Δt} − I] B z` is not `κ`. + TimeIndependentEffectIsNotContinuousIntercept, + /// Driver Eq. 3 time-independent discrete effect was treated as + /// the fourth-summand impulse. `A^{-1}[e^{A Δt} − I] B z` is not + /// `M x`. + TimeIndependentEffectIsNotTimeDependentImpulse, + /// Driver Eq. 3 time-independent discrete effect was treated as + /// Voelkle et al. (2012, Eq. 14). `A^{-1}[e^{A Δt} − I] B z` is + /// not `a_{yx} Δt`. + TimeIndependentEffectIsNotTimeVaryingDiscreteEffect, + /// Driver Table 2 `TIPREDEFFECT` was treated as the discrete + /// increment. `B` is not `A^{-1}[e^{A Δt} − I] B z`. + TimeIndependentCoefficientIsNotDiscreteEffect, } impl fmt::Display for PsychometricError { + #[allow(clippy::too_many_lines)] fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { let message = match self { Self::RawProportionForbidden => { @@ -206,6 +221,18 @@ impl fmt::Display for PsychometricError { Self::TimeDependentImpulseIsNotTimeVaryingDiscreteEffect => { "time-dependent predictor impulse is not the time-varying discrete effect" } + Self::TimeIndependentEffectIsNotContinuousIntercept => { + "time-independent predictor effect is not the continuous intercept" + } + Self::TimeIndependentEffectIsNotTimeDependentImpulse => { + "time-independent predictor effect is not the time-dependent impulse" + } + Self::TimeIndependentEffectIsNotTimeVaryingDiscreteEffect => { + "time-independent predictor effect is not the time-varying discrete effect" + } + Self::TimeIndependentCoefficientIsNotDiscreteEffect => { + "time-independent predictor coefficient is not the discrete effect" + } }; formatter.write_str(message) } @@ -371,5 +398,21 @@ mod tests { PsychometricError::TimeDependentImpulseIsNotTimeVaryingDiscreteEffect.to_string(), "time-dependent predictor impulse is not the time-varying discrete effect" ); + assert_eq!( + PsychometricError::TimeIndependentEffectIsNotContinuousIntercept.to_string(), + "time-independent predictor effect is not the continuous intercept" + ); + assert_eq!( + PsychometricError::TimeIndependentEffectIsNotTimeDependentImpulse.to_string(), + "time-independent predictor effect is not the time-dependent impulse" + ); + assert_eq!( + PsychometricError::TimeIndependentEffectIsNotTimeVaryingDiscreteEffect.to_string(), + "time-independent predictor effect is not the time-varying discrete effect" + ); + assert_eq!( + PsychometricError::TimeIndependentCoefficientIsNotDiscreteEffect.to_string(), + "time-independent predictor coefficient is not the discrete effect" + ); } } diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index 4c5b553a..37f55908 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -41,7 +41,10 @@ //! (`T0MEANS` is not `μ_t`; `CINT` is not that discrete increment). Equation 3's //! fourth summand is the contemporaneous Dirac impulse `M x` (Table 2 //! `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not -//! Voelkle et al., 2012, Eq. 14). The JSS article +//! Voelkle et al., 2012, Eq. 14). Equation 3's second summand also +//! maps the time-independent predictor as `A^{-1}[e^{A Δt} − I] B z` +//! (Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle +//! Eq. 14). The JSS article //! has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). //! The difference quotient `(x(t+Δt) − x(t)) / Δt` (their //! Eqs. 3–4) is refused. This is not DSEM and not a matrix `expm`. @@ -1422,6 +1425,181 @@ pub fn refuse_time_dependent_impulse_as_time_varying_discrete_effect( Err(PsychometricError::TimeDependentImpulseIsNotTimeVaryingDiscreteEffect) } +/// Exact scalar discrete time-independent predictor effect from +/// Driver Equation 3. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; +/// JSS PDF re-opened 2026-08-20T10:13Z from +/// ) +/// write the latent SDE +/// `dη = (A η + b + A_{ηξ} ξ + B z) dt + G dW + M dχ`. Equation 3's +/// second summand is `A^{-1}[e^{A Δt} − I](b + A_{ηξ} ξ + B z)`. +/// Table 2 names `B` `TIPREDEFFECT`, `κ`/`b` `CINT`, and `M` +/// `TDPREDEFFECT`. The scalar map of the time-independent predictor +/// is `(e^{a Δt} − 1)/a · B z` for `a ≠ 0`. Form `B z` first, then +/// the discrete intercept map. A zero drift is the Eq. 3 integral +/// `B z Δt`. A zero effect or zero predictor is exactly zero. +/// `TIPREDEFFECT` is `B`, not that discrete increment. `B z` is not +/// `CINT`. `A^{-1}[e^{A Δt} − I] B z` is not the contemporaneous +/// impulse `M x` and is not Voelkle et al. (2012, Eq. 14) `a_{yx} Δt`. +/// This is not a Kalman filter and not ctsem estimation. +/// +/// # Errors +/// +/// Returns [`PsychometricError::EventTimeRequired`] for any non-event +/// clock, [`PsychometricError::NonPositiveInterval`] when +/// `event_delta` is not strictly positive, and +/// [`PsychometricError::InvalidNumericInput`] when an input is +/// non-finite or `B z` or the mapped increment overflows. +pub fn recover_discrete_time_independent_predictor_effect( + time_independent_effect: f64, + time_independent_predictor: f64, + log_rate: f64, + event_delta: f64, + clock: LagClock, +) -> Result { + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + if !event_delta.is_finite() || event_delta <= 0.0 { + return Err(PsychometricError::NonPositiveInterval); + } + if !time_independent_effect.is_finite() + || !time_independent_predictor.is_finite() + || !log_rate.is_finite() + { + return Err(PsychometricError::InvalidNumericInput); + } + if time_independent_effect == 0.0 || time_independent_predictor == 0.0 { + return Ok(0.0); + } + let continuous = require_finite(time_independent_effect * time_independent_predictor)?; + if log_rate == 0.0 { + return require_finite(continuous * event_delta); + } + recover_discrete_constant_predictor_effect(continuous, log_rate, event_delta, clock) +} + +/// Exact scalar evolved latent mean plus a time-independent predictor. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 3, p. 5) write the first two +/// summands as the carried `T0MEANS`, the `CINT` increment, and the +/// `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z`. Form `μ_t` +/// first, then add that increment. A zero time-independent increment +/// is exactly `μ_t`. A zero evolved mean is exactly the increment. +/// Adding `B z` to `μ_t` is not this map. Adding `M x` is not this +/// map. +/// +/// # Errors +/// +/// Propagates [`recover_discrete_latent_mean`] and +/// [`recover_discrete_time_independent_predictor_effect`], and +/// returns [`PsychometricError::InvalidNumericInput`] when the sum +/// overflows. +pub fn recover_discrete_latent_mean_with_time_independent_predictor( + initial_latent_mean: f64, + log_rate: f64, + continuous_intercept: f64, + time_independent_effect: f64, + time_independent_predictor: f64, + event_delta: f64, + clock: LagClock, +) -> Result { + let evolved_latent_mean = recover_discrete_latent_mean( + initial_latent_mean, + log_rate, + continuous_intercept, + event_delta, + clock, + )?; + let time_independent_increment = recover_discrete_time_independent_predictor_effect( + time_independent_effect, + time_independent_predictor, + log_rate, + event_delta, + clock, + )?; + if time_independent_increment == 0.0 { + return Ok(evolved_latent_mean); + } + if evolved_latent_mean == 0.0 { + return Ok(time_independent_increment); + } + require_finite(evolved_latent_mean + time_independent_increment) +} + +/// Refuse treating the Eq. 3 time-independent increment as `CINT`. +/// +/// Table 2 names `B` `TIPREDEFFECT` and `κ` `CINT`. The discrete +/// increment is `A^{-1}[e^{A Δt} − I] B z`. The continuous intercept +/// is not that increment. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::TimeIndependentEffectIsNotContinuousIntercept`]. +pub fn refuse_time_independent_effect_as_continuous_intercept( + time_independent_increment: f64, + continuous_intercept: f64, +) -> Result { + let _ = (time_independent_increment, continuous_intercept); + Err(PsychometricError::TimeIndependentEffectIsNotContinuousIntercept) +} + +/// Refuse treating the Eq. 3 time-independent increment as `M x`. +/// +/// The fourth-summand impulse is contemporaneous. The second-summand +/// `TIPREDEFFECT` map integrates `B z` over the event interval. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::TimeIndependentEffectIsNotTimeDependentImpulse`]. +pub fn refuse_time_independent_effect_as_time_dependent_impulse( + time_independent_increment: f64, + time_dependent_impulse: f64, +) -> Result { + let _ = (time_independent_increment, time_dependent_impulse); + Err(PsychometricError::TimeIndependentEffectIsNotTimeDependentImpulse) +} + +/// Refuse treating the Eq. 3 time-independent increment as Voelkle +/// et al. (2012, Eq. 14). +/// +/// Equation 14 is `a_{yx} Δt` for a piecewise-constant time-varying +/// predictor whose sampling interval equals its constancy interval. +/// `TIPREDEFFECT` integrates a constant `z` through the drift. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::TimeIndependentEffectIsNotTimeVaryingDiscreteEffect`]. +pub fn refuse_time_independent_effect_as_time_varying_discrete_effect( + time_independent_increment: f64, + time_varying_discrete_effect: f64, +) -> Result { + let _ = (time_independent_increment, time_varying_discrete_effect); + Err(PsychometricError::TimeIndependentEffectIsNotTimeVaryingDiscreteEffect) +} + +/// Refuse treating Driver Table 2 `TIPREDEFFECT` as the discrete +/// increment. +/// +/// `B` is the continuous-time coefficient. Equation 3 maps +/// `A^{-1}[e^{A Δt} − I] B z`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::TimeIndependentCoefficientIsNotDiscreteEffect`]. +pub fn refuse_time_independent_coefficient_as_discrete_effect( + time_independent_coefficient: f64, + time_independent_increment: f64, +) -> Result { + let _ = (time_independent_coefficient, time_independent_increment); + Err(PsychometricError::TimeIndependentCoefficientIsNotDiscreteEffect) +} + /// Refuse treating Driver Table 2 `T0MEANS` as the evolved latent mean. /// /// Equation 3 maps `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`. @@ -1725,15 +1903,16 @@ mod tests { recover_discrete_continuous_intercept_effect, recover_discrete_lag_from_log_rate, recover_discrete_lag_one, recover_discrete_lagged_latent_covariance, recover_discrete_latent_mean, recover_discrete_latent_mean_with_impulse, + recover_discrete_latent_mean_with_time_independent_predictor, recover_discrete_latent_variance, recover_discrete_observed_mean, - recover_discrete_process_noise, recover_discrete_time_varying_predictor_effect, - recover_event_series_mean_log_rate, recover_event_time_discrete_lag_and_log_rate, - recover_irregular_centered_residual_log_rate, recover_local_log_rate, - recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, - recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, - recover_stationary_latent_variance, recover_time_dependent_predictor_impulse, - recover_trait_plus_state_lagged_covariance, recover_trait_plus_state_latent_variance, - recover_within_residual_event_time_log_rate, + recover_discrete_process_noise, recover_discrete_time_independent_predictor_effect, + recover_discrete_time_varying_predictor_effect, recover_event_series_mean_log_rate, + recover_event_time_discrete_lag_and_log_rate, recover_irregular_centered_residual_log_rate, + recover_local_log_rate, recover_manifest_lagged_observed_covariance, + recover_manifest_observed_mean, recover_manifest_observed_variance, + recover_manifest_trait_plus_state_observed_variance, recover_stationary_latent_variance, + recover_time_dependent_predictor_impulse, recover_trait_plus_state_lagged_covariance, + recover_trait_plus_state_latent_variance, recover_within_residual_event_time_log_rate, refuse_continuous_intercept_as_discrete_mean_increment, refuse_continuous_intercept_as_initial_latent_mean, refuse_continuous_intercept_as_manifest_means, refuse_difference_quotient_as_local_rate, @@ -1751,6 +1930,10 @@ mod tests { refuse_time_dependent_impulse_as_continuous_intercept, refuse_time_dependent_impulse_as_time_independent_effect, refuse_time_dependent_impulse_as_time_varying_discrete_effect, + refuse_time_independent_coefficient_as_discrete_effect, + refuse_time_independent_effect_as_continuous_intercept, + refuse_time_independent_effect_as_time_dependent_impulse, + refuse_time_independent_effect_as_time_varying_discrete_effect, refuse_trait_variance_as_process_noise, refuse_trait_variance_as_stationary_within_subject, refuse_unmatched_time_varying_predictor_interval, }; @@ -3775,4 +3958,252 @@ mod tests { Err(PsychometricError::InvalidNumericInput) ); } + + #[test] + fn time_independent_predictor_recovers_driver_equation_three_second_summand() { + let effect = 0.4_f64; + let predictor = 3.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let increment = recover_discrete_time_independent_predictor_effect( + effect, + predictor, + drift, + delta, + LagClock::EventTime, + ) + .expect("tipred"); + let expected = + recover_discrete_constant_predictor_effect(1.2, drift, delta, LagClock::EventTime) + .expect("bz-map"); + assert!((increment - expected).abs() < 1e-15); + assert_eq!( + recover_discrete_time_independent_predictor_effect( + 0.0, + predictor, + drift, + delta, + LagClock::EventTime + ), + Ok(0.0) + ); + assert_eq!( + recover_discrete_time_independent_predictor_effect( + effect, + 0.0, + drift, + delta, + LagClock::EventTime + ), + Ok(0.0) + ); + let zero_drift = recover_discrete_time_independent_predictor_effect( + effect, + predictor, + 0.0, + delta, + LagClock::EventTime, + ) + .expect("zero-drift"); + assert!((zero_drift - 2.4).abs() < 1e-15); + let intercept_effect = + recover_discrete_continuous_intercept_effect(effect, drift, delta, LagClock::EventTime) + .expect("cint"); + let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("tdpred"); + let equation_fourteen = recover_discrete_time_varying_predictor_effect( + effect, + delta, + delta, + delta, + LagClock::EventTime, + ) + .expect("eq14"); + assert!((increment - intercept_effect).abs() > 1e-3); + assert!((increment - impulse).abs() > 1e-3); + assert!((increment - equation_fourteen).abs() > 1e-3); + assert!((increment - effect).abs() > 1e-3); + } + + #[test] + fn time_independent_predictor_composes_evolved_mean_and_keeps_scale() { + let effect = 0.4_f64; + let predictor = 3.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let increment = recover_discrete_time_independent_predictor_effect( + effect, + predictor, + drift, + delta, + LagClock::EventTime, + ) + .expect("tipred"); + let initial = 1.0_f64; + let intercept = 0.3_f64; + let composed = recover_discrete_latent_mean_with_time_independent_predictor( + initial, + drift, + intercept, + effect, + predictor, + delta, + LagClock::EventTime, + ) + .expect("eq3-tipred"); + let evolved = + recover_discrete_latent_mean(initial, drift, intercept, delta, LagClock::EventTime) + .expect("mu-t"); + assert!((composed - (evolved + increment)).abs() < 1e-15); + assert_eq!( + recover_discrete_latent_mean_with_time_independent_predictor( + initial, + drift, + intercept, + 0.0, + predictor, + delta, + LagClock::EventTime + ), + Ok(evolved) + ); + assert_eq!( + recover_discrete_latent_mean_with_time_independent_predictor( + 0.0, + drift, + 0.0, + effect, + predictor, + delta, + LagClock::EventTime + ), + Ok(increment) + ); + let scaled = recover_discrete_time_independent_predictor_effect( + 1e308, + 1e-308, + 0.0, + 1.0, + LagClock::EventTime, + ) + .expect("scale"); + assert!((scaled - 1.0).abs() < 1e-15); + } + + #[test] + fn time_independent_predictor_refuses_cint_impulse_equation_fourteen_and_coefficient() { + let effect = 0.4_f64; + let predictor = 3.0_f64; + let increment = recover_discrete_time_independent_predictor_effect( + effect, + predictor, + -0.5, + 2.0, + LagClock::EventTime, + ) + .expect("tipred"); + let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("tdpred"); + let equation_fourteen = recover_discrete_time_varying_predictor_effect( + effect, + 2.0, + 2.0, + 2.0, + LagClock::EventTime, + ) + .expect("eq14"); + assert_eq!( + refuse_time_independent_effect_as_continuous_intercept(increment, effect), + Err(PsychometricError::TimeIndependentEffectIsNotContinuousIntercept) + ); + assert_eq!( + refuse_time_independent_effect_as_time_dependent_impulse(increment, impulse), + Err(PsychometricError::TimeIndependentEffectIsNotTimeDependentImpulse) + ); + assert_eq!( + refuse_time_independent_effect_as_time_varying_discrete_effect( + increment, + equation_fourteen + ), + Err(PsychometricError::TimeIndependentEffectIsNotTimeVaryingDiscreteEffect) + ); + assert_eq!( + refuse_time_independent_coefficient_as_discrete_effect(effect, increment), + Err(PsychometricError::TimeIndependentCoefficientIsNotDiscreteEffect) + ); + } + + #[test] + fn time_independent_predictor_invalid_inputs_fail_closed() { + assert_eq!( + recover_discrete_time_independent_predictor_effect( + f64::NAN, + 1.0, + -0.5, + 2.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_time_independent_predictor_effect( + 1.0, + f64::INFINITY, + -0.5, + 2.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_time_independent_predictor_effect( + 1e308, + 2.0, + -0.5, + 2.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_time_independent_predictor_effect( + 1e308, + 1.0, + 0.0, + 2.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_time_independent_predictor_effect( + 0.4, + 3.0, + -0.5, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_time_independent_predictor_effect( + 0.4, + 3.0, + -0.5, + 2.0, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_discrete_latent_mean_with_time_independent_predictor( + 1e308, + 0.0, + 0.0, + 1.0, + 1e308, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + } } diff --git a/crates/psychometric_core/src/lib.rs b/crates/psychometric_core/src/lib.rs index 6727764a..a2e7a5e1 100644 --- a/crates/psychometric_core/src/lib.rs +++ b/crates/psychometric_core/src/lib.rs @@ -36,6 +36,10 @@ //! first-occasion map `τ + λ μ_0` is not `E(y_t)`), recovers the //! Driver Eq. 3 fourth-summand impulse `m x` (Table 2 `TDPREDEFFECT` //! is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), +//! recovers the Driver Eq. 3 second-summand time-independent +//! predictor increment `A^{-1}[e^{A Δt} − I] B z` (Table 2 +//! `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle Eq. 14; +//! `B` is not that discrete increment), //! and refuses //! latent-mean comparison below strong invariance. @@ -98,12 +102,16 @@ pub use event_time::recover_discrete_lagged_latent_covariance; pub use event_time::recover_discrete_latent_mean; /// Exact scalar evolved latent mean plus a contemporaneous impulse. pub use event_time::recover_discrete_latent_mean_with_impulse; +/// Exact scalar evolved latent mean plus a time-independent predictor. +pub use event_time::recover_discrete_latent_mean_with_time_independent_predictor; /// Exact scalar discrete latent variance `A_Δt P A_Δt⊤ + Q_Δt`. pub use event_time::recover_discrete_latent_variance; /// Exact scalar discrete observed mean `τ + λ μ_t` from Eq. 3 then Eq. 5. pub use event_time::recover_discrete_observed_mean; /// Exact scalar discrete process noise `Q_Δt` on event time. pub use event_time::recover_discrete_process_noise; +/// Exact scalar discrete `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z`. +pub use event_time::recover_discrete_time_independent_predictor_effect; /// First-order discrete effect of a time-varying event-time predictor. pub use event_time::recover_discrete_time_varying_predictor_effect; /// Mean local log-rate on a sorted event-time series. @@ -170,6 +178,14 @@ pub use event_time::refuse_time_dependent_impulse_as_continuous_intercept; pub use event_time::refuse_time_dependent_impulse_as_time_independent_effect; /// Refuse treating Driver Eq. 3 impulse as Voelkle Eq. 14. pub use event_time::refuse_time_dependent_impulse_as_time_varying_discrete_effect; +/// Refuse treating Driver Table 2 `TIPREDEFFECT` as the discrete increment. +pub use event_time::refuse_time_independent_coefficient_as_discrete_effect; +/// Refuse treating Driver Eq. 3 `TIPREDEFFECT` increment as `CINT`. +pub use event_time::refuse_time_independent_effect_as_continuous_intercept; +/// Refuse treating Driver Eq. 3 `TIPREDEFFECT` increment as `M x`. +pub use event_time::refuse_time_independent_effect_as_time_dependent_impulse; +/// Refuse treating Driver Eq. 3 `TIPREDEFFECT` increment as Voelkle Eq. 14. +pub use event_time::refuse_time_independent_effect_as_time_varying_discrete_effect; /// Refuse treating Driver §4.3 trait variance as process noise. pub use event_time::refuse_trait_variance_as_process_noise; /// Refuse treating Driver §4.3 trait variance as `asymDIFFUSION`. 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 d0c0ff51..34876cb0 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -8,15 +8,16 @@ use psychometric_core::{ recover_discrete_constant_predictor_effect, recover_discrete_continuous_intercept_effect, recover_discrete_lag_from_log_rate, recover_discrete_lagged_latent_covariance, recover_discrete_latent_mean, recover_discrete_latent_mean_with_impulse, - recover_discrete_latent_variance, recover_discrete_observed_mean, - recover_discrete_process_noise, recover_discrete_time_varying_predictor_effect, - recover_event_series_mean_log_rate, recover_event_time_discrete_lag_and_log_rate, - recover_irregular_centered_residual_log_rate, recover_kish_weighted_slope, - recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, - recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, - recover_stationary_latent_variance, recover_time_dependent_predictor_impulse, - recover_trait_plus_state_lagged_covariance, recover_trait_plus_state_latent_variance, - recover_within_residual_event_time_log_rate, + recover_discrete_latent_mean_with_time_independent_predictor, recover_discrete_latent_variance, + recover_discrete_observed_mean, recover_discrete_process_noise, + recover_discrete_time_independent_predictor_effect, + recover_discrete_time_varying_predictor_effect, recover_event_series_mean_log_rate, + recover_event_time_discrete_lag_and_log_rate, recover_irregular_centered_residual_log_rate, + recover_kish_weighted_slope, recover_manifest_lagged_observed_covariance, + recover_manifest_observed_mean, recover_manifest_observed_variance, + recover_manifest_trait_plus_state_observed_variance, recover_stationary_latent_variance, + recover_time_dependent_predictor_impulse, recover_trait_plus_state_lagged_covariance, + recover_trait_plus_state_latent_variance, recover_within_residual_event_time_log_rate, refuse_continuous_intercept_as_discrete_mean_increment, refuse_continuous_intercept_as_initial_latent_mean, refuse_continuous_intercept_as_manifest_means, refuse_difference_quotient_as_local_rate, @@ -33,6 +34,10 @@ use psychometric_core::{ refuse_time_dependent_impulse_as_continuous_intercept, refuse_time_dependent_impulse_as_time_independent_effect, refuse_time_dependent_impulse_as_time_varying_discrete_effect, + refuse_time_independent_coefficient_as_discrete_effect, + refuse_time_independent_effect_as_continuous_intercept, + refuse_time_independent_effect_as_time_dependent_impulse, + refuse_time_independent_effect_as_time_varying_discrete_effect, refuse_trait_variance_as_process_noise, refuse_trait_variance_as_stationary_within_subject, refuse_unmatched_time_varying_predictor_interval, }; @@ -1300,6 +1305,120 @@ fn time_dependent_impulse_refuses_overflow_and_non_event_clocks() { ); } +#[test] +fn time_independent_predictor_recovers_driver_equation_three_second_summand() { + let effect = 0.4_f64; + let predictor = 3.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let increment = recover_discrete_time_independent_predictor_effect( + effect, + predictor, + drift, + delta, + LagClock::EventTime, + ) + .expect("tipred"); + let expected = + recover_discrete_constant_predictor_effect(1.2, drift, delta, LagClock::EventTime) + .expect("bz-map"); + let error = rmse(&[expected], &[increment]); + assert!( + error < 1e-15, + "Driver Eq. 3 TIPREDEFFECT RMSE {error}: got {increment}" + ); + let intercept_effect = + recover_discrete_continuous_intercept_effect(effect, drift, delta, LagClock::EventTime) + .expect("cint"); + let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("tdpred"); + let equation_fourteen = recover_discrete_time_varying_predictor_effect( + effect, + delta, + delta, + delta, + LagClock::EventTime, + ) + .expect("eq14"); + assert!(rmse(&[increment], &[intercept_effect]) > rmse(&[expected], &[increment])); + assert!(rmse(&[increment], &[impulse]) > rmse(&[expected], &[increment])); + assert!(rmse(&[increment], &[equation_fourteen]) > rmse(&[expected], &[increment])); + assert!(rmse(&[increment], &[effect]) > rmse(&[expected], &[increment])); + let composed = recover_discrete_latent_mean_with_time_independent_predictor( + 1.0, + drift, + 0.3, + effect, + predictor, + delta, + LagClock::EventTime, + ) + .expect("eq3-tipred"); + let evolved = + recover_discrete_latent_mean(1.0, drift, 0.3, delta, LagClock::EventTime).expect("mu-t"); + let composed_error = rmse(&[evolved + increment], &[composed]); + assert!( + composed_error < 1e-15, + "Driver Eq. 3 μ_t + A^{{-1}}[e^{{A Δt}} − I] B z RMSE {composed_error}: got {composed}" + ); + assert_eq!( + refuse_time_independent_effect_as_continuous_intercept(increment, effect), + Err(PsychometricError::TimeIndependentEffectIsNotContinuousIntercept) + ); + assert_eq!( + refuse_time_independent_effect_as_time_dependent_impulse(increment, impulse), + Err(PsychometricError::TimeIndependentEffectIsNotTimeDependentImpulse) + ); + assert_eq!( + refuse_time_independent_effect_as_time_varying_discrete_effect( + increment, + equation_fourteen + ), + Err(PsychometricError::TimeIndependentEffectIsNotTimeVaryingDiscreteEffect) + ); + assert_eq!( + refuse_time_independent_coefficient_as_discrete_effect(effect, increment), + Err(PsychometricError::TimeIndependentCoefficientIsNotDiscreteEffect) + ); +} + +#[test] +fn time_independent_predictor_refuses_overflow_and_non_event_clocks() { + assert_eq!( + recover_discrete_time_independent_predictor_effect( + 1e308, + 2.0, + -0.5, + 2.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_latent_mean_with_time_independent_predictor( + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 2.0, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_discrete_latent_mean_with_time_independent_predictor( + 1e308, + 0.0, + 0.0, + 1e308, + 1.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); +} + #[test] fn admitted_coordinates_still_required_for_multilevel_weights() { assert_eq!( diff --git a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs index 446bad25..53fc7dbe 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -6,13 +6,15 @@ use psychometric_core::{ recover_cluster_mean_within_between_slopes, recover_discrete_constant_predictor_effect, recover_discrete_continuous_intercept_effect, recover_discrete_lagged_latent_covariance, recover_discrete_latent_mean, recover_discrete_latent_mean_with_impulse, - recover_discrete_latent_variance, recover_discrete_observed_mean, - recover_discrete_process_noise, recover_discrete_time_varying_predictor_effect, - recover_irregular_centered_residual_log_rate, recover_loading_point_estimate_mean, - recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, - recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, - recover_stationary_latent_variance, recover_time_dependent_predictor_impulse, - recover_trait_plus_state_latent_variance, recover_within_residual_event_time_log_rate, + recover_discrete_latent_mean_with_time_independent_predictor, recover_discrete_latent_variance, + recover_discrete_observed_mean, recover_discrete_process_noise, + recover_discrete_time_independent_predictor_effect, + recover_discrete_time_varying_predictor_effect, recover_irregular_centered_residual_log_rate, + recover_loading_point_estimate_mean, recover_manifest_lagged_observed_covariance, + recover_manifest_observed_mean, recover_manifest_observed_variance, + recover_manifest_trait_plus_state_observed_variance, recover_stationary_latent_variance, + recover_time_dependent_predictor_impulse, recover_trait_plus_state_latent_variance, + recover_within_residual_event_time_log_rate, refuse_continuous_intercept_as_discrete_mean_increment, refuse_continuous_intercept_as_initial_latent_mean, refuse_continuous_intercept_as_manifest_means, @@ -27,6 +29,10 @@ use psychometric_core::{ refuse_time_dependent_impulse_as_continuous_intercept, refuse_time_dependent_impulse_as_time_independent_effect, refuse_time_dependent_impulse_as_time_varying_discrete_effect, + refuse_time_independent_coefficient_as_discrete_effect, + refuse_time_independent_effect_as_continuous_intercept, + refuse_time_independent_effect_as_time_dependent_impulse, + refuse_time_independent_effect_as_time_varying_discrete_effect, refuse_trait_variance_as_process_noise, refuse_trait_variance_as_stationary_within_subject, }; @@ -603,3 +609,79 @@ fn time_dependent_impulse_is_not_cint_tipred_or_equation_fourteen() { Err(psychometric_core::PsychometricError::TimeDependentImpulseIsNotTimeVaryingDiscreteEffect) ); } + +#[test] +fn time_independent_predictor_is_not_cint_impulse_equation_fourteen_or_coefficient() { + let effect = 0.4_f64; + let predictor = 3.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let increment = recover_discrete_time_independent_predictor_effect( + effect, + predictor, + drift, + delta, + LagClock::EventTime, + ) + .expect("tipred"); + let intercept_effect = + recover_discrete_continuous_intercept_effect(effect, drift, delta, LagClock::EventTime) + .expect("cint"); + let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("tdpred"); + let equation_fourteen = recover_discrete_time_varying_predictor_effect( + effect, + delta, + delta, + delta, + LagClock::EventTime, + ) + .expect("eq14"); + let evolved = + recover_discrete_latent_mean(1.0, drift, 0.3, delta, LagClock::EventTime).expect("mu-t"); + let composed = recover_discrete_latent_mean_with_time_independent_predictor( + 1.0, + drift, + 0.3, + effect, + predictor, + delta, + LagClock::EventTime, + ) + .expect("eq3-tipred"); + assert!( + (increment - effect).abs() > 1e-3, + "Driver et al. (2017, Eq. 3 / Table 2 p. 12): TIPREDEFFECT is not the discrete increment" + ); + assert!( + (increment - intercept_effect).abs() > 1e-3, + "Driver et al. (2017, Eq. 3): A^{{-1}}[e^{{A Δt}} − I] B z is not CINT" + ); + assert!( + (increment - impulse).abs() > 1e-3, + "Driver et al. (2017, Eq. 3): A^{{-1}}[e^{{A Δt}} − I] B z is not M x" + ); + assert!( + (increment - equation_fourteen).abs() > 1e-3, + "Driver et al. (2017, Eq. 3): A^{{-1}}[e^{{A Δt}} − I] B z is not Voelkle Eq. 14" + ); + assert!( + (composed - evolved).abs() > 1e-3, + "Driver et al. (2017, Eq. 3): μ_t is not μ_t + A^{{-1}}[e^{{A Δt}} − I] B z" + ); + assert_eq!( + refuse_time_independent_effect_as_continuous_intercept(increment, effect), + Err(psychometric_core::PsychometricError::TimeIndependentEffectIsNotContinuousIntercept) + ); + assert_eq!( + refuse_time_independent_effect_as_time_dependent_impulse(increment, impulse), + Err(psychometric_core::PsychometricError::TimeIndependentEffectIsNotTimeDependentImpulse) + ); + assert_eq!( + refuse_time_independent_effect_as_time_varying_discrete_effect(increment, equation_fourteen), + Err(psychometric_core::PsychometricError::TimeIndependentEffectIsNotTimeVaryingDiscreteEffect) + ); + assert_eq!( + refuse_time_independent_coefficient_as_discrete_effect(effect, increment), + Err(psychometric_core::PsychometricError::TimeIndependentCoefficientIsNotDiscreteEffect) + ); +} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 091f12c6..b3e49784 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -1,7 +1,7 @@ # TEPP Requirements, Research, and Evidence Traceability **Status:** Accepted cross-cutting traceability baseline -**Last reviewed:** 2026-08-13 +**Last reviewed:** 2026-08-20 The full APA 7th standards/literature register remains `docs/research/standards-and-literature.md`. This matrix links durable requirements to their owning decisions and implementation/evidence maturity without duplicating the bibliography. @@ -28,7 +28,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | report template/section/copied/style/modality method effects | ADR 0004/0012; PRD/TRD | simulation truth factors implemented; estimator-side method model remains future | partial | | candidate K statistical/Pareto gates + blinded LLM review | ADR 0012; research | future `model_selection` | accepted-target | | compositional topic correlation / stable clustering | ADR 0005/0012; research | future `network_analysis` | accepted-target | -| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z); full ESEM/DSEM remaining | partial | +| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z); full ESEM/DSEM remaining | partial | | CPU bounded multithreading + GPU/VRAM streaming/parity | ADR 0001/0006 | future `compute_backend` | accepted-target | | TDT detection/tracking vs CHRONOS schema/prediction/temporal consistency | ADR 0016; PRD/research | future `event_intelligence` | accepted-target | | evidence-bounded LLM interpretation | ADR 0010/0012; PRD | future `interpretation_gateway` | accepted-target | diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index 1ddd14bf..0ec10eb1 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -1,7 +1,7 @@ # ADR 0005 — Posterior-aware ESEM/DSEM and structural interpretation **Decision status:** Accepted -**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), exact scalar contemporaneous `TDPREDEFFECT` impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; the §7.2 level-change form is not that impulse), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target +**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), exact scalar contemporaneous `TDPREDEFFECT` impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; the §7.2 level-change form is not that impulse), exact scalar time-independent `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle Eq. 14; `B` is not that discrete increment), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target **Date:** 2026-08-05 **Supersedes:** None. ADR 0012 governs upstream topic measurement/network coordinates; this ADR governs higher-order psychometric structure and longitudinal interpretation. @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), recovers the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z; `TDPREDEFFECT` is `M`, not `CINT`; `M x` is not `A^{-1}[e^{A Δt} − I] B z`; `M x` is not Voelkle et al., 2012, Eq. 14; the level-change form is not that impulse), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), recovers the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z; `TDPREDEFFECT` is `M`, not `CINT`; `M x` is not `A^{-1}[e^{A Δt} − I] B z`; `M x` is not Voelkle et al., 2012, Eq. 14; the level-change form is not that impulse), recovers the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z; `TIPREDEFFECT` is `B`, not `κ`; form `B z` first, then the discrete intercept map; a zero drift is `B z Δt`; `B` is not the discrete increment; `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle Eq. 14), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. 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 86fdc35f..79e1ccc5 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -23,15 +23,16 @@ This slice stays inside `psychometric_core`. It does not add a second invariance 17. recover the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; Table 2 `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment) and refuse treating `T0MEANS` or `CINT` as `μ_t`; 18. recover the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`) and refuse treating that first-occasion mean as `E(y_t)`; 19. recover the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`) and refuse treating that impulse as `CINT`, `TIPREDEFFECT`, or Voelkle et al. (2012, Eq. 14); -20. refuse pooling discrete lags from unequal event intervals as one coefficient; -21. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); -22. refuse the difference quotient as a continuous-time rate; -23. apply the same event-time map to CWC residuals (still not DSEM); -24. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). +20. recover the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`) and refuse treating that increment as `CINT`, `TDPREDEFFECT`, Voelkle et al. (2012, Eq. 14), or the coefficient `B`; +21. refuse pooling discrete lags from unequal event intervals as one coefficient; +22. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); +23. refuse the difference quotient as a continuous-time rate; +24. apply the same event-time map to CWC residuals (still not DSEM); +25. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). ## Claim boundary -This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. The contemporaneous `TDPREDEFFECT` impulse is not the continuous intercept, not the time-independent discrete effect, and not Voelkle et al. (2012, Eq. 14). +This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. The contemporaneous `TDPREDEFFECT` impulse is not the continuous intercept, not the time-independent discrete effect, and not Voelkle et al. (2012, Eq. 14). The time-independent `TIPREDEFFECT` increment is not the continuous intercept, not the contemporaneous impulse, not Voelkle et al. (2012, Eq. 14), and not the coefficient `B`. ## Authoritative sources @@ -71,6 +72,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - **Discrete latent mean.** Driver et al. (2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z): \(\eta(t)=\exp(A\Delta t)\eta(t_0)+\int\exp(A(t-s))(b+\cdots)\,ds\) plus a stochastic integral of mean zero. Table 2 names the first-occasion latent mean `T0MEANS` and \(\kappa\) `CINT`. The scalar map is \(\mu_t=\exp(a\Delta t)\mu_0+(\exp(a\Delta t)-1)/a\,\kappa\). Form the `CINT` increment first, then add the carried `T0MEANS` term. A zero drift is the Eq. 3 integral \(\kappa\Delta t\) (\(A=0\) has no inverse). A zero intercept is exactly \(\exp(a\Delta t)\mu_0\). A zero initial mean is exactly the increment. As \(\Delta t\to\infty\) with stable \(a<0\), \(\mu_t\to-\kappa/a\). Binary64 underflow of \(\exp(a\Delta t)\) to `+0` drops the carried `T0MEANS` and keeps that equilibrium increment. `T0MEANS` is not \(\mu_t\). `CINT` is not the discrete increment. `CINT` is not `T0MEANS`. An overflowing exponential, product, or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **Discrete observed-indicator mean.** Driver et al. (2017, Eq. 3, p. 5; Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T22:10Z): `η_i(t)=exp(AΔt)η_i(t0)+A^{-1}[exp(AΔt)−I]ξ_i+…` with `ξ_i∼N(κ,φ_ξ)` and a stochastic integral of mean zero, then `y_i(t)=Γ_i+Λη_i(t)+ζ_i(t)` with `Γ∼N(τ,Ψ)`. The scalar composition is `E(y_t)=τ+λμ_t`. Form `μ_t` first, then `τ+λμ_t`. A zero loading or zero evolved latent mean is exactly `τ`. A zero intercept is exactly `λμ_t`. A zero drift is `τ+λ(μ_0+κΔt)`. Underflow of `exp(aΔt)` to `+0` keeps `τ+λ(−κ/a)`. The first-occasion map `τ+λμ_0` is not `E(y_t)`. `MANIFESTMEANS` is not `E(y_t)`. `T0MEANS` is not `E(y_t)`. `μ_t` is not `E(y_t)`. An overflowing exponential, product, or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **Time-dependent predictor impulse.** Driver et al. (2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z): `χ_i(t)=Σ x_{i,u} δ(t−u)` and the fourth summand is `M Σ x_{i,u} δ(t−u)`. Table 2 names `M` `TDPREDEFFECT`. Section 7.2 calls this a sudden impulse that dissipates back to the process mean. The scalar contemporaneous jump is `m x`. Form `μ_t` first, then add `m x`. A zero effect or zero predictor is exactly zero. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{AΔt}−I] B z` (`TIPREDEFFECT`). `M x` is not Voelkle et al. (2012, Eq. 14) `a_{yx}Δt`. The §7.2 level-change form is an extra latent process with near-zero drift and is not this map. An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. +- **Time-independent predictor effect.** Driver et al. (2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z): Equation 1 writes `dη=(Aη+b+A_{ηξ}ξ+Bz)dt+GdW+Mdχ`. Equation 3's second summand is `A^{-1}[e^{AΔt}−I](b+A_{ηξ}ξ+Bz)`. Table 2 names `B` `TIPREDEFFECT`. The scalar map is `(e^{aΔt}−1)/a·Bz` for `a≠0`. Form `Bz` first, then the discrete intercept map. A zero drift is `BzΔt`. Form `μ_t` first, then add that increment. A zero effect or zero predictor is exactly zero. `TIPREDEFFECT` is `B`, not the discrete increment. `A^{-1}[e^{AΔt}−I]Bz` is not `CINT`, not `Mx`, and not Voelkle et al. (2012, Eq. 14) `a_{yx}Δt`. An overflowing product, increment, or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **CWC-then-lag.** Sample cluster means are removed first. Consecutive within residuals are then fitted by least squares to \(r_{t+\Delta t}\approx\exp(a\Delta t)\,r_{t}\) on event time. Same-sign pair-wise logs initialize the scalar Newton step. Sign-flipping \(T=2\) CWC pairs have no real logarithm and fail closed. Curran and Bauer (2011, pp. 607–608) show that this person-mean subtraction on a raw autoregressive series does **not** isolate the lagged within-person effect; the helper therefore does not claim to recover the raw-process drift. - **Already-centered irregular residual.** The caller supplies lagged within residuals. The mean of \(a=\ln(r_{t+\Delta t}/r_t)/\Delta t\) is the exact scalar map. Intervals may be irregular. The helper does not center again. This is not DSEM. @@ -94,6 +96,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - Driver et al. (2017, Eq. 3 expected-value latent mean; Table 2, p. 12) recovers a known \(\mu_t=\exp(a\Delta t)\mu_0+(\exp(a\Delta t)-1)/a\,\kappa\) at machine-scale RMSE, and that RMSE is smaller than treating `T0MEANS` or `CINT` as \(\mu_t\); a zero drift is \(\mu_0+\kappa\Delta t\); underflow of \(\exp(a\Delta t)\) to `+0` drops `T0MEANS` and keeps \(-\kappa/a\); `CINT` is not the discrete increment; an overflowing exponential, product, or sum fails closed; - Driver et al. (2017, Eq. 5 of the Eq. 3 evolved mean; Table 2, p. 12) recovers a known \(E(y_t)=\tau+\lambda\mu_t\) at machine-scale RMSE, and that RMSE is smaller than treating \(\tau+\lambda\mu_0\), `MANIFESTMEANS`, or \(\mu_t\) as \(E(y_t)\); a zero loading is \(\tau\); a zero drift is \(\tau+\lambda(\mu_0+\kappa\Delta t)\); underflow of \(\exp(a\Delta t)\) keeps \(\tau+\lambda(-\kappa/a)\); a zero-CINT overflow of \(\exp(a\Delta t)\) fails closed; an overflowing product or sum fails closed; - Driver et al. (2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT`; §7.2) recovers a known contemporaneous impulse \(m x\) at machine-scale RMSE, and that RMSE is smaller than treating `CINT`, the time-independent discrete effect, or Voelkle et al. (2012, Eq. 14) as the impulse; composing \(\mu_t+m x\) recovers the known sum; a zero effect or zero predictor is exactly zero; an overflowing product or sum, a non-event clock, and a non-positive interval fail closed; +- Driver et al. (2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT`) recovers a known increment \(A^{-1}[e^{A\Delta t}-I]Bz\) at machine-scale RMSE, and that RMSE is smaller than treating `CINT`, `M x`, Voelkle et al. (2012, Eq. 14), or the coefficient `B` as the increment; composing \(\mu_t\) plus that increment recovers the known sum; a zero drift is \(Bz\Delta t\); a zero effect or zero predictor is exactly zero; an overflowing product, increment, or sum, a non-event clock, and a non-positive interval fail closed; - pooling discrete lags from unequal intervals fails closed; - CWC-then-lag on a two-cluster decaying series has smaller computed RMSE than a level-pooled series when the latter is identified; - already-centered irregular residuals recover a known drift at machine-scale RMSE, and that RMSE is smaller than CWC of the corresponding raw autoregressive series (Curran & Bauer, 2011, pp. 607–608); diff --git a/docs/research/posterior-esem-input-gates.md b/docs/research/posterior-esem-input-gates.md index f9bb4aa9..ab5b7a23 100644 --- a/docs/research/posterior-esem-input-gates.md +++ b/docs/research/posterior-esem-input-gates.md @@ -12,7 +12,7 @@ This slice delivers the first executable ADR 0005 contract in `psychometric_core 6. refuse latent-mean comparison without invariance evidence, and recover a mean difference only under strong or strict two-group OLS status (Putnick & Bornstein, 2016: scalar licenses means; residual invariance is not required; two-observation series cap at strong because residual variance is identically `0`); 7. refuse causal language that rests only on temporal precedence, document linkage, event tracking, or model prediction. -Cluster-mean CWC, the CWC contextual effect, Kish WLS, event-time log-rate, CWC-then-lag, irregular already-centered residual log-rate, the Driver Eq. 5 of the Eq. 3 evolved mean, and the Driver Eq. 3 contemporaneous `TDPREDEFFECT` impulse live in the same crate and are documented in `docs/research/multilevel-event-time-recovery.md`. Full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target. +Cluster-mean CWC, the CWC contextual effect, Kish WLS, event-time log-rate, CWC-then-lag, irregular already-centered residual log-rate, the Driver Eq. 5 of the Eq. 3 evolved mean, the Driver Eq. 3 contemporaneous `TDPREDEFFECT` impulse, and the Driver Eq. 3 `TIPREDEFFECT` increment live in the same crate and are documented in `docs/research/multilevel-event-time-recovery.md`. Full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target. ## Authoritative sources diff --git a/docs/validation/temporal-event-foundation.md b/docs/validation/temporal-event-foundation.md index a598cb80..1aae5030 100644 --- a/docs/validation/temporal-event-foundation.md +++ b/docs/validation/temporal-event-foundation.md @@ -1,7 +1,7 @@ # Temporal Event Foundation — validation and release-readiness report **Status:** Living validation ledger for the Temporal/Event foundation program -**Last reviewed:** 2026-08-12 +**Last reviewed:** 2026-08-20 **Authority:** ADR 0014 (claim promotion), ADR 0007 (quality gates), AGENTS.md scientific acceptance ## Scope @@ -23,7 +23,7 @@ This report tracks exact-head scientific and engineering evidence required befor | Truth corpora / manifests | `tepp_simulation` | implemented-main | — | deterministic generator tests | Task 10 / PR #18 | | Recovery metrics | `validation_core` | implemented-main | — | RMSE/bias/coverage/MC gates | Task 11 / PR #19 | | Versioned API/export contracts | `tepp_api` | implemented-main | naruon HTTP interchange | unknown-field/version/limit + naruon HTTPS interchange tests | Task 12 / PR #21; live HTTP service remaining | -| Psychometric structural input gates | `psychometric_core` | partial | stacked psychometric PR | construct-class refusal + ALR/ILR boundary + true-loading RMSE + posterior-draw point-estimate mean + Rubin `T` + CWC within/between + CWC contextual effect + event-time log-rate + constant- and time-varying-predictor discrete effects + exact scalar discrete process noise + lagged latent covariance and unconditional latent variance + stationary within-subject variance + trait-plus-state variance + observed-indicator variance + discrete latent mean (`T0MEANS`/`CINT`) + evolved observed mean (`τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`) + contemporaneous `TDPREDEFFECT` impulse (`m x`; not `CINT`, not `TIPREDEFFECT`, not Voelkle Eq. 14) + irregular already-centered residual lag + strong-gated latent means (n=2 residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016); full ESEM/DSEM remaining | ADR 0005; `docs/research/posterior-esem-input-gates.md`; `docs/research/multilevel-event-time-recovery.md`; `docs/research/rubin-total-variance.md`; `docs/research/strong-invariance-latent-means.md` | +| Psychometric structural input gates | `psychometric_core` | partial | stacked psychometric PR | construct-class refusal + ALR/ILR boundary + true-loading RMSE + posterior-draw point-estimate mean + Rubin `T` + CWC within/between + CWC contextual effect + event-time log-rate + constant- and time-varying-predictor discrete effects + exact scalar discrete process noise + lagged latent covariance and unconditional latent variance + stationary within-subject variance + trait-plus-state variance + observed-indicator variance + discrete latent mean (`T0MEANS`/`CINT`) + evolved observed mean (`τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`) + contemporaneous `TDPREDEFFECT` impulse (`m x`; not `CINT`, not `TIPREDEFFECT`, not Voelkle Eq. 14) + time-independent `TIPREDEFFECT` increment (`A^{-1}[e^{A Δt} − I] B z`; not `CINT`, not `M x`, not Voelkle Eq. 14, not the coefficient `B`) + irregular already-centered residual lag + strong-gated latent means (n=2 residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016); full ESEM/DSEM remaining | ADR 0005; `docs/research/posterior-esem-input-gates.md`; `docs/research/multilevel-event-time-recovery.md`; `docs/research/rubin-total-variance.md`; `docs/research/strong-invariance-latent-means.md` | | CWL modular connectors | `docs/connectors/*` | implemented-main | — | contract docs + examples | PR #22; live HTTP ports remaining | | Release SBOM/provenance generator | `scripts/release_evidence.py` | partial | — | generate+validate in CI | Task 13 partial / PR #28 | From 410a40c84688ac8164839ed2f37a9de0e5e053ed Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 01:43:13 +0000 Subject: [PATCH 36/62] feat(psychometric): recover Driver Eq. 1-2 TDPRED impulse carry Map a time-dependent impulse that occurred strictly inside (t0, t) as e^{A(t-u)} M x. The printed Eq. 3 fourth summand remains the contemporaneous Dirac. This is not CINT, not TIPREDEFFECT, and not Voelkle Eq. 14. --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 1 + CLAUDE.md | 2 +- README.md | 2 +- crates/psychometric_core/Cargo.toml | 2 +- crates/psychometric_core/src/error.rs | 43 ++ crates/psychometric_core/src/event_time.rs | 550 +++++++++++++++++- crates/psychometric_core/src/lib.rs | 23 +- ...multilevel_event_time_recovery_contract.rs | 145 ++++- .../scientific_claim_boundary_contract.rs | 101 +++- docs/TRACEABILITY.md | 2 +- docs/adr/0005-posterior-esem-dsem.md | 4 +- .../multilevel-event-time-recovery.md | 17 +- docs/research/posterior-esem-input-gates.md | 2 +- docs/validation/temporal-event-foundation.md | 2 +- 15 files changed, 871 insertions(+), 27 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 3f3abb4e..1512ca3e 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -61,7 +61,7 @@ boundaries above remain the target modular MSA architecture. | `tepp_simulation` | known-truth temporal/event data generation | | `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics | | `tepp_api` | versioned DTO, schema, and export contracts | -| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | +| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | No crate exposes placeholder production behavior in Task 1. This prevents an empty façade from becoming a de facto public API before its invariants and tests diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cd33950..c0c01e9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar within-interval time-dependent predictor impulse carry. Equation 1 writes `dη = (A η + ξ + B z + M χ(t)) dt + G dW`. Equation 2 writes `χ_i(t) = Σ x_{i,u} δ(t − u)`. The Green-function integral of that Dirac on `(t0, t)` is `e^{A(t−u)} M x`. The printed Eq. 3 fourth summand is the contemporaneous jump `M x` at `u = t`. This map is the strictly within-interval case `t0 < u < t`. Form `m x` first, then `e^{a(t−u)} m x`. A zero drift is `m x` with no dissipation. Underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation back to the process mean and is kept. Form `μ_t` first, then add the carry. `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `A^{-1}[e^{A Δt} − I] B z` (`TIPREDEFFECT`), and not Voelkle et al. (2012, Eq. 14) `a_{yx} Δt`. An impulse at `u = t` is the contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. The §7.2 level-change form is a different specification and is not this map. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993), Vandenberg and Lance (2000), and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T10:33Z: `is_oa: false`). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar time-independent predictor increment. Equation 1 writes `dη = (A η + b + A_{ηξ} ξ + B z) dt + G dW + M dχ`. Equation 3's second summand is `A^{-1}[e^{A Δt} − I](b + A_{ηξ} ξ + B z)`. Table 2 names `B` `TIPREDEFFECT`. Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. Form `μ_t` first, then add that increment. `TIPREDEFFECT` is `B`, not the discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x` (`TDPREDEFFECT`), and not Voelkle et al. (2012, Eq. 14) `a_{yx} Δt`. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993), Vandenberg and Lance (2000), and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T10:13Z: `is_oa: false`). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar contemporaneous time-dependent predictor impulse. Equation 2 writes `χ_i(t) = Σ x_{i,u} δ(t − u)`. Equation 3's fourth summand is `M Σ x_{i,u} δ(t − u)`. Table 2 names `M` `TDPREDEFFECT`. Section 7.2 calls this a sudden impulse that dissipates back to the process mean and reports `TDPREDEFFECT` as the initial impact. The scalar jump is `m x`. Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` (`TIPREDEFFECT`). `M x` is not Voelkle et al. (2012, Eq. 14) `a_{yx} Δt`. The §7.2 level-change form is a different specification and is not this map. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993), Vandenberg and Lance (2000), and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T07:10Z: `is_oa: false`). - `psychometric_core` caps two-observation two-group OLS residual invariance at strong/scalar. `ordinary_least_squares_fit` returns residual variance `0` when `n ≤ 2`; that identity is not an estimated residual and is not strict. Putnick and Bornstein (2016, PMC author manuscript PMC5145197 opened 2026-08-19T22:15Z from https://pmc.ncbi.nlm.nih.gov/articles/PMC5145197/) require scalar invariance before latent-mean comparison; residual invariance is not a prerequisite because residuals are not part of the latent factor. Matching loading and intercept with `n = 2` therefore stay strong/scalar and still license `(ȳ_c − ȳ_r)/λ`. This is still two-group OLS, not MGCFA. Meredith (1993) remains unread (OpenAlex/Semantic Scholar 2026-08-19T22:15Z: closed; Springer `content/pdf` is HTML 200). Vandenberg and Lance (2000) remains unread (cited by Putnick for the residual-not-required claim). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread. diff --git a/CLAUDE.md b/CLAUDE.md index bb198893..ad19f356 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. Putnick and Bornstein (2016, PMC5145197 opened 2026-08-19T22:15Z) require scalar invariance before latent-mean comparison; residual invariance is not a prerequisite. Two-observation series have no residual degrees of freedom (`ordinary_least_squares_fit` returns residual variance `0`) and cap at strong/scalar; they still license means. This is two-group OLS, not MGCFA. Meredith (1993) names remain unread labels. -- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. The contemporaneous time-dependent predictor impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`). Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). The §7.2 level-change form is not that impulse. The time-independent predictor increment is `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`). Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. `TIPREDEFFECT` is `B`, not that discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle et al. (2012, Eq. 14). Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. The contemporaneous time-dependent predictor impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`). Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). The §7.2 level-change form is not that impulse. The time-independent predictor increment is `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`). Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. `TIPREDEFFECT` is `B`, not that discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle et al. (2012, Eq. 14). The within-interval time-dependent impulse carry is `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation). Form `m x` first, then `e^{a(t−u)} m x`. A zero drift is `m x` with no dissipation. Underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept. `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). An impulse at `u = t` is the contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. diff --git a/README.md b/README.md index 1486b42f..7cd9bb3c 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ crates/corpus_split crates/tepp_simulation crates/validation_core crates/tepp_api -crates/psychometric_core # input gates, CWC/event-time/contextual, irregular residual lag, Rubin T, strong means, Driver Eq. 3 TDPRED/TIPRED maps; not a full ESEM/DSEM estimator +crates/psychometric_core # input gates, CWC/event-time/contextual, irregular residual lag, Rubin T, strong means, Driver Eq. 3 TDPRED/TIPRED maps including within-interval impulse carry; not a full ESEM/DSEM estimator ``` ## Local verification diff --git a/crates/psychometric_core/Cargo.toml b/crates/psychometric_core/Cargo.toml index d33ec543..5cb09c8b 100644 --- a/crates/psychometric_core/Cargo.toml +++ b/crates/psychometric_core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "psychometric_core" -description = "Posterior-aware ESEM/DSEM input gates, multilevel/event-time recovery, CWC contextual effect, Voelkle Eqs. 12 and 14, Driver Eq. 3 TDPRED/TIPRED maps, Rubin T, and strong-invariance latent means." +description = "Posterior-aware ESEM/DSEM input gates, multilevel/event-time recovery, CWC contextual effect, Voelkle Eqs. 12 and 14, Driver Eq. 3 TDPRED/TIPRED maps including within-interval impulse carry, Rubin T, and strong-invariance latent means." version.workspace = true edition.workspace = true rust-version.workspace = true diff --git a/crates/psychometric_core/src/error.rs b/crates/psychometric_core/src/error.rs index 08ab3a1d..e7883700 100644 --- a/crates/psychometric_core/src/error.rs +++ b/crates/psychometric_core/src/error.rs @@ -125,6 +125,21 @@ pub enum PsychometricError { /// Driver Table 2 `TIPREDEFFECT` was treated as the discrete /// increment. `B` is not `A^{-1}[e^{A Δt} − I] B z`. TimeIndependentCoefficientIsNotDiscreteEffect, + /// Driver Eq. 1–2 within-interval impulse carry was treated as + /// the contemporaneous Dirac. `e^{A(t−u)} M x` for `t0 < u < t` + /// is not `M x`. + TimeDependentImpulseCarryIsNotContemporaneousImpulse, + /// Driver Eq. 1–2 within-interval impulse carry was treated as + /// `CINT`. `e^{A(t−u)} M x` is not `κ`. + TimeDependentImpulseCarryIsNotContinuousIntercept, + /// Driver Eq. 1–2 within-interval impulse carry was treated as + /// the time-independent discrete effect. `e^{A(t−u)} M x` is not + /// `A^{-1}[e^{A Δt} − I] B z`. + TimeDependentImpulseCarryIsNotTimeIndependentEffect, + /// Driver Eq. 1–2 within-interval impulse carry was treated as + /// Voelkle et al. (2012, Eq. 14). `e^{A(t−u)} M x` is not + /// `a_{yx} Δt`. + TimeDependentImpulseCarryIsNotTimeVaryingDiscreteEffect, } impl fmt::Display for PsychometricError { @@ -233,6 +248,18 @@ impl fmt::Display for PsychometricError { Self::TimeIndependentCoefficientIsNotDiscreteEffect => { "time-independent predictor coefficient is not the discrete effect" } + Self::TimeDependentImpulseCarryIsNotContemporaneousImpulse => { + "time-dependent predictor impulse carry is not the contemporaneous impulse" + } + Self::TimeDependentImpulseCarryIsNotContinuousIntercept => { + "time-dependent predictor impulse carry is not the continuous intercept" + } + Self::TimeDependentImpulseCarryIsNotTimeIndependentEffect => { + "time-dependent predictor impulse carry is not the time-independent discrete effect" + } + Self::TimeDependentImpulseCarryIsNotTimeVaryingDiscreteEffect => { + "time-dependent predictor impulse carry is not the time-varying discrete effect" + } }; formatter.write_str(message) } @@ -414,5 +441,21 @@ mod tests { PsychometricError::TimeIndependentCoefficientIsNotDiscreteEffect.to_string(), "time-independent predictor coefficient is not the discrete effect" ); + assert_eq!( + PsychometricError::TimeDependentImpulseCarryIsNotContemporaneousImpulse.to_string(), + "time-dependent predictor impulse carry is not the contemporaneous impulse" + ); + assert_eq!( + PsychometricError::TimeDependentImpulseCarryIsNotContinuousIntercept.to_string(), + "time-dependent predictor impulse carry is not the continuous intercept" + ); + assert_eq!( + PsychometricError::TimeDependentImpulseCarryIsNotTimeIndependentEffect.to_string(), + "time-dependent predictor impulse carry is not the time-independent discrete effect" + ); + assert_eq!( + PsychometricError::TimeDependentImpulseCarryIsNotTimeVaryingDiscreteEffect.to_string(), + "time-dependent predictor impulse carry is not the time-varying discrete effect" + ); } } diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index 37f55908..b6032c70 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -41,7 +41,12 @@ //! (`T0MEANS` is not `μ_t`; `CINT` is not that discrete increment). Equation 3's //! fourth summand is the contemporaneous Dirac impulse `M x` (Table 2 //! `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not -//! Voelkle et al., 2012, Eq. 14). Equation 3's second summand also +//! Voelkle et al., 2012, Eq. 14). Equations 1–2 plus the Eq. 3 +//! exponential map a time-dependent impulse that occurred strictly +//! inside `(t0, t)` as `e^{A(t−u)} M x` (`t0 < u < t`). That carry +//! is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, +//! and not Voelkle Eq. 14. Section 7.2 calls this dissipation back to +//! the process mean. Equation 3's second summand also //! maps the time-independent predictor as `A^{-1}[e^{A Δt} − I] B z` //! (Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle //! Eq. 14). The JSS article @@ -1600,6 +1605,200 @@ pub fn refuse_time_independent_coefficient_as_discrete_effect( Err(PsychometricError::TimeIndependentCoefficientIsNotDiscreteEffect) } +/// Exact scalar within-interval time-dependent impulse carry from +/// Driver Equations 1–2. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; +/// §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z from +/// ) +/// write `dη = (A η + ξ + B z + M χ(t)) dt + G dW` with +/// `χ_i(t) = Σ_{u ∈ U_i} x_{i,u} δ(t − u)`. The Green-function +/// integral of that Dirac on `(t0, t)` is `e^{A(t−u)} M x`. The +/// printed Eq. 3 fourth summand is the contemporaneous jump `M x` +/// at `u = t`. This map is the strictly within-interval case +/// `t0 < u < t`: form `m x` first, then `e^{a(t−u)} m x`. A zero +/// drift is `m x` with no dissipation. Binary64 underflow of +/// `e^{a(t−u)}` to `+0` is vanishing dissipation back to the process +/// mean (§7.2) and is kept. A zero effect or zero predictor is +/// exactly zero even if the exponential overflows. When `e^{a(t−u)}` +/// overflows at a finite `a(t−u)`, rewrite as +/// `sign(m x) exp(ln|m x| + a(t−u))`. An impulse at `u = t` is the +/// contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. +/// The §7.2 level-change form is a different specification and is +/// not this map. This is not a Kalman filter and not ctsem +/// estimation. +/// +/// # Errors +/// +/// Returns [`PsychometricError::EventTimeRequired`] for any non-event +/// clock, [`PsychometricError::NonPositiveInterval`] when +/// `event_delta` or `elapsed_after_impulse` is not strictly positive +/// or the impulse is not strictly inside `(t0, t)`, and +/// [`PsychometricError::InvalidNumericInput`] when an input is +/// non-finite or `m x` or the carried product overflows. +pub fn recover_time_dependent_predictor_impulse_carry( + time_dependent_effect: f64, + time_dependent_predictor: f64, + log_rate: f64, + event_delta: f64, + elapsed_after_impulse: f64, + clock: LagClock, +) -> Result { + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + if !event_delta.is_finite() || event_delta <= 0.0 { + return Err(PsychometricError::NonPositiveInterval); + } + if !elapsed_after_impulse.is_finite() || elapsed_after_impulse <= 0.0 { + return Err(PsychometricError::NonPositiveInterval); + } + // I_{t0 < u < t}: t−u strictly less than t−t0, so u−t0 > 0. + if elapsed_after_impulse >= event_delta { + return Err(PsychometricError::NonPositiveInterval); + } + if !log_rate.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + let impulse = + recover_time_dependent_predictor_impulse(time_dependent_effect, time_dependent_predictor)?; + if impulse == 0.0 { + return Ok(0.0); + } + let drift_interval = log_rate * elapsed_after_impulse; + let auto_effect = drift_interval.exp(); + if auto_effect.is_finite() { + // +0 underflow is vanishing dissipation (§7.2). + return require_finite(auto_effect * impulse); + } + if !drift_interval.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + // Finite a(t−u), overflowed exp. + // e^{a(t−u)} m x = sign(m x) exp(ln|m x| + a(t−u)). + require_finite(impulse.signum() * (impulse.abs().ln() + drift_interval).exp()) +} + +/// Exact scalar evolved latent mean plus a within-interval impulse carry. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 1–3, p. 5; §7.2) write the +/// first two summands as the carried `T0MEANS` and `CINT` increment, +/// then add a Dirac impulse that occurred strictly inside `(t0, t)` +/// after it has dissipated by `e^{A(t−u)}`. Form `μ_t` first, then +/// add `e^{a(t−u)} m x`. A zero carry is exactly `μ_t`. A zero +/// evolved mean is exactly the carry. Adding the contemporaneous +/// `m x` is not this composition when `u ≠ t`. The level-change +/// form is not this map. +/// +/// # Errors +/// +/// Propagates [`recover_discrete_latent_mean`] and +/// [`recover_time_dependent_predictor_impulse_carry`], and returns +/// [`PsychometricError::InvalidNumericInput`] when the sum overflows. +#[allow(clippy::too_many_arguments)] +pub fn recover_discrete_latent_mean_with_impulse_carry( + initial_latent_mean: f64, + log_rate: f64, + continuous_intercept: f64, + time_dependent_effect: f64, + time_dependent_predictor: f64, + event_delta: f64, + elapsed_after_impulse: f64, + clock: LagClock, +) -> Result { + let evolved_latent_mean = recover_discrete_latent_mean( + initial_latent_mean, + log_rate, + continuous_intercept, + event_delta, + clock, + )?; + let impulse_carry = recover_time_dependent_predictor_impulse_carry( + time_dependent_effect, + time_dependent_predictor, + log_rate, + event_delta, + elapsed_after_impulse, + clock, + )?; + if impulse_carry == 0.0 { + return Ok(evolved_latent_mean); + } + if evolved_latent_mean == 0.0 { + return Ok(impulse_carry); + } + require_finite(evolved_latent_mean + impulse_carry) +} + +/// Refuse treating the Eq. 1–2 impulse carry as the contemporaneous Dirac. +/// +/// The printed Eq. 3 fourth summand is `M x` at `u = t`. The +/// within-interval carry is `e^{A(t−u)} M x` for `t0 < u < t`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::TimeDependentImpulseCarryIsNotContemporaneousImpulse`]. +pub fn refuse_time_dependent_impulse_carry_as_contemporaneous_impulse( + time_dependent_impulse_carry: f64, + time_dependent_impulse: f64, +) -> Result { + let _ = (time_dependent_impulse_carry, time_dependent_impulse); + Err(PsychometricError::TimeDependentImpulseCarryIsNotContemporaneousImpulse) +} + +/// Refuse treating the Eq. 1–2 impulse carry as `CINT`. +/// +/// Table 2 names `M` `TDPREDEFFECT` and `κ` `CINT`. The dissipated +/// impulse is not the continuous intercept. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::TimeDependentImpulseCarryIsNotContinuousIntercept`]. +pub fn refuse_time_dependent_impulse_carry_as_continuous_intercept( + time_dependent_impulse_carry: f64, + continuous_intercept: f64, +) -> Result { + let _ = (time_dependent_impulse_carry, continuous_intercept); + Err(PsychometricError::TimeDependentImpulseCarryIsNotContinuousIntercept) +} + +/// Refuse treating the Eq. 1–2 impulse carry as `TIPREDEFFECT`. +/// +/// The second-summand map integrates a constant `B z` over the event +/// interval. The within-interval TDPRED carry dissipates a Dirac. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::TimeDependentImpulseCarryIsNotTimeIndependentEffect`]. +pub fn refuse_time_dependent_impulse_carry_as_time_independent_effect( + time_dependent_impulse_carry: f64, + time_independent_effect: f64, +) -> Result { + let _ = (time_dependent_impulse_carry, time_independent_effect); + Err(PsychometricError::TimeDependentImpulseCarryIsNotTimeIndependentEffect) +} + +/// Refuse treating the Eq. 1–2 impulse carry as Voelkle et al. +/// (2012, Eq. 14). +/// +/// Equation 14 is `a_{yx} Δt` for a piecewise-constant time-varying +/// predictor. The Dirac carry is `e^{A(t−u)} M x`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::TimeDependentImpulseCarryIsNotTimeVaryingDiscreteEffect`]. +pub fn refuse_time_dependent_impulse_carry_as_time_varying_discrete_effect( + time_dependent_impulse_carry: f64, + time_varying_discrete_effect: f64, +) -> Result { + let _ = (time_dependent_impulse_carry, time_varying_discrete_effect); + Err(PsychometricError::TimeDependentImpulseCarryIsNotTimeVaryingDiscreteEffect) +} + /// Refuse treating Driver Table 2 `T0MEANS` as the evolved latent mean. /// /// Equation 3 maps `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`. @@ -1903,6 +2102,7 @@ mod tests { recover_discrete_continuous_intercept_effect, recover_discrete_lag_from_log_rate, recover_discrete_lag_one, recover_discrete_lagged_latent_covariance, recover_discrete_latent_mean, recover_discrete_latent_mean_with_impulse, + recover_discrete_latent_mean_with_impulse_carry, recover_discrete_latent_mean_with_time_independent_predictor, recover_discrete_latent_variance, recover_discrete_observed_mean, recover_discrete_process_noise, recover_discrete_time_independent_predictor_effect, @@ -1911,8 +2111,9 @@ mod tests { recover_local_log_rate, recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, recover_stationary_latent_variance, - recover_time_dependent_predictor_impulse, recover_trait_plus_state_lagged_covariance, - recover_trait_plus_state_latent_variance, recover_within_residual_event_time_log_rate, + 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_continuous_intercept_as_discrete_mean_increment, refuse_continuous_intercept_as_initial_latent_mean, refuse_continuous_intercept_as_manifest_means, refuse_difference_quotient_as_local_rate, @@ -1930,6 +2131,10 @@ mod tests { refuse_time_dependent_impulse_as_continuous_intercept, refuse_time_dependent_impulse_as_time_independent_effect, refuse_time_dependent_impulse_as_time_varying_discrete_effect, + refuse_time_dependent_impulse_carry_as_contemporaneous_impulse, + refuse_time_dependent_impulse_carry_as_continuous_intercept, + refuse_time_dependent_impulse_carry_as_time_independent_effect, + refuse_time_dependent_impulse_carry_as_time_varying_discrete_effect, refuse_time_independent_coefficient_as_discrete_effect, refuse_time_independent_effect_as_continuous_intercept, refuse_time_independent_effect_as_time_dependent_impulse, @@ -4206,4 +4411,343 @@ mod tests { Err(PsychometricError::InvalidNumericInput) ); } + + #[test] + fn time_dependent_impulse_carry_recovers_driver_equation_one_two_dissipation() { + let effect = 0.4_f64; + let predictor = 3.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let elapsed = 1.0_f64; + let carry = recover_time_dependent_predictor_impulse_carry( + effect, + predictor, + drift, + delta, + elapsed, + LagClock::EventTime, + ) + .expect("tdpred-carry"); + let expected = (-0.5_f64).exp() * 1.2; + assert!((carry - expected).abs() < 1e-15); + assert_eq!( + recover_time_dependent_predictor_impulse_carry( + 0.0, + predictor, + drift, + delta, + elapsed, + LagClock::EventTime + ), + Ok(0.0) + ); + assert_eq!( + recover_time_dependent_predictor_impulse_carry( + effect, + 0.0, + drift, + delta, + elapsed, + LagClock::EventTime + ), + Ok(0.0) + ); + let zero_drift = recover_time_dependent_predictor_impulse_carry( + effect, + predictor, + 0.0, + delta, + elapsed, + LagClock::EventTime, + ) + .expect("zero-drift"); + assert!((zero_drift - 1.2).abs() < 1e-15); + let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("tdpred"); + assert!((carry - impulse).abs() > 1e-3); + let vanished = recover_time_dependent_predictor_impulse_carry( + effect, + predictor, + -800.0, + delta, + elapsed, + LagClock::EventTime, + ) + .expect("vanish"); + assert_eq!(vanished.to_bits(), 0.0_f64.to_bits()); + } + + #[test] + fn time_dependent_impulse_carry_composes_evolved_mean_and_keeps_scale() { + let effect = 0.4_f64; + let predictor = 3.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let elapsed = 1.0_f64; + let carry = recover_time_dependent_predictor_impulse_carry( + effect, + predictor, + drift, + delta, + elapsed, + LagClock::EventTime, + ) + .expect("tdpred-carry"); + let initial = 1.0_f64; + let intercept = 0.3_f64; + let composed = recover_discrete_latent_mean_with_impulse_carry( + initial, + drift, + intercept, + effect, + predictor, + delta, + elapsed, + LagClock::EventTime, + ) + .expect("eq3-carry"); + let evolved = + recover_discrete_latent_mean(initial, drift, intercept, delta, LagClock::EventTime) + .expect("mu-t"); + assert!((composed - (evolved + carry)).abs() < 1e-15); + assert_eq!( + recover_discrete_latent_mean_with_impulse_carry( + initial, + drift, + intercept, + 0.0, + predictor, + delta, + elapsed, + LagClock::EventTime + ), + Ok(evolved) + ); + assert_eq!( + recover_discrete_latent_mean_with_impulse_carry( + 0.0, + drift, + 0.0, + effect, + predictor, + delta, + elapsed, + LagClock::EventTime + ), + Ok(carry) + ); + let scaled = recover_time_dependent_predictor_impulse_carry( + 1e308, + 1e-308, + 0.0, + 2.0, + 1.0, + LagClock::EventTime, + ) + .expect("scale"); + assert!((scaled - 1.0).abs() < 1e-15); + let rewritten = recover_time_dependent_predictor_impulse_carry( + 1e-308, + 1.0, + 710.0, + 2.0, + 1.0, + LagClock::EventTime, + ) + .expect("rewrite"); + let expected_rewrite = (1e-308_f64.ln() + 710.0).exp(); + assert!((rewritten - expected_rewrite).abs() <= expected_rewrite * 1e-12); + } + + #[test] + fn time_dependent_impulse_carry_refuses_contemporaneous_cint_tipred_and_equation_fourteen() { + let effect = 0.4_f64; + let predictor = 3.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let elapsed = 1.0_f64; + let carry = recover_time_dependent_predictor_impulse_carry( + effect, + predictor, + drift, + delta, + elapsed, + LagClock::EventTime, + ) + .expect("tdpred-carry"); + let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("tdpred"); + let intercept_effect = + recover_discrete_continuous_intercept_effect(effect, drift, delta, LagClock::EventTime) + .expect("cint"); + let time_independent = recover_discrete_time_independent_predictor_effect( + effect, + predictor, + drift, + delta, + LagClock::EventTime, + ) + .expect("tipred"); + let equation_fourteen = recover_discrete_time_varying_predictor_effect( + effect, + delta, + delta, + delta, + LagClock::EventTime, + ) + .expect("eq14"); + assert!((carry - impulse).abs() > 1e-3); + assert!((carry - intercept_effect).abs() > 1e-3); + assert!((carry - time_independent).abs() > 1e-3); + assert!((carry - equation_fourteen).abs() > 1e-3); + assert_eq!( + refuse_time_dependent_impulse_carry_as_contemporaneous_impulse(carry, impulse), + Err(PsychometricError::TimeDependentImpulseCarryIsNotContemporaneousImpulse) + ); + assert_eq!( + refuse_time_dependent_impulse_carry_as_continuous_intercept(carry, effect), + Err(PsychometricError::TimeDependentImpulseCarryIsNotContinuousIntercept) + ); + assert_eq!( + refuse_time_dependent_impulse_carry_as_time_independent_effect(carry, time_independent), + Err(PsychometricError::TimeDependentImpulseCarryIsNotTimeIndependentEffect) + ); + assert_eq!( + refuse_time_dependent_impulse_carry_as_time_varying_discrete_effect( + carry, + equation_fourteen + ), + Err(PsychometricError::TimeDependentImpulseCarryIsNotTimeVaryingDiscreteEffect) + ); + } + + #[test] + fn time_dependent_impulse_carry_invalid_inputs_fail_closed() { + assert_eq!( + recover_time_dependent_predictor_impulse_carry( + f64::NAN, + 1.0, + -0.5, + 2.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_time_dependent_predictor_impulse_carry( + 0.4, + 3.0, + f64::INFINITY, + 2.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_time_dependent_predictor_impulse_carry( + 1e308, + 2.0, + -0.5, + 2.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_time_dependent_predictor_impulse_carry( + 1.2, + 1.0, + 800.0, + 2.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_time_dependent_predictor_impulse_carry( + 0.0, + 3.0, + 800.0, + 2.0, + 1.0, + LagClock::EventTime + ), + Ok(0.0) + ); + assert_eq!( + recover_time_dependent_predictor_impulse_carry( + -1e-308, + 1.0, + 710.0, + 2.0, + 1.0, + LagClock::EventTime + ) + .map(f64::signum), + Ok(-1.0) + ); + } + + #[test] + fn time_dependent_impulse_carry_interval_and_clock_fail_closed() { + assert_eq!( + recover_time_dependent_predictor_impulse_carry( + 0.4, + 3.0, + -0.5, + 0.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_time_dependent_predictor_impulse_carry( + 0.4, + 3.0, + -0.5, + 2.0, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_time_dependent_predictor_impulse_carry( + 0.4, + 3.0, + -0.5, + 2.0, + 2.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_time_dependent_predictor_impulse_carry( + 0.4, + 3.0, + -0.5, + 2.0, + 1.0, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_discrete_latent_mean_with_impulse_carry( + 1e308, + 0.0, + 0.0, + 1e308, + 1.0, + 2.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + } } diff --git a/crates/psychometric_core/src/lib.rs b/crates/psychometric_core/src/lib.rs index a2e7a5e1..be3b50dc 100644 --- a/crates/psychometric_core/src/lib.rs +++ b/crates/psychometric_core/src/lib.rs @@ -36,10 +36,13 @@ //! first-occasion map `τ + λ μ_0` is not `E(y_t)`), recovers the //! Driver Eq. 3 fourth-summand impulse `m x` (Table 2 `TDPREDEFFECT` //! is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), -//! recovers the Driver Eq. 3 second-summand time-independent -//! predictor increment `A^{-1}[e^{A Δt} − I] B z` (Table 2 -//! `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle Eq. 14; -//! `B` is not that discrete increment), +//! recovers the Driver Eq. 1–2 within-interval impulse carry +//! `e^{A(t−u)} M x` for `t0 < u < t` (not the contemporaneous Dirac, +//! not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; §7.2 +//! dissipation), recovers the Driver Eq. 3 second-summand +//! time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` +//! (Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle +//! Eq. 14; `B` is not that discrete increment), //! and refuses //! latent-mean comparison below strong invariance. @@ -102,6 +105,8 @@ pub use event_time::recover_discrete_lagged_latent_covariance; pub use event_time::recover_discrete_latent_mean; /// Exact scalar evolved latent mean plus a contemporaneous impulse. pub use event_time::recover_discrete_latent_mean_with_impulse; +/// Exact scalar evolved latent mean plus a within-interval impulse carry. +pub use event_time::recover_discrete_latent_mean_with_impulse_carry; /// Exact scalar evolved latent mean plus a time-independent predictor. pub use event_time::recover_discrete_latent_mean_with_time_independent_predictor; /// Exact scalar discrete latent variance `A_Δt P A_Δt⊤ + Q_Δt`. @@ -134,6 +139,8 @@ pub use event_time::recover_manifest_trait_plus_state_observed_variance; pub use event_time::recover_stationary_latent_variance; /// Exact scalar contemporaneous `TDPREDEFFECT` impulse `m x`. pub use event_time::recover_time_dependent_predictor_impulse; +/// Exact scalar within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x`. +pub use event_time::recover_time_dependent_predictor_impulse_carry; /// Exact scalar trait-plus-state lagged covariance. pub use event_time::recover_trait_plus_state_lagged_covariance; /// Exact scalar trait-plus-state latent variance. @@ -178,6 +185,14 @@ pub use event_time::refuse_time_dependent_impulse_as_continuous_intercept; pub use event_time::refuse_time_dependent_impulse_as_time_independent_effect; /// Refuse treating Driver Eq. 3 impulse as Voelkle Eq. 14. pub use event_time::refuse_time_dependent_impulse_as_time_varying_discrete_effect; +/// Refuse treating Driver Eq. 1–2 impulse carry as the contemporaneous Dirac. +pub use event_time::refuse_time_dependent_impulse_carry_as_contemporaneous_impulse; +/// Refuse treating Driver Eq. 1–2 impulse carry as `CINT`. +pub use event_time::refuse_time_dependent_impulse_carry_as_continuous_intercept; +/// Refuse treating Driver Eq. 1–2 impulse carry as `TIPREDEFFECT`. +pub use event_time::refuse_time_dependent_impulse_carry_as_time_independent_effect; +/// Refuse treating Driver Eq. 1–2 impulse carry as Voelkle Eq. 14. +pub use event_time::refuse_time_dependent_impulse_carry_as_time_varying_discrete_effect; /// Refuse treating Driver Table 2 `TIPREDEFFECT` as the discrete increment. pub use event_time::refuse_time_independent_coefficient_as_discrete_effect; /// Refuse treating Driver Eq. 3 `TIPREDEFFECT` increment as `CINT`. 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 34876cb0..7b973d72 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -8,6 +8,7 @@ use psychometric_core::{ recover_discrete_constant_predictor_effect, recover_discrete_continuous_intercept_effect, recover_discrete_lag_from_log_rate, recover_discrete_lagged_latent_covariance, recover_discrete_latent_mean, recover_discrete_latent_mean_with_impulse, + recover_discrete_latent_mean_with_impulse_carry, recover_discrete_latent_mean_with_time_independent_predictor, recover_discrete_latent_variance, recover_discrete_observed_mean, recover_discrete_process_noise, recover_discrete_time_independent_predictor_effect, @@ -16,8 +17,9 @@ use psychometric_core::{ recover_kish_weighted_slope, recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, recover_stationary_latent_variance, - recover_time_dependent_predictor_impulse, recover_trait_plus_state_lagged_covariance, - recover_trait_plus_state_latent_variance, recover_within_residual_event_time_log_rate, + 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_continuous_intercept_as_discrete_mean_increment, refuse_continuous_intercept_as_initial_latent_mean, refuse_continuous_intercept_as_manifest_means, refuse_difference_quotient_as_local_rate, @@ -34,6 +36,10 @@ use psychometric_core::{ refuse_time_dependent_impulse_as_continuous_intercept, refuse_time_dependent_impulse_as_time_independent_effect, refuse_time_dependent_impulse_as_time_varying_discrete_effect, + refuse_time_dependent_impulse_carry_as_contemporaneous_impulse, + refuse_time_dependent_impulse_carry_as_continuous_intercept, + refuse_time_dependent_impulse_carry_as_time_independent_effect, + refuse_time_dependent_impulse_carry_as_time_varying_discrete_effect, refuse_time_independent_coefficient_as_discrete_effect, refuse_time_independent_effect_as_continuous_intercept, refuse_time_independent_effect_as_time_dependent_impulse, @@ -1419,6 +1425,141 @@ fn time_independent_predictor_refuses_overflow_and_non_event_clocks() { ); } +#[test] +fn time_dependent_impulse_carry_recovers_driver_equation_one_two_dissipation() { + let effect = 0.4_f64; + let predictor = 3.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let elapsed = 1.0_f64; + let carry = recover_time_dependent_predictor_impulse_carry( + effect, + predictor, + drift, + delta, + elapsed, + LagClock::EventTime, + ) + .expect("tdpred-carry"); + let expected = (-0.5_f64).exp() * 1.2; + let error = rmse(&[expected], &[carry]); + assert!( + error < 1e-15, + "Driver Eq. 1–2 impulse carry RMSE {error}: got {carry}" + ); + let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("tdpred"); + let intercept_effect = + recover_discrete_continuous_intercept_effect(effect, drift, delta, LagClock::EventTime) + .expect("cint"); + let time_independent = recover_discrete_time_independent_predictor_effect( + effect, + predictor, + drift, + delta, + LagClock::EventTime, + ) + .expect("tipred"); + let equation_fourteen = recover_discrete_time_varying_predictor_effect( + effect, + delta, + delta, + delta, + LagClock::EventTime, + ) + .expect("eq14"); + assert!(rmse(&[carry], &[impulse]) > rmse(&[expected], &[carry])); + assert!(rmse(&[carry], &[intercept_effect]) > rmse(&[expected], &[carry])); + assert!(rmse(&[carry], &[time_independent]) > rmse(&[expected], &[carry])); + assert!(rmse(&[carry], &[equation_fourteen]) > rmse(&[expected], &[carry])); + let composed = recover_discrete_latent_mean_with_impulse_carry( + 1.0, + drift, + 0.3, + effect, + predictor, + delta, + elapsed, + LagClock::EventTime, + ) + .expect("eq3-carry"); + let evolved = + recover_discrete_latent_mean(1.0, drift, 0.3, delta, LagClock::EventTime).expect("mu-t"); + let composed_error = rmse(&[evolved + carry], &[composed]); + assert!( + composed_error < 1e-15, + "Driver Eq. 1–2 μ_t + e^{{A(t−u)}} M x RMSE {composed_error}: got {composed}" + ); + assert_eq!( + refuse_time_dependent_impulse_carry_as_contemporaneous_impulse(carry, impulse), + Err(PsychometricError::TimeDependentImpulseCarryIsNotContemporaneousImpulse) + ); + assert_eq!( + refuse_time_dependent_impulse_carry_as_continuous_intercept(carry, effect), + Err(PsychometricError::TimeDependentImpulseCarryIsNotContinuousIntercept) + ); + assert_eq!( + refuse_time_dependent_impulse_carry_as_time_independent_effect(carry, time_independent), + Err(PsychometricError::TimeDependentImpulseCarryIsNotTimeIndependentEffect) + ); + assert_eq!( + refuse_time_dependent_impulse_carry_as_time_varying_discrete_effect( + carry, + equation_fourteen + ), + Err(PsychometricError::TimeDependentImpulseCarryIsNotTimeVaryingDiscreteEffect) + ); +} + +#[test] +fn time_dependent_impulse_carry_refuses_overflow_and_non_event_clocks() { + assert_eq!( + recover_time_dependent_predictor_impulse_carry( + 1e308, + 2.0, + -0.5, + 2.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_time_dependent_predictor_impulse_carry( + 0.4, + 3.0, + -0.5, + 2.0, + 1.0, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_time_dependent_predictor_impulse_carry( + 0.4, + 3.0, + -0.5, + 2.0, + 2.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_latent_mean_with_impulse_carry( + 1e308, + 0.0, + 0.0, + 1e308, + 1.0, + 2.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); +} + #[test] fn admitted_coordinates_still_required_for_multilevel_weights() { assert_eq!( diff --git a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs index 53fc7dbe..5c252713 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -6,6 +6,7 @@ use psychometric_core::{ recover_cluster_mean_within_between_slopes, recover_discrete_constant_predictor_effect, recover_discrete_continuous_intercept_effect, recover_discrete_lagged_latent_covariance, recover_discrete_latent_mean, recover_discrete_latent_mean_with_impulse, + recover_discrete_latent_mean_with_impulse_carry, recover_discrete_latent_mean_with_time_independent_predictor, recover_discrete_latent_variance, recover_discrete_observed_mean, recover_discrete_process_noise, recover_discrete_time_independent_predictor_effect, @@ -13,8 +14,8 @@ use psychometric_core::{ recover_loading_point_estimate_mean, recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, recover_stationary_latent_variance, - recover_time_dependent_predictor_impulse, recover_trait_plus_state_latent_variance, - recover_within_residual_event_time_log_rate, + recover_time_dependent_predictor_impulse, recover_time_dependent_predictor_impulse_carry, + recover_trait_plus_state_latent_variance, recover_within_residual_event_time_log_rate, refuse_continuous_intercept_as_discrete_mean_increment, refuse_continuous_intercept_as_initial_latent_mean, refuse_continuous_intercept_as_manifest_means, @@ -29,6 +30,10 @@ use psychometric_core::{ refuse_time_dependent_impulse_as_continuous_intercept, refuse_time_dependent_impulse_as_time_independent_effect, refuse_time_dependent_impulse_as_time_varying_discrete_effect, + refuse_time_dependent_impulse_carry_as_contemporaneous_impulse, + refuse_time_dependent_impulse_carry_as_continuous_intercept, + refuse_time_dependent_impulse_carry_as_time_independent_effect, + refuse_time_dependent_impulse_carry_as_time_varying_discrete_effect, refuse_time_independent_coefficient_as_discrete_effect, refuse_time_independent_effect_as_continuous_intercept, refuse_time_independent_effect_as_time_dependent_impulse, @@ -685,3 +690,95 @@ fn time_independent_predictor_is_not_cint_impulse_equation_fourteen_or_coefficie Err(psychometric_core::PsychometricError::TimeIndependentCoefficientIsNotDiscreteEffect) ); } + +#[test] +fn time_dependent_impulse_carry_is_not_contemporaneous_cint_tipred_or_equation_fourteen() { + let effect = 0.4_f64; + let predictor = 3.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let elapsed = 1.0_f64; + let carry = recover_time_dependent_predictor_impulse_carry( + effect, + predictor, + drift, + delta, + elapsed, + LagClock::EventTime, + ) + .expect("tdpred-carry"); + let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("tdpred"); + let intercept_effect = + recover_discrete_continuous_intercept_effect(effect, drift, delta, LagClock::EventTime) + .expect("cint"); + let time_independent = recover_discrete_time_independent_predictor_effect( + effect, + predictor, + drift, + delta, + LagClock::EventTime, + ) + .expect("tipred"); + let equation_fourteen = recover_discrete_time_varying_predictor_effect( + effect, + delta, + delta, + delta, + LagClock::EventTime, + ) + .expect("eq14"); + let evolved = + recover_discrete_latent_mean(1.0, drift, 0.3, delta, LagClock::EventTime).expect("mu-t"); + let composed = recover_discrete_latent_mean_with_impulse_carry( + 1.0, + drift, + 0.3, + effect, + predictor, + delta, + elapsed, + LagClock::EventTime, + ) + .expect("eq3-carry"); + assert!( + (carry - impulse).abs() > 1e-3, + "Driver et al. (2017, Eq. 1–2 / §7.2): e^{{A(t−u)}} M x is not the contemporaneous Dirac" + ); + assert!( + (carry - intercept_effect).abs() > 1e-3, + "Driver et al. (2017, Eq. 1–2): e^{{A(t−u)}} M x is not CINT" + ); + assert!( + (carry - time_independent).abs() > 1e-3, + "Driver et al. (2017, Eq. 1–2): e^{{A(t−u)}} M x is not TIPREDEFFECT" + ); + assert!( + (carry - equation_fourteen).abs() > 1e-3, + "Driver et al. (2017, Eq. 1–2): e^{{A(t−u)}} M x is not Voelkle Eq. 14" + ); + assert!( + (composed - evolved).abs() > 1e-3, + "Driver et al. (2017, Eq. 1–2): μ_t is not μ_t + e^{{A(t−u)}} M x" + ); + assert_eq!( + refuse_time_dependent_impulse_carry_as_contemporaneous_impulse(carry, impulse), + Err(psychometric_core::PsychometricError::TimeDependentImpulseCarryIsNotContemporaneousImpulse) + ); + assert_eq!( + refuse_time_dependent_impulse_carry_as_continuous_intercept(carry, effect), + Err( + psychometric_core::PsychometricError::TimeDependentImpulseCarryIsNotContinuousIntercept + ) + ); + assert_eq!( + refuse_time_dependent_impulse_carry_as_time_independent_effect(carry, time_independent), + Err(psychometric_core::PsychometricError::TimeDependentImpulseCarryIsNotTimeIndependentEffect) + ); + assert_eq!( + refuse_time_dependent_impulse_carry_as_time_varying_discrete_effect( + carry, + equation_fourteen + ), + Err(psychometric_core::PsychometricError::TimeDependentImpulseCarryIsNotTimeVaryingDiscreteEffect) + ); +} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index b3e49784..55468383 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -28,7 +28,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | report template/section/copied/style/modality method effects | ADR 0004/0012; PRD/TRD | simulation truth factors implemented; estimator-side method model remains future | partial | | candidate K statistical/Pareto gates + blinded LLM review | ADR 0012; research | future `model_selection` | accepted-target | | compositional topic correlation / stable clustering | ADR 0005/0012; research | future `network_analysis` | accepted-target | -| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z); full ESEM/DSEM remaining | partial | +| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z); full ESEM/DSEM remaining | partial | | CPU bounded multithreading + GPU/VRAM streaming/parity | ADR 0001/0006 | future `compute_backend` | accepted-target | | TDT detection/tracking vs CHRONOS schema/prediction/temporal consistency | ADR 0016; PRD/research | future `event_intelligence` | accepted-target | | evidence-bounded LLM interpretation | ADR 0010/0012; PRD | future `interpretation_gateway` | accepted-target | diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index 0ec10eb1..64580cf7 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -1,7 +1,7 @@ # ADR 0005 — Posterior-aware ESEM/DSEM and structural interpretation **Decision status:** Accepted -**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), exact scalar contemporaneous `TDPREDEFFECT` impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; the §7.2 level-change form is not that impulse), exact scalar time-independent `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle Eq. 14; `B` is not that discrete increment), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target +**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), exact scalar contemporaneous `TDPREDEFFECT` impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; the §7.2 level-change form is not that impulse), exact scalar time-independent `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle Eq. 14; `B` is not that discrete increment), exact scalar within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target **Date:** 2026-08-05 **Supersedes:** None. ADR 0012 governs upstream topic measurement/network coordinates; this ADR governs higher-order psychometric structure and longitudinal interpretation. @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), recovers the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z; `TDPREDEFFECT` is `M`, not `CINT`; `M x` is not `A^{-1}[e^{A Δt} − I] B z`; `M x` is not Voelkle et al., 2012, Eq. 14; the level-change form is not that impulse), recovers the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z; `TIPREDEFFECT` is `B`, not `κ`; form `B z` first, then the discrete intercept map; a zero drift is `B z Δt`; `B` is not the discrete increment; `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle Eq. 14), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), recovers the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z; `TDPREDEFFECT` is `M`, not `CINT`; `M x` is not `A^{-1}[e^{A Δt} − I] B z`; `M x` is not Voelkle et al., 2012, Eq. 14; the level-change form is not that impulse), recovers the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z; `TIPREDEFFECT` is `B`, not `κ`; form `B z` first, then the discrete intercept map; a zero drift is `B z Δt`; `B` is not the discrete increment; `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle Eq. 14), recovers the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z; form `m x` first, then `e^{a(t−u)} m x`; a zero drift is `m x`; underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept; `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; an impulse at `u = t` is the contemporaneous map; an impulse at `u ≤ t0` is already in `η(t0)`), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. 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 79e1ccc5..0f9ee45c 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -24,15 +24,16 @@ This slice stays inside `psychometric_core`. It does not add a second invariance 18. recover the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`) and refuse treating that first-occasion mean as `E(y_t)`; 19. recover the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`) and refuse treating that impulse as `CINT`, `TIPREDEFFECT`, or Voelkle et al. (2012, Eq. 14); 20. recover the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`) and refuse treating that increment as `CINT`, `TDPREDEFFECT`, Voelkle et al. (2012, Eq. 14), or the coefficient `B`; -21. refuse pooling discrete lags from unequal event intervals as one coefficient; -22. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); -23. refuse the difference quotient as a continuous-time rate; -24. apply the same event-time map to CWC residuals (still not DSEM); -25. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). +21. recover the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; the printed Eq. 3 fourth summand is the contemporaneous Dirac) and refuse treating that carry as the contemporaneous impulse, `CINT`, `TIPREDEFFECT`, or Voelkle et al. (2012, Eq. 14); +22. refuse pooling discrete lags from unequal event intervals as one coefficient; +23. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); +24. refuse the difference quotient as a continuous-time rate; +25. apply the same event-time map to CWC residuals (still not DSEM); +26. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). ## Claim boundary -This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. The contemporaneous `TDPREDEFFECT` impulse is not the continuous intercept, not the time-independent discrete effect, and not Voelkle et al. (2012, Eq. 14). The time-independent `TIPREDEFFECT` increment is not the continuous intercept, not the contemporaneous impulse, not Voelkle et al. (2012, Eq. 14), and not the coefficient `B`. +This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. The contemporaneous `TDPREDEFFECT` impulse is not the continuous intercept, not the time-independent discrete effect, and not Voelkle et al. (2012, Eq. 14). The time-independent `TIPREDEFFECT` increment is not the continuous intercept, not the contemporaneous impulse, not Voelkle et al. (2012, Eq. 14), and not the coefficient `B`. The within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). ## Authoritative sources @@ -50,7 +51,7 @@ Kish, L. (1965). *Survey sampling*. John Wiley & Sons. Oud, J. H. L., & Jansen, R. A. R. G. (2000). Continuous time state space modeling of panel data by means of SEM. *Psychometrika, 65*(2), 199–215. https://doi.org/10.1007/BF02294374 (cited by Voelkle et al., 2012, Eq. 14 discussion; PDF not opened). -The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:07Z (https://www.zora.uzh.ch/handle/20.500.14742/72792; bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`). Page 16 writes discrete auto-effects as \(A^{*}(\Delta t)=\exp(A\Delta t)\) (Eq. 7). Introducing Intercepts (manuscript p. 20, Eq. 12) adds a continuous-time intercept \(b\) and writes the expected-value solution whose discrete increment is \(A^{-1}(\exp(A\Delta t)-I)b\); the scalar constant-predictor effect is \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). The next paragraph (manuscript p. 21, Eq. 14) writes the discrete effect of a **time-varying** predictor, when the sampling interval equals the constancy interval, as \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product does not depend on the predictor auto-effect. The manuscript calls Eq. 14 a first-order approximation that deteriorates as \(\Delta t\) grows, and defers unmatched sampling/constancy intervals to Oud and Jansen (2000), which is unread. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4; JSS PDF re-opened 2026-08-19T04:10Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\), the discrete intercept \(b_{\Delta t}=A^{-1}[A_{\Delta t}-I]b\), and the discrete process-noise covariance \(Q_{\Delta t}=\int_{0}^{\Delta t}\operatorname{expm}(A(\Delta t-\tau))LGG^{\top}L^{\top}\operatorname{expm}(A(\Delta t-\tau))^{\top}\,d\tau\). Equation 4 (p. 5) writes that the integral exhibits covariance \(Q_{\Delta t}=\operatorname{irow}(A^{\#^{-1}}[e^{A^{\#}\Delta t}-I]\operatorname{row}(Q))\) with \(A^{\#}=A\otimes I+I\otimes A\). The homogeneous-process consequence (`ξ`, `z` given) is \(Q_{\Delta t}=\operatorname{cov}(\eta_{ti}\mid\eta_{t-1,i})\) and \(\operatorname{cov}(\eta_{ti},\eta_{t-1,i})=A_{\Delta t}\operatorname{cov}(\eta_{t-1,i})\). The law of total variance on that pair is \(\operatorname{Var}(\eta_{ti})=A_{\Delta t}\operatorname{Var}(\eta_{t-1,i})A_{\Delta t}^{\top}+Q_{\Delta t}\). As \(\Delta t\to\infty\) with stable \(a<0\), Eq. 4 becomes \(-q/(2a)\). The JSS summary names that limit `asymDIFFUSION` and takes it as the total within-subject variance (p. 16). Section 4.3 (pp. 9–10) constrains a stationary `T0VAR` to that model-predicted variance and distinguishes it from a predetermined first occasion. The same section (p. 9) adds a stable trait process with `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is that time-invariant between-subject variance. The JSS article has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). This slice takes scalar \(L=1\) (every latent subject to system noise); it does not implement the 0/1 selector matrix and is not a Kalman filter. Discrete auto-effects are strictly positive for finite real drift and interval; the inverse \(a=\ln\varphi/\Delta t\) therefore requires \(\varphi>0\). Binary64 `exp` of a large negative argument is `+0` and is refused as a discrete lag; the same underflow is a vanishing lagged covariance and is kept. Equations 3–4 of Voelkle et al. (2012) are the discouraged difference-quotient approximation. Meredith (1993) remains unread (Unpaywall/OpenAlex 2026-08-19T04:10Z: closed; Springer `content/pdf` is HTML 200, not a PDF; archive.org title search empty). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex 2026-08-19T04:10Z: closed; Springer `content/pdf` is HTML 200; ETS landing page is HTML). ERIC ED334221 is Singer and Willett (1991), *From whether to when*, not the 1991 journal article. ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title is not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex 2026-08-18T21:07Z: closed; Radboud landing and bitstream 403). +The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:07Z (https://www.zora.uzh.ch/handle/20.500.14742/72792; bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`). Page 16 writes discrete auto-effects as \(A^{*}(\Delta t)=\exp(A\Delta t)\) (Eq. 7). Introducing Intercepts (manuscript p. 20, Eq. 12) adds a continuous-time intercept \(b\) and writes the expected-value solution whose discrete increment is \(A^{-1}(\exp(A\Delta t)-I)b\); the scalar constant-predictor effect is \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). The next paragraph (manuscript p. 21, Eq. 14) writes the discrete effect of a **time-varying** predictor, when the sampling interval equals the constancy interval, as \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product does not depend on the predictor auto-effect. The manuscript calls Eq. 14 a first-order approximation that deteriorates as \(\Delta t\) grows, and defers unmatched sampling/constancy intervals to Oud and Jansen (2000), which is unread. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4; JSS PDF re-opened 2026-08-19T04:10Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\), the discrete intercept \(b_{\Delta t}=A^{-1}[A_{\Delta t}-I]b\), and the discrete process-noise covariance \(Q_{\Delta t}=\int_{0}^{\Delta t}\operatorname{expm}(A(\Delta t-\tau))LGG^{\top}L^{\top}\operatorname{expm}(A(\Delta t-\tau))^{\top}\,d\tau\). Equation 4 (p. 5) writes that the integral exhibits covariance \(Q_{\Delta t}=\operatorname{irow}(A^{\#^{-1}}[e^{A^{\#}\Delta t}-I]\operatorname{row}(Q))\) with \(A^{\#}=A\otimes I+I\otimes A\). The homogeneous-process consequence (`ξ`, `z` given) is \(Q_{\Delta t}=\operatorname{cov}(\eta_{ti}\mid\eta_{t-1,i})\) and \(\operatorname{cov}(\eta_{ti},\eta_{t-1,i})=A_{\Delta t}\operatorname{cov}(\eta_{t-1,i})\). The law of total variance on that pair is \(\operatorname{Var}(\eta_{ti})=A_{\Delta t}\operatorname{Var}(\eta_{t-1,i})A_{\Delta t}^{\top}+Q_{\Delta t}\). As \(\Delta t\to\infty\) with stable \(a<0\), Eq. 4 becomes \(-q/(2a)\). The JSS summary names that limit `asymDIFFUSION` and takes it as the total within-subject variance (p. 16). Section 4.3 (pp. 9–10) constrains a stationary `T0VAR` to that model-predicted variance and distinguishes it from a predetermined first occasion. The same section (p. 9) adds a stable trait process with `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is that time-invariant between-subject variance. The JSS article has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). This slice takes scalar \(L=1\) (every latent subject to system noise); it does not implement the 0/1 selector matrix and is not a Kalman filter. Discrete auto-effects are strictly positive for finite real drift and interval; the inverse \(a=\ln\varphi/\Delta t\) therefore requires \(\varphi>0\). Binary64 `exp` of a large negative argument is `+0` and is refused as a discrete lag; the same underflow is a vanishing lagged covariance and is kept. Equations 3–4 of Voelkle et al. (2012) are the discouraged difference-quotient approximation. Meredith (1993) remains unread (Unpaywall/OpenAlex 2026-08-20T10:33Z: closed; Springer `content/pdf` is HTML 200, not a PDF; archive.org title search empty). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex 2026-08-20T10:33Z: closed; Springer `content/pdf` is HTML 200; ETS landing page is HTML). ERIC ED334221 is Singer and Willett (1991), *From whether to when*, not the 1991 journal article. ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title is not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex 2026-08-18T21:07Z: closed; Radboud landing and bitstream 403). ## Formula notes @@ -73,6 +74,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - **Discrete observed-indicator mean.** Driver et al. (2017, Eq. 3, p. 5; Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T22:10Z): `η_i(t)=exp(AΔt)η_i(t0)+A^{-1}[exp(AΔt)−I]ξ_i+…` with `ξ_i∼N(κ,φ_ξ)` and a stochastic integral of mean zero, then `y_i(t)=Γ_i+Λη_i(t)+ζ_i(t)` with `Γ∼N(τ,Ψ)`. The scalar composition is `E(y_t)=τ+λμ_t`. Form `μ_t` first, then `τ+λμ_t`. A zero loading or zero evolved latent mean is exactly `τ`. A zero intercept is exactly `λμ_t`. A zero drift is `τ+λ(μ_0+κΔt)`. Underflow of `exp(aΔt)` to `+0` keeps `τ+λ(−κ/a)`. The first-occasion map `τ+λμ_0` is not `E(y_t)`. `MANIFESTMEANS` is not `E(y_t)`. `T0MEANS` is not `E(y_t)`. `μ_t` is not `E(y_t)`. An overflowing exponential, product, or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **Time-dependent predictor impulse.** Driver et al. (2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z): `χ_i(t)=Σ x_{i,u} δ(t−u)` and the fourth summand is `M Σ x_{i,u} δ(t−u)`. Table 2 names `M` `TDPREDEFFECT`. Section 7.2 calls this a sudden impulse that dissipates back to the process mean. The scalar contemporaneous jump is `m x`. Form `μ_t` first, then add `m x`. A zero effect or zero predictor is exactly zero. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{AΔt}−I] B z` (`TIPREDEFFECT`). `M x` is not Voelkle et al. (2012, Eq. 14) `a_{yx}Δt`. The §7.2 level-change form is an extra latent process with near-zero drift and is not this map. An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **Time-independent predictor effect.** Driver et al. (2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z): Equation 1 writes `dη=(Aη+b+A_{ηξ}ξ+Bz)dt+GdW+Mdχ`. Equation 3's second summand is `A^{-1}[e^{AΔt}−I](b+A_{ηξ}ξ+Bz)`. Table 2 names `B` `TIPREDEFFECT`. The scalar map is `(e^{aΔt}−1)/a·Bz` for `a≠0`. Form `Bz` first, then the discrete intercept map. A zero drift is `BzΔt`. Form `μ_t` first, then add that increment. A zero effect or zero predictor is exactly zero. `TIPREDEFFECT` is `B`, not the discrete increment. `A^{-1}[e^{AΔt}−I]Bz` is not `CINT`, not `Mx`, and not Voelkle et al. (2012, Eq. 14) `a_{yx}Δt`. An overflowing product, increment, or sum fails closed. This is not a Kalman filter and not ctsem estimation. +- **Time-dependent predictor impulse carry.** Driver et al. (2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z): Equation 1 writes `dη=(Aη+ξ+Bz+Mχ(t))dt+GdW`. Equation 2 writes `χ_i(t)=Σ x_{i,u} δ(t−u)`. The Green-function integral of that Dirac on `(t0,t)` is `e^{A(t−u)}Mx`. The printed Eq. 3 fourth summand is the contemporaneous jump `Mx` at `u=t`. This map is the strictly within-interval case `t0 Date: Thu, 20 Aug 2026 05:18:20 +0000 Subject: [PATCH 37/62] feat(psychometric): recover Driver Eq. 5 of the Eq. 1-2 impulse carry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compose E(y_t) = τ + λ(μ_t + e^{a(t-u)} m x) from the re-opened Driver, Oud, and Voelkle (2017) JSS PDF (Eq. 5, p. 5; Eq. 1-2, pp. 4-5; §7.2). Form the carried latent mean first. τ + λ μ_t is not that observed mean. The contemporaneous map τ + λ(μ_t + m x) is not that observed mean when u ≠ t. --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 1 + CLAUDE.md | 2 +- README.md | 2 +- crates/psychometric_core/src/error.rs | 11 + crates/psychometric_core/src/event_time.rs | 411 +++++++++++++++++- crates/psychometric_core/src/lib.rs | 8 +- ...multilevel_event_time_recovery_contract.rs | 251 ++++++++++- .../scientific_claim_boundary_contract.rs | 96 +++- docs/TRACEABILITY.md | 2 +- docs/adr/0005-posterior-esem-dsem.md | 4 +- docs/adr/README.md | 2 +- .../multilevel-event-time-recovery.md | 17 +- docs/research/posterior-esem-input-gates.md | 2 +- docs/validation/temporal-event-foundation.md | 2 +- 15 files changed, 790 insertions(+), 23 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 1512ca3e..9cd3e1d2 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -61,7 +61,7 @@ boundaries above remain the target modular MSA architecture. | `tepp_simulation` | known-truth temporal/event data generation | | `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics | | `tepp_api` | versioned DTO, schema, and export contracts | -| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | +| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | No crate exposes placeholder production behavior in Task 1. This prevents an empty façade from becoming a de facto public API before its invariants and tests diff --git a/CHANGELOG.md b/CHANGELOG.md index c0c01e9a..8fd4bebb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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, Eq. 5, p. 5; Eq. 1–2, pp. 4–5; Eq. 3 exponential map; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T05:12Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar observed-indicator mean of a within-interval time-dependent impulse carry. Equation 5 writes `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and `Γ ~ N(τ, Ψ)`. The latent process at `t` after a Dirac that occurred strictly inside `(t0, t)` is `μ_t + e^{a(t−u)} m x`. The scalar composition is `E(y_t) = τ + λ(μ_t + e^{a(t−u)} m x)`. Form the carried latent mean first, then `τ + λ` of that mean. The evolved observed mean `τ + λ μ_t` is not this composition. The contemporaneous map `τ + λ(μ_t + m x)` is not this composition when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The carried latent mean is not `E(y_t)`. A zero loading is exactly `τ`. The §7.2 level-change form is a different specification and is not this map. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993), Vandenberg and Lance (2000), and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T05:12Z: `is_oa: false`). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar within-interval time-dependent predictor impulse carry. Equation 1 writes `dη = (A η + ξ + B z + M χ(t)) dt + G dW`. Equation 2 writes `χ_i(t) = Σ x_{i,u} δ(t − u)`. The Green-function integral of that Dirac on `(t0, t)` is `e^{A(t−u)} M x`. The printed Eq. 3 fourth summand is the contemporaneous jump `M x` at `u = t`. This map is the strictly within-interval case `t0 < u < t`. Form `m x` first, then `e^{a(t−u)} m x`. A zero drift is `m x` with no dissipation. Underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation back to the process mean and is kept. Form `μ_t` first, then add the carry. `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `A^{-1}[e^{A Δt} − I] B z` (`TIPREDEFFECT`), and not Voelkle et al. (2012, Eq. 14) `a_{yx} Δt`. An impulse at `u = t` is the contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. The §7.2 level-change form is a different specification and is not this map. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993), Vandenberg and Lance (2000), and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T10:33Z: `is_oa: false`). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar time-independent predictor increment. Equation 1 writes `dη = (A η + b + A_{ηξ} ξ + B z) dt + G dW + M dχ`. Equation 3's second summand is `A^{-1}[e^{A Δt} − I](b + A_{ηξ} ξ + B z)`. Table 2 names `B` `TIPREDEFFECT`. Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. Form `μ_t` first, then add that increment. `TIPREDEFFECT` is `B`, not the discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x` (`TDPREDEFFECT`), and not Voelkle et al. (2012, Eq. 14) `a_{yx} Δt`. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993), Vandenberg and Lance (2000), and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T10:13Z: `is_oa: false`). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar contemporaneous time-dependent predictor impulse. Equation 2 writes `χ_i(t) = Σ x_{i,u} δ(t − u)`. Equation 3's fourth summand is `M Σ x_{i,u} δ(t − u)`. Table 2 names `M` `TDPREDEFFECT`. Section 7.2 calls this a sudden impulse that dissipates back to the process mean and reports `TDPREDEFFECT` as the initial impact. The scalar jump is `m x`. Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` (`TIPREDEFFECT`). `M x` is not Voelkle et al. (2012, Eq. 14) `a_{yx} Δt`. The §7.2 level-change form is a different specification and is not this map. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993), Vandenberg and Lance (2000), and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T07:10Z: `is_oa: false`). diff --git a/CLAUDE.md b/CLAUDE.md index ad19f356..b41c6aa0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. Putnick and Bornstein (2016, PMC5145197 opened 2026-08-19T22:15Z) require scalar invariance before latent-mean comparison; residual invariance is not a prerequisite. Two-observation series have no residual degrees of freedom (`ordinary_least_squares_fit` returns residual variance `0`) and cap at strong/scalar; they still license means. This is two-group OLS, not MGCFA. Meredith (1993) names remain unread labels. -- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. The contemporaneous time-dependent predictor impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`). Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). The §7.2 level-change form is not that impulse. The time-independent predictor increment is `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`). Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. `TIPREDEFFECT` is `B`, not that discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle et al. (2012, Eq. 14). The within-interval time-dependent impulse carry is `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation). Form `m x` first, then `e^{a(t−u)} m x`. A zero drift is `m x` with no dissipation. Underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept. `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). An impulse at `u = t` is the contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. The contemporaneous time-dependent predictor impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`). Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). The §7.2 level-change form is not that impulse. The time-independent predictor increment is `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`). Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. `TIPREDEFFECT` is `B`, not that discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle et al. (2012, Eq. 14). The within-interval time-dependent impulse carry is `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation). Form `m x` first, then `e^{a(t−u)} m x`. A zero drift is `m x` with no dissipation. Underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept. `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). An impulse at `u = t` is the contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. The observed mean of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The carried latent mean is not `E(y_t)`. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. diff --git a/README.md b/README.md index 7cd9bb3c..26a8fce1 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ crates/corpus_split crates/tepp_simulation crates/validation_core crates/tepp_api -crates/psychometric_core # input gates, CWC/event-time/contextual, irregular residual lag, Rubin T, strong means, Driver Eq. 3 TDPRED/TIPRED maps including within-interval impulse carry; not a full ESEM/DSEM estimator +crates/psychometric_core # input gates, CWC/event-time/contextual, irregular residual lag, Rubin T, strong means, Driver Eq. 3 TDPRED/TIPRED maps including Eq. 5 of within-interval impulse carry; not a full ESEM/DSEM estimator ``` ## Local verification diff --git a/crates/psychometric_core/src/error.rs b/crates/psychometric_core/src/error.rs index e7883700..8a0638ed 100644 --- a/crates/psychometric_core/src/error.rs +++ b/crates/psychometric_core/src/error.rs @@ -140,6 +140,10 @@ pub enum PsychometricError { /// Voelkle et al. (2012, Eq. 14). `e^{A(t−u)} M x` is not /// `a_{yx} Δt`. TimeDependentImpulseCarryIsNotTimeVaryingDiscreteEffect, + /// Driver Eq. 5 of the Eq. 3 evolved mean was treated as + /// Equation 5 of the Eq. 1–2 carried latent mean. + /// `τ + λ μ_t` is not `τ + λ(μ_t + e^{a(t−u)} m x)`. + EvolvedObservedMeanIsNotImpulseCarryObservedMean, } impl fmt::Display for PsychometricError { @@ -260,6 +264,9 @@ impl fmt::Display for PsychometricError { Self::TimeDependentImpulseCarryIsNotTimeVaryingDiscreteEffect => { "time-dependent predictor impulse carry is not the time-varying discrete effect" } + Self::EvolvedObservedMeanIsNotImpulseCarryObservedMean => { + "evolved observed mean is not the impulse-carry observed mean" + } }; formatter.write_str(message) } @@ -457,5 +464,9 @@ mod tests { PsychometricError::TimeDependentImpulseCarryIsNotTimeVaryingDiscreteEffect.to_string(), "time-dependent predictor impulse carry is not the time-varying discrete effect" ); + assert_eq!( + PsychometricError::EvolvedObservedMeanIsNotImpulseCarryObservedMean.to_string(), + "evolved observed mean is not the impulse-carry observed mean" + ); } } diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index b6032c70..867ca969 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -45,7 +45,9 @@ //! exponential map a time-dependent impulse that occurred strictly //! inside `(t0, t)` as `e^{A(t−u)} M x` (`t0 < u < t`). That carry //! is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, -//! and not Voelkle Eq. 14. Section 7.2 calls this dissipation back to +//! and not Voelkle Eq. 14. Equation 5 of that carried latent mean +//! is `τ + λ(μ_t + e^{a(t−u)} m x)` (`τ + λ μ_t` is not that +//! observed mean). Section 7.2 calls this dissipation back to //! the process mean. Equation 3's second summand also //! maps the time-independent predictor as `A^{-1}[e^{A Δt} − I] B z` //! (Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle @@ -1730,6 +1732,77 @@ pub fn recover_discrete_latent_mean_with_impulse_carry( require_finite(evolved_latent_mean + impulse_carry) } +/// Exact scalar observed mean of a within-interval impulse carry. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Eq. 1–2, pp. 4–5; +/// Eq. 3 exponential map; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF +/// re-opened 2026-08-20T05:12Z from +/// ) +/// write `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and +/// `Γ ~ N(τ, Ψ)`. The expected intercept is `τ`. The latent process +/// at `t` after a Dirac that occurred strictly inside `(t0, t)` is +/// `μ_t + e^{a(t−u)} m x`. The scalar composition is +/// `E(y_t) = τ + λ(μ_t + e^{a(t−u)} m x)`. Form the carried latent +/// mean first, then `τ + λ` of that mean. Table 2 names `τ` +/// `MANIFESTMEANS`. A zero loading is exactly `τ`. A zero +/// evolved-plus-carry latent mean is exactly `τ`. A zero intercept +/// is exactly `λ(μ_t + carry)`. The evolved observed mean +/// `τ + λ μ_t` is not this composition when the carry is nonzero. +/// The contemporaneous map `τ + λ(μ_t + m x)` is not this +/// composition when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The +/// carried latent mean is not `E(y_t)`. The §7.2 level-change form +/// is a different specification and is not this map. This is not a +/// Kalman filter and not ctsem estimation. +/// +/// # Errors +/// +/// Propagates [`recover_discrete_latent_mean_with_impulse_carry`] and +/// [`recover_manifest_observed_mean`]. +#[allow(clippy::too_many_arguments)] +pub fn recover_discrete_observed_mean_with_impulse_carry( + loading: f64, + initial_latent_mean: f64, + log_rate: f64, + continuous_intercept: f64, + time_dependent_effect: f64, + time_dependent_predictor: f64, + manifest_mean: f64, + event_delta: f64, + elapsed_after_impulse: f64, + clock: LagClock, +) -> Result { + let carried_latent_mean = recover_discrete_latent_mean_with_impulse_carry( + initial_latent_mean, + log_rate, + continuous_intercept, + time_dependent_effect, + time_dependent_predictor, + event_delta, + elapsed_after_impulse, + clock, + )?; + recover_manifest_observed_mean(loading, carried_latent_mean, manifest_mean) +} + +/// Refuse treating the evolved observed mean as the impulse-carry +/// observed mean. +/// +/// Equation 5 of the Eq. 3 evolved mean is `τ + λ μ_t`. Equation 5 +/// of the Eq. 1–2 carried latent mean is +/// `τ + λ(μ_t + e^{a(t−u)} m x)`. Those are not the same map. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::EvolvedObservedMeanIsNotImpulseCarryObservedMean`]. +pub fn refuse_evolved_observed_mean_as_impulse_carry_observed_mean( + evolved_observed_mean: f64, + impulse_carry_observed_mean: f64, +) -> Result { + let _ = (evolved_observed_mean, impulse_carry_observed_mean); + Err(PsychometricError::EvolvedObservedMeanIsNotImpulseCarryObservedMean) +} + /// Refuse treating the Eq. 1–2 impulse carry as the contemporaneous Dirac. /// /// The printed Eq. 3 fourth summand is `M x` at `u = t`. The @@ -2105,7 +2178,8 @@ mod tests { recover_discrete_latent_mean_with_impulse_carry, recover_discrete_latent_mean_with_time_independent_predictor, recover_discrete_latent_variance, recover_discrete_observed_mean, - recover_discrete_process_noise, recover_discrete_time_independent_predictor_effect, + recover_discrete_observed_mean_with_impulse_carry, recover_discrete_process_noise, + recover_discrete_time_independent_predictor_effect, recover_discrete_time_varying_predictor_effect, recover_event_series_mean_log_rate, recover_event_time_discrete_lag_and_log_rate, recover_irregular_centered_residual_log_rate, recover_local_log_rate, recover_manifest_lagged_observed_covariance, @@ -2117,6 +2191,7 @@ mod tests { refuse_continuous_intercept_as_discrete_mean_increment, refuse_continuous_intercept_as_initial_latent_mean, refuse_continuous_intercept_as_manifest_means, refuse_difference_quotient_as_local_rate, + refuse_evolved_observed_mean_as_impulse_carry_observed_mean, refuse_finite_interval_process_noise_as_stationary_variance, refuse_initial_latent_mean_as_evolved_mean, refuse_initial_observed_mean_as_evolved_observed_mean, @@ -4558,6 +4633,338 @@ mod tests { assert!((rewritten - expected_rewrite).abs() <= expected_rewrite * 1e-12); } + #[test] + fn discrete_observed_mean_with_impulse_carry_recovers_driver_equation_five() { + let loading = 2.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let elapsed = 1.0_f64; + let effect = 0.4_f64; + let predictor = 3.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let recovered = recover_discrete_observed_mean_with_impulse_carry( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + elapsed, + LagClock::EventTime, + ) + .expect("eq5-carry-mean"); + let carried = recover_discrete_latent_mean_with_impulse_carry( + initial, + drift, + intercept, + effect, + predictor, + delta, + elapsed, + LagClock::EventTime, + ) + .expect("carried"); + let expected = manifest_mean + loading * carried; + assert!((recovered - expected).abs() < 1e-15); + let evolved_observed = recover_discrete_observed_mean( + loading, + initial, + drift, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq3-eq5-mean"); + assert!((evolved_observed - recovered).abs() > 1e-3); + assert_eq!( + recover_discrete_observed_mean_with_impulse_carry( + 0.0, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + elapsed, + LagClock::EventTime + ), + Ok(manifest_mean) + ); + assert_eq!( + recover_discrete_observed_mean_with_impulse_carry( + loading, + initial, + drift, + intercept, + effect, + predictor, + 0.0, + delta, + elapsed, + LagClock::EventTime + ), + Ok(loading * carried) + ); + } + + #[test] + fn discrete_observed_mean_with_impulse_carry_is_not_contemporaneous_or_zero_carry() { + let loading = 2.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let elapsed = 1.0_f64; + let effect = 0.4_f64; + let predictor = 3.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let recovered = recover_discrete_observed_mean_with_impulse_carry( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + elapsed, + LagClock::EventTime, + ) + .expect("eq5-carry-mean"); + let contemporaneous_latent = recover_discrete_latent_mean_with_impulse( + initial, + drift, + intercept, + effect, + predictor, + delta, + LagClock::EventTime, + ) + .expect("mx"); + let contemporaneous = + recover_manifest_observed_mean(loading, contemporaneous_latent, manifest_mean) + .expect("eq5-mx"); + assert!((contemporaneous - recovered).abs() > 1e-3); + let evolved_observed = recover_discrete_observed_mean( + loading, + initial, + drift, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq3-eq5-mean"); + let zero_carry = recover_discrete_observed_mean_with_impulse_carry( + loading, + initial, + drift, + intercept, + 0.0, + predictor, + manifest_mean, + delta, + elapsed, + LagClock::EventTime, + ) + .expect("zero-carry"); + assert!((zero_carry - evolved_observed).abs() < 1e-15); + } + + #[test] + fn discrete_observed_mean_with_impulse_carry_refuses_evolved_mean_and_overflow() { + let loading = 2.0_f64; + let recovered = recover_discrete_observed_mean_with_impulse_carry( + loading, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, + 1.0, + LagClock::EventTime, + ) + .expect("eq5-carry-mean"); + let carried = recover_discrete_latent_mean_with_impulse_carry( + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 2.0, + 1.0, + LagClock::EventTime, + ) + .expect("carried"); + let evolved_observed = + recover_discrete_observed_mean(loading, 1.0, -0.5, 0.3, 0.5, 2.0, LagClock::EventTime) + .expect("eq3-eq5-mean"); + assert_eq!( + refuse_evolved_observed_mean_as_impulse_carry_observed_mean( + evolved_observed, + recovered + ), + Err(PsychometricError::EvolvedObservedMeanIsNotImpulseCarryObservedMean) + ); + assert_eq!( + refuse_latent_mean_as_observed_mean(carried, recovered), + Err(PsychometricError::LatentMeanIsNotObservedMean) + ); + assert_eq!( + refuse_manifest_means_as_observed_mean(0.5, recovered), + Err(PsychometricError::ManifestMeansIsNotObservedMean) + ); + let scaled = recover_discrete_observed_mean_with_impulse_carry( + 1e308, + 1e-308, + 0.0, + 0.0, + 0.0, + 3.0, + 0.0, + 2.0, + 1.0, + LagClock::EventTime, + ) + .expect("scale"); + assert!((scaled - 1.0).abs() < 1e-15); + let finite_loaded = recover_discrete_observed_mean_with_impulse_carry( + 1e308, + 1.0, + 0.0, + 0.0, + 0.0, + 3.0, + 0.0, + 2.0, + 1.0, + LagClock::EventTime, + ) + .expect("lambda-mu"); + assert!((finite_loaded - 1e308).abs() / 1e308 < 1e-15); + } + + #[test] + fn discrete_observed_mean_with_impulse_carry_invalid_inputs_fail_closed() { + assert_eq!( + recover_discrete_observed_mean_with_impulse_carry( + f64::NAN, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_observed_mean_with_impulse_carry( + 1e308, + 2.0, + 0.0, + 0.0, + 0.0, + 3.0, + 0.0, + 2.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_observed_mean_with_impulse_carry( + 1.0, + 1.0, + 710.0, + 0.0, + 0.0, + 3.0, + 0.5, + 1.0, + 0.5, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_observed_mean_with_impulse_carry( + 1e308, + 0.0, + 0.0, + 0.0, + 1e308, + 1.0, + 0.0, + 2.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + } + + #[test] + fn discrete_observed_mean_with_impulse_carry_interval_and_clock_fail_closed() { + assert_eq!( + recover_discrete_observed_mean_with_impulse_carry( + 2.0, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_observed_mean_with_impulse_carry( + 2.0, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, + 2.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_observed_mean_with_impulse_carry( + 2.0, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, + 1.0, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + } + #[test] fn time_dependent_impulse_carry_refuses_contemporaneous_cint_tipred_and_equation_fourteen() { let effect = 0.4_f64; diff --git a/crates/psychometric_core/src/lib.rs b/crates/psychometric_core/src/lib.rs index be3b50dc..15522b66 100644 --- a/crates/psychometric_core/src/lib.rs +++ b/crates/psychometric_core/src/lib.rs @@ -39,7 +39,9 @@ //! recovers the Driver Eq. 1–2 within-interval impulse carry //! `e^{A(t−u)} M x` for `t0 < u < t` (not the contemporaneous Dirac, //! not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; §7.2 -//! dissipation), recovers the Driver Eq. 3 second-summand +//! dissipation), recovers the Driver Eq. 5 of that carried latent +//! mean as `τ + λ(μ_t + e^{a(t−u)} m x)` (`τ + λ μ_t` is not that +//! observed mean), recovers the Driver Eq. 3 second-summand //! time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` //! (Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle //! Eq. 14; `B` is not that discrete increment), @@ -113,6 +115,8 @@ pub use event_time::recover_discrete_latent_mean_with_time_independent_predictor pub use event_time::recover_discrete_latent_variance; /// Exact scalar discrete observed mean `τ + λ μ_t` from Eq. 3 then Eq. 5. pub use event_time::recover_discrete_observed_mean; +/// Exact scalar discrete observed mean of a within-interval impulse carry. +pub use event_time::recover_discrete_observed_mean_with_impulse_carry; /// Exact scalar discrete process noise `Q_Δt` on event time. pub use event_time::recover_discrete_process_noise; /// Exact scalar discrete `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z`. @@ -155,6 +159,8 @@ pub use event_time::refuse_continuous_intercept_as_initial_latent_mean; pub use event_time::refuse_continuous_intercept_as_manifest_means; /// Refuse the difference quotient as a continuous-time rate. pub use event_time::refuse_difference_quotient_as_local_rate; +/// Refuse treating evolved `τ + λ μ_t` as the impulse-carry observed mean. +pub use event_time::refuse_evolved_observed_mean_as_impulse_carry_observed_mean; /// Refuse treating finite-interval `Q_Δt` as `asymDIFFUSION`. pub use event_time::refuse_finite_interval_process_noise_as_stationary_variance; /// Refuse treating Driver Table 2 `T0MEANS` as the evolved latent mean. diff --git a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs index 7b973d72..c6d11b65 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -10,8 +10,8 @@ use psychometric_core::{ recover_discrete_latent_mean, recover_discrete_latent_mean_with_impulse, recover_discrete_latent_mean_with_impulse_carry, recover_discrete_latent_mean_with_time_independent_predictor, recover_discrete_latent_variance, - recover_discrete_observed_mean, recover_discrete_process_noise, - recover_discrete_time_independent_predictor_effect, + recover_discrete_observed_mean, recover_discrete_observed_mean_with_impulse_carry, + recover_discrete_process_noise, recover_discrete_time_independent_predictor_effect, recover_discrete_time_varying_predictor_effect, recover_event_series_mean_log_rate, recover_event_time_discrete_lag_and_log_rate, recover_irregular_centered_residual_log_rate, recover_kish_weighted_slope, recover_manifest_lagged_observed_covariance, @@ -23,6 +23,7 @@ use psychometric_core::{ refuse_continuous_intercept_as_discrete_mean_increment, refuse_continuous_intercept_as_initial_latent_mean, refuse_continuous_intercept_as_manifest_means, refuse_difference_quotient_as_local_rate, + refuse_evolved_observed_mean_as_impulse_carry_observed_mean, refuse_finite_interval_process_noise_as_stationary_variance, refuse_initial_latent_mean_as_evolved_mean, refuse_initial_observed_mean_as_evolved_observed_mean, @@ -1560,6 +1561,252 @@ fn time_dependent_impulse_carry_refuses_overflow_and_non_event_clocks() { ); } +#[test] +fn discrete_observed_mean_with_impulse_carry_recovers_driver_equation_five() { + let loading = 2.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let elapsed = 1.0_f64; + let effect = 0.4_f64; + let predictor = 3.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let observed = recover_discrete_observed_mean_with_impulse_carry( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + elapsed, + LagClock::EventTime, + ) + .expect("eq5-carry-mean"); + let carried = recover_discrete_latent_mean_with_impulse_carry( + initial, + drift, + intercept, + effect, + predictor, + delta, + elapsed, + LagClock::EventTime, + ) + .expect("carried"); + let expected = manifest_mean + loading * carried; + let error = rmse(&[expected], &[observed]); + assert!( + error < 1e-15, + "Driver Eq. 5 of Eq. 1–2 impulse carry RMSE {error}" + ); + let evolved_observed = recover_discrete_observed_mean( + loading, + initial, + drift, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq3-eq5-mean"); + let evolved_error = rmse(&[expected], &[evolved_observed]); + assert!( + evolved_error > error, + "τ + λ μ_t is not impulse-carry E(y_t): RMSE {evolved_error} must exceed {error}" + ); + let intercept_error = rmse(&[expected], &[manifest_mean]); + assert!( + intercept_error > error, + "MANIFESTMEANS is not impulse-carry E(y_t): RMSE {intercept_error} must exceed {error}" + ); + let latent_error = rmse(&[expected], &[carried]); + assert!( + latent_error > error, + "carried latent mean is not E(y_t): RMSE {latent_error} must exceed {error}" + ); +} + +#[test] +fn discrete_observed_mean_with_impulse_carry_refuses_evolved_mean_and_contemporaneous() { + let loading = 2.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let elapsed = 1.0_f64; + let effect = 0.4_f64; + let predictor = 3.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let observed = recover_discrete_observed_mean_with_impulse_carry( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + elapsed, + LagClock::EventTime, + ) + .expect("eq5-carry-mean"); + let carried = recover_discrete_latent_mean_with_impulse_carry( + initial, + drift, + intercept, + effect, + predictor, + delta, + elapsed, + LagClock::EventTime, + ) + .expect("carried"); + let expected = manifest_mean + loading * carried; + let error = rmse(&[expected], &[observed]); + let evolved_observed = recover_discrete_observed_mean( + loading, + initial, + drift, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq3-eq5-mean"); + let contemporaneous_latent = recover_discrete_latent_mean_with_impulse( + initial, + drift, + intercept, + effect, + predictor, + delta, + LagClock::EventTime, + ) + .expect("mx"); + let contemporaneous = + recover_manifest_observed_mean(loading, contemporaneous_latent, manifest_mean) + .expect("eq5-mx"); + let contemporaneous_error = rmse(&[expected], &[contemporaneous]); + assert!( + contemporaneous_error > error, + "τ + λ(μ_t + m x) is not impulse-carry E(y_t): RMSE {contemporaneous_error} must exceed {error}" + ); + let zero_loading = recover_discrete_observed_mean_with_impulse_carry( + 0.0, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + elapsed, + LagClock::EventTime, + ) + .expect("lambda0"); + let dropped_error = rmse(&[expected], &[zero_loading]); + assert!( + dropped_error > error, + "zero loading is τ, not τ + λ(μ_t + carry): RMSE {dropped_error} must exceed {error}" + ); + assert_eq!( + refuse_evolved_observed_mean_as_impulse_carry_observed_mean(evolved_observed, observed), + Err(PsychometricError::EvolvedObservedMeanIsNotImpulseCarryObservedMean) + ); + assert_eq!( + refuse_latent_mean_as_observed_mean(carried, observed), + Err(PsychometricError::LatentMeanIsNotObservedMean) + ); + assert_eq!( + refuse_manifest_means_as_observed_mean(manifest_mean, observed), + Err(PsychometricError::ManifestMeansIsNotObservedMean) + ); +} + +#[test] +fn discrete_observed_mean_with_impulse_carry_refuses_overflow_and_non_event_clocks() { + assert_eq!( + recover_discrete_observed_mean_with_impulse_carry( + 1e308, + 2.0, + 0.0, + 0.0, + 0.0, + 3.0, + 0.0, + 2.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_observed_mean_with_impulse_carry( + 2.0, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, + 1.0, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_discrete_observed_mean_with_impulse_carry( + 2.0, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, + 2.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_observed_mean_with_impulse_carry( + 1e308, + 0.0, + 0.0, + 0.0, + 1e308, + 1.0, + 0.0, + 2.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + let scaled = recover_discrete_observed_mean_with_impulse_carry( + 1e308, + 1e-308, + 0.0, + 0.0, + 0.0, + 3.0, + 0.0, + 2.0, + 1.0, + LagClock::EventTime, + ) + .expect("scale"); + assert!( + (scaled - 1.0).abs() < 1e-15, + "Driver Eq. 5 of Eq. 1–2 carry must keep λ=1e308, μ=1e-308: got {scaled}" + ); +} + #[test] fn admitted_coordinates_still_required_for_multilevel_weights() { assert_eq!( diff --git a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs index 5c252713..0e3785bd 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -8,8 +8,8 @@ use psychometric_core::{ recover_discrete_latent_mean, recover_discrete_latent_mean_with_impulse, recover_discrete_latent_mean_with_impulse_carry, recover_discrete_latent_mean_with_time_independent_predictor, recover_discrete_latent_variance, - recover_discrete_observed_mean, recover_discrete_process_noise, - recover_discrete_time_independent_predictor_effect, + recover_discrete_observed_mean, recover_discrete_observed_mean_with_impulse_carry, + recover_discrete_process_noise, recover_discrete_time_independent_predictor_effect, recover_discrete_time_varying_predictor_effect, recover_irregular_centered_residual_log_rate, recover_loading_point_estimate_mean, recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, recover_manifest_observed_variance, @@ -19,6 +19,7 @@ use psychometric_core::{ refuse_continuous_intercept_as_discrete_mean_increment, refuse_continuous_intercept_as_initial_latent_mean, refuse_continuous_intercept_as_manifest_means, + refuse_evolved_observed_mean_as_impulse_carry_observed_mean, refuse_finite_interval_process_noise_as_stationary_variance, refuse_initial_latent_mean_as_evolved_mean, refuse_initial_observed_mean_as_evolved_observed_mean, @@ -782,3 +783,94 @@ fn time_dependent_impulse_carry_is_not_contemporaneous_cint_tipred_or_equation_f Err(psychometric_core::PsychometricError::TimeDependentImpulseCarryIsNotTimeVaryingDiscreteEffect) ); } + +#[test] +fn evolved_observed_mean_is_not_impulse_carry_observed_mean() { + let loading = 2.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let elapsed = 1.0_f64; + let effect = 0.4_f64; + let predictor = 3.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let impulse_carry_observed = recover_discrete_observed_mean_with_impulse_carry( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + elapsed, + LagClock::EventTime, + ) + .expect("eq5-carry-mean"); + let evolved_observed = recover_discrete_observed_mean( + loading, + initial, + drift, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq3-eq5-mean"); + let carried = recover_discrete_latent_mean_with_impulse_carry( + initial, + drift, + intercept, + effect, + predictor, + delta, + elapsed, + LagClock::EventTime, + ) + .expect("carried"); + let contemporaneous_latent = recover_discrete_latent_mean_with_impulse( + initial, + drift, + intercept, + effect, + predictor, + delta, + LagClock::EventTime, + ) + .expect("mx"); + let contemporaneous = + recover_manifest_observed_mean(loading, contemporaneous_latent, manifest_mean) + .expect("eq5-mx"); + assert!( + (evolved_observed - impulse_carry_observed).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 of Eq. 1–2): τ + λ μ_t is not impulse-carry E(y_t)" + ); + assert!( + (manifest_mean - impulse_carry_observed).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 / Table 2 p. 12): MANIFESTMEANS is not impulse-carry E(y_t)" + ); + assert!( + (carried - impulse_carry_observed).abs() > 1e-3, + "Driver et al. (2017, Eq. 5): carried latent mean is not E(y_t)" + ); + assert!( + (contemporaneous - impulse_carry_observed).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 of Eq. 1–2): τ + λ(μ_t + m x) is not impulse-carry E(y_t)" + ); + assert_eq!( + refuse_evolved_observed_mean_as_impulse_carry_observed_mean( + evolved_observed, + impulse_carry_observed + ), + Err(psychometric_core::PsychometricError::EvolvedObservedMeanIsNotImpulseCarryObservedMean) + ); + assert_eq!( + refuse_latent_mean_as_observed_mean(carried, impulse_carry_observed), + Err(psychometric_core::PsychometricError::LatentMeanIsNotObservedMean) + ); + assert_eq!( + refuse_manifest_means_as_observed_mean(manifest_mean, impulse_carry_observed), + Err(psychometric_core::PsychometricError::ManifestMeansIsNotObservedMean) + ); +} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 55468383..a43feb18 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -28,7 +28,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | report template/section/copied/style/modality method effects | ADR 0004/0012; PRD/TRD | simulation truth factors implemented; estimator-side method model remains future | partial | | candidate K statistical/Pareto gates + blinded LLM review | ADR 0012; research | future `model_selection` | accepted-target | | compositional topic correlation / stable clustering | ADR 0005/0012; research | future `network_analysis` | accepted-target | -| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z); full ESEM/DSEM remaining | partial | +| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z); full ESEM/DSEM remaining | partial | | CPU bounded multithreading + GPU/VRAM streaming/parity | ADR 0001/0006 | future `compute_backend` | accepted-target | | TDT detection/tracking vs CHRONOS schema/prediction/temporal consistency | ADR 0016; PRD/research | future `event_intelligence` | accepted-target | | evidence-bounded LLM interpretation | ADR 0010/0012; PRD | future `interpretation_gateway` | accepted-target | diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index 64580cf7..21bbf215 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -1,7 +1,7 @@ # ADR 0005 — Posterior-aware ESEM/DSEM and structural interpretation **Decision status:** Accepted -**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), exact scalar contemporaneous `TDPREDEFFECT` impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; the §7.2 level-change form is not that impulse), exact scalar time-independent `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle Eq. 14; `B` is not that discrete increment), exact scalar within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target +**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), exact scalar contemporaneous `TDPREDEFFECT` impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; the §7.2 level-change form is not that impulse), exact scalar time-independent `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle Eq. 14; `B` is not that discrete increment), exact scalar within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of that carried latent mean; `τ + λ μ_t` is not that observed mean), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target **Date:** 2026-08-05 **Supersedes:** None. ADR 0012 governs upstream topic measurement/network coordinates; this ADR governs higher-order psychometric structure and longitudinal interpretation. @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), recovers the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z; `TDPREDEFFECT` is `M`, not `CINT`; `M x` is not `A^{-1}[e^{A Δt} − I] B z`; `M x` is not Voelkle et al., 2012, Eq. 14; the level-change form is not that impulse), recovers the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z; `TIPREDEFFECT` is `B`, not `κ`; form `B z` first, then the discrete intercept map; a zero drift is `B z Δt`; `B` is not the discrete increment; `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle Eq. 14), recovers the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z; form `m x` first, then `e^{a(t−u)} m x`; a zero drift is `m x`; underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept; `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; an impulse at `u = t` is the contemporaneous map; an impulse at `u ≤ t0` is already in `η(t0)`), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), recovers the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z; `TDPREDEFFECT` is `M`, not `CINT`; `M x` is not `A^{-1}[e^{A Δt} − I] B z`; `M x` is not Voelkle et al., 2012, Eq. 14; the level-change form is not that impulse), recovers the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z; `TIPREDEFFECT` is `B`, not `κ`; form `B z` first, then the discrete intercept map; a zero drift is `B z Δt`; `B` is not the discrete increment; `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle Eq. 14), recovers the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z; form `m x` first, then `e^{a(t−u)} m x`; a zero drift is `m x`; underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept; `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; an impulse at `u = t` is the contemporaneous map; an impulse at `u ≤ t0` is already in `η(t0)`), recovers the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; JSS PDF re-opened 2026-08-20T05:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the carried latent mean is not `E(y_t)`), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. 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/adr/README.md b/docs/adr/README.md index b02fed4a..95c54bcf 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -10,7 +10,7 @@ Read [`ADR_POLICY.md`](ADR_POLICY.md) first. **Decision status and implementatio | [0002](0002-six-clock-temporal-semantics.md) | Six-clock temporal semantics and fail-closed historical leakage prevention | Accepted | active-PR | Unmerged PR #8 is the canonical Task 3 replacement implementing typed clocks/intervals against the current protected-main lineage; conflicted PR #5 is superseded lineage. Later graph/split enforcement remains target work. | | [0003](0003-relational-event-multiple-membership.md) | Relational event ontology and time-varying cross-classified multiple membership | Accepted | partial | Weighted time-varying membership network/roles are active-PR (PR #12); full multilevel estimators, graph ontology, and persistence remain accepted-target. ADR 0016 owns event-intelligence tasks. | | [0004](0004-shared-multilingual-latent-space.md) | One shared multilingual latent space with explicit invariance status | Accepted | accepted-target | ADR 0012 owns the full topic-estimator/backend/global-topic contract. | -| [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | partial | Input gates, posterior-draw point estimates, Rubin `T` on OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects, exact scalar discrete process noise, lagged latent covariance and unconditional latent variance, stationary within-subject variance (`asymDIFFUSION`), trait-plus-state variance (`TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`), irregular already-centered residual lag, and strong-gated latent means are on the stacked psychometric PR; full ESEM/DSEM estimator remains accepted-target. | +| [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | partial | Input gates, posterior-draw point estimates, Rubin `T` on OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects, exact scalar discrete process noise, lagged latent covariance and unconditional latent variance, stationary within-subject variance (`asymDIFFUSION`), trait-plus-state variance (`TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; Eq. 5 of the within-interval impulse carry is `τ + λ(μ_t + e^{a(t−u)} m x)`; `τ + λ μ_t` is not that observed mean), irregular already-centered residual lag, and strong-gated latent means are on the stacked psychometric PR; full ESEM/DSEM estimator remains accepted-target. | | [0006](0006-vram-gpu-nvidia-orchestration.md) | VRAM-adaptive GPU compute and model-credential boundary | Accepted | accepted-target | LLM orchestration policy superseded by ADR 0010; autonomous development authority governed by ADR 0015. | | [0007](0007-rust-workspace-quality-gates.md) | Explicit Rust workspace, pinned toolchains, and exact quality gates | Accepted | implemented-main | ADR 0014 governs scientific/product claim promotion beyond repository-quality tooling. | | [0008](0008-immutable-evidence-identities-digests-and-spans.md) | Immutable evidence identities, `SHA-256` digests, exact spans, and strict wire reconstruction | Accepted | implemented-main | ADR 0013 governs future persistence/reproducibility/split authority. | diff --git a/docs/research/multilevel-event-time-recovery.md b/docs/research/multilevel-event-time-recovery.md index 0f9ee45c..bf5f94c9 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -25,15 +25,16 @@ This slice stays inside `psychometric_core`. It does not add a second invariance 19. recover the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`) and refuse treating that impulse as `CINT`, `TIPREDEFFECT`, or Voelkle et al. (2012, Eq. 14); 20. recover the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`) and refuse treating that increment as `CINT`, `TDPREDEFFECT`, Voelkle et al. (2012, Eq. 14), or the coefficient `B`; 21. recover the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; the printed Eq. 3 fourth summand is the contemporaneous Dirac) and refuse treating that carry as the contemporaneous impulse, `CINT`, `TIPREDEFFECT`, or Voelkle et al. (2012, Eq. 14); -22. refuse pooling discrete lags from unequal event intervals as one coefficient; -23. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); -24. refuse the difference quotient as a continuous-time rate; -25. apply the same event-time map to CWC residuals (still not DSEM); -26. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). +22. recover the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; the evolved map `τ + λ μ_t` is not that observed mean) and refuse treating `τ + λ μ_t` as that observed mean; +23. refuse pooling discrete lags from unequal event intervals as one coefficient; +24. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); +25. refuse the difference quotient as a continuous-time rate; +26. apply the same event-time map to CWC residuals (still not DSEM); +27. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). ## Claim boundary -This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. The contemporaneous `TDPREDEFFECT` impulse is not the continuous intercept, not the time-independent discrete effect, and not Voelkle et al. (2012, Eq. 14). The time-independent `TIPREDEFFECT` increment is not the continuous intercept, not the contemporaneous impulse, not Voelkle et al. (2012, Eq. 14), and not the coefficient `B`. The within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). +This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. The contemporaneous `TDPREDEFFECT` impulse is not the continuous intercept, not the time-independent discrete effect, and not Voelkle et al. (2012, Eq. 14). The time-independent `TIPREDEFFECT` increment is not the continuous intercept, not the contemporaneous impulse, not Voelkle et al. (2012, Eq. 14), and not the coefficient `B`. The within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). The evolved observed mean `τ + λ μ_t` is not the impulse-carry observed mean `τ + λ(μ_t + e^{a(t−u)} m x)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`. The carried latent mean is not `E(y_t)`. ## Authoritative sources @@ -51,7 +52,7 @@ Kish, L. (1965). *Survey sampling*. John Wiley & Sons. Oud, J. H. L., & Jansen, R. A. R. G. (2000). Continuous time state space modeling of panel data by means of SEM. *Psychometrika, 65*(2), 199–215. https://doi.org/10.1007/BF02294374 (cited by Voelkle et al., 2012, Eq. 14 discussion; PDF not opened). -The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:07Z (https://www.zora.uzh.ch/handle/20.500.14742/72792; bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`). Page 16 writes discrete auto-effects as \(A^{*}(\Delta t)=\exp(A\Delta t)\) (Eq. 7). Introducing Intercepts (manuscript p. 20, Eq. 12) adds a continuous-time intercept \(b\) and writes the expected-value solution whose discrete increment is \(A^{-1}(\exp(A\Delta t)-I)b\); the scalar constant-predictor effect is \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). The next paragraph (manuscript p. 21, Eq. 14) writes the discrete effect of a **time-varying** predictor, when the sampling interval equals the constancy interval, as \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product does not depend on the predictor auto-effect. The manuscript calls Eq. 14 a first-order approximation that deteriorates as \(\Delta t\) grows, and defers unmatched sampling/constancy intervals to Oud and Jansen (2000), which is unread. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4; JSS PDF re-opened 2026-08-19T04:10Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\), the discrete intercept \(b_{\Delta t}=A^{-1}[A_{\Delta t}-I]b\), and the discrete process-noise covariance \(Q_{\Delta t}=\int_{0}^{\Delta t}\operatorname{expm}(A(\Delta t-\tau))LGG^{\top}L^{\top}\operatorname{expm}(A(\Delta t-\tau))^{\top}\,d\tau\). Equation 4 (p. 5) writes that the integral exhibits covariance \(Q_{\Delta t}=\operatorname{irow}(A^{\#^{-1}}[e^{A^{\#}\Delta t}-I]\operatorname{row}(Q))\) with \(A^{\#}=A\otimes I+I\otimes A\). The homogeneous-process consequence (`ξ`, `z` given) is \(Q_{\Delta t}=\operatorname{cov}(\eta_{ti}\mid\eta_{t-1,i})\) and \(\operatorname{cov}(\eta_{ti},\eta_{t-1,i})=A_{\Delta t}\operatorname{cov}(\eta_{t-1,i})\). The law of total variance on that pair is \(\operatorname{Var}(\eta_{ti})=A_{\Delta t}\operatorname{Var}(\eta_{t-1,i})A_{\Delta t}^{\top}+Q_{\Delta t}\). As \(\Delta t\to\infty\) with stable \(a<0\), Eq. 4 becomes \(-q/(2a)\). The JSS summary names that limit `asymDIFFUSION` and takes it as the total within-subject variance (p. 16). Section 4.3 (pp. 9–10) constrains a stationary `T0VAR` to that model-predicted variance and distinguishes it from a predetermined first occasion. The same section (p. 9) adds a stable trait process with `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is that time-invariant between-subject variance. The JSS article has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). This slice takes scalar \(L=1\) (every latent subject to system noise); it does not implement the 0/1 selector matrix and is not a Kalman filter. Discrete auto-effects are strictly positive for finite real drift and interval; the inverse \(a=\ln\varphi/\Delta t\) therefore requires \(\varphi>0\). Binary64 `exp` of a large negative argument is `+0` and is refused as a discrete lag; the same underflow is a vanishing lagged covariance and is kept. Equations 3–4 of Voelkle et al. (2012) are the discouraged difference-quotient approximation. Meredith (1993) remains unread (Unpaywall/OpenAlex 2026-08-20T10:33Z: closed; Springer `content/pdf` is HTML 200, not a PDF; archive.org title search empty). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex 2026-08-20T10:33Z: closed; Springer `content/pdf` is HTML 200; ETS landing page is HTML). ERIC ED334221 is Singer and Willett (1991), *From whether to when*, not the 1991 journal article. ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title is not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex 2026-08-18T21:07Z: closed; Radboud landing and bitstream 403). +The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:07Z (https://www.zora.uzh.ch/handle/20.500.14742/72792; bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`). Page 16 writes discrete auto-effects as \(A^{*}(\Delta t)=\exp(A\Delta t)\) (Eq. 7). Introducing Intercepts (manuscript p. 20, Eq. 12) adds a continuous-time intercept \(b\) and writes the expected-value solution whose discrete increment is \(A^{-1}(\exp(A\Delta t)-I)b\); the scalar constant-predictor effect is \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). The next paragraph (manuscript p. 21, Eq. 14) writes the discrete effect of a **time-varying** predictor, when the sampling interval equals the constancy interval, as \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product does not depend on the predictor auto-effect. The manuscript calls Eq. 14 a first-order approximation that deteriorates as \(\Delta t\) grows, and defers unmatched sampling/constancy intervals to Oud and Jansen (2000), which is unread. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4; JSS PDF re-opened 2026-08-19T04:10Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\), the discrete intercept \(b_{\Delta t}=A^{-1}[A_{\Delta t}-I]b\), and the discrete process-noise covariance \(Q_{\Delta t}=\int_{0}^{\Delta t}\operatorname{expm}(A(\Delta t-\tau))LGG^{\top}L^{\top}\operatorname{expm}(A(\Delta t-\tau))^{\top}\,d\tau\). Equation 4 (p. 5) writes that the integral exhibits covariance \(Q_{\Delta t}=\operatorname{irow}(A^{\#^{-1}}[e^{A^{\#}\Delta t}-I]\operatorname{row}(Q))\) with \(A^{\#}=A\otimes I+I\otimes A\). The homogeneous-process consequence (`ξ`, `z` given) is \(Q_{\Delta t}=\operatorname{cov}(\eta_{ti}\mid\eta_{t-1,i})\) and \(\operatorname{cov}(\eta_{ti},\eta_{t-1,i})=A_{\Delta t}\operatorname{cov}(\eta_{t-1,i})\). The law of total variance on that pair is \(\operatorname{Var}(\eta_{ti})=A_{\Delta t}\operatorname{Var}(\eta_{t-1,i})A_{\Delta t}^{\top}+Q_{\Delta t}\). As \(\Delta t\to\infty\) with stable \(a<0\), Eq. 4 becomes \(-q/(2a)\). The JSS summary names that limit `asymDIFFUSION` and takes it as the total within-subject variance (p. 16). Section 4.3 (pp. 9–10) constrains a stationary `T0VAR` to that model-predicted variance and distinguishes it from a predetermined first occasion. The same section (p. 9) adds a stable trait process with `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is that time-invariant between-subject variance. The JSS article has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). This slice takes scalar \(L=1\) (every latent subject to system noise); it does not implement the 0/1 selector matrix and is not a Kalman filter. Discrete auto-effects are strictly positive for finite real drift and interval; the inverse \(a=\ln\varphi/\Delta t\) therefore requires \(\varphi>0\). Binary64 `exp` of a large negative argument is `+0` and is refused as a discrete lag; the same underflow is a vanishing lagged covariance and is kept. Equations 3–4 of Voelkle et al. (2012) are the discouraged difference-quotient approximation. Meredith (1993) remains unread (Unpaywall/OpenAlex 2026-08-20T05:12Z: closed; Springer `content/pdf` is HTML 200, not a PDF; archive.org title search empty). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex 2026-08-20T05:12Z: closed; Springer `content/pdf` is HTML 200; ETS landing page is HTML). ERIC ED334221 is Singer and Willett (1991), *From whether to when*, not the 1991 journal article. ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title is not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex 2026-08-18T21:07Z: closed; Radboud landing and bitstream 403). ## Formula notes @@ -75,6 +76,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - **Time-dependent predictor impulse.** Driver et al. (2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z): `χ_i(t)=Σ x_{i,u} δ(t−u)` and the fourth summand is `M Σ x_{i,u} δ(t−u)`. Table 2 names `M` `TDPREDEFFECT`. Section 7.2 calls this a sudden impulse that dissipates back to the process mean. The scalar contemporaneous jump is `m x`. Form `μ_t` first, then add `m x`. A zero effect or zero predictor is exactly zero. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{AΔt}−I] B z` (`TIPREDEFFECT`). `M x` is not Voelkle et al. (2012, Eq. 14) `a_{yx}Δt`. The §7.2 level-change form is an extra latent process with near-zero drift and is not this map. An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **Time-independent predictor effect.** Driver et al. (2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z): Equation 1 writes `dη=(Aη+b+A_{ηξ}ξ+Bz)dt+GdW+Mdχ`. Equation 3's second summand is `A^{-1}[e^{AΔt}−I](b+A_{ηξ}ξ+Bz)`. Table 2 names `B` `TIPREDEFFECT`. The scalar map is `(e^{aΔt}−1)/a·Bz` for `a≠0`. Form `Bz` first, then the discrete intercept map. A zero drift is `BzΔt`. Form `μ_t` first, then add that increment. A zero effect or zero predictor is exactly zero. `TIPREDEFFECT` is `B`, not the discrete increment. `A^{-1}[e^{AΔt}−I]Bz` is not `CINT`, not `Mx`, and not Voelkle et al. (2012, Eq. 14) `a_{yx}Δt`. An overflowing product, increment, or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **Time-dependent predictor impulse carry.** Driver et al. (2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z): Equation 1 writes `dη=(Aη+ξ+Bz+Mχ(t))dt+GdW`. Equation 2 writes `χ_i(t)=Σ x_{i,u} δ(t−u)`. The Green-function integral of that Dirac on `(t0,t)` is `e^{A(t−u)}Mx`. The printed Eq. 3 fourth summand is the contemporaneous jump `Mx` at `u=t`. This map is the strictly within-interval case `t0 Date: Thu, 20 Aug 2026 14:45:20 +0900 Subject: [PATCH 38/62] test(psychometric): close multilevel recovery review gaps --- crates/psychometric_core/src/cluster_mean.rs | 8 +++ crates/psychometric_core/src/loading.rs | 4 +- ...multilevel_event_time_recovery_contract.rs | 2 +- .../tests/rubin_and_mean_gate_contract.rs | 51 +++++++++++++++++++ .../scientific_claim_boundary_contract.rs | 26 ---------- ...tilevel-multiple-membership-measurement.md | 4 ++ .../strong-invariance-latent-means.md | 12 ++++- 7 files changed, 77 insertions(+), 30 deletions(-) diff --git a/crates/psychometric_core/src/cluster_mean.rs b/crates/psychometric_core/src/cluster_mean.rs index 63492056..b32bd6f1 100644 --- a/crates/psychometric_core/src/cluster_mean.rs +++ b/crates/psychometric_core/src/cluster_mean.rs @@ -399,6 +399,10 @@ mod tests { kish_effective_sample_size(&[0.0, 0.0]), Err(PsychometricError::InvalidWeight) ); + assert_eq!( + kish_effective_sample_size(&[f64::MAX, f64::MAX]), + Err(PsychometricError::InvalidNumericInput) + ); assert_eq!( recover_kish_weighted_slope(&[0.0], &[1.0], &[1.0]), Err(PsychometricError::InvalidNumericInput) @@ -427,5 +431,9 @@ mod tests { recover_kish_weighted_slope(&[1.0, 1.0], &[2.0, 3.0], &[1.0, 1.0]), Err(PsychometricError::SingularDesign) ); + assert_eq!( + recover_kish_weighted_slope(&[0.0, f64::MAX], &[0.0, f64::MAX], &[1.0, 1.0]), + Err(PsychometricError::InvalidNumericInput) + ); } } diff --git a/crates/psychometric_core/src/loading.rs b/crates/psychometric_core/src/loading.rs index 11165d2d..5ef7a05f 100644 --- a/crates/psychometric_core/src/loading.rs +++ b/crates/psychometric_core/src/loading.rs @@ -62,8 +62,8 @@ pub fn ordinary_least_squares_fit( } let intercept = require_finite(outcome_sum / n - slope * (predictor_sum / n))?; let mut sse = 0.0_f64; - for (&pred, &out) in predictor.iter().zip(outcome) { - let residual = out - (intercept + slope * pred); + for (&pred, &out) in pred_dev.iter().zip(&out_dev) { + let residual = out - slope * pred; sse += residual * residual; } let residual_variance = if n > 2.0 { 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 c6d11b65..c4c21e24 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -515,7 +515,7 @@ fn discrete_process_noise_recovers_driver_equation_three() { fn within_residual_event_time_log_rate_beats_pooled_levels() { let true_drift = -0.3_f64; let mut rows = Vec::new(); - for (cluster, person_mean, start) in [(1_u64, 8.0_f64, 1.0_f64), (2, -5.0, 1.4)] { + for (cluster, person_mean, start) in [(1_u64, 8.0_f64, 1.0_f64), (2, 5.0, 1.4)] { for step in 0..6 { let time = f64::from(step); rows.push(ClusteredEventScore { diff --git a/crates/psychometric_core/tests/rubin_and_mean_gate_contract.rs b/crates/psychometric_core/tests/rubin_and_mean_gate_contract.rs index d69460f6..215dd4ca 100644 --- a/crates/psychometric_core/tests/rubin_and_mean_gate_contract.rs +++ b/crates/psychometric_core/tests/rubin_and_mean_gate_contract.rs @@ -52,6 +52,57 @@ fn rubin_t_mean_recovers_true_loading_and_is_not_a_point_estimate_alias() { assert!(combined.between_variance > 0.0); } +#[test] +fn rubin_t_noisy_truth_reports_bias_rmse_and_interval_coverage() { + let true_loading = 0.8_f64; + let factors: Vec = (0..24).map(|index| f64::from(index) - 11.5).collect(); + let mut recovered = Vec::new(); + let mut covered = 0_usize; + + for replicate in 0..40 { + let mut draws = Vec::new(); + for draw in 0..8 { + let phase = f64::from(replicate) * 0.37 + f64::from(draw) * 0.91; + draws.push( + factors + .iter() + .enumerate() + .map(|(index, factor)| { + let position = + (f64::from(u32::try_from(index).expect("tiny")) + 1.0) * 0.73 + phase; + 0.4 + true_loading * factor + + 0.25 * position.sin() + + 0.12 * (1.7 * position).cos() + }) + .collect::>(), + ); + } + + let combined = + combine_draw_level_ols_loadings(&factors, &draws, IndicatorKind::LogisticNormal) + .expect("noisy Rubin draw"); + assert!(combined.within_variance > 0.0); + let half_width = 1.96 * combined.total_variance.sqrt(); + if (combined.mean_loading - true_loading).abs() <= half_width { + covered += 1; + } + recovered.push(combined.mean_loading); + } + + let mean = recovered.iter().sum::() / recovered.len() as f64; + let bias = mean - true_loading; + let rmse = (recovered + .iter() + .map(|estimate| (estimate - true_loading).powi(2)) + .sum::() + / recovered.len() as f64) + .sqrt(); + let coverage = covered as f64 / recovered.len() as f64; + assert!(bias.abs() < 0.01, "loading bias {bias}"); + assert!(rmse < 0.02, "loading RMSE {rmse}"); + assert!(coverage >= 0.9, "95% interval coverage {coverage}"); +} + #[test] fn metric_status_matches_hash84_metric_and_refuses_latent_means() { assert_eq!( diff --git a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs index 0e3785bd..f647f59a 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -206,22 +206,6 @@ fn discrete_process_noise_is_not_the_continuous_diffusion() { ); let expected = diffusion * ((2.0 * drift * delta).exp() - 1.0) / (2.0 * drift); assert!((discrete - expected).abs() < 1e-15); - let twice_rate_overflow = - recover_discrete_process_noise(1.0, 1e308, 1e-308, LagClock::EventTime) - .expect("2a overflow"); - let expected_twice_rate = 0.5 * 2.0_f64.exp_m1() / 1e308; - assert!((twice_rate_overflow - expected_twice_rate).abs() / expected_twice_rate < 1e-12); - let overflowed_equilibrium = - recover_discrete_process_noise(1e308, -1e308, 2.0, LagClock::EventTime).expect("2a eq var"); - assert!((overflowed_equilibrium - 0.5).abs() < 1e-15); - assert_eq!( - recover_discrete_process_noise(1e308, 0.1, 4000.0, LagClock::EventTime), - Err(psychometric_core::PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_discrete_process_noise(1.0, 1e308, 2.0, LagClock::EventTime), - Err(psychometric_core::PsychometricError::InvalidNumericInput) - ); assert_eq!( recover_discrete_process_noise(0.4, -0.5, f64::NAN, LagClock::EventTime), Err(psychometric_core::PsychometricError::NonPositiveInterval) @@ -298,16 +282,6 @@ fn finite_interval_process_noise_is_not_the_stationary_variance() { recover_stationary_latent_variance(diffusion, 0.0, LagClock::EventTime), Err(psychometric_core::PsychometricError::StationaryVarianceRequiresStableDrift) ); - let min_subnormal = f64::from_bits(1); - let subnormal_ratio = - recover_stationary_latent_variance(min_subnormal, -min_subnormal, LagClock::EventTime) - .expect("subnormal ratio"); - assert!((subnormal_ratio - 0.5).abs() < 1e-15); - assert!(!(f64::MAX / -0.75_f64).is_finite()); - let quotient_overflow = - recover_stationary_latent_variance(f64::MAX, -0.75, LagClock::EventTime) - .expect("q/a overflow"); - assert_eq!(quotient_overflow.to_bits(), (f64::MAX / 1.5).to_bits()); } #[test] diff --git a/docs/research/multilevel-multiple-membership-measurement.md b/docs/research/multilevel-multiple-membership-measurement.md index 23fccf82..564c5969 100644 --- a/docs/research/multilevel-multiple-membership-measurement.md +++ b/docs/research/multilevel-multiple-membership-measurement.md @@ -21,3 +21,7 @@ Raudenbush, S. W., & Bryk, A. S. (2002). *Hierarchical linear models: Applicatio Snijders, T. A. B. (2011). Statistical models for social networks. *Annual Review of Sociology, 37*, 131–153. https://doi.org/10.1146/annurev.soc.012809.102709 Browne, W. J., Goldstein, H., & Rasbash, J. (2001). Multiple membership multiple classification (MMMC) models. *Statistical Modelling, 1*(2), 103–124. https://doi.org/10.1177/1471082X0100100202 + +Enders, C. K., & Tofighi, D. (2007). Centering predictor variables in cross-sectional multilevel models: A new look at an old issue. *Psychological Methods, 12*(2), 121–138. https://doi.org/10.1037/1082-989X.12.2.121 + +Kish, L. (1965). *Survey sampling*. John Wiley & Sons. diff --git a/docs/research/strong-invariance-latent-means.md b/docs/research/strong-invariance-latent-means.md index 101089b9..83a4db00 100644 --- a/docs/research/strong-invariance-latent-means.md +++ b/docs/research/strong-invariance-latent-means.md @@ -13,7 +13,7 @@ This slice does **not** import the unpublished `measurement_invariance` crate on - Strict (also equal residual variance) also licenses latent means. Residual invariance is **not** required for those means. - Two-observation series have no residual degrees of freedom. OLS residual variance is then identically `0` and is not an estimated residual. Those series cap at strong/scalar and still license means. - This is two-group OLS, not MGCFA, not partial invariance, and not alignment optimization. -- Meredith (1993) names weak/strong/strict are used only as conventional labels. That PDF was not opened (Unpaywall/OpenAlex/Semantic Scholar 2026-08-19T22:15Z: closed; Springer `content/pdf` is HTML 200). Do not cite Meredith equations as having been read. Putnick and Bornstein (2016) cite Meredith for residual invariance as part of *full factorial invariance*; that citation is not a reading of Meredith. +- The weak/strong/strict labels remain conventional labels here. Meredith (1993) is listed for terminology only; its PDF was not opened. Putnick and Bornstein (2016) cite Meredith for residual invariance as part of *full factorial invariance*; that citation is not a reading of Meredith. ## Authoritative sources used for the mean gate @@ -23,6 +23,14 @@ PMC author manuscript (PMC5145197) opened 2026-08-19T22:15Z from https://pmc.ncb Putnick and Bornstein write that measurement invariance is a prerequisite to comparing group means. Metric invariance is equivalence of item loadings: each item contributes to the latent construct to a similar degree across groups. Scalar invariance is equivalence of item intercepts after metric: “mean differences in the latent construct capture all mean differences in the shared variance of the items.” After those steps, “the researcher is free to compare group means on the latent factors.” Residual invariance “is not a prerequisite for testing mean differences because the residuals are not part of the latent factor” (they cite Vandenberg & Lance, 2000, unread). Configural, metric, and scalar “are required prior to group mean comparisons.” This crate’s `#84` `metric` / `scalar` split follows that terminology. The executable map remains two-group OLS, not their multiple-group CFA. +Steenkamp, J.-B. E. M., & Baumgartner, H. (1998). Assessing measurement invariance in cross-national consumer research. *Journal of Consumer Research, 25*(1), 78–90. https://doi.org/10.1086/209528 + +The Oxford Academic article page and abstract were opened 2026-08-20. Steenkamp and Baumgartner connect sequential measurement-invariance requirements to when comparisons of construct means are meaningful and illustrate the procedure with multisample factor models. This is the primary source for the gate's comparison-purpose boundary; the implementation remains a narrower two-group OLS contract. + +Baumgartner, H., & Steenkamp, J.-B. E. M. (1998). Multi-group latent variable models for varying numbers of items and factors with cross-national and longitudinal applications. *Marketing Letters, 9*, 21–35. https://doi.org/10.1023/A:1007911903032 + +The Springer Nature article page and abstract were opened 2026-08-20. Its simulation and empirical study concerns estimates of differences between latent means. In this repository, subtracting the two-group model \(y=ν+λ f+e\) under equal loading and intercept gives \((\bar y_c-\bar y_r)/\lambda\); that algebra is an explicit derivation of this OLS slice, not a claim that the source states the same implementation formula. + Opened sources that constrain the surrounding longitudinal/invariance stance: Asparouhov, T., & Muthén, B. (2009). Exploratory structural equation modeling. *Structural Equation Modeling: A Multidisciplinary Journal, 16*(3), 397–438. https://doi.org/10.1080/10705510903008204 @@ -42,6 +50,8 @@ Per group, \(y=\nu+\lambda f+e\) is fit by OLS. Status is: The latent-mean difference is \((\bar y_c-\bar y_r)/\lambda\) with \(\lambda\) the midpoint of the two loadings, and only after strong or strict. +The formula follows by subtracting the group means of \(y=\nu+\lambda f+e\) after the equal-loading/equal-intercept restrictions have been accepted; the cited multi-group latent-mean study supplies the comparison target, while this document records the narrower OLS derivation. + ## Verification - strong/strict series recover a known mean difference with computed RMSE; From 75b2e98995aeeaf7082b9eacdfc2f76134ccfeb5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 09:17:43 +0000 Subject: [PATCH 39/62] feat(psychometric): recover Driver Eq. 5 of the contemporaneous impulse MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compose E(y_t) = τ + λ(μ_t + m x) from the re-opened Driver, Oud, and Voelkle (2017) JSS PDF (Eq. 5, p. 5; Eq. 1-3, pp. 4-5; §7.2). Form the evolved-plus-impulse latent mean first. τ + λ μ_t is not that observed mean. The carry map τ + λ(μ_t + e^{a(t-u)} m x) is not that observed mean when u ≠ t. --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 1 + CLAUDE.md | 2 +- crates/psychometric_core/src/error.rs | 22 + crates/psychometric_core/src/event_time.rs | 432 +++++++++++++++++- crates/psychometric_core/src/lib.rs | 11 +- ...multilevel_event_time_recovery_contract.rs | 258 ++++++++++- .../scientific_claim_boundary_contract.rs | 115 ++++- docs/TRACEABILITY.md | 2 +- docs/adr/0005-posterior-esem-dsem.md | 4 +- docs/adr/README.md | 2 +- .../multilevel-event-time-recovery.md | 21 +- docs/research/posterior-esem-input-gates.md | 2 +- 13 files changed, 838 insertions(+), 36 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 9cd3e1d2..fd3109dd 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -61,7 +61,7 @@ boundaries above remain the target modular MSA architecture. | `tepp_simulation` | known-truth temporal/event data generation | | `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics | | `tepp_api` | versioned DTO, schema, and export contracts | -| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | +| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | No crate exposes placeholder production behavior in Task 1. This prevents an empty façade from becoming a de facto public API before its invariants and tests diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fd4bebb..06da1e50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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, Eq. 5, p. 5; Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T09:01Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar observed-indicator mean of a contemporaneous time-dependent impulse. Equation 5 writes `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and `Γ ~ N(τ, Ψ)`. The latent process at `t` after a contemporaneous Dirac (`u = t`) is `μ_t + m x`. The scalar composition is `E(y_t) = τ + λ(μ_t + m x)`. Form the evolved-plus-impulse latent mean first, then `τ + λ` of that mean. The evolved observed mean `τ + λ μ_t` is not this composition. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not this composition when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The evolved-plus-impulse latent mean is not `E(y_t)`. A zero loading is exactly `τ`. The §7.2 level-change form is a different specification and is not this map. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993), Vandenberg and Lance (2000), and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T05:12Z: `is_oa: false`). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Eq. 1–2, pp. 4–5; Eq. 3 exponential map; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T05:12Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar observed-indicator mean of a within-interval time-dependent impulse carry. Equation 5 writes `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and `Γ ~ N(τ, Ψ)`. The latent process at `t` after a Dirac that occurred strictly inside `(t0, t)` is `μ_t + e^{a(t−u)} m x`. The scalar composition is `E(y_t) = τ + λ(μ_t + e^{a(t−u)} m x)`. Form the carried latent mean first, then `τ + λ` of that mean. The evolved observed mean `τ + λ μ_t` is not this composition. The contemporaneous map `τ + λ(μ_t + m x)` is not this composition when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The carried latent mean is not `E(y_t)`. A zero loading is exactly `τ`. The §7.2 level-change form is a different specification and is not this map. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993), Vandenberg and Lance (2000), and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T05:12Z: `is_oa: false`). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar within-interval time-dependent predictor impulse carry. Equation 1 writes `dη = (A η + ξ + B z + M χ(t)) dt + G dW`. Equation 2 writes `χ_i(t) = Σ x_{i,u} δ(t − u)`. The Green-function integral of that Dirac on `(t0, t)` is `e^{A(t−u)} M x`. The printed Eq. 3 fourth summand is the contemporaneous jump `M x` at `u = t`. This map is the strictly within-interval case `t0 < u < t`. Form `m x` first, then `e^{a(t−u)} m x`. A zero drift is `m x` with no dissipation. Underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation back to the process mean and is kept. Form `μ_t` first, then add the carry. `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `A^{-1}[e^{A Δt} − I] B z` (`TIPREDEFFECT`), and not Voelkle et al. (2012, Eq. 14) `a_{yx} Δt`. An impulse at `u = t` is the contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. The §7.2 level-change form is a different specification and is not this map. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993), Vandenberg and Lance (2000), and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T10:33Z: `is_oa: false`). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar time-independent predictor increment. Equation 1 writes `dη = (A η + b + A_{ηξ} ξ + B z) dt + G dW + M dχ`. Equation 3's second summand is `A^{-1}[e^{A Δt} − I](b + A_{ηξ} ξ + B z)`. Table 2 names `B` `TIPREDEFFECT`. Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. Form `μ_t` first, then add that increment. `TIPREDEFFECT` is `B`, not the discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x` (`TDPREDEFFECT`), and not Voelkle et al. (2012, Eq. 14) `a_{yx} Δt`. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993), Vandenberg and Lance (2000), and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T10:13Z: `is_oa: false`). diff --git a/CLAUDE.md b/CLAUDE.md index b41c6aa0..e7cfa4c3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. Putnick and Bornstein (2016, PMC5145197 opened 2026-08-19T22:15Z) require scalar invariance before latent-mean comparison; residual invariance is not a prerequisite. Two-observation series have no residual degrees of freedom (`ordinary_least_squares_fit` returns residual variance `0`) and cap at strong/scalar; they still license means. This is two-group OLS, not MGCFA. Meredith (1993) names remain unread labels. -- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. The contemporaneous time-dependent predictor impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`). Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). The §7.2 level-change form is not that impulse. The time-independent predictor increment is `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`). Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. `TIPREDEFFECT` is `B`, not that discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle et al. (2012, Eq. 14). The within-interval time-dependent impulse carry is `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation). Form `m x` first, then `e^{a(t−u)} m x`. A zero drift is `m x` with no dissipation. Underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept. `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). An impulse at `u = t` is the contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. The observed mean of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The carried latent mean is not `E(y_t)`. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. The contemporaneous time-dependent predictor impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`). Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). The §7.2 level-change form is not that impulse. The observed mean of that contemporaneous impulse is `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-impulse latent mean is not `E(y_t)`. The time-independent predictor increment is `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`). Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. `TIPREDEFFECT` is `B`, not that discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle et al. (2012, Eq. 14). The within-interval time-dependent impulse carry is `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation). Form `m x` first, then `e^{a(t−u)} m x`. A zero drift is `m x` with no dissipation. Underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept. `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). An impulse at `u = t` is the contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. The observed mean of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The carried latent mean is not `E(y_t)`. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. diff --git a/crates/psychometric_core/src/error.rs b/crates/psychometric_core/src/error.rs index 8a0638ed..669f3e79 100644 --- a/crates/psychometric_core/src/error.rs +++ b/crates/psychometric_core/src/error.rs @@ -144,6 +144,14 @@ pub enum PsychometricError { /// Equation 5 of the Eq. 1–2 carried latent mean. /// `τ + λ μ_t` is not `τ + λ(μ_t + e^{a(t−u)} m x)`. EvolvedObservedMeanIsNotImpulseCarryObservedMean, + /// Driver Eq. 5 of the Eq. 3 evolved mean was treated as + /// Equation 5 of the contemporaneous impulse. + /// `τ + λ μ_t` is not `τ + λ(μ_t + m x)`. + EvolvedObservedMeanIsNotImpulseObservedMean, + /// Driver Eq. 5 of the contemporaneous impulse was treated as + /// Equation 5 of the Eq. 1–2 carried latent mean. + /// `τ + λ(μ_t + m x)` is not `τ + λ(μ_t + e^{a(t−u)} m x)`. + ImpulseObservedMeanIsNotImpulseCarryObservedMean, } impl fmt::Display for PsychometricError { @@ -267,6 +275,12 @@ impl fmt::Display for PsychometricError { Self::EvolvedObservedMeanIsNotImpulseCarryObservedMean => { "evolved observed mean is not the impulse-carry observed mean" } + Self::EvolvedObservedMeanIsNotImpulseObservedMean => { + "evolved observed mean is not the contemporaneous-impulse observed mean" + } + Self::ImpulseObservedMeanIsNotImpulseCarryObservedMean => { + "contemporaneous-impulse observed mean is not the impulse-carry observed mean" + } }; formatter.write_str(message) } @@ -468,5 +482,13 @@ mod tests { PsychometricError::EvolvedObservedMeanIsNotImpulseCarryObservedMean.to_string(), "evolved observed mean is not the impulse-carry observed mean" ); + assert_eq!( + PsychometricError::EvolvedObservedMeanIsNotImpulseObservedMean.to_string(), + "evolved observed mean is not the contemporaneous-impulse observed mean" + ); + assert_eq!( + PsychometricError::ImpulseObservedMeanIsNotImpulseCarryObservedMean.to_string(), + "contemporaneous-impulse observed mean is not the impulse-carry observed mean" + ); } } diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index 867ca969..61857ce4 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -1380,6 +1380,95 @@ pub fn recover_discrete_latent_mean_with_impulse( require_finite(evolved_latent_mean + impulse) } +/// Exact scalar observed mean of a contemporaneous impulse. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Eq. 1–3, pp. 4–5; +/// Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T09:01Z +/// from +/// ) +/// write `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and +/// `Γ ~ N(τ, Ψ)`. The expected intercept is `τ`. The latent process +/// at `t` after a contemporaneous Dirac (`u = t`) is `μ_t + m x`. +/// The scalar composition is `E(y_t) = τ + λ(μ_t + m x)`. Form the +/// evolved-plus-impulse latent mean first, then `τ + λ` of that +/// mean. Table 2 names `τ` `MANIFESTMEANS`. A zero loading is +/// exactly `τ`. A zero evolved-plus-impulse latent mean is exactly +/// `τ`. A zero intercept is exactly `λ(μ_t + m x)`. The evolved +/// observed mean `τ + λ μ_t` is not this composition when the +/// impulse is nonzero. The carry map +/// `τ + λ(μ_t + e^{a(t−u)} m x)` is not this composition when +/// `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The +/// evolved-plus-impulse latent mean is not `E(y_t)`. The §7.2 +/// level-change form is a different specification and is not this +/// map. This is not a Kalman filter and not ctsem estimation. +/// +/// # Errors +/// +/// Propagates [`recover_discrete_latent_mean_with_impulse`] and +/// [`recover_manifest_observed_mean`]. +#[allow(clippy::too_many_arguments)] +pub fn recover_discrete_observed_mean_with_impulse( + loading: f64, + initial_latent_mean: f64, + log_rate: f64, + continuous_intercept: f64, + time_dependent_effect: f64, + time_dependent_predictor: f64, + manifest_mean: f64, + event_delta: f64, + clock: LagClock, +) -> Result { + let impulse_latent_mean = recover_discrete_latent_mean_with_impulse( + initial_latent_mean, + log_rate, + continuous_intercept, + time_dependent_effect, + time_dependent_predictor, + event_delta, + clock, + )?; + recover_manifest_observed_mean(loading, impulse_latent_mean, manifest_mean) +} + +/// Refuse treating the evolved observed mean as the contemporaneous- +/// impulse observed mean. +/// +/// Equation 5 of the Eq. 3 evolved mean is `τ + λ μ_t`. Equation 5 +/// of the Eq. 3 contemporaneous impulse is `τ + λ(μ_t + m x)`. +/// Those are not the same map. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::EvolvedObservedMeanIsNotImpulseObservedMean`]. +pub fn refuse_evolved_observed_mean_as_impulse_observed_mean( + evolved_observed_mean: f64, + impulse_observed_mean: f64, +) -> Result { + let _ = (evolved_observed_mean, impulse_observed_mean); + Err(PsychometricError::EvolvedObservedMeanIsNotImpulseObservedMean) +} + +/// Refuse treating the contemporaneous-impulse observed mean as the +/// impulse-carry observed mean. +/// +/// Equation 5 of the contemporaneous Dirac is `τ + λ(μ_t + m x)`. +/// Equation 5 of the Eq. 1–2 carried latent mean is +/// `τ + λ(μ_t + e^{a(t−u)} m x)`. Those are not the same map when +/// `u ≠ t`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::ImpulseObservedMeanIsNotImpulseCarryObservedMean`]. +pub fn refuse_impulse_observed_mean_as_impulse_carry_observed_mean( + impulse_observed_mean: f64, + impulse_carry_observed_mean: f64, +) -> Result { + let _ = (impulse_observed_mean, impulse_carry_observed_mean); + Err(PsychometricError::ImpulseObservedMeanIsNotImpulseCarryObservedMean) +} + /// Refuse treating the Eq. 3 impulse as `CINT`. /// /// Table 2 names `M` `TDPREDEFFECT` and `κ` `CINT`. The impulse is @@ -2178,6 +2267,7 @@ mod tests { recover_discrete_latent_mean_with_impulse_carry, recover_discrete_latent_mean_with_time_independent_predictor, recover_discrete_latent_variance, recover_discrete_observed_mean, + recover_discrete_observed_mean_with_impulse, recover_discrete_observed_mean_with_impulse_carry, recover_discrete_process_noise, recover_discrete_time_independent_predictor_effect, recover_discrete_time_varying_predictor_effect, recover_event_series_mean_log_rate, @@ -2192,7 +2282,9 @@ mod tests { refuse_continuous_intercept_as_initial_latent_mean, refuse_continuous_intercept_as_manifest_means, refuse_difference_quotient_as_local_rate, refuse_evolved_observed_mean_as_impulse_carry_observed_mean, + refuse_evolved_observed_mean_as_impulse_observed_mean, refuse_finite_interval_process_noise_as_stationary_variance, + refuse_impulse_observed_mean_as_impulse_carry_observed_mean, refuse_initial_latent_mean_as_evolved_mean, refuse_initial_observed_mean_as_evolved_observed_mean, refuse_latent_lagged_covariance_as_observed_covariance, @@ -4239,6 +4331,319 @@ mod tests { ); } + #[test] + fn discrete_observed_mean_with_impulse_recovers_driver_equation_five() { + let loading = 2.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let effect = 0.4_f64; + let predictor = 3.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let recovered = recover_discrete_observed_mean_with_impulse( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-impulse-mean"); + let composed = recover_discrete_latent_mean_with_impulse( + initial, + drift, + intercept, + effect, + predictor, + delta, + LagClock::EventTime, + ) + .expect("mx"); + let expected = manifest_mean + loading * composed; + assert!((recovered - expected).abs() < 1e-15); + let evolved_observed = recover_discrete_observed_mean( + loading, + initial, + drift, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq3-eq5-mean"); + assert!((evolved_observed - recovered).abs() > 1e-3); + let carried_observed = recover_discrete_observed_mean_with_impulse_carry( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + 1.0, + LagClock::EventTime, + ) + .expect("eq5-carry-mean"); + assert!((carried_observed - recovered).abs() > 1e-3); + assert_eq!( + recover_discrete_observed_mean_with_impulse( + 0.0, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime + ), + Ok(manifest_mean) + ); + assert_eq!( + recover_discrete_observed_mean_with_impulse( + loading, + initial, + drift, + intercept, + effect, + predictor, + 0.0, + delta, + LagClock::EventTime + ), + Ok(loading * composed) + ); + } + + #[test] + fn discrete_observed_mean_with_impulse_is_not_evolved_or_zero_impulse() { + let loading = 2.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let effect = 0.4_f64; + let predictor = 3.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let recovered = recover_discrete_observed_mean_with_impulse( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-impulse-mean"); + let evolved_observed = recover_discrete_observed_mean( + loading, + initial, + drift, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq3-eq5-mean"); + let zero_impulse = recover_discrete_observed_mean_with_impulse( + loading, + initial, + drift, + intercept, + 0.0, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("zero-impulse"); + assert!((zero_impulse - evolved_observed).abs() < 1e-15); + assert!((recovered - evolved_observed).abs() > 1e-3); + } + + #[test] + fn discrete_observed_mean_with_impulse_refuses_evolved_mean_and_overflow() { + let loading = 2.0_f64; + let recovered = recover_discrete_observed_mean_with_impulse( + loading, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, + LagClock::EventTime, + ) + .expect("eq5-impulse-mean"); + let composed = recover_discrete_latent_mean_with_impulse( + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 2.0, + LagClock::EventTime, + ) + .expect("mx"); + let evolved_observed = + recover_discrete_observed_mean(loading, 1.0, -0.5, 0.3, 0.5, 2.0, LagClock::EventTime) + .expect("eq3-eq5-mean"); + let carried_observed = recover_discrete_observed_mean_with_impulse_carry( + loading, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, + 1.0, + LagClock::EventTime, + ) + .expect("eq5-carry-mean"); + assert_eq!( + refuse_evolved_observed_mean_as_impulse_observed_mean(evolved_observed, recovered), + Err(PsychometricError::EvolvedObservedMeanIsNotImpulseObservedMean) + ); + assert_eq!( + refuse_impulse_observed_mean_as_impulse_carry_observed_mean( + recovered, + carried_observed + ), + Err(PsychometricError::ImpulseObservedMeanIsNotImpulseCarryObservedMean) + ); + assert_eq!( + refuse_latent_mean_as_observed_mean(composed, recovered), + Err(PsychometricError::LatentMeanIsNotObservedMean) + ); + assert_eq!( + refuse_manifest_means_as_observed_mean(0.5, recovered), + Err(PsychometricError::ManifestMeansIsNotObservedMean) + ); + let scaled = recover_discrete_observed_mean_with_impulse( + 1e308, + 1e-308, + 0.0, + 0.0, + 0.0, + 3.0, + 0.0, + 1.0, + LagClock::EventTime, + ) + .expect("scale"); + assert!((scaled - 1.0).abs() < 1e-15); + let finite_loaded = recover_discrete_observed_mean_with_impulse( + 1e308, + 1.0, + 0.0, + 0.0, + 0.0, + 3.0, + 0.0, + 1.0, + LagClock::EventTime, + ) + .expect("lambda-mu"); + assert!((finite_loaded - 1e308).abs() / 1e308 < 1e-15); + } + + #[test] + fn discrete_observed_mean_with_impulse_invalid_inputs_fail_closed() { + assert_eq!( + recover_discrete_observed_mean_with_impulse( + f64::NAN, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_observed_mean_with_impulse( + 1e308, + 2.0, + 0.0, + 0.0, + 0.0, + 3.0, + 0.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_observed_mean_with_impulse( + 1.0, + 1.0, + 710.0, + 0.0, + 0.0, + 3.0, + 0.5, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_observed_mean_with_impulse( + 1e308, + 0.0, + 0.0, + 0.0, + 1e308, + 1.0, + 0.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_observed_mean_with_impulse( + 2.0, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_observed_mean_with_impulse( + 2.0, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + } + #[test] fn time_independent_predictor_recovers_driver_equation_three_second_summand() { let effect = 0.4_f64; @@ -4737,19 +5142,18 @@ mod tests { LagClock::EventTime, ) .expect("eq5-carry-mean"); - let contemporaneous_latent = recover_discrete_latent_mean_with_impulse( + let contemporaneous = recover_discrete_observed_mean_with_impulse( + loading, initial, drift, intercept, effect, predictor, + manifest_mean, delta, LagClock::EventTime, ) - .expect("mx"); - let contemporaneous = - recover_manifest_observed_mean(loading, contemporaneous_latent, manifest_mean) - .expect("eq5-mx"); + .expect("eq5-mx"); assert!((contemporaneous - recovered).abs() > 1e-3); let evolved_observed = recover_discrete_observed_mean( loading, @@ -4814,6 +5218,24 @@ mod tests { ), Err(PsychometricError::EvolvedObservedMeanIsNotImpulseCarryObservedMean) ); + assert_eq!( + refuse_impulse_observed_mean_as_impulse_carry_observed_mean( + recover_discrete_observed_mean_with_impulse( + loading, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, + LagClock::EventTime, + ) + .expect("eq5-mx"), + recovered + ), + Err(PsychometricError::ImpulseObservedMeanIsNotImpulseCarryObservedMean) + ); assert_eq!( refuse_latent_mean_as_observed_mean(carried, recovered), Err(PsychometricError::LatentMeanIsNotObservedMean) diff --git a/crates/psychometric_core/src/lib.rs b/crates/psychometric_core/src/lib.rs index 15522b66..7cd09d8a 100644 --- a/crates/psychometric_core/src/lib.rs +++ b/crates/psychometric_core/src/lib.rs @@ -36,7 +36,10 @@ //! first-occasion map `τ + λ μ_0` is not `E(y_t)`), recovers the //! Driver Eq. 3 fourth-summand impulse `m x` (Table 2 `TDPREDEFFECT` //! is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), -//! recovers the Driver Eq. 1–2 within-interval impulse carry +//! recovers the Driver Eq. 5 of that contemporaneous impulse as +//! `τ + λ(μ_t + m x)` (`τ + λ μ_t` is not that observed mean; +//! `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when +//! `u ≠ t`), recovers the Driver Eq. 1–2 within-interval impulse carry //! `e^{A(t−u)} M x` for `t0 < u < t` (not the contemporaneous Dirac, //! not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; §7.2 //! dissipation), recovers the Driver Eq. 5 of that carried latent @@ -115,6 +118,8 @@ pub use event_time::recover_discrete_latent_mean_with_time_independent_predictor pub use event_time::recover_discrete_latent_variance; /// Exact scalar discrete observed mean `τ + λ μ_t` from Eq. 3 then Eq. 5. pub use event_time::recover_discrete_observed_mean; +/// Exact scalar discrete observed mean of a contemporaneous impulse. +pub use event_time::recover_discrete_observed_mean_with_impulse; /// Exact scalar discrete observed mean of a within-interval impulse carry. pub use event_time::recover_discrete_observed_mean_with_impulse_carry; /// Exact scalar discrete process noise `Q_Δt` on event time. @@ -161,8 +166,12 @@ pub use event_time::refuse_continuous_intercept_as_manifest_means; pub use event_time::refuse_difference_quotient_as_local_rate; /// Refuse treating evolved `τ + λ μ_t` as the impulse-carry observed mean. pub use event_time::refuse_evolved_observed_mean_as_impulse_carry_observed_mean; +/// Refuse treating evolved `τ + λ μ_t` as the contemporaneous-impulse observed mean. +pub use event_time::refuse_evolved_observed_mean_as_impulse_observed_mean; /// Refuse treating finite-interval `Q_Δt` as `asymDIFFUSION`. pub use event_time::refuse_finite_interval_process_noise_as_stationary_variance; +/// Refuse treating contemporaneous `τ + λ(μ_t + m x)` as the impulse-carry observed mean. +pub use event_time::refuse_impulse_observed_mean_as_impulse_carry_observed_mean; /// Refuse treating Driver Table 2 `T0MEANS` as the evolved latent mean. pub use event_time::refuse_initial_latent_mean_as_evolved_mean; /// Refuse treating first-occasion `τ + λ μ_0` as `E(y_t)`. 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 c4c21e24..7db72687 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -10,8 +10,9 @@ use psychometric_core::{ recover_discrete_latent_mean, recover_discrete_latent_mean_with_impulse, recover_discrete_latent_mean_with_impulse_carry, recover_discrete_latent_mean_with_time_independent_predictor, recover_discrete_latent_variance, - recover_discrete_observed_mean, recover_discrete_observed_mean_with_impulse_carry, - recover_discrete_process_noise, recover_discrete_time_independent_predictor_effect, + recover_discrete_observed_mean, recover_discrete_observed_mean_with_impulse, + recover_discrete_observed_mean_with_impulse_carry, recover_discrete_process_noise, + recover_discrete_time_independent_predictor_effect, recover_discrete_time_varying_predictor_effect, recover_event_series_mean_log_rate, recover_event_time_discrete_lag_and_log_rate, recover_irregular_centered_residual_log_rate, recover_kish_weighted_slope, recover_manifest_lagged_observed_covariance, @@ -24,7 +25,9 @@ use psychometric_core::{ refuse_continuous_intercept_as_initial_latent_mean, refuse_continuous_intercept_as_manifest_means, refuse_difference_quotient_as_local_rate, refuse_evolved_observed_mean_as_impulse_carry_observed_mean, + refuse_evolved_observed_mean_as_impulse_observed_mean, refuse_finite_interval_process_noise_as_stationary_variance, + refuse_impulse_observed_mean_as_impulse_carry_observed_mean, refuse_initial_latent_mean_as_evolved_mean, refuse_initial_observed_mean_as_evolved_observed_mean, refuse_latent_lagged_covariance_as_observed_covariance, refuse_latent_mean_as_observed_mean, @@ -1312,6 +1315,244 @@ fn time_dependent_impulse_refuses_overflow_and_non_event_clocks() { ); } +#[test] +fn discrete_observed_mean_with_impulse_recovers_driver_equation_five() { + let loading = 2.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let effect = 0.4_f64; + let predictor = 3.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let observed = recover_discrete_observed_mean_with_impulse( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-impulse-mean"); + let composed = recover_discrete_latent_mean_with_impulse( + initial, + drift, + intercept, + effect, + predictor, + delta, + LagClock::EventTime, + ) + .expect("mx"); + let expected = manifest_mean + loading * composed; + let error = rmse(&[expected], &[observed]); + assert!( + error < 1e-15, + "Driver Eq. 5 of Eq. 3 contemporaneous impulse RMSE {error}" + ); + let evolved_observed = recover_discrete_observed_mean( + loading, + initial, + drift, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq3-eq5-mean"); + let evolved_error = rmse(&[expected], &[evolved_observed]); + assert!( + evolved_error > error, + "τ + λ μ_t is not contemporaneous-impulse E(y_t): RMSE {evolved_error} must exceed {error}" + ); + let intercept_error = rmse(&[expected], &[manifest_mean]); + assert!( + intercept_error > error, + "MANIFESTMEANS is not contemporaneous-impulse E(y_t): RMSE {intercept_error} must exceed {error}" + ); + let latent_error = rmse(&[expected], &[composed]); + assert!( + latent_error > error, + "evolved-plus-impulse latent mean is not E(y_t): RMSE {latent_error} must exceed {error}" + ); +} + +#[test] +fn discrete_observed_mean_with_impulse_refuses_evolved_mean_and_carry() { + let loading = 2.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let effect = 0.4_f64; + let predictor = 3.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let observed = recover_discrete_observed_mean_with_impulse( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-impulse-mean"); + let composed = recover_discrete_latent_mean_with_impulse( + initial, + drift, + intercept, + effect, + predictor, + delta, + LagClock::EventTime, + ) + .expect("mx"); + let expected = manifest_mean + loading * composed; + let error = rmse(&[expected], &[observed]); + let evolved_observed = recover_discrete_observed_mean( + loading, + initial, + drift, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq3-eq5-mean"); + let carried_observed = recover_discrete_observed_mean_with_impulse_carry( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + 1.0, + LagClock::EventTime, + ) + .expect("eq5-carry-mean"); + let carry_error = rmse(&[expected], &[carried_observed]); + assert!( + carry_error > error, + "τ + λ(μ_t + carry) is not contemporaneous-impulse E(y_t): RMSE {carry_error} must exceed {error}" + ); + let zero_loading = recover_discrete_observed_mean_with_impulse( + 0.0, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("lambda0"); + let dropped_error = rmse(&[expected], &[zero_loading]); + assert!( + dropped_error > error, + "zero loading is τ, not τ + λ(μ_t + m x): RMSE {dropped_error} must exceed {error}" + ); + assert_eq!( + refuse_evolved_observed_mean_as_impulse_observed_mean(evolved_observed, observed), + Err(PsychometricError::EvolvedObservedMeanIsNotImpulseObservedMean) + ); + assert_eq!( + refuse_impulse_observed_mean_as_impulse_carry_observed_mean(observed, carried_observed), + Err(PsychometricError::ImpulseObservedMeanIsNotImpulseCarryObservedMean) + ); + assert_eq!( + refuse_latent_mean_as_observed_mean(composed, observed), + Err(PsychometricError::LatentMeanIsNotObservedMean) + ); + assert_eq!( + refuse_manifest_means_as_observed_mean(manifest_mean, observed), + Err(PsychometricError::ManifestMeansIsNotObservedMean) + ); +} + +#[test] +fn discrete_observed_mean_with_impulse_refuses_overflow_and_non_event_clocks() { + assert_eq!( + recover_discrete_observed_mean_with_impulse( + 1e308, + 2.0, + 0.0, + 0.0, + 0.0, + 3.0, + 0.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_observed_mean_with_impulse( + 2.0, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_discrete_observed_mean_with_impulse( + 2.0, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_observed_mean_with_impulse( + 1e308, + 0.0, + 0.0, + 0.0, + 1e308, + 1.0, + 0.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + let scaled = recover_discrete_observed_mean_with_impulse( + 1e308, + 1e-308, + 0.0, + 0.0, + 0.0, + 3.0, + 0.0, + 1.0, + LagClock::EventTime, + ) + .expect("scale"); + assert!( + (scaled - 1.0).abs() < 1e-15, + "Driver Eq. 5 of Eq. 3 impulse must keep λ=1e308, μ=1e-308: got {scaled}" + ); +} + #[test] fn time_independent_predictor_recovers_driver_equation_three_second_summand() { let effect = 0.4_f64; @@ -1676,19 +1917,18 @@ fn discrete_observed_mean_with_impulse_carry_refuses_evolved_mean_and_contempora LagClock::EventTime, ) .expect("eq3-eq5-mean"); - let contemporaneous_latent = recover_discrete_latent_mean_with_impulse( + let contemporaneous = recover_discrete_observed_mean_with_impulse( + loading, initial, drift, intercept, effect, predictor, + manifest_mean, delta, LagClock::EventTime, ) - .expect("mx"); - let contemporaneous = - recover_manifest_observed_mean(loading, contemporaneous_latent, manifest_mean) - .expect("eq5-mx"); + .expect("eq5-mx"); let contemporaneous_error = rmse(&[expected], &[contemporaneous]); assert!( contemporaneous_error > error, @@ -1716,6 +1956,10 @@ fn discrete_observed_mean_with_impulse_carry_refuses_evolved_mean_and_contempora refuse_evolved_observed_mean_as_impulse_carry_observed_mean(evolved_observed, observed), Err(PsychometricError::EvolvedObservedMeanIsNotImpulseCarryObservedMean) ); + assert_eq!( + refuse_impulse_observed_mean_as_impulse_carry_observed_mean(contemporaneous, observed), + Err(PsychometricError::ImpulseObservedMeanIsNotImpulseCarryObservedMean) + ); assert_eq!( refuse_latent_mean_as_observed_mean(carried, observed), Err(PsychometricError::LatentMeanIsNotObservedMean) diff --git a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs index f647f59a..6950a977 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -8,8 +8,9 @@ use psychometric_core::{ recover_discrete_latent_mean, recover_discrete_latent_mean_with_impulse, recover_discrete_latent_mean_with_impulse_carry, recover_discrete_latent_mean_with_time_independent_predictor, recover_discrete_latent_variance, - recover_discrete_observed_mean, recover_discrete_observed_mean_with_impulse_carry, - recover_discrete_process_noise, recover_discrete_time_independent_predictor_effect, + recover_discrete_observed_mean, recover_discrete_observed_mean_with_impulse, + recover_discrete_observed_mean_with_impulse_carry, recover_discrete_process_noise, + recover_discrete_time_independent_predictor_effect, recover_discrete_time_varying_predictor_effect, recover_irregular_centered_residual_log_rate, recover_loading_point_estimate_mean, recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, recover_manifest_observed_variance, @@ -20,7 +21,9 @@ use psychometric_core::{ refuse_continuous_intercept_as_initial_latent_mean, refuse_continuous_intercept_as_manifest_means, refuse_evolved_observed_mean_as_impulse_carry_observed_mean, + refuse_evolved_observed_mean_as_impulse_observed_mean, refuse_finite_interval_process_noise_as_stationary_variance, + refuse_impulse_observed_mean_as_impulse_carry_observed_mean, refuse_initial_latent_mean_as_evolved_mean, refuse_initial_observed_mean_as_evolved_observed_mean, refuse_latent_lagged_covariance_as_observed_covariance, refuse_latent_mean_as_observed_mean, @@ -530,6 +533,98 @@ fn first_occasion_observed_mean_is_not_evolved_observed_mean() { ); } +#[test] +fn evolved_observed_mean_is_not_impulse_observed_mean() { + let loading = 2.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let effect = 0.4_f64; + let predictor = 3.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let impulse_observed = recover_discrete_observed_mean_with_impulse( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-impulse-mean"); + let evolved_observed = recover_discrete_observed_mean( + loading, + initial, + drift, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq3-eq5-mean"); + let composed = recover_discrete_latent_mean_with_impulse( + initial, + drift, + intercept, + effect, + predictor, + delta, + LagClock::EventTime, + ) + .expect("mx"); + let carried_observed = recover_discrete_observed_mean_with_impulse_carry( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + 1.0, + LagClock::EventTime, + ) + .expect("eq5-carry-mean"); + assert!( + (evolved_observed - impulse_observed).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 of Eq. 3 impulse): τ + λ μ_t is not contemporaneous-impulse E(y_t)" + ); + assert!( + (manifest_mean - impulse_observed).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 / Table 2 p. 12): MANIFESTMEANS is not contemporaneous-impulse E(y_t)" + ); + assert!( + (composed - impulse_observed).abs() > 1e-3, + "Driver et al. (2017, Eq. 5): evolved-plus-impulse latent mean is not E(y_t)" + ); + assert!( + (carried_observed - impulse_observed).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 of Eq. 1–2): τ + λ(μ_t + carry) is not contemporaneous-impulse E(y_t)" + ); + assert_eq!( + refuse_evolved_observed_mean_as_impulse_observed_mean(evolved_observed, impulse_observed), + Err(psychometric_core::PsychometricError::EvolvedObservedMeanIsNotImpulseObservedMean) + ); + assert_eq!( + refuse_impulse_observed_mean_as_impulse_carry_observed_mean( + impulse_observed, + carried_observed + ), + Err(psychometric_core::PsychometricError::ImpulseObservedMeanIsNotImpulseCarryObservedMean) + ); + assert_eq!( + refuse_latent_mean_as_observed_mean(composed, impulse_observed), + Err(psychometric_core::PsychometricError::LatentMeanIsNotObservedMean) + ); + assert_eq!( + refuse_manifest_means_as_observed_mean(manifest_mean, impulse_observed), + Err(psychometric_core::PsychometricError::ManifestMeansIsNotObservedMean) + ); +} + #[test] fn time_dependent_impulse_is_not_cint_tipred_or_equation_fourteen() { let effect = 0.4_f64; @@ -803,19 +898,18 @@ fn evolved_observed_mean_is_not_impulse_carry_observed_mean() { LagClock::EventTime, ) .expect("carried"); - let contemporaneous_latent = recover_discrete_latent_mean_with_impulse( + let contemporaneous = recover_discrete_observed_mean_with_impulse( + loading, initial, drift, intercept, effect, predictor, + manifest_mean, delta, LagClock::EventTime, ) - .expect("mx"); - let contemporaneous = - recover_manifest_observed_mean(loading, contemporaneous_latent, manifest_mean) - .expect("eq5-mx"); + .expect("eq5-mx"); assert!( (evolved_observed - impulse_carry_observed).abs() > 1e-3, "Driver et al. (2017, Eq. 5 of Eq. 1–2): τ + λ μ_t is not impulse-carry E(y_t)" @@ -839,6 +933,13 @@ fn evolved_observed_mean_is_not_impulse_carry_observed_mean() { ), Err(psychometric_core::PsychometricError::EvolvedObservedMeanIsNotImpulseCarryObservedMean) ); + assert_eq!( + refuse_impulse_observed_mean_as_impulse_carry_observed_mean( + contemporaneous, + impulse_carry_observed + ), + Err(psychometric_core::PsychometricError::ImpulseObservedMeanIsNotImpulseCarryObservedMean) + ); assert_eq!( refuse_latent_mean_as_observed_mean(carried, impulse_carry_observed), Err(psychometric_core::PsychometricError::LatentMeanIsNotObservedMean) diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index a43feb18..cf5060e7 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -28,7 +28,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | report template/section/copied/style/modality method effects | ADR 0004/0012; PRD/TRD | simulation truth factors implemented; estimator-side method model remains future | partial | | candidate K statistical/Pareto gates + blinded LLM review | ADR 0012; research | future `model_selection` | accepted-target | | compositional topic correlation / stable clustering | ADR 0005/0012; research | future `network_analysis` | accepted-target | -| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z); full ESEM/DSEM remaining | partial | +| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z); full ESEM/DSEM remaining | partial | | CPU bounded multithreading + GPU/VRAM streaming/parity | ADR 0001/0006 | future `compute_backend` | accepted-target | | TDT detection/tracking vs CHRONOS schema/prediction/temporal consistency | ADR 0016; PRD/research | future `event_intelligence` | accepted-target | | evidence-bounded LLM interpretation | ADR 0010/0012; PRD | future `interpretation_gateway` | accepted-target | diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index 21bbf215..8fdb1d7a 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -1,7 +1,7 @@ # ADR 0005 — Posterior-aware ESEM/DSEM and structural interpretation **Decision status:** Accepted -**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), exact scalar contemporaneous `TDPREDEFFECT` impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; the §7.2 level-change form is not that impulse), exact scalar time-independent `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle Eq. 14; `B` is not that discrete increment), exact scalar within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of that carried latent mean; `τ + λ μ_t` is not that observed mean), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target +**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), exact scalar contemporaneous `TDPREDEFFECT` impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; the §7.2 level-change form is not that impulse), exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of that Eq. 3 composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar time-independent `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle Eq. 14; `B` is not that discrete increment), exact scalar within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of that carried latent mean; `τ + λ μ_t` is not that observed mean), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target **Date:** 2026-08-05 **Supersedes:** None. ADR 0012 governs upstream topic measurement/network coordinates; this ADR governs higher-order psychometric structure and longitudinal interpretation. @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), recovers the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z; `TDPREDEFFECT` is `M`, not `CINT`; `M x` is not `A^{-1}[e^{A Δt} − I] B z`; `M x` is not Voelkle et al., 2012, Eq. 14; the level-change form is not that impulse), recovers the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z; `TIPREDEFFECT` is `B`, not `κ`; form `B z` first, then the discrete intercept map; a zero drift is `B z Δt`; `B` is not the discrete increment; `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle Eq. 14), recovers the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z; form `m x` first, then `e^{a(t−u)} m x`; a zero drift is `m x`; underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept; `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; an impulse at `u = t` is the contemporaneous map; an impulse at `u ≤ t0` is already in `η(t0)`), recovers the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; JSS PDF re-opened 2026-08-20T05:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the carried latent mean is not `E(y_t)`), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), recovers the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z; `TDPREDEFFECT` is `M`, not `CINT`; `M x` is not `A^{-1}[e^{A Δt} − I] B z`; `M x` is not Voelkle et al., 2012, Eq. 14; the level-change form is not that impulse), recovers the exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition; JSS PDF re-opened 2026-08-20T09:01Z; the evolved map `τ + λ μ_t` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-impulse latent mean is not `E(y_t)`), recovers the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z; `TIPREDEFFECT` is `B`, not `κ`; form `B z` first, then the discrete intercept map; a zero drift is `B z Δt`; `B` is not the discrete increment; `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle Eq. 14), recovers the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z; form `m x` first, then `e^{a(t−u)} m x`; a zero drift is `m x`; underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept; `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; an impulse at `u = t` is the contemporaneous map; an impulse at `u ≤ t0` is already in `η(t0)`), recovers the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; JSS PDF re-opened 2026-08-20T05:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the carried latent mean is not `E(y_t)`), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. 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/adr/README.md b/docs/adr/README.md index 95c54bcf..069cb5b3 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -10,7 +10,7 @@ Read [`ADR_POLICY.md`](ADR_POLICY.md) first. **Decision status and implementatio | [0002](0002-six-clock-temporal-semantics.md) | Six-clock temporal semantics and fail-closed historical leakage prevention | Accepted | active-PR | Unmerged PR #8 is the canonical Task 3 replacement implementing typed clocks/intervals against the current protected-main lineage; conflicted PR #5 is superseded lineage. Later graph/split enforcement remains target work. | | [0003](0003-relational-event-multiple-membership.md) | Relational event ontology and time-varying cross-classified multiple membership | Accepted | partial | Weighted time-varying membership network/roles are active-PR (PR #12); full multilevel estimators, graph ontology, and persistence remain accepted-target. ADR 0016 owns event-intelligence tasks. | | [0004](0004-shared-multilingual-latent-space.md) | One shared multilingual latent space with explicit invariance status | Accepted | accepted-target | ADR 0012 owns the full topic-estimator/backend/global-topic contract. | -| [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | partial | Input gates, posterior-draw point estimates, Rubin `T` on OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects, exact scalar discrete process noise, lagged latent covariance and unconditional latent variance, stationary within-subject variance (`asymDIFFUSION`), trait-plus-state variance (`TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; Eq. 5 of the within-interval impulse carry is `τ + λ(μ_t + e^{a(t−u)} m x)`; `τ + λ μ_t` is not that observed mean), irregular already-centered residual lag, and strong-gated latent means are on the stacked psychometric PR; full ESEM/DSEM estimator remains accepted-target. | +| [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | partial | Input gates, posterior-draw point estimates, Rubin `T` on OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects, exact scalar discrete process noise, lagged latent covariance and unconditional latent variance, stationary within-subject variance (`asymDIFFUSION`), trait-plus-state variance (`TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; Eq. 5 of the contemporaneous impulse is `τ + λ(μ_t + m x)`; `τ + λ μ_t` is not that observed mean; Eq. 5 of the within-interval impulse carry is `τ + λ(μ_t + e^{a(t−u)} m x)`; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`), irregular already-centered residual lag, and strong-gated latent means are on the stacked psychometric PR; full ESEM/DSEM estimator remains accepted-target. | | [0006](0006-vram-gpu-nvidia-orchestration.md) | VRAM-adaptive GPU compute and model-credential boundary | Accepted | accepted-target | LLM orchestration policy superseded by ADR 0010; autonomous development authority governed by ADR 0015. | | [0007](0007-rust-workspace-quality-gates.md) | Explicit Rust workspace, pinned toolchains, and exact quality gates | Accepted | implemented-main | ADR 0014 governs scientific/product claim promotion beyond repository-quality tooling. | | [0008](0008-immutable-evidence-identities-digests-and-spans.md) | Immutable evidence identities, `SHA-256` digests, exact spans, and strict wire reconstruction | Accepted | implemented-main | ADR 0013 governs future persistence/reproducibility/split authority. | diff --git a/docs/research/multilevel-event-time-recovery.md b/docs/research/multilevel-event-time-recovery.md index bf5f94c9..370ea55a 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -23,18 +23,19 @@ This slice stays inside `psychometric_core`. It does not add a second invariance 17. recover the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; Table 2 `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment) and refuse treating `T0MEANS` or `CINT` as `μ_t`; 18. recover the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`) and refuse treating that first-occasion mean as `E(y_t)`; 19. recover the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`) and refuse treating that impulse as `CINT`, `TIPREDEFFECT`, or Voelkle et al. (2012, Eq. 14); -20. recover the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`) and refuse treating that increment as `CINT`, `TDPREDEFFECT`, Voelkle et al. (2012, Eq. 14), or the coefficient `B`; -21. recover the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; the printed Eq. 3 fourth summand is the contemporaneous Dirac) and refuse treating that carry as the contemporaneous impulse, `CINT`, `TIPREDEFFECT`, or Voelkle et al. (2012, Eq. 14); -22. recover the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; the evolved map `τ + λ μ_t` is not that observed mean) and refuse treating `τ + λ μ_t` as that observed mean; -23. refuse pooling discrete lags from unequal event intervals as one coefficient; -24. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); -25. refuse the difference quotient as a continuous-time rate; -26. apply the same event-time map to CWC residuals (still not DSEM); -27. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). +20. recover the exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition; the evolved map `τ + λ μ_t` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`) and refuse treating `τ + λ μ_t` as that observed mean; +21. recover the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`) and refuse treating that increment as `CINT`, `TDPREDEFFECT`, Voelkle et al. (2012, Eq. 14), or the coefficient `B`; +22. recover the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; the printed Eq. 3 fourth summand is the contemporaneous Dirac) and refuse treating that carry as the contemporaneous impulse, `CINT`, `TIPREDEFFECT`, or Voelkle et al. (2012, Eq. 14); +23. recover the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`) and refuse treating `τ + λ μ_t` or `τ + λ(μ_t + m x)` as that observed mean; +24. refuse pooling discrete lags from unequal event intervals as one coefficient; +25. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); +26. refuse the difference quotient as a continuous-time rate; +27. apply the same event-time map to CWC residuals (still not DSEM); +28. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). ## Claim boundary -This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. The contemporaneous `TDPREDEFFECT` impulse is not the continuous intercept, not the time-independent discrete effect, and not Voelkle et al. (2012, Eq. 14). The time-independent `TIPREDEFFECT` increment is not the continuous intercept, not the contemporaneous impulse, not Voelkle et al. (2012, Eq. 14), and not the coefficient `B`. The within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). The evolved observed mean `τ + λ μ_t` is not the impulse-carry observed mean `τ + λ(μ_t + e^{a(t−u)} m x)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`. The carried latent mean is not `E(y_t)`. +This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. The contemporaneous `TDPREDEFFECT` impulse is not the continuous intercept, not the time-independent discrete effect, and not Voelkle et al. (2012, Eq. 14). The time-independent `TIPREDEFFECT` increment is not the continuous intercept, not the contemporaneous impulse, not Voelkle et al. (2012, Eq. 14), and not the coefficient `B`. The within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). The evolved observed mean `τ + λ μ_t` is not the contemporaneous-impulse observed mean `τ + λ(μ_t + m x)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not the impulse-carry observed mean `τ + λ(μ_t + e^{a(t−u)} m x)` when `u ≠ t`. The evolved observed mean `τ + λ μ_t` is not the impulse-carry observed mean. The carried latent mean is not `E(y_t)`. The evolved-plus-impulse latent mean is not `E(y_t)`. ## Authoritative sources @@ -74,6 +75,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - **Discrete latent mean.** Driver et al. (2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z): \(\eta(t)=\exp(A\Delta t)\eta(t_0)+\int\exp(A(t-s))(b+\cdots)\,ds\) plus a stochastic integral of mean zero. Table 2 names the first-occasion latent mean `T0MEANS` and \(\kappa\) `CINT`. The scalar map is \(\mu_t=\exp(a\Delta t)\mu_0+(\exp(a\Delta t)-1)/a\,\kappa\). Form the `CINT` increment first, then add the carried `T0MEANS` term. A zero drift is the Eq. 3 integral \(\kappa\Delta t\) (\(A=0\) has no inverse). A zero intercept is exactly \(\exp(a\Delta t)\mu_0\). A zero initial mean is exactly the increment. As \(\Delta t\to\infty\) with stable \(a<0\), \(\mu_t\to-\kappa/a\). Binary64 underflow of \(\exp(a\Delta t)\) to `+0` drops the carried `T0MEANS` and keeps that equilibrium increment. `T0MEANS` is not \(\mu_t\). `CINT` is not the discrete increment. `CINT` is not `T0MEANS`. An overflowing exponential, product, or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **Discrete observed-indicator mean.** Driver et al. (2017, Eq. 3, p. 5; Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T22:10Z): `η_i(t)=exp(AΔt)η_i(t0)+A^{-1}[exp(AΔt)−I]ξ_i+…` with `ξ_i∼N(κ,φ_ξ)` and a stochastic integral of mean zero, then `y_i(t)=Γ_i+Λη_i(t)+ζ_i(t)` with `Γ∼N(τ,Ψ)`. The scalar composition is `E(y_t)=τ+λμ_t`. Form `μ_t` first, then `τ+λμ_t`. A zero loading or zero evolved latent mean is exactly `τ`. A zero intercept is exactly `λμ_t`. A zero drift is `τ+λ(μ_0+κΔt)`. Underflow of `exp(aΔt)` to `+0` keeps `τ+λ(−κ/a)`. The first-occasion map `τ+λμ_0` is not `E(y_t)`. `MANIFESTMEANS` is not `E(y_t)`. `T0MEANS` is not `E(y_t)`. `μ_t` is not `E(y_t)`. An overflowing exponential, product, or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **Time-dependent predictor impulse.** Driver et al. (2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z): `χ_i(t)=Σ x_{i,u} δ(t−u)` and the fourth summand is `M Σ x_{i,u} δ(t−u)`. Table 2 names `M` `TDPREDEFFECT`. Section 7.2 calls this a sudden impulse that dissipates back to the process mean. The scalar contemporaneous jump is `m x`. Form `μ_t` first, then add `m x`. A zero effect or zero predictor is exactly zero. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{AΔt}−I] B z` (`TIPREDEFFECT`). `M x` is not Voelkle et al. (2012, Eq. 14) `a_{yx}Δt`. The §7.2 level-change form is an extra latent process with near-zero drift and is not this map. An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. +- **Contemporaneous-impulse observed-indicator mean.** Driver et al. (2017, Eq. 5, p. 5; Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T09:01Z): `y_i(t)=Γ+Λη_i(t)+ζ_i(t)` with `ζ∼N(0,Θ)` and `Γ∼N(τ,Ψ)`. The latent process at `t` after a contemporaneous Dirac (`u=t`) is `μ_t+mx`. The scalar composition is `E(y_t)=τ+λ(μ_t+mx)`. Form the evolved-plus-impulse latent mean first, then `τ+λ` of that mean. A zero loading is exactly `τ`. A zero evolved-plus-impulse latent mean is exactly `τ`. A zero intercept is exactly `λ(μ_t+mx)`. The evolved observed mean `τ+λμ_t` is not this composition. The carry map `τ+λ(μ_t+e^{a(t−u)}mx)` is not this composition when `u≠t`. `MANIFESTMEANS` is not `E(y_t)`. The evolved-plus-impulse latent mean is not `E(y_t)`. The §7.2 level-change form is not this map. An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **Time-independent predictor effect.** Driver et al. (2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z): Equation 1 writes `dη=(Aη+b+A_{ηξ}ξ+Bz)dt+GdW+Mdχ`. Equation 3's second summand is `A^{-1}[e^{AΔt}−I](b+A_{ηξ}ξ+Bz)`. Table 2 names `B` `TIPREDEFFECT`. The scalar map is `(e^{aΔt}−1)/a·Bz` for `a≠0`. Form `Bz` first, then the discrete intercept map. A zero drift is `BzΔt`. Form `μ_t` first, then add that increment. A zero effect or zero predictor is exactly zero. `TIPREDEFFECT` is `B`, not the discrete increment. `A^{-1}[e^{AΔt}−I]Bz` is not `CINT`, not `Mx`, and not Voelkle et al. (2012, Eq. 14) `a_{yx}Δt`. An overflowing product, increment, or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **Time-dependent predictor impulse carry.** Driver et al. (2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z): Equation 1 writes `dη=(Aη+ξ+Bz+Mχ(t))dt+GdW`. Equation 2 writes `χ_i(t)=Σ x_{i,u} δ(t−u)`. The Green-function integral of that Dirac on `(t0,t)` is `e^{A(t−u)}Mx`. The printed Eq. 3 fourth summand is the contemporaneous jump `Mx` at `u=t`. This map is the strictly within-interval case `t0 Date: Thu, 20 Aug 2026 18:37:39 +0900 Subject: [PATCH 40/62] test: exercise valid pooled event-time comparison --- .../tests/multilevel_event_time_recovery_contract.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 7db72687..bde9e539 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -533,7 +533,7 @@ fn within_residual_event_time_log_rate_beats_pooled_levels() { let within_error = rmse(&[true_drift], &[recovered]); let mut pooled = Vec::new(); - for (cluster, person_mean, start) in [(1_u64, 8.0_f64, 1.0_f64), (2, -5.0, 1.4)] { + for (cluster, person_mean, start) in [(1_u64, 8.0_f64, 1.0_f64), (2, 1.0, 1.4)] { let time = cluster as f64; pooled.push(EventOccasion { event_time: time, From 789d4d16e3fcabe27ea3df57980c370dd9df255f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 05:10:05 -0700 Subject: [PATCH 41/62] chore: probe GitHub write access (will revert) --- .tepp-write-probe-2026-08-20 | 1 + 1 file changed, 1 insertion(+) create mode 100644 .tepp-write-probe-2026-08-20 diff --git a/.tepp-write-probe-2026-08-20 b/.tepp-write-probe-2026-08-20 new file mode 100644 index 00000000..1fc67c81 --- /dev/null +++ b/.tepp-write-probe-2026-08-20 @@ -0,0 +1 @@ +write-probe From c074718f0ece38566c0a31ed112c9a066596ce44 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 05:10:46 -0700 Subject: [PATCH 42/62] chore: revert GitHub write-access probe --- .tepp-write-probe-2026-08-20 | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .tepp-write-probe-2026-08-20 diff --git a/.tepp-write-probe-2026-08-20 b/.tepp-write-probe-2026-08-20 deleted file mode 100644 index 1fc67c81..00000000 --- a/.tepp-write-probe-2026-08-20 +++ /dev/null @@ -1 +0,0 @@ -write-probe From d125515ae73c33de2e7cf900dc0fa3b7deb39614 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 12:31:20 +0000 Subject: [PATCH 43/62] feat(psychometric): recover Driver Eq. 5 of the TIPREDEFFECT mean MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compose E(y_t) = τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z) from the re-opened Driver, Oud, and Voelkle (2017) JSS PDF (Eq. 5, p. 5; Eq. 3, p. 5; Table 2, p. 12). Equation 3 prints that increment as the addend after the T0MEANS carry and the CINT increment. Form the evolved-plus-increment latent mean first. τ + λ μ_t is not that observed mean. τ + λ(μ_t + m x) is not that observed mean. τ + λ(μ_t + e^{a(t−u)} m x) is not that observed mean when u ≠ t. --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 1 + CLAUDE.md | 2 +- README.md | 2 +- crates/psychometric_core/src/error.rs | 34 ++ crates/psychometric_core/src/event_time.rs | 437 +++++++++++++++++- crates/psychometric_core/src/lib.rs | 15 +- ...multilevel_event_time_recovery_contract.rs | 340 +++++++++++++- .../scientific_claim_boundary_contract.rs | 154 +++++- docs/TRACEABILITY.md | 2 +- docs/adr/0005-posterior-esem-dsem.md | 4 +- docs/adr/README.md | 2 +- .../multilevel-event-time-recovery.md | 19 +- docs/research/posterior-esem-input-gates.md | 2 +- docs/validation/temporal-event-foundation.md | 2 +- 15 files changed, 996 insertions(+), 22 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index fd3109dd..928203eb 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -61,7 +61,7 @@ boundaries above remain the target modular MSA architecture. | `tepp_simulation` | known-truth temporal/event data generation | | `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics | | `tepp_api` | versioned DTO, schema, and export contracts | -| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | +| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | No crate exposes placeholder production behavior in Task 1. This prevents an empty façade from becoming a de facto public API before its invariants and tests diff --git a/CHANGELOG.md b/CHANGELOG.md index 06da1e50..0720ce53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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, Eq. 5, p. 5; Eq. 3, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T12:12Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar observed-indicator mean of a time-independent predictor. Equation 5 writes `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and `Γ ~ N(τ, Ψ)`. Equation 3 prints the `TIPREDEFFECT` increment as the addend `A^{-1}[e^{A(t−t0)} − I] B z_i` after the `T0MEANS` carry and the `CINT` increment. Table 2 names `B` `TIPREDEFFECT`. The scalar composition is `E(y_t) = τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. Form the evolved-plus-increment latent mean first, then `τ + λ` of that mean. The evolved observed mean `τ + λ μ_t` is not this composition. The contemporaneous map `τ + λ(μ_t + m x)` is not this composition. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not this composition when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The evolved-plus-increment latent mean is not `E(y_t)`. `TIPREDEFFECT` is `B`, not that observed mean. A zero loading is exactly `τ`. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993), Vandenberg and Lance (2000), and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T05:12Z: `is_oa: false`). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T09:01Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar observed-indicator mean of a contemporaneous time-dependent impulse. Equation 5 writes `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and `Γ ~ N(τ, Ψ)`. The latent process at `t` after a contemporaneous Dirac (`u = t`) is `μ_t + m x`. The scalar composition is `E(y_t) = τ + λ(μ_t + m x)`. Form the evolved-plus-impulse latent mean first, then `τ + λ` of that mean. The evolved observed mean `τ + λ μ_t` is not this composition. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not this composition when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The evolved-plus-impulse latent mean is not `E(y_t)`. A zero loading is exactly `τ`. The §7.2 level-change form is a different specification and is not this map. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993), Vandenberg and Lance (2000), and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T05:12Z: `is_oa: false`). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Eq. 1–2, pp. 4–5; Eq. 3 exponential map; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T05:12Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar observed-indicator mean of a within-interval time-dependent impulse carry. Equation 5 writes `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and `Γ ~ N(τ, Ψ)`. The latent process at `t` after a Dirac that occurred strictly inside `(t0, t)` is `μ_t + e^{a(t−u)} m x`. The scalar composition is `E(y_t) = τ + λ(μ_t + e^{a(t−u)} m x)`. Form the carried latent mean first, then `τ + λ` of that mean. The evolved observed mean `τ + λ μ_t` is not this composition. The contemporaneous map `τ + λ(μ_t + m x)` is not this composition when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The carried latent mean is not `E(y_t)`. A zero loading is exactly `τ`. The §7.2 level-change form is a different specification and is not this map. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993), Vandenberg and Lance (2000), and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T05:12Z: `is_oa: false`). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar within-interval time-dependent predictor impulse carry. Equation 1 writes `dη = (A η + ξ + B z + M χ(t)) dt + G dW`. Equation 2 writes `χ_i(t) = Σ x_{i,u} δ(t − u)`. The Green-function integral of that Dirac on `(t0, t)` is `e^{A(t−u)} M x`. The printed Eq. 3 fourth summand is the contemporaneous jump `M x` at `u = t`. This map is the strictly within-interval case `t0 < u < t`. Form `m x` first, then `e^{a(t−u)} m x`. A zero drift is `m x` with no dissipation. Underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation back to the process mean and is kept. Form `μ_t` first, then add the carry. `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `A^{-1}[e^{A Δt} − I] B z` (`TIPREDEFFECT`), and not Voelkle et al. (2012, Eq. 14) `a_{yx} Δt`. An impulse at `u = t` is the contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. The §7.2 level-change form is a different specification and is not this map. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993), Vandenberg and Lance (2000), and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T10:33Z: `is_oa: false`). diff --git a/CLAUDE.md b/CLAUDE.md index e7cfa4c3..47cd49e9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. Putnick and Bornstein (2016, PMC5145197 opened 2026-08-19T22:15Z) require scalar invariance before latent-mean comparison; residual invariance is not a prerequisite. Two-observation series have no residual degrees of freedom (`ordinary_least_squares_fit` returns residual variance `0`) and cap at strong/scalar; they still license means. This is two-group OLS, not MGCFA. Meredith (1993) names remain unread labels. -- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. The contemporaneous time-dependent predictor impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`). Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). The §7.2 level-change form is not that impulse. The observed mean of that contemporaneous impulse is `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-impulse latent mean is not `E(y_t)`. The time-independent predictor increment is `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`). Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. `TIPREDEFFECT` is `B`, not that discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle et al. (2012, Eq. 14). The within-interval time-dependent impulse carry is `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation). Form `m x` first, then `e^{a(t−u)} m x`. A zero drift is `m x` with no dissipation. Underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept. `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). An impulse at `u = t` is the contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. The observed mean of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The carried latent mean is not `E(y_t)`. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. The contemporaneous time-dependent predictor impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`). Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). The §7.2 level-change form is not that impulse. The observed mean of that contemporaneous impulse is `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-impulse latent mean is not `E(y_t)`. The time-independent predictor increment is `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`). Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. `TIPREDEFFECT` is `B`, not that discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle et al. (2012, Eq. 14). The observed mean of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The within-interval time-dependent impulse carry is `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation). Form `m x` first, then `e^{a(t−u)} m x`. A zero drift is `m x` with no dissipation. Underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept. `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). An impulse at `u = t` is the contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. The observed mean of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The carried latent mean is not `E(y_t)`. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. diff --git a/README.md b/README.md index 26a8fce1..ab3c9b59 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ crates/corpus_split crates/tepp_simulation crates/validation_core crates/tepp_api -crates/psychometric_core # input gates, CWC/event-time/contextual, irregular residual lag, Rubin T, strong means, Driver Eq. 3 TDPRED/TIPRED maps including Eq. 5 of within-interval impulse carry; not a full ESEM/DSEM estimator +crates/psychometric_core # input gates, CWC/event-time/contextual, irregular residual lag, Rubin T, strong means, Driver Eq. 3 TDPRED/TIPRED maps including Eq. 5 of TIPREDEFFECT; not a full ESEM/DSEM estimator ``` ## Local verification diff --git a/crates/psychometric_core/src/error.rs b/crates/psychometric_core/src/error.rs index 669f3e79..777645f5 100644 --- a/crates/psychometric_core/src/error.rs +++ b/crates/psychometric_core/src/error.rs @@ -152,6 +152,19 @@ pub enum PsychometricError { /// Equation 5 of the Eq. 1–2 carried latent mean. /// `τ + λ(μ_t + m x)` is not `τ + λ(μ_t + e^{a(t−u)} m x)`. ImpulseObservedMeanIsNotImpulseCarryObservedMean, + /// Driver Eq. 5 of the Eq. 3 evolved mean was treated as + /// Equation 5 of the time-independent predictor. + /// `τ + λ μ_t` is not `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. + EvolvedObservedMeanIsNotTimeIndependentObservedMean, + /// Driver Eq. 5 of the contemporaneous impulse was treated as + /// Equation 5 of the time-independent predictor. + /// `τ + λ(μ_t + m x)` is not `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. + ImpulseObservedMeanIsNotTimeIndependentObservedMean, + /// Driver Eq. 5 of the Eq. 1–2 carried latent mean was treated as + /// Equation 5 of the time-independent predictor. + /// `τ + λ(μ_t + e^{a(t−u)} m x)` is not + /// `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. + ImpulseCarryObservedMeanIsNotTimeIndependentObservedMean, } impl fmt::Display for PsychometricError { @@ -281,6 +294,15 @@ impl fmt::Display for PsychometricError { Self::ImpulseObservedMeanIsNotImpulseCarryObservedMean => { "contemporaneous-impulse observed mean is not the impulse-carry observed mean" } + Self::EvolvedObservedMeanIsNotTimeIndependentObservedMean => { + "evolved observed mean is not the time-independent-predictor observed mean" + } + Self::ImpulseObservedMeanIsNotTimeIndependentObservedMean => { + "contemporaneous-impulse observed mean is not the time-independent-predictor observed mean" + } + Self::ImpulseCarryObservedMeanIsNotTimeIndependentObservedMean => { + "impulse-carry observed mean is not the time-independent-predictor observed mean" + } }; formatter.write_str(message) } @@ -490,5 +512,17 @@ mod tests { PsychometricError::ImpulseObservedMeanIsNotImpulseCarryObservedMean.to_string(), "contemporaneous-impulse observed mean is not the impulse-carry observed mean" ); + assert_eq!( + PsychometricError::EvolvedObservedMeanIsNotTimeIndependentObservedMean.to_string(), + "evolved observed mean is not the time-independent-predictor observed mean" + ); + assert_eq!( + PsychometricError::ImpulseObservedMeanIsNotTimeIndependentObservedMean.to_string(), + "contemporaneous-impulse observed mean is not the time-independent-predictor observed mean" + ); + assert_eq!( + PsychometricError::ImpulseCarryObservedMeanIsNotTimeIndependentObservedMean.to_string(), + "impulse-carry observed mean is not the time-independent-predictor observed mean" + ); } } diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index 61857ce4..572102ec 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -1696,6 +1696,121 @@ pub fn refuse_time_independent_coefficient_as_discrete_effect( Err(PsychometricError::TimeIndependentCoefficientIsNotDiscreteEffect) } +/// Exact scalar observed mean of a time-independent predictor. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Eq. 3, p. 5; Table 2, +/// p. 12; JSS PDF re-opened 2026-08-20T12:12Z from +/// ) +/// write `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and +/// `Γ ~ N(τ, Ψ)`. Equation 3 (p. 5) writes the time-independent +/// predictor as the printed addend `A^{-1}[e^{A(t−t0)} − I] B z_i` +/// after the `T0MEANS` carry and the `CINT` increment. Table 2 names +/// `B` `TIPREDEFFECT`. The expected intercept is `τ`. The latent +/// process at `t` after that increment is +/// `μ_t + A^{-1}[e^{A Δt} − I] B z`. The scalar composition is +/// `E(y_t) = τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. Form the +/// evolved-plus-increment latent mean first, then `τ + λ` of that +/// mean. A zero loading is exactly `τ`. A zero evolved-plus-increment +/// latent mean is exactly `τ`. A zero intercept is exactly +/// `λ(μ_t + increment)`. The evolved observed mean `τ + λ μ_t` is +/// not this composition when the increment is nonzero. The +/// contemporaneous map `τ + λ(μ_t + m x)` is not this composition. +/// The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not this +/// composition when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The +/// evolved-plus-increment latent mean is not `E(y_t)`. `TIPREDEFFECT` +/// is `B`, not that observed mean. This is not a Kalman filter and +/// not ctsem estimation. +/// +/// # Errors +/// +/// Propagates +/// [`recover_discrete_latent_mean_with_time_independent_predictor`] +/// and [`recover_manifest_observed_mean`]. +#[allow(clippy::too_many_arguments)] +pub fn recover_discrete_observed_mean_with_time_independent_predictor( + loading: f64, + initial_latent_mean: f64, + log_rate: f64, + continuous_intercept: f64, + time_independent_effect: f64, + time_independent_predictor: f64, + manifest_mean: f64, + event_delta: f64, + clock: LagClock, +) -> Result { + let composed_latent_mean = recover_discrete_latent_mean_with_time_independent_predictor( + initial_latent_mean, + log_rate, + continuous_intercept, + time_independent_effect, + time_independent_predictor, + event_delta, + clock, + )?; + recover_manifest_observed_mean(loading, composed_latent_mean, manifest_mean) +} + +/// Refuse treating the evolved observed mean as the time-independent- +/// predictor observed mean. +/// +/// Equation 5 of the Eq. 3 evolved mean is `τ + λ μ_t`. Equation 5 +/// of the Eq. 3 time-independent predictor is +/// `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. Those are not the same +/// map. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::EvolvedObservedMeanIsNotTimeIndependentObservedMean`]. +pub fn refuse_evolved_observed_mean_as_time_independent_observed_mean( + evolved_observed_mean: f64, + time_independent_observed_mean: f64, +) -> Result { + let _ = (evolved_observed_mean, time_independent_observed_mean); + Err(PsychometricError::EvolvedObservedMeanIsNotTimeIndependentObservedMean) +} + +/// Refuse treating the contemporaneous-impulse observed mean as the +/// time-independent-predictor observed mean. +/// +/// Equation 5 of the contemporaneous Dirac is `τ + λ(μ_t + m x)`. +/// Equation 5 of the Eq. 3 time-independent predictor is +/// `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. Those are not the same +/// map. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::ImpulseObservedMeanIsNotTimeIndependentObservedMean`]. +pub fn refuse_impulse_observed_mean_as_time_independent_observed_mean( + impulse_observed_mean: f64, + time_independent_observed_mean: f64, +) -> Result { + let _ = (impulse_observed_mean, time_independent_observed_mean); + Err(PsychometricError::ImpulseObservedMeanIsNotTimeIndependentObservedMean) +} + +/// Refuse treating the impulse-carry observed mean as the +/// time-independent-predictor observed mean. +/// +/// Equation 5 of the Eq. 1–2 carried latent mean is +/// `τ + λ(μ_t + e^{a(t−u)} m x)`. Equation 5 of the Eq. 3 +/// time-independent predictor is +/// `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. Those are not the same +/// map. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::ImpulseCarryObservedMeanIsNotTimeIndependentObservedMean`]. +pub fn refuse_impulse_carry_observed_mean_as_time_independent_observed_mean( + impulse_carry_observed_mean: f64, + time_independent_observed_mean: f64, +) -> Result { + let _ = (impulse_carry_observed_mean, time_independent_observed_mean); + Err(PsychometricError::ImpulseCarryObservedMeanIsNotTimeIndependentObservedMean) +} + /// Exact scalar within-interval time-dependent impulse carry from /// Driver Equations 1–2. /// @@ -2268,8 +2383,9 @@ mod tests { recover_discrete_latent_mean_with_time_independent_predictor, recover_discrete_latent_variance, recover_discrete_observed_mean, recover_discrete_observed_mean_with_impulse, - recover_discrete_observed_mean_with_impulse_carry, recover_discrete_process_noise, - recover_discrete_time_independent_predictor_effect, + recover_discrete_observed_mean_with_impulse_carry, + recover_discrete_observed_mean_with_time_independent_predictor, + recover_discrete_process_noise, recover_discrete_time_independent_predictor_effect, recover_discrete_time_varying_predictor_effect, recover_event_series_mean_log_rate, recover_event_time_discrete_lag_and_log_rate, recover_irregular_centered_residual_log_rate, recover_local_log_rate, recover_manifest_lagged_observed_covariance, @@ -2283,8 +2399,11 @@ mod tests { refuse_continuous_intercept_as_manifest_means, refuse_difference_quotient_as_local_rate, refuse_evolved_observed_mean_as_impulse_carry_observed_mean, refuse_evolved_observed_mean_as_impulse_observed_mean, + refuse_evolved_observed_mean_as_time_independent_observed_mean, refuse_finite_interval_process_noise_as_stationary_variance, + refuse_impulse_carry_observed_mean_as_time_independent_observed_mean, refuse_impulse_observed_mean_as_impulse_carry_observed_mean, + refuse_impulse_observed_mean_as_time_independent_observed_mean, refuse_initial_latent_mean_as_evolved_mean, refuse_initial_observed_mean_as_evolved_observed_mean, refuse_latent_lagged_covariance_as_observed_covariance, @@ -4890,6 +5009,307 @@ mod tests { ), Err(PsychometricError::InvalidNumericInput) ); + // Latent mean is finite; Bz overflows. That `?` is not the sum overflow. + assert_eq!( + recover_discrete_latent_mean_with_time_independent_predictor( + 1.0, + -0.5, + 0.3, + 1e308, + 2.0, + 2.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + } + + #[test] + fn discrete_observed_mean_with_time_independent_predictor_recovers_driver_equation_five() { + let loading = 2.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let effect = 0.4_f64; + let predictor = 3.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let recovered = recover_discrete_observed_mean_with_time_independent_predictor( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-tipred-mean"); + let composed = recover_discrete_latent_mean_with_time_independent_predictor( + initial, + drift, + intercept, + effect, + predictor, + delta, + LagClock::EventTime, + ) + .expect("eq3-tipred"); + let expected = manifest_mean + loading * composed; + assert!((recovered - expected).abs() < 1e-15); + let evolved_observed = recover_discrete_observed_mean( + loading, + initial, + drift, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq3-eq5-mean"); + let impulse_observed = recover_discrete_observed_mean_with_impulse( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-impulse-mean"); + let carried_observed = recover_discrete_observed_mean_with_impulse_carry( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + 1.0, + LagClock::EventTime, + ) + .expect("eq5-carry-mean"); + assert!((evolved_observed - recovered).abs() > 1e-3); + assert!((impulse_observed - recovered).abs() > 1e-3); + assert!((carried_observed - recovered).abs() > 1e-3); + assert_eq!( + recover_discrete_observed_mean_with_time_independent_predictor( + 0.0, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime + ), + Ok(manifest_mean) + ); + } + + #[test] + fn discrete_observed_mean_with_time_independent_predictor_is_not_evolved_or_zero_increment() { + let loading = 2.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let effect = 0.4_f64; + let predictor = 3.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let recovered = recover_discrete_observed_mean_with_time_independent_predictor( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-tipred-mean"); + let evolved_observed = recover_discrete_observed_mean( + loading, + initial, + drift, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq3-eq5-mean"); + let zero_increment = recover_discrete_observed_mean_with_time_independent_predictor( + loading, + initial, + drift, + intercept, + 0.0, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("zero-increment"); + assert!((zero_increment - evolved_observed).abs() < 1e-15); + assert!((recovered - evolved_observed).abs() > 1e-3); + } + + #[test] + fn discrete_observed_mean_with_time_independent_predictor_refuses_evolved_mean_and_overflow() { + let loading = 2.0_f64; + let recovered = recover_discrete_observed_mean_with_time_independent_predictor( + loading, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, + LagClock::EventTime, + ) + .expect("eq5-tipred-mean"); + let evolved_observed = + recover_discrete_observed_mean(loading, 1.0, -0.5, 0.3, 0.5, 2.0, LagClock::EventTime) + .expect("eq3-eq5-mean"); + let impulse_observed = recover_discrete_observed_mean_with_impulse( + loading, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, + LagClock::EventTime, + ) + .expect("eq5-impulse-mean"); + let carried_observed = recover_discrete_observed_mean_with_impulse_carry( + loading, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, + 1.0, + LagClock::EventTime, + ) + .expect("eq5-carry-mean"); + assert_eq!( + refuse_evolved_observed_mean_as_time_independent_observed_mean( + evolved_observed, + recovered + ), + Err(PsychometricError::EvolvedObservedMeanIsNotTimeIndependentObservedMean) + ); + assert_eq!( + refuse_impulse_observed_mean_as_time_independent_observed_mean( + impulse_observed, + recovered + ), + Err(PsychometricError::ImpulseObservedMeanIsNotTimeIndependentObservedMean) + ); + assert_eq!( + refuse_impulse_carry_observed_mean_as_time_independent_observed_mean( + carried_observed, + recovered + ), + Err(PsychometricError::ImpulseCarryObservedMeanIsNotTimeIndependentObservedMean) + ); + } + + #[test] + fn discrete_observed_mean_with_time_independent_predictor_invalid_inputs_fail_closed() { + let scaled = recover_discrete_observed_mean_with_time_independent_predictor( + 1e308, + 1e-308, + 0.0, + 0.0, + 0.0, + 3.0, + 0.0, + 1.0, + LagClock::EventTime, + ) + .expect("scale"); + assert!((scaled - 1.0).abs() < 1e-15); + let finite_loaded = recover_discrete_observed_mean_with_time_independent_predictor( + 1e308, + 0.0, + 0.0, + 0.0, + 0.0, + 3.0, + 0.0, + 1.0, + LagClock::EventTime, + ) + .expect("lambda-mu0"); + assert!((finite_loaded - 0.0).abs() < 1e-15); + assert_eq!( + recover_discrete_observed_mean_with_time_independent_predictor( + 1e308, + 2.0, + 0.0, + 0.0, + 0.0, + 3.0, + 0.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_observed_mean_with_time_independent_predictor( + 2.0, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_discrete_observed_mean_with_time_independent_predictor( + 2.0, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_observed_mean_with_time_independent_predictor( + 1e308, + 0.0, + 0.0, + 0.0, + 1e308, + 1.0, + 0.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); } #[test] @@ -5494,6 +5914,19 @@ mod tests { ), Err(PsychometricError::InvalidNumericInput) ); + // Finite log-rate whose product with elapsed overflows. exp(±∞) + // is not finite, then the non-finite drift interval fails closed. + assert_eq!( + recover_time_dependent_predictor_impulse_carry( + 0.4, + 3.0, + 1e308, + 3.0, + 2.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); assert_eq!( recover_time_dependent_predictor_impulse_carry( 0.0, diff --git a/crates/psychometric_core/src/lib.rs b/crates/psychometric_core/src/lib.rs index 7cd09d8a..fc841c7e 100644 --- a/crates/psychometric_core/src/lib.rs +++ b/crates/psychometric_core/src/lib.rs @@ -47,7 +47,12 @@ //! observed mean), recovers the Driver Eq. 3 second-summand //! time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` //! (Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle -//! Eq. 14; `B` is not that discrete increment), +//! Eq. 14; `B` is not that discrete increment), recovers the Driver +//! Eq. 5 of that increment as +//! `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (`τ + λ μ_t` is not that +//! observed mean; `τ + λ(μ_t + m x)` is not that observed mean; +//! `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when +//! `u ≠ t`), //! and refuses //! latent-mean comparison below strong invariance. @@ -122,6 +127,8 @@ pub use event_time::recover_discrete_observed_mean; pub use event_time::recover_discrete_observed_mean_with_impulse; /// Exact scalar discrete observed mean of a within-interval impulse carry. pub use event_time::recover_discrete_observed_mean_with_impulse_carry; +/// Exact scalar discrete observed mean of a time-independent predictor. +pub use event_time::recover_discrete_observed_mean_with_time_independent_predictor; /// Exact scalar discrete process noise `Q_Δt` on event time. pub use event_time::recover_discrete_process_noise; /// Exact scalar discrete `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z`. @@ -168,10 +175,16 @@ pub use event_time::refuse_difference_quotient_as_local_rate; pub use event_time::refuse_evolved_observed_mean_as_impulse_carry_observed_mean; /// Refuse treating evolved `τ + λ μ_t` as the contemporaneous-impulse observed mean. pub use event_time::refuse_evolved_observed_mean_as_impulse_observed_mean; +/// Refuse treating evolved `τ + λ μ_t` as the time-independent-predictor observed mean. +pub use event_time::refuse_evolved_observed_mean_as_time_independent_observed_mean; /// Refuse treating finite-interval `Q_Δt` as `asymDIFFUSION`. pub use event_time::refuse_finite_interval_process_noise_as_stationary_variance; +/// Refuse treating impulse-carry `τ + λ(μ_t + e^{a(t−u)} m x)` as the time-independent-predictor observed mean. +pub use event_time::refuse_impulse_carry_observed_mean_as_time_independent_observed_mean; /// Refuse treating contemporaneous `τ + λ(μ_t + m x)` as the impulse-carry observed mean. pub use event_time::refuse_impulse_observed_mean_as_impulse_carry_observed_mean; +/// Refuse treating contemporaneous `τ + λ(μ_t + m x)` as the time-independent-predictor observed mean. +pub use event_time::refuse_impulse_observed_mean_as_time_independent_observed_mean; /// Refuse treating Driver Table 2 `T0MEANS` as the evolved latent mean. pub use event_time::refuse_initial_latent_mean_as_evolved_mean; /// Refuse treating first-occasion `τ + λ μ_0` as `E(y_t)`. 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 bde9e539..485ce413 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -11,7 +11,8 @@ use psychometric_core::{ recover_discrete_latent_mean_with_impulse_carry, recover_discrete_latent_mean_with_time_independent_predictor, recover_discrete_latent_variance, recover_discrete_observed_mean, recover_discrete_observed_mean_with_impulse, - recover_discrete_observed_mean_with_impulse_carry, recover_discrete_process_noise, + recover_discrete_observed_mean_with_impulse_carry, + recover_discrete_observed_mean_with_time_independent_predictor, recover_discrete_process_noise, recover_discrete_time_independent_predictor_effect, recover_discrete_time_varying_predictor_effect, recover_event_series_mean_log_rate, recover_event_time_discrete_lag_and_log_rate, recover_irregular_centered_residual_log_rate, @@ -26,8 +27,11 @@ use psychometric_core::{ refuse_continuous_intercept_as_manifest_means, refuse_difference_quotient_as_local_rate, refuse_evolved_observed_mean_as_impulse_carry_observed_mean, refuse_evolved_observed_mean_as_impulse_observed_mean, + refuse_evolved_observed_mean_as_time_independent_observed_mean, refuse_finite_interval_process_noise_as_stationary_variance, + refuse_impulse_carry_observed_mean_as_time_independent_observed_mean, refuse_impulse_observed_mean_as_impulse_carry_observed_mean, + refuse_impulse_observed_mean_as_time_independent_observed_mean, refuse_initial_latent_mean_as_evolved_mean, refuse_initial_observed_mean_as_evolved_observed_mean, refuse_latent_lagged_covariance_as_observed_covariance, refuse_latent_mean_as_observed_mean, @@ -1665,6 +1669,329 @@ fn time_independent_predictor_refuses_overflow_and_non_event_clocks() { ), Err(PsychometricError::InvalidNumericInput) ); + assert_eq!( + recover_discrete_latent_mean_with_time_independent_predictor( + 1.0, + -0.5, + 0.3, + 1e308, + 2.0, + 2.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); +} + +#[test] +fn discrete_observed_mean_with_time_independent_predictor_recovers_driver_equation_five() { + let loading = 2.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let effect = 0.4_f64; + let predictor = 3.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let observed = recover_discrete_observed_mean_with_time_independent_predictor( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-tipred-mean"); + let composed = recover_discrete_latent_mean_with_time_independent_predictor( + initial, + drift, + intercept, + effect, + predictor, + delta, + LagClock::EventTime, + ) + .expect("eq3-tipred"); + let expected = manifest_mean + loading * composed; + let error = rmse(&[expected], &[observed]); + assert!( + error < 1e-15, + "Driver Eq. 5 of Eq. 3 TIPREDEFFECT RMSE {error}" + ); + let evolved_observed = recover_discrete_observed_mean( + loading, + initial, + drift, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq3-eq5-mean"); + let evolved_error = rmse(&[expected], &[evolved_observed]); + assert!( + evolved_error > error, + "τ + λ μ_t is not TIPREDEFFECT E(y_t): RMSE {evolved_error} must exceed {error}" + ); + let intercept_error = rmse(&[expected], &[manifest_mean]); + assert!( + intercept_error > error, + "MANIFESTMEANS is not TIPREDEFFECT E(y_t): RMSE {intercept_error} must exceed {error}" + ); + let latent_error = rmse(&[expected], &[composed]); + assert!( + latent_error > error, + "evolved-plus-increment latent mean is not E(y_t): RMSE {latent_error} must exceed {error}" + ); + let impulse_observed = recover_discrete_observed_mean_with_impulse( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-impulse-mean"); + let impulse_error = rmse(&[expected], &[impulse_observed]); + assert!( + impulse_error > error, + "τ + λ(μ_t + m x) is not TIPREDEFFECT E(y_t): RMSE {impulse_error} must exceed {error}" + ); + let carried_observed = recover_discrete_observed_mean_with_impulse_carry( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + 1.0, + LagClock::EventTime, + ) + .expect("eq5-carry-mean"); + let carry_error = rmse(&[expected], &[carried_observed]); + assert!( + carry_error > error, + "τ + λ(μ_t + carry) is not TIPREDEFFECT E(y_t): RMSE {carry_error} must exceed {error}" + ); +} + +#[test] +fn discrete_observed_mean_with_time_independent_predictor_refuses_evolved_mean_impulse_and_carry() { + let loading = 2.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let effect = 0.4_f64; + let predictor = 3.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let observed = recover_discrete_observed_mean_with_time_independent_predictor( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-tipred-mean"); + let evolved_observed = recover_discrete_observed_mean( + loading, + initial, + drift, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq3-eq5-mean"); + let impulse_observed = recover_discrete_observed_mean_with_impulse( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-impulse-mean"); + let carried_observed = recover_discrete_observed_mean_with_impulse_carry( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + 1.0, + LagClock::EventTime, + ) + .expect("eq5-carry-mean"); + assert_eq!( + refuse_evolved_observed_mean_as_time_independent_observed_mean(evolved_observed, observed), + Err(PsychometricError::EvolvedObservedMeanIsNotTimeIndependentObservedMean) + ); + assert_eq!( + refuse_impulse_observed_mean_as_time_independent_observed_mean(impulse_observed, observed), + Err(PsychometricError::ImpulseObservedMeanIsNotTimeIndependentObservedMean) + ); + assert_eq!( + refuse_impulse_carry_observed_mean_as_time_independent_observed_mean( + carried_observed, + observed + ), + Err(PsychometricError::ImpulseCarryObservedMeanIsNotTimeIndependentObservedMean) + ); +} + +#[test] +fn discrete_observed_mean_with_time_independent_predictor_zero_loading_is_manifest_mean() { + let loading = 2.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let effect = 0.4_f64; + let predictor = 3.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let observed = recover_discrete_observed_mean_with_time_independent_predictor( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-tipred-mean"); + let composed = recover_discrete_latent_mean_with_time_independent_predictor( + initial, + drift, + intercept, + effect, + predictor, + delta, + LagClock::EventTime, + ) + .expect("eq3-tipred"); + let expected = manifest_mean + loading * composed; + let error = rmse(&[expected], &[observed]); + let zero_loading = recover_discrete_observed_mean_with_time_independent_predictor( + 0.0, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("lambda0"); + let dropped_error = rmse(&[expected], &[zero_loading]); + assert!( + dropped_error > error, + "zero loading is τ, not τ + λ(μ_t + increment): RMSE {dropped_error} must exceed {error}" + ); + assert_eq!( + refuse_latent_mean_as_observed_mean(composed, observed), + Err(PsychometricError::LatentMeanIsNotObservedMean) + ); + assert_eq!( + refuse_manifest_means_as_observed_mean(manifest_mean, observed), + Err(PsychometricError::ManifestMeansIsNotObservedMean) + ); +} + +#[test] +fn discrete_observed_mean_with_time_independent_predictor_refuses_overflow_and_non_event_clocks() { + assert_eq!( + recover_discrete_observed_mean_with_time_independent_predictor( + 1e308, + 2.0, + 0.0, + 0.0, + 0.0, + 3.0, + 0.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_observed_mean_with_time_independent_predictor( + 2.0, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_discrete_observed_mean_with_time_independent_predictor( + 2.0, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_observed_mean_with_time_independent_predictor( + 1e308, + 0.0, + 0.0, + 0.0, + 1e308, + 1.0, + 0.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + let scaled = recover_discrete_observed_mean_with_time_independent_predictor( + 1e308, + 1e-308, + 0.0, + 0.0, + 0.0, + 3.0, + 0.0, + 1.0, + LagClock::EventTime, + ) + .expect("scale"); + assert!( + (scaled - 1.0).abs() < 1e-15, + "Driver Eq. 5 of Eq. 3 TIPREDEFFECT must keep λ=1e308, μ=1e-308: got {scaled}" + ); } #[test] @@ -1800,6 +2127,17 @@ fn time_dependent_impulse_carry_refuses_overflow_and_non_event_clocks() { ), Err(PsychometricError::InvalidNumericInput) ); + assert_eq!( + recover_time_dependent_predictor_impulse_carry( + 0.4, + 3.0, + 1e308, + 3.0, + 2.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); } #[test] diff --git a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs index 6950a977..5c26b6c6 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -9,7 +9,8 @@ use psychometric_core::{ recover_discrete_latent_mean_with_impulse_carry, recover_discrete_latent_mean_with_time_independent_predictor, recover_discrete_latent_variance, recover_discrete_observed_mean, recover_discrete_observed_mean_with_impulse, - recover_discrete_observed_mean_with_impulse_carry, recover_discrete_process_noise, + recover_discrete_observed_mean_with_impulse_carry, + recover_discrete_observed_mean_with_time_independent_predictor, recover_discrete_process_noise, recover_discrete_time_independent_predictor_effect, recover_discrete_time_varying_predictor_effect, recover_irregular_centered_residual_log_rate, recover_loading_point_estimate_mean, recover_manifest_lagged_observed_covariance, @@ -22,8 +23,11 @@ use psychometric_core::{ refuse_continuous_intercept_as_manifest_means, refuse_evolved_observed_mean_as_impulse_carry_observed_mean, refuse_evolved_observed_mean_as_impulse_observed_mean, + refuse_evolved_observed_mean_as_time_independent_observed_mean, refuse_finite_interval_process_noise_as_stationary_variance, + refuse_impulse_carry_observed_mean_as_time_independent_observed_mean, refuse_impulse_observed_mean_as_impulse_carry_observed_mean, + refuse_impulse_observed_mean_as_time_independent_observed_mean, refuse_initial_latent_mean_as_evolved_mean, refuse_initial_observed_mean_as_evolved_observed_mean, refuse_latent_lagged_covariance_as_observed_covariance, refuse_latent_mean_as_observed_mean, @@ -949,3 +953,151 @@ fn evolved_observed_mean_is_not_impulse_carry_observed_mean() { Err(psychometric_core::PsychometricError::ManifestMeansIsNotObservedMean) ); } + +#[test] +fn evolved_observed_mean_is_not_time_independent_observed_mean() { + let loading = 2.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let effect = 0.4_f64; + let predictor = 3.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let time_independent_observed = recover_discrete_observed_mean_with_time_independent_predictor( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-tipred-mean"); + let evolved_observed = recover_discrete_observed_mean( + loading, + initial, + drift, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq3-eq5-mean"); + let composed = recover_discrete_latent_mean_with_time_independent_predictor( + initial, + drift, + intercept, + effect, + predictor, + delta, + LagClock::EventTime, + ) + .expect("eq3-tipred"); + assert!( + (evolved_observed - time_independent_observed).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 of Eq. 3 TIPREDEFFECT): τ + λ μ_t is not TIPREDEFFECT E(y_t)" + ); + assert!( + (manifest_mean - time_independent_observed).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 / Table 2 p. 12): MANIFESTMEANS is not TIPREDEFFECT E(y_t)" + ); + assert!( + (composed - time_independent_observed).abs() > 1e-3, + "Driver et al. (2017, Eq. 5): evolved-plus-increment latent mean is not E(y_t)" + ); + assert_eq!( + refuse_evolved_observed_mean_as_time_independent_observed_mean( + evolved_observed, + time_independent_observed + ), + Err( + psychometric_core::PsychometricError::EvolvedObservedMeanIsNotTimeIndependentObservedMean + ) + ); + assert_eq!( + refuse_latent_mean_as_observed_mean(composed, time_independent_observed), + Err(psychometric_core::PsychometricError::LatentMeanIsNotObservedMean) + ); + assert_eq!( + refuse_manifest_means_as_observed_mean(manifest_mean, time_independent_observed), + Err(psychometric_core::PsychometricError::ManifestMeansIsNotObservedMean) + ); +} + +#[test] +fn impulse_and_carry_observed_mean_are_not_time_independent_observed_mean() { + let loading = 2.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let effect = 0.4_f64; + let predictor = 3.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let time_independent_observed = recover_discrete_observed_mean_with_time_independent_predictor( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-tipred-mean"); + let impulse_observed = recover_discrete_observed_mean_with_impulse( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-impulse-mean"); + let carried_observed = recover_discrete_observed_mean_with_impulse_carry( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + 1.0, + LagClock::EventTime, + ) + .expect("eq5-carry-mean"); + assert!( + (impulse_observed - time_independent_observed).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 of Eq. 3 impulse): τ + λ(μ_t + m x) is not TIPREDEFFECT E(y_t)" + ); + assert!( + (carried_observed - time_independent_observed).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 of Eq. 1–2): τ + λ(μ_t + carry) is not TIPREDEFFECT E(y_t)" + ); + assert_eq!( + refuse_impulse_observed_mean_as_time_independent_observed_mean( + impulse_observed, + time_independent_observed + ), + Err( + psychometric_core::PsychometricError::ImpulseObservedMeanIsNotTimeIndependentObservedMean + ) + ); + assert_eq!( + refuse_impulse_carry_observed_mean_as_time_independent_observed_mean( + carried_observed, + time_independent_observed + ), + Err( + psychometric_core::PsychometricError::ImpulseCarryObservedMeanIsNotTimeIndependentObservedMean + ) + ); +} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index cf5060e7..48aac916 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -28,7 +28,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | report template/section/copied/style/modality method effects | ADR 0004/0012; PRD/TRD | simulation truth factors implemented; estimator-side method model remains future | partial | | candidate K statistical/Pareto gates + blinded LLM review | ADR 0012; research | future `model_selection` | accepted-target | | compositional topic correlation / stable clustering | ADR 0005/0012; research | future `network_analysis` | accepted-target | -| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z); full ESEM/DSEM remaining | partial | +| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z); full ESEM/DSEM remaining | partial | | CPU bounded multithreading + GPU/VRAM streaming/parity | ADR 0001/0006 | future `compute_backend` | accepted-target | | TDT detection/tracking vs CHRONOS schema/prediction/temporal consistency | ADR 0016; PRD/research | future `event_intelligence` | accepted-target | | evidence-bounded LLM interpretation | ADR 0010/0012; PRD | future `interpretation_gateway` | accepted-target | diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index 8fdb1d7a..1dc779c0 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -1,7 +1,7 @@ # ADR 0005 — Posterior-aware ESEM/DSEM and structural interpretation **Decision status:** Accepted -**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), exact scalar contemporaneous `TDPREDEFFECT` impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; the §7.2 level-change form is not that impulse), exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of that Eq. 3 composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar time-independent `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle Eq. 14; `B` is not that discrete increment), exact scalar within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of that carried latent mean; `τ + λ μ_t` is not that observed mean), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target +**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), exact scalar contemporaneous `TDPREDEFFECT` impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; the §7.2 level-change form is not that impulse), exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of that Eq. 3 composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar time-independent `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle Eq. 14; `B` is not that discrete increment), exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of that Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of that carried latent mean; `τ + λ μ_t` is not that observed mean), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target **Date:** 2026-08-05 **Supersedes:** None. ADR 0012 governs upstream topic measurement/network coordinates; this ADR governs higher-order psychometric structure and longitudinal interpretation. @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), recovers the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z; `TDPREDEFFECT` is `M`, not `CINT`; `M x` is not `A^{-1}[e^{A Δt} − I] B z`; `M x` is not Voelkle et al., 2012, Eq. 14; the level-change form is not that impulse), recovers the exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition; JSS PDF re-opened 2026-08-20T09:01Z; the evolved map `τ + λ μ_t` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-impulse latent mean is not `E(y_t)`), recovers the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z; `TIPREDEFFECT` is `B`, not `κ`; form `B z` first, then the discrete intercept map; a zero drift is `B z Δt`; `B` is not the discrete increment; `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle Eq. 14), recovers the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z; form `m x` first, then `e^{a(t−u)} m x`; a zero drift is `m x`; underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept; `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; an impulse at `u = t` is the contemporaneous map; an impulse at `u ≤ t0` is already in `η(t0)`), recovers the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; JSS PDF re-opened 2026-08-20T05:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the carried latent mean is not `E(y_t)`), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), recovers the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z; `TDPREDEFFECT` is `M`, not `CINT`; `M x` is not `A^{-1}[e^{A Δt} − I] B z`; `M x` is not Voelkle et al., 2012, Eq. 14; the level-change form is not that impulse), recovers the exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition; JSS PDF re-opened 2026-08-20T09:01Z; the evolved map `τ + λ μ_t` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-impulse latent mean is not `E(y_t)`), recovers the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z; `TIPREDEFFECT` is `B`, not `κ`; form `B z` first, then the discrete intercept map; a zero drift is `B z Δt`; `B` is not the discrete increment; `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle Eq. 14), recovers the exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; JSS PDF re-opened 2026-08-20T12:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-increment latent mean is not `E(y_t)`), recovers the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z; form `m x` first, then `e^{a(t−u)} m x`; a zero drift is `m x`; underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept; `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; an impulse at `u = t` is the contemporaneous map; an impulse at `u ≤ t0` is already in `η(t0)`), recovers the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; JSS PDF re-opened 2026-08-20T05:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the carried latent mean is not `E(y_t)`), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. 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/adr/README.md b/docs/adr/README.md index 069cb5b3..fac7323b 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -10,7 +10,7 @@ Read [`ADR_POLICY.md`](ADR_POLICY.md) first. **Decision status and implementatio | [0002](0002-six-clock-temporal-semantics.md) | Six-clock temporal semantics and fail-closed historical leakage prevention | Accepted | active-PR | Unmerged PR #8 is the canonical Task 3 replacement implementing typed clocks/intervals against the current protected-main lineage; conflicted PR #5 is superseded lineage. Later graph/split enforcement remains target work. | | [0003](0003-relational-event-multiple-membership.md) | Relational event ontology and time-varying cross-classified multiple membership | Accepted | partial | Weighted time-varying membership network/roles are active-PR (PR #12); full multilevel estimators, graph ontology, and persistence remain accepted-target. ADR 0016 owns event-intelligence tasks. | | [0004](0004-shared-multilingual-latent-space.md) | One shared multilingual latent space with explicit invariance status | Accepted | accepted-target | ADR 0012 owns the full topic-estimator/backend/global-topic contract. | -| [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | partial | Input gates, posterior-draw point estimates, Rubin `T` on OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects, exact scalar discrete process noise, lagged latent covariance and unconditional latent variance, stationary within-subject variance (`asymDIFFUSION`), trait-plus-state variance (`TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; Eq. 5 of the contemporaneous impulse is `τ + λ(μ_t + m x)`; `τ + λ μ_t` is not that observed mean; Eq. 5 of the within-interval impulse carry is `τ + λ(μ_t + e^{a(t−u)} m x)`; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`), irregular already-centered residual lag, and strong-gated latent means are on the stacked psychometric PR; full ESEM/DSEM estimator remains accepted-target. | +| [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | partial | Input gates, posterior-draw point estimates, Rubin `T` on OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects, exact scalar discrete process noise, lagged latent covariance and unconditional latent variance, stationary within-subject variance (`asymDIFFUSION`), trait-plus-state variance (`TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; Eq. 5 of the contemporaneous impulse is `τ + λ(μ_t + m x)`; `τ + λ μ_t` is not that observed mean; Eq. 5 of the time-independent predictor is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; Eq. 5 of the within-interval impulse carry is `τ + λ(μ_t + e^{a(t−u)} m x)`; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`), irregular already-centered residual lag, and strong-gated latent means are on the stacked psychometric PR; full ESEM/DSEM estimator remains accepted-target. | | [0006](0006-vram-gpu-nvidia-orchestration.md) | VRAM-adaptive GPU compute and model-credential boundary | Accepted | accepted-target | LLM orchestration policy superseded by ADR 0010; autonomous development authority governed by ADR 0015. | | [0007](0007-rust-workspace-quality-gates.md) | Explicit Rust workspace, pinned toolchains, and exact quality gates | Accepted | implemented-main | ADR 0014 governs scientific/product claim promotion beyond repository-quality tooling. | | [0008](0008-immutable-evidence-identities-digests-and-spans.md) | Immutable evidence identities, `SHA-256` digests, exact spans, and strict wire reconstruction | Accepted | implemented-main | ADR 0013 governs future persistence/reproducibility/split authority. | diff --git a/docs/research/multilevel-event-time-recovery.md b/docs/research/multilevel-event-time-recovery.md index 370ea55a..f1c08720 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -25,17 +25,18 @@ This slice stays inside `psychometric_core`. It does not add a second invariance 19. recover the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`) and refuse treating that impulse as `CINT`, `TIPREDEFFECT`, or Voelkle et al. (2012, Eq. 14); 20. recover the exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition; the evolved map `τ + λ μ_t` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`) and refuse treating `τ + λ μ_t` as that observed mean; 21. recover the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`) and refuse treating that increment as `CINT`, `TDPREDEFFECT`, Voelkle et al. (2012, Eq. 14), or the coefficient `B`; -22. recover the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; the printed Eq. 3 fourth summand is the contemporaneous Dirac) and refuse treating that carry as the contemporaneous impulse, `CINT`, `TIPREDEFFECT`, or Voelkle et al. (2012, Eq. 14); -23. recover the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`) and refuse treating `τ + λ μ_t` or `τ + λ(μ_t + m x)` as that observed mean; -24. refuse pooling discrete lags from unequal event intervals as one coefficient; -25. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); -26. refuse the difference quotient as a continuous-time rate; -27. apply the same event-time map to CWC residuals (still not DSEM); -28. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). +22. recover the exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`) and refuse treating `τ + λ μ_t`, `τ + λ(μ_t + m x)`, or `τ + λ(μ_t + e^{a(t−u)} m x)` as that observed mean; +23. recover the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; the printed Eq. 3 fourth summand is the contemporaneous Dirac) and refuse treating that carry as the contemporaneous impulse, `CINT`, `TIPREDEFFECT`, or Voelkle et al. (2012, Eq. 14); +24. recover the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`) and refuse treating `τ + λ μ_t` or `τ + λ(μ_t + m x)` as that observed mean; +25. refuse pooling discrete lags from unequal event intervals as one coefficient; +26. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); +27. refuse the difference quotient as a continuous-time rate; +28. apply the same event-time map to CWC residuals (still not DSEM); +29. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). ## Claim boundary -This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. The contemporaneous `TDPREDEFFECT` impulse is not the continuous intercept, not the time-independent discrete effect, and not Voelkle et al. (2012, Eq. 14). The time-independent `TIPREDEFFECT` increment is not the continuous intercept, not the contemporaneous impulse, not Voelkle et al. (2012, Eq. 14), and not the coefficient `B`. The within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). The evolved observed mean `τ + λ μ_t` is not the contemporaneous-impulse observed mean `τ + λ(μ_t + m x)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not the impulse-carry observed mean `τ + λ(μ_t + e^{a(t−u)} m x)` when `u ≠ t`. The evolved observed mean `τ + λ μ_t` is not the impulse-carry observed mean. The carried latent mean is not `E(y_t)`. The evolved-plus-impulse latent mean is not `E(y_t)`. +This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. The contemporaneous `TDPREDEFFECT` impulse is not the continuous intercept, not the time-independent discrete effect, and not Voelkle et al. (2012, Eq. 14). The time-independent `TIPREDEFFECT` increment is not the continuous intercept, not the contemporaneous impulse, not Voelkle et al. (2012, Eq. 14), and not the coefficient `B`. The within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). The evolved observed mean `τ + λ μ_t` is not the contemporaneous-impulse observed mean `τ + λ(μ_t + m x)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not the impulse-carry observed mean `τ + λ(μ_t + e^{a(t−u)} m x)` when `u ≠ t`. The evolved observed mean `τ + λ μ_t` is not the impulse-carry observed mean. The carried latent mean is not `E(y_t)`. The evolved-plus-impulse latent mean is not `E(y_t)`. The evolved observed mean `τ + λ μ_t` is not the time-independent-predictor observed mean `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not that time-independent-predictor observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that time-independent-predictor observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. ## Authoritative sources @@ -77,6 +78,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - **Time-dependent predictor impulse.** Driver et al. (2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z): `χ_i(t)=Σ x_{i,u} δ(t−u)` and the fourth summand is `M Σ x_{i,u} δ(t−u)`. Table 2 names `M` `TDPREDEFFECT`. Section 7.2 calls this a sudden impulse that dissipates back to the process mean. The scalar contemporaneous jump is `m x`. Form `μ_t` first, then add `m x`. A zero effect or zero predictor is exactly zero. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{AΔt}−I] B z` (`TIPREDEFFECT`). `M x` is not Voelkle et al. (2012, Eq. 14) `a_{yx}Δt`. The §7.2 level-change form is an extra latent process with near-zero drift and is not this map. An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **Contemporaneous-impulse observed-indicator mean.** Driver et al. (2017, Eq. 5, p. 5; Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T09:01Z): `y_i(t)=Γ+Λη_i(t)+ζ_i(t)` with `ζ∼N(0,Θ)` and `Γ∼N(τ,Ψ)`. The latent process at `t` after a contemporaneous Dirac (`u=t`) is `μ_t+mx`. The scalar composition is `E(y_t)=τ+λ(μ_t+mx)`. Form the evolved-plus-impulse latent mean first, then `τ+λ` of that mean. A zero loading is exactly `τ`. A zero evolved-plus-impulse latent mean is exactly `τ`. A zero intercept is exactly `λ(μ_t+mx)`. The evolved observed mean `τ+λμ_t` is not this composition. The carry map `τ+λ(μ_t+e^{a(t−u)}mx)` is not this composition when `u≠t`. `MANIFESTMEANS` is not `E(y_t)`. The evolved-plus-impulse latent mean is not `E(y_t)`. The §7.2 level-change form is not this map. An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **Time-independent predictor effect.** Driver et al. (2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z): Equation 1 writes `dη=(Aη+b+A_{ηξ}ξ+Bz)dt+GdW+Mdχ`. Equation 3's second summand is `A^{-1}[e^{AΔt}−I](b+A_{ηξ}ξ+Bz)`. Table 2 names `B` `TIPREDEFFECT`. The scalar map is `(e^{aΔt}−1)/a·Bz` for `a≠0`. Form `Bz` first, then the discrete intercept map. A zero drift is `BzΔt`. Form `μ_t` first, then add that increment. A zero effect or zero predictor is exactly zero. `TIPREDEFFECT` is `B`, not the discrete increment. `A^{-1}[e^{AΔt}−I]Bz` is not `CINT`, not `Mx`, and not Voelkle et al. (2012, Eq. 14) `a_{yx}Δt`. An overflowing product, increment, or sum fails closed. This is not a Kalman filter and not ctsem estimation. +- **Time-independent-predictor observed-indicator mean.** Driver et al. (2017, Eq. 5, p. 5; Eq. 3, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T12:12Z): `y_i(t)=Γ+Λη_i(t)+ζ_i(t)` with `ζ∼N(0,Θ)` and `Γ∼N(τ,Ψ)`. Equation 3 prints the `TIPREDEFFECT` increment as the addend `A^{-1}[e^{A(t−t0)}−I]Bz_i` after the `T0MEANS` carry and the `CINT` increment. The scalar composition is `E(y_t)=τ+λ(μ_t+A^{-1}[e^{AΔt}−I]Bz)`. Form the evolved-plus-increment latent mean first, then `τ+λ` of that mean. A zero loading is exactly `τ`. A zero evolved-plus-increment latent mean is exactly `τ`. A zero intercept is exactly `λ(μ_t+increment)`. The evolved observed mean `τ+λμ_t` is not this composition. The contemporaneous map `τ+λ(μ_t+mx)` is not this composition. The carry map `τ+λ(μ_t+e^{a(t−u)}mx)` is not this composition when `u≠t`. `MANIFESTMEANS` is not `E(y_t)`. The evolved-plus-increment latent mean is not `E(y_t)`. `TIPREDEFFECT` is `B`, not that observed mean. An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **Time-dependent predictor impulse carry.** Driver et al. (2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z): Equation 1 writes `dη=(Aη+ξ+Bz+Mχ(t))dt+GdW`. Equation 2 writes `χ_i(t)=Σ x_{i,u} δ(t−u)`. The Green-function integral of that Dirac on `(t0,t)` is `e^{A(t−u)}Mx`. The printed Eq. 3 fourth summand is the contemporaneous jump `Mx` at `u=t`. This map is the strictly within-interval case `t0 Date: Thu, 20 Aug 2026 15:27:02 +0000 Subject: [PATCH 44/62] feat(psychometric): recover Driver Table 3 T0TIPREDEFFECT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Map the first-occasion time-independent predictor as t0_b z and carry it as e^{A Δt} t0_b z. Table 3 names T0TIPREDEFFECT for latents at T0; TIPREDEFFECT B is the process increment. Fail closed on overflow. --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 1 + CLAUDE.md | 2 +- crates/psychometric_core/src/error.rs | 51 ++ crates/psychometric_core/src/event_time.rs | 518 +++++++++++++++++- crates/psychometric_core/src/lib.rs | 22 +- ...multilevel_event_time_recovery_contract.rs | 128 ++++- .../scientific_claim_boundary_contract.rs | 115 +++- docs/adr/0005-posterior-esem-dsem.md | 4 +- .../multilevel-event-time-recovery.md | 17 +- 10 files changed, 827 insertions(+), 33 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 928203eb..4b491d00 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -61,7 +61,7 @@ boundaries above remain the target modular MSA architecture. | `tepp_simulation` | known-truth temporal/event data generation | | `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics | | `tepp_api` | versioned DTO, schema, and export contracts | -| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | +| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; first-occasion `T0TIPREDEFFECT` shift is `t0_b z` and Eq. 3 first-summand carry is `e^{A Δt} t0_b z` (`T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | No crate exposes placeholder production behavior in Task 1. This prevents an empty façade from becoming a de facto public API before its invariants and tests diff --git a/CHANGELOG.md b/CHANGELOG.md index 0720ce53..0e140ed6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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, Table 3, p. 13; Eq. 3 first summand, p. 5; JSS PDF opened 2026-08-20T15:14Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar first-occasion time-independent predictor shift and its carry. Table 3 names `T0TIPREDEFFECT` the effect of time-independent predictors on latents at `T0`. Table 2 / Table 3 name `TIPREDEFFECT` `B`, which enters Equation 3 as `A^{-1}[e^{A(t−t0)} − I] B z`. Those are not the same matrix. The scalar first-occasion shift is `t0_b z`. Equation 3's first summand carries that shift as `e^{A Δt} t0_b z`. Form `t0_b z` first, then `e^{a Δt} t0_b z`. Form `μ_t` first, then add that carry. A zero drift is `t0_b z` with no dissipation. Binary64 underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `κ`, and not `M x`. `e^{A Δt} t0_b z` is not `t0_b z`. `T0TIPREDEFFECT` is the coefficient, not `t0_b z`. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T15:14Z: `is_oa: false`; Springer `content/pdf` is HTML 200). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Eq. 3, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T12:12Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar observed-indicator mean of a time-independent predictor. Equation 5 writes `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and `Γ ~ N(τ, Ψ)`. Equation 3 prints the `TIPREDEFFECT` increment as the addend `A^{-1}[e^{A(t−t0)} − I] B z_i` after the `T0MEANS` carry and the `CINT` increment. Table 2 names `B` `TIPREDEFFECT`. The scalar composition is `E(y_t) = τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. Form the evolved-plus-increment latent mean first, then `τ + λ` of that mean. The evolved observed mean `τ + λ μ_t` is not this composition. The contemporaneous map `τ + λ(μ_t + m x)` is not this composition. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not this composition when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The evolved-plus-increment latent mean is not `E(y_t)`. `TIPREDEFFECT` is `B`, not that observed mean. A zero loading is exactly `τ`. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993), Vandenberg and Lance (2000), and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T05:12Z: `is_oa: false`). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T09:01Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar observed-indicator mean of a contemporaneous time-dependent impulse. Equation 5 writes `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and `Γ ~ N(τ, Ψ)`. The latent process at `t` after a contemporaneous Dirac (`u = t`) is `μ_t + m x`. The scalar composition is `E(y_t) = τ + λ(μ_t + m x)`. Form the evolved-plus-impulse latent mean first, then `τ + λ` of that mean. The evolved observed mean `τ + λ μ_t` is not this composition. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not this composition when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The evolved-plus-impulse latent mean is not `E(y_t)`. A zero loading is exactly `τ`. The §7.2 level-change form is a different specification and is not this map. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993), Vandenberg and Lance (2000), and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T05:12Z: `is_oa: false`). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Eq. 1–2, pp. 4–5; Eq. 3 exponential map; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T05:12Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar observed-indicator mean of a within-interval time-dependent impulse carry. Equation 5 writes `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and `Γ ~ N(τ, Ψ)`. The latent process at `t` after a Dirac that occurred strictly inside `(t0, t)` is `μ_t + e^{a(t−u)} m x`. The scalar composition is `E(y_t) = τ + λ(μ_t + e^{a(t−u)} m x)`. Form the carried latent mean first, then `τ + λ` of that mean. The evolved observed mean `τ + λ μ_t` is not this composition. The contemporaneous map `τ + λ(μ_t + m x)` is not this composition when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The carried latent mean is not `E(y_t)`. A zero loading is exactly `τ`. The §7.2 level-change form is a different specification and is not this map. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993), Vandenberg and Lance (2000), and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T05:12Z: `is_oa: false`). diff --git a/CLAUDE.md b/CLAUDE.md index 47cd49e9..a278da6a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. Putnick and Bornstein (2016, PMC5145197 opened 2026-08-19T22:15Z) require scalar invariance before latent-mean comparison; residual invariance is not a prerequisite. Two-observation series have no residual degrees of freedom (`ordinary_least_squares_fit` returns residual variance `0`) and cap at strong/scalar; they still license means. This is two-group OLS, not MGCFA. Meredith (1993) names remain unread labels. -- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. The contemporaneous time-dependent predictor impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`). Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). The §7.2 level-change form is not that impulse. The observed mean of that contemporaneous impulse is `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-impulse latent mean is not `E(y_t)`. The time-independent predictor increment is `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`). Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. `TIPREDEFFECT` is `B`, not that discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle et al. (2012, Eq. 14). The observed mean of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The within-interval time-dependent impulse carry is `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation). Form `m x` first, then `e^{a(t−u)} m x`. A zero drift is `m x` with no dissipation. Underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept. `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). An impulse at `u = t` is the contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. The observed mean of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The carried latent mean is not `E(y_t)`. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. The contemporaneous time-dependent predictor impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`). Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). The §7.2 level-change form is not that impulse. The observed mean of that contemporaneous impulse is `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-impulse latent mean is not `E(y_t)`. The time-independent predictor increment is `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`). Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. `TIPREDEFFECT` is `B`, not that discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle et al. (2012, Eq. 14). The observed mean of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The within-interval time-dependent impulse carry is `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation). Form `m x` first, then `e^{a(t−u)} m x`. A zero drift is `m x` with no dissipation. Underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept. `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). An impulse at `u = t` is the contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. The observed mean of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The carried latent mean is not `E(y_t)`. The first-occasion time-independent predictor shift is `t0_b z` (Driver et al., 2017, Table 3 `T0TIPREDEFFECT`; Eq. 3 first summand). Form `t0_b z` first, then `e^{a Δt} t0_b z`. Form `μ_t` first, then add that carry. A zero drift is `t0_b z`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. `e^{A Δt} t0_b z` is not `t0_b z`. `T0TIPREDEFFECT` is the coefficient, not the shift. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. diff --git a/crates/psychometric_core/src/error.rs b/crates/psychometric_core/src/error.rs index 777645f5..abedd79d 100644 --- a/crates/psychometric_core/src/error.rs +++ b/crates/psychometric_core/src/error.rs @@ -165,6 +165,22 @@ pub enum PsychometricError { /// `τ + λ(μ_t + e^{a(t−u)} m x)` is not /// `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. ImpulseCarryObservedMeanIsNotTimeIndependentObservedMean, + /// Driver Table 3 `T0TIPREDEFFECT` first-occasion shift was treated + /// as the Eq. 3 process increment. `t0_b z` is not + /// `A^{-1}[e^{A Δt} − I] B z`. + InitialTimeIndependentEffectIsNotProcessIncrement, + /// Driver Eq. 3 carry of `T0TIPREDEFFECT` was treated as the + /// first-occasion shift. `e^{A Δt} t0_b z` is not `t0_b z`. + InitialTimeIndependentCarryIsNotInitialEffect, + /// Driver Table 3 `T0TIPREDEFFECT` first-occasion shift was treated + /// as `CINT`. `t0_b z` is not `κ`. + InitialTimeIndependentEffectIsNotContinuousIntercept, + /// Driver Table 3 `T0TIPREDEFFECT` first-occasion shift was treated + /// as the fourth-summand impulse. `t0_b z` is not `M x`. + InitialTimeIndependentEffectIsNotTimeDependentImpulse, + /// Driver Table 3 `T0TIPREDEFFECT` was treated as the first-occasion + /// shift. The coefficient is not `t0_b z`. + InitialTimeIndependentCoefficientIsNotInitialEffect, } impl fmt::Display for PsychometricError { @@ -303,6 +319,21 @@ impl fmt::Display for PsychometricError { Self::ImpulseCarryObservedMeanIsNotTimeIndependentObservedMean => { "impulse-carry observed mean is not the time-independent-predictor observed mean" } + Self::InitialTimeIndependentEffectIsNotProcessIncrement => { + "first-occasion time-independent predictor shift is not the process increment" + } + Self::InitialTimeIndependentCarryIsNotInitialEffect => { + "carried first-occasion time-independent predictor is not the first-occasion shift" + } + Self::InitialTimeIndependentEffectIsNotContinuousIntercept => { + "first-occasion time-independent predictor shift is not the continuous intercept" + } + Self::InitialTimeIndependentEffectIsNotTimeDependentImpulse => { + "first-occasion time-independent predictor shift is not the time-dependent impulse" + } + Self::InitialTimeIndependentCoefficientIsNotInitialEffect => { + "first-occasion time-independent predictor coefficient is not the first-occasion shift" + } }; formatter.write_str(message) } @@ -524,5 +555,25 @@ mod tests { PsychometricError::ImpulseCarryObservedMeanIsNotTimeIndependentObservedMean.to_string(), "impulse-carry observed mean is not the time-independent-predictor observed mean" ); + assert_eq!( + PsychometricError::InitialTimeIndependentEffectIsNotProcessIncrement.to_string(), + "first-occasion time-independent predictor shift is not the process increment" + ); + assert_eq!( + PsychometricError::InitialTimeIndependentCarryIsNotInitialEffect.to_string(), + "carried first-occasion time-independent predictor is not the first-occasion shift" + ); + assert_eq!( + PsychometricError::InitialTimeIndependentEffectIsNotContinuousIntercept.to_string(), + "first-occasion time-independent predictor shift is not the continuous intercept" + ); + assert_eq!( + PsychometricError::InitialTimeIndependentEffectIsNotTimeDependentImpulse.to_string(), + "first-occasion time-independent predictor shift is not the time-dependent impulse" + ); + assert_eq!( + PsychometricError::InitialTimeIndependentCoefficientIsNotInitialEffect.to_string(), + "first-occasion time-independent predictor coefficient is not the first-occasion shift" + ); } } diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index 572102ec..39179946 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -51,7 +51,12 @@ //! the process mean. Equation 3's second summand also //! maps the time-independent predictor as `A^{-1}[e^{A Δt} − I] B z` //! (Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle -//! Eq. 14). The JSS article +//! Eq. 14). Table 3 (p. 13) names a different matrix +//! `T0TIPREDEFFECT` for time-independent predictors on latents at +//! `T0`. The scalar first-occasion shift is `t0_b z`. Equation 3's +//! first summand carries that shift as `e^{A Δt} t0_b z`. That carry +//! is not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and +//! not `M x`. The JSS article //! has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). //! The difference quotient `(x(t+Δt) − x(t)) / Δt` (their //! Eqs. 3–4) is refused. This is not DSEM and not a matrix `expm`. @@ -1811,6 +1816,237 @@ pub fn refuse_impulse_carry_observed_mean_as_time_independent_observed_mean( Err(PsychometricError::ImpulseCarryObservedMeanIsNotTimeIndependentObservedMean) } +/// Exact scalar first-occasion time-independent predictor shift. +/// +/// Driver, Oud, and Voelkle (2017, Table 3, p. 13; Eq. 3 first +/// summand, p. 5; JSS PDF opened 2026-08-20T15:14Z from +/// ) +/// name `T0TIPREDEFFECT` the effect of time-independent predictors on +/// latents at `T0`. Table 2 / Table 3 name `TIPREDEFFECT` `B`, which +/// enters Equation 3 as the printed addend +/// `A^{-1}[e^{A(t−t0)} − I] B z`. Those are not the same matrix. The +/// scalar first-occasion shift is `t0_b z`. It is not `B`, not +/// `A^{-1}[e^{A Δt} − I] B z`, not `κ`, and not `M x`. A zero effect +/// or zero predictor is exactly zero. This is not a Kalman filter and +/// not ctsem estimation. +/// +/// # Errors +/// +/// Returns [`PsychometricError::InvalidNumericInput`] when the effect +/// or predictor is non-finite or the product overflows. +pub fn recover_initial_time_independent_predictor_effect( + initial_time_independent_effect: f64, + time_independent_predictor: f64, +) -> Result { + if !initial_time_independent_effect.is_finite() || !time_independent_predictor.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + if initial_time_independent_effect == 0.0 || time_independent_predictor == 0.0 { + return Ok(0.0); + } + require_finite(initial_time_independent_effect * time_independent_predictor) +} + +/// Exact scalar carried first-occasion time-independent predictor. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 3, p. 5; Table 3, p. 13; JSS +/// PDF opened 2026-08-20T15:14Z) write the first summand as +/// `e^{A(t−t0)} η_i(t0)`. A Table 3 `T0TIPREDEFFECT` shift that is +/// already in `η(t0)` therefore appears at `t` as `e^{A Δt} t0_b z`. +/// Form `t0_b z` first, then `e^{a Δt} t0_b z`. A zero drift is +/// `t0_b z` with no dissipation of the first-occasion shift. Binary64 +/// underflow of `e^{a Δt}` to `+0` is a vanishing carry of that +/// shift and is kept. This carry is not the first-occasion shift, not +/// `A^{-1}[e^{A Δt} − I] B z` (`TIPREDEFFECT`), not `CINT`, and not +/// `M x`. When `exp` overflows at a finite `a Δt`, rewrite as +/// `sign(t0_b z) exp(ln|t0_b z| + a Δt)`. An overflowing rewrite +/// fails closed. This is not a Kalman filter and not ctsem estimation. +/// +/// # Errors +/// +/// Returns [`PsychometricError::EventTimeRequired`] for any non-event +/// clock, [`PsychometricError::NonPositiveInterval`] when +/// `event_delta` is not strictly positive, and +/// [`PsychometricError::InvalidNumericInput`] when an input is +/// non-finite or the mapped carry overflows. +pub fn recover_initial_time_independent_predictor_carry( + initial_time_independent_effect: f64, + time_independent_predictor: f64, + log_rate: f64, + event_delta: f64, + clock: LagClock, +) -> Result { + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + if !event_delta.is_finite() || event_delta <= 0.0 { + return Err(PsychometricError::NonPositiveInterval); + } + if !log_rate.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + let initial_shift = recover_initial_time_independent_predictor_effect( + initial_time_independent_effect, + time_independent_predictor, + )?; + if initial_shift == 0.0 { + return Ok(0.0); + } + let drift_interval = log_rate * event_delta; + let auto_effect = drift_interval.exp(); + if auto_effect.is_finite() { + // +0 underflow is a vanishing carry of the T0 shift. + return require_finite(auto_effect * initial_shift); + } + if !drift_interval.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + // Finite a Δt, overflowed exp. + // e^{a Δt} t0_b z = sign(t0_b z) exp(ln|t0_b z| + a Δt). + require_finite(initial_shift.signum() * (initial_shift.abs().ln() + drift_interval).exp()) +} + +/// Exact scalar evolved latent mean plus a first-occasion TI predictor. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 3, p. 5; Table 3, p. 13) write +/// the first summand as the carried `T0MEANS`, which includes any +/// `T0TIPREDEFFECT` shift already in `η(t0)`. Form `μ_t` first, then +/// add `e^{a Δt} t0_b z`. A zero carry is exactly `μ_t`. A zero +/// evolved mean is exactly the carry. Adding `t0_b z` without the +/// exponential is not this composition when `a Δt ≠ 0`. Adding +/// `A^{-1}[e^{A Δt} − I] B z` is not this composition. +/// +/// # Errors +/// +/// Propagates [`recover_discrete_latent_mean`] and +/// [`recover_initial_time_independent_predictor_carry`], and returns +/// [`PsychometricError::InvalidNumericInput`] when the sum overflows. +#[allow(clippy::too_many_arguments)] +pub fn recover_discrete_latent_mean_with_initial_time_independent_predictor( + initial_latent_mean: f64, + log_rate: f64, + continuous_intercept: f64, + initial_time_independent_effect: f64, + time_independent_predictor: f64, + event_delta: f64, + clock: LagClock, +) -> Result { + let evolved_latent_mean = recover_discrete_latent_mean( + initial_latent_mean, + log_rate, + continuous_intercept, + event_delta, + clock, + )?; + let initial_carry = recover_initial_time_independent_predictor_carry( + initial_time_independent_effect, + time_independent_predictor, + log_rate, + event_delta, + clock, + )?; + if initial_carry == 0.0 { + return Ok(evolved_latent_mean); + } + if evolved_latent_mean == 0.0 { + return Ok(initial_carry); + } + require_finite(evolved_latent_mean + initial_carry) +} + +/// Refuse treating the Table 3 first-occasion shift as the Eq. 3 +/// process increment. +/// +/// `T0TIPREDEFFECT` shifts `η(t0)`. `TIPREDEFFECT` `B` enters the +/// SDE and maps as `A^{-1}[e^{A Δt} − I] B z`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::InitialTimeIndependentEffectIsNotProcessIncrement`]. +pub fn refuse_initial_time_independent_effect_as_process_increment( + initial_time_independent_effect: f64, + time_independent_increment: f64, +) -> Result { + let _ = (initial_time_independent_effect, time_independent_increment); + Err(PsychometricError::InitialTimeIndependentEffectIsNotProcessIncrement) +} + +/// Refuse treating the Eq. 3 carry of `T0TIPREDEFFECT` as the +/// first-occasion shift. +/// +/// `e^{A Δt} t0_b z` is the first summand's contribution at `t`. +/// `t0_b z` is the shift at `T0`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::InitialTimeIndependentCarryIsNotInitialEffect`]. +pub fn refuse_initial_time_independent_carry_as_initial_effect( + initial_time_independent_carry: f64, + initial_time_independent_effect: f64, +) -> Result { + let _ = ( + initial_time_independent_carry, + initial_time_independent_effect, + ); + Err(PsychometricError::InitialTimeIndependentCarryIsNotInitialEffect) +} + +/// Refuse treating the Table 3 first-occasion shift as `CINT`. +/// +/// `t0_b z` is an initial-mean shift. `κ` is the continuous intercept. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::InitialTimeIndependentEffectIsNotContinuousIntercept`]. +pub fn refuse_initial_time_independent_effect_as_continuous_intercept( + initial_time_independent_effect: f64, + continuous_intercept: f64, +) -> Result { + let _ = (initial_time_independent_effect, continuous_intercept); + Err(PsychometricError::InitialTimeIndependentEffectIsNotContinuousIntercept) +} + +/// Refuse treating the Table 3 first-occasion shift as `M x`. +/// +/// The product `t0_b z` is algebraically a product, as is `M x`. +/// Table 3 names `T0TIPREDEFFECT` for `T0`. Table 2 names `M` +/// `TDPREDEFFECT` for the Dirac impulse. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::InitialTimeIndependentEffectIsNotTimeDependentImpulse`]. +pub fn refuse_initial_time_independent_effect_as_time_dependent_impulse( + initial_time_independent_effect: f64, + time_dependent_impulse: f64, +) -> Result { + let _ = (initial_time_independent_effect, time_dependent_impulse); + Err(PsychometricError::InitialTimeIndependentEffectIsNotTimeDependentImpulse) +} + +/// Refuse treating Driver Table 3 `T0TIPREDEFFECT` as the +/// first-occasion shift. +/// +/// `T0TIPREDEFFECT` is the coefficient. The shift is `t0_b z`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::InitialTimeIndependentCoefficientIsNotInitialEffect`]. +pub fn refuse_initial_time_independent_coefficient_as_initial_effect( + initial_time_independent_coefficient: f64, + initial_time_independent_effect: f64, +) -> Result { + let _ = ( + initial_time_independent_coefficient, + initial_time_independent_effect, + ); + Err(PsychometricError::InitialTimeIndependentCoefficientIsNotInitialEffect) +} + /// Exact scalar within-interval time-dependent impulse carry from /// Driver Equations 1–2. /// @@ -2380,6 +2616,7 @@ mod tests { recover_discrete_lag_one, recover_discrete_lagged_latent_covariance, recover_discrete_latent_mean, recover_discrete_latent_mean_with_impulse, recover_discrete_latent_mean_with_impulse_carry, + recover_discrete_latent_mean_with_initial_time_independent_predictor, recover_discrete_latent_mean_with_time_independent_predictor, recover_discrete_latent_variance, recover_discrete_observed_mean, recover_discrete_observed_mean_with_impulse, @@ -2387,13 +2624,15 @@ mod tests { recover_discrete_observed_mean_with_time_independent_predictor, recover_discrete_process_noise, recover_discrete_time_independent_predictor_effect, recover_discrete_time_varying_predictor_effect, recover_event_series_mean_log_rate, - recover_event_time_discrete_lag_and_log_rate, recover_irregular_centered_residual_log_rate, - recover_local_log_rate, recover_manifest_lagged_observed_covariance, - recover_manifest_observed_mean, recover_manifest_observed_variance, - recover_manifest_trait_plus_state_observed_variance, recover_stationary_latent_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_event_time_discrete_lag_and_log_rate, + recover_initial_time_independent_predictor_carry, + recover_initial_time_independent_predictor_effect, + recover_irregular_centered_residual_log_rate, recover_local_log_rate, + recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, + recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, + recover_stationary_latent_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_continuous_intercept_as_discrete_mean_increment, refuse_continuous_intercept_as_initial_latent_mean, refuse_continuous_intercept_as_manifest_means, refuse_difference_quotient_as_local_rate, @@ -2406,6 +2645,11 @@ mod tests { refuse_impulse_observed_mean_as_time_independent_observed_mean, refuse_initial_latent_mean_as_evolved_mean, refuse_initial_observed_mean_as_evolved_observed_mean, + refuse_initial_time_independent_carry_as_initial_effect, + refuse_initial_time_independent_coefficient_as_initial_effect, + refuse_initial_time_independent_effect_as_continuous_intercept, + refuse_initial_time_independent_effect_as_process_increment, + refuse_initial_time_independent_effect_as_time_dependent_impulse, refuse_latent_lagged_covariance_as_observed_covariance, refuse_latent_mean_as_observed_mean, refuse_latent_variance_as_observed_variance, refuse_manifest_means_as_observed_mean, @@ -6012,4 +6256,262 @@ mod tests { Err(PsychometricError::InvalidNumericInput) ); } + + #[test] + fn initial_time_independent_predictor_recovers_table_three_t0_shift_and_carry() { + let effect = 0.4_f64; + let predictor = 3.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let shift = recover_initial_time_independent_predictor_effect(effect, predictor) + .expect("t0-tipred"); + assert!((shift - 1.2).abs() < 1e-15); + assert_eq!( + recover_initial_time_independent_predictor_effect(0.0, predictor), + Ok(0.0) + ); + assert_eq!( + recover_initial_time_independent_predictor_effect(effect, 0.0), + Ok(0.0) + ); + let carry = recover_initial_time_independent_predictor_carry( + effect, + predictor, + drift, + delta, + LagClock::EventTime, + ) + .expect("t0-carry"); + let expected = 1.2 * (drift * delta).exp(); + assert!((carry - expected).abs() < 1e-15); + let zero_drift = recover_initial_time_independent_predictor_carry( + effect, + predictor, + 0.0, + delta, + LagClock::EventTime, + ) + .expect("zero-drift"); + assert!((zero_drift - 1.2).abs() < 1e-15); + let vanished = recover_initial_time_independent_predictor_carry( + effect, + predictor, + -800.0, + 1.0, + LagClock::EventTime, + ) + .expect("underflow"); + assert_eq!(vanished.to_bits(), 0.0_f64.to_bits()); + let increment = recover_discrete_time_independent_predictor_effect( + effect, + predictor, + drift, + delta, + LagClock::EventTime, + ) + .expect("tipred"); + let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("tdpred"); + assert!((carry - shift).abs() > 1e-3); + assert!((carry - increment).abs() > 1e-3); + assert!((shift - increment).abs() > 1e-3); + assert!((shift - effect).abs() > 1e-3); + // Algebraically a product, like M x, but Table 3 names a different matrix. + assert!((shift - impulse).abs() < 1e-15); + } + + #[test] + fn initial_time_independent_predictor_composes_evolved_mean_and_keeps_scale() { + let effect = 0.4_f64; + let predictor = 3.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let carry = recover_initial_time_independent_predictor_carry( + effect, + predictor, + drift, + delta, + LagClock::EventTime, + ) + .expect("t0-carry"); + let initial = 1.0_f64; + let intercept = 0.3_f64; + let composed = recover_discrete_latent_mean_with_initial_time_independent_predictor( + initial, + drift, + intercept, + effect, + predictor, + delta, + LagClock::EventTime, + ) + .expect("eq3-t0tipred"); + let evolved = + recover_discrete_latent_mean(initial, drift, intercept, delta, LagClock::EventTime) + .expect("mu-t"); + assert!((composed - (evolved + carry)).abs() < 1e-15); + assert_eq!( + recover_discrete_latent_mean_with_initial_time_independent_predictor( + initial, + drift, + intercept, + 0.0, + predictor, + delta, + LagClock::EventTime + ), + Ok(evolved) + ); + assert_eq!( + recover_discrete_latent_mean_with_initial_time_independent_predictor( + 0.0, + drift, + 0.0, + effect, + predictor, + delta, + LagClock::EventTime + ), + Ok(carry) + ); + let scaled = recover_initial_time_independent_predictor_carry( + 1e308, + 1e-308, + 0.0, + 1.0, + LagClock::EventTime, + ) + .expect("scale"); + assert!((scaled - 1.0).abs() < 1e-15); + let rewritten = recover_initial_time_independent_predictor_carry( + 2.0, + 0.5, + 710.0, + 1.0, + LagClock::EventTime, + ); + assert_eq!(rewritten, Err(PsychometricError::InvalidNumericInput)); + let finite_rewrite = recover_initial_time_independent_predictor_carry( + 1e-308, + 1.0, + 700.0, + 1.0, + LagClock::EventTime, + ) + .expect("log-rewrite"); + let expected_rewrite = (1e-308_f64.ln() + 700.0).exp(); + assert!((finite_rewrite - expected_rewrite).abs() / expected_rewrite < 1e-12); + } + + #[test] + fn initial_time_independent_predictor_refuses_process_increment_cint_impulse_and_coefficient() { + let effect = 0.4_f64; + let predictor = 3.0_f64; + let shift = recover_initial_time_independent_predictor_effect(effect, predictor) + .expect("t0-tipred"); + let carry = recover_initial_time_independent_predictor_carry( + effect, + predictor, + -0.5, + 2.0, + LagClock::EventTime, + ) + .expect("t0-carry"); + let increment = recover_discrete_time_independent_predictor_effect( + effect, + predictor, + -0.5, + 2.0, + LagClock::EventTime, + ) + .expect("tipred"); + let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("tdpred"); + assert_eq!( + refuse_initial_time_independent_effect_as_process_increment(shift, increment), + Err(PsychometricError::InitialTimeIndependentEffectIsNotProcessIncrement) + ); + assert_eq!( + refuse_initial_time_independent_carry_as_initial_effect(carry, shift), + Err(PsychometricError::InitialTimeIndependentCarryIsNotInitialEffect) + ); + assert_eq!( + refuse_initial_time_independent_effect_as_continuous_intercept(shift, 0.4), + Err(PsychometricError::InitialTimeIndependentEffectIsNotContinuousIntercept) + ); + assert_eq!( + refuse_initial_time_independent_effect_as_time_dependent_impulse(shift, impulse), + Err(PsychometricError::InitialTimeIndependentEffectIsNotTimeDependentImpulse) + ); + assert_eq!( + refuse_initial_time_independent_coefficient_as_initial_effect(effect, shift), + Err(PsychometricError::InitialTimeIndependentCoefficientIsNotInitialEffect) + ); + } + + #[test] + fn initial_time_independent_predictor_invalid_inputs_fail_closed() { + assert_eq!( + recover_initial_time_independent_predictor_effect(f64::NAN, 1.0), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_initial_time_independent_predictor_effect(1.0, f64::INFINITY), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_initial_time_independent_predictor_effect(1e308, 2.0), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_initial_time_independent_predictor_carry( + 0.4, + 3.0, + f64::NAN, + 2.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_initial_time_independent_predictor_carry( + 0.4, + 3.0, + -0.5, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_initial_time_independent_predictor_carry( + 0.4, + 3.0, + -0.5, + 2.0, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_discrete_latent_mean_with_initial_time_independent_predictor( + 1e308, + 0.0, + 0.0, + 1e308, + 1.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_initial_time_independent_predictor_carry( + 1.0, + 1.0, + f64::INFINITY, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + } } diff --git a/crates/psychometric_core/src/lib.rs b/crates/psychometric_core/src/lib.rs index fc841c7e..46bf76ed 100644 --- a/crates/psychometric_core/src/lib.rs +++ b/crates/psychometric_core/src/lib.rs @@ -52,7 +52,11 @@ //! `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (`τ + λ μ_t` is not that //! observed mean; `τ + λ(μ_t + m x)` is not that observed mean; //! `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when -//! `u ≠ t`), +//! `u ≠ t`), recovers the Driver Table 3 first-occasion +//! `T0TIPREDEFFECT` shift `t0_b z` and its Eq. 3 first-summand carry +//! `e^{A Δt} t0_b z` (`T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; +//! `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is +//! not `t0_b z`), //! and refuses //! latent-mean comparison below strong invariance. @@ -117,6 +121,8 @@ pub use event_time::recover_discrete_latent_mean; pub use event_time::recover_discrete_latent_mean_with_impulse; /// Exact scalar evolved latent mean plus a within-interval impulse carry. pub use event_time::recover_discrete_latent_mean_with_impulse_carry; +/// Exact scalar evolved latent mean plus a first-occasion TI predictor. +pub use event_time::recover_discrete_latent_mean_with_initial_time_independent_predictor; /// Exact scalar evolved latent mean plus a time-independent predictor. pub use event_time::recover_discrete_latent_mean_with_time_independent_predictor; /// Exact scalar discrete latent variance `A_Δt P A_Δt⊤ + Q_Δt`. @@ -139,6 +145,10 @@ pub use event_time::recover_discrete_time_varying_predictor_effect; pub use event_time::recover_event_series_mean_log_rate; /// Exact scalar pair `(φ, a)` on event time. pub use event_time::recover_event_time_discrete_lag_and_log_rate; +/// Exact scalar carried first-occasion `T0TIPREDEFFECT` `e^{A Δt} t0_b z`. +pub use event_time::recover_initial_time_independent_predictor_carry; +/// Exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z`. +pub use event_time::recover_initial_time_independent_predictor_effect; /// Mean exact log-rate on already-centered irregular residuals. pub use event_time::recover_irregular_centered_residual_log_rate; /// Exact scalar inverse `a = ln(φ) / Δt`. @@ -189,6 +199,16 @@ pub use event_time::refuse_impulse_observed_mean_as_time_independent_observed_me pub use event_time::refuse_initial_latent_mean_as_evolved_mean; /// Refuse treating first-occasion `τ + λ μ_0` as `E(y_t)`. pub use event_time::refuse_initial_observed_mean_as_evolved_observed_mean; +/// Refuse treating the Eq. 3 `T0TIPREDEFFECT` carry as the first-occasion shift. +pub use event_time::refuse_initial_time_independent_carry_as_initial_effect; +/// Refuse treating Driver Table 3 `T0TIPREDEFFECT` as the first-occasion shift. +pub use event_time::refuse_initial_time_independent_coefficient_as_initial_effect; +/// Refuse treating the Table 3 first-occasion shift as `CINT`. +pub use event_time::refuse_initial_time_independent_effect_as_continuous_intercept; +/// Refuse treating the Table 3 first-occasion shift as the Eq. 3 process increment. +pub use event_time::refuse_initial_time_independent_effect_as_process_increment; +/// Refuse treating the Table 3 first-occasion shift as `M x`. +pub use event_time::refuse_initial_time_independent_effect_as_time_dependent_impulse; /// Refuse treating Driver Eq. 3–4 lagged latent covariance as `cov(y_t, y_{t-1})`. pub use event_time::refuse_latent_lagged_covariance_as_observed_covariance; /// Refuse treating Driver Eq. 5 latent mean as `E(y)`. 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 485ce413..30ceb58d 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -9,19 +9,21 @@ use psychometric_core::{ recover_discrete_lag_from_log_rate, recover_discrete_lagged_latent_covariance, recover_discrete_latent_mean, recover_discrete_latent_mean_with_impulse, recover_discrete_latent_mean_with_impulse_carry, + recover_discrete_latent_mean_with_initial_time_independent_predictor, recover_discrete_latent_mean_with_time_independent_predictor, recover_discrete_latent_variance, recover_discrete_observed_mean, recover_discrete_observed_mean_with_impulse, recover_discrete_observed_mean_with_impulse_carry, recover_discrete_observed_mean_with_time_independent_predictor, recover_discrete_process_noise, recover_discrete_time_independent_predictor_effect, recover_discrete_time_varying_predictor_effect, recover_event_series_mean_log_rate, - recover_event_time_discrete_lag_and_log_rate, recover_irregular_centered_residual_log_rate, - recover_kish_weighted_slope, recover_manifest_lagged_observed_covariance, - recover_manifest_observed_mean, recover_manifest_observed_variance, - recover_manifest_trait_plus_state_observed_variance, recover_stationary_latent_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_event_time_discrete_lag_and_log_rate, recover_initial_time_independent_predictor_carry, + recover_initial_time_independent_predictor_effect, + recover_irregular_centered_residual_log_rate, recover_kish_weighted_slope, + recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, + recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, + recover_stationary_latent_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_continuous_intercept_as_discrete_mean_increment, refuse_continuous_intercept_as_initial_latent_mean, refuse_continuous_intercept_as_manifest_means, refuse_difference_quotient_as_local_rate, @@ -34,6 +36,11 @@ use psychometric_core::{ refuse_impulse_observed_mean_as_time_independent_observed_mean, refuse_initial_latent_mean_as_evolved_mean, refuse_initial_observed_mean_as_evolved_observed_mean, + refuse_initial_time_independent_carry_as_initial_effect, + refuse_initial_time_independent_coefficient_as_initial_effect, + refuse_initial_time_independent_effect_as_continuous_intercept, + refuse_initial_time_independent_effect_as_process_increment, + refuse_initial_time_independent_effect_as_time_dependent_impulse, refuse_latent_lagged_covariance_as_observed_covariance, refuse_latent_mean_as_observed_mean, refuse_latent_variance_as_observed_variance, refuse_manifest_means_as_observed_mean, refuse_manifest_trait_variance_as_measurement_error, @@ -1683,6 +1690,113 @@ fn time_independent_predictor_refuses_overflow_and_non_event_clocks() { ); } +#[test] +fn initial_time_independent_predictor_recovers_driver_table_three_t0_shift() { + let effect = 0.4_f64; + let predictor = 3.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let shift = + recover_initial_time_independent_predictor_effect(effect, predictor).expect("t0-tipred"); + let error = rmse(&[1.2], &[shift]); + assert!( + error < 1e-15, + "Driver Table 3 T0TIPREDEFFECT RMSE {error}: got {shift}" + ); + let carry = recover_initial_time_independent_predictor_carry( + effect, + predictor, + drift, + delta, + LagClock::EventTime, + ) + .expect("t0-carry"); + let expected_carry = 1.2 * (drift * delta).exp(); + let carry_error = rmse(&[expected_carry], &[carry]); + assert!( + carry_error < 1e-15, + "Driver Eq. 3 first-summand T0TIPREDEFFECT carry RMSE {carry_error}: got {carry}" + ); + let increment = recover_discrete_time_independent_predictor_effect( + effect, + predictor, + drift, + delta, + LagClock::EventTime, + ) + .expect("tipred"); + let intercept_effect = + recover_discrete_continuous_intercept_effect(effect, drift, delta, LagClock::EventTime) + .expect("cint"); + assert!(rmse(&[carry], &[shift]) > rmse(&[expected_carry], &[carry])); + assert!(rmse(&[carry], &[increment]) > rmse(&[expected_carry], &[carry])); + assert!(rmse(&[shift], &[increment]) > rmse(&[1.2], &[shift])); + assert!(rmse(&[shift], &[intercept_effect]) > rmse(&[1.2], &[shift])); + assert!(rmse(&[shift], &[effect]) > rmse(&[1.2], &[shift])); + let composed = recover_discrete_latent_mean_with_initial_time_independent_predictor( + 1.0, + drift, + 0.3, + effect, + predictor, + delta, + LagClock::EventTime, + ) + .expect("eq3-t0tipred"); + let evolved = + recover_discrete_latent_mean(1.0, drift, 0.3, delta, LagClock::EventTime).expect("mu-t"); + let composed_error = rmse(&[evolved + carry], &[composed]); + assert!( + composed_error < 1e-15, + "Driver Eq. 3 μ_t + e^{{A Δt}} t0_b z RMSE {composed_error}: got {composed}" + ); + assert_eq!( + refuse_initial_time_independent_effect_as_process_increment(shift, increment), + Err(PsychometricError::InitialTimeIndependentEffectIsNotProcessIncrement) + ); + assert_eq!( + refuse_initial_time_independent_carry_as_initial_effect(carry, shift), + Err(PsychometricError::InitialTimeIndependentCarryIsNotInitialEffect) + ); + assert_eq!( + refuse_initial_time_independent_effect_as_continuous_intercept(shift, effect), + Err(PsychometricError::InitialTimeIndependentEffectIsNotContinuousIntercept) + ); + let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("tdpred"); + assert_eq!( + refuse_initial_time_independent_effect_as_time_dependent_impulse(shift, impulse), + Err(PsychometricError::InitialTimeIndependentEffectIsNotTimeDependentImpulse) + ); + assert_eq!( + refuse_initial_time_independent_coefficient_as_initial_effect(effect, shift), + Err(PsychometricError::InitialTimeIndependentCoefficientIsNotInitialEffect) + ); +} + +#[test] +fn initial_time_independent_predictor_refuses_overflow_and_non_event_clocks() { + assert_eq!( + recover_initial_time_independent_predictor_effect(1e308, 2.0), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_initial_time_independent_predictor_carry(0.4, 3.0, -0.5, 2.0, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_discrete_latent_mean_with_initial_time_independent_predictor( + 1e308, + 0.0, + 0.0, + 1e308, + 1.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); +} + #[test] fn discrete_observed_mean_with_time_independent_predictor_recovers_driver_equation_five() { let loading = 2.0_f64; diff --git a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs index 5c26b6c6..aa6eb90e 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -7,17 +7,21 @@ use psychometric_core::{ recover_discrete_continuous_intercept_effect, recover_discrete_lagged_latent_covariance, recover_discrete_latent_mean, recover_discrete_latent_mean_with_impulse, recover_discrete_latent_mean_with_impulse_carry, + recover_discrete_latent_mean_with_initial_time_independent_predictor, recover_discrete_latent_mean_with_time_independent_predictor, recover_discrete_latent_variance, recover_discrete_observed_mean, recover_discrete_observed_mean_with_impulse, recover_discrete_observed_mean_with_impulse_carry, recover_discrete_observed_mean_with_time_independent_predictor, recover_discrete_process_noise, recover_discrete_time_independent_predictor_effect, - recover_discrete_time_varying_predictor_effect, recover_irregular_centered_residual_log_rate, - recover_loading_point_estimate_mean, recover_manifest_lagged_observed_covariance, - recover_manifest_observed_mean, recover_manifest_observed_variance, - recover_manifest_trait_plus_state_observed_variance, recover_stationary_latent_variance, - recover_time_dependent_predictor_impulse, recover_time_dependent_predictor_impulse_carry, - recover_trait_plus_state_latent_variance, recover_within_residual_event_time_log_rate, + recover_discrete_time_varying_predictor_effect, + recover_initial_time_independent_predictor_carry, + recover_initial_time_independent_predictor_effect, + recover_irregular_centered_residual_log_rate, recover_loading_point_estimate_mean, + recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, + recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, + recover_stationary_latent_variance, recover_time_dependent_predictor_impulse, + recover_time_dependent_predictor_impulse_carry, recover_trait_plus_state_latent_variance, + recover_within_residual_event_time_log_rate, refuse_continuous_intercept_as_discrete_mean_increment, refuse_continuous_intercept_as_initial_latent_mean, refuse_continuous_intercept_as_manifest_means, @@ -30,6 +34,11 @@ use psychometric_core::{ refuse_impulse_observed_mean_as_time_independent_observed_mean, refuse_initial_latent_mean_as_evolved_mean, refuse_initial_observed_mean_as_evolved_observed_mean, + refuse_initial_time_independent_carry_as_initial_effect, + refuse_initial_time_independent_coefficient_as_initial_effect, + refuse_initial_time_independent_effect_as_continuous_intercept, + refuse_initial_time_independent_effect_as_process_increment, + refuse_initial_time_independent_effect_as_time_dependent_impulse, refuse_latent_lagged_covariance_as_observed_covariance, refuse_latent_mean_as_observed_mean, refuse_latent_variance_as_observed_variance, refuse_manifest_means_as_observed_mean, refuse_manifest_trait_variance_as_measurement_error, @@ -765,6 +774,100 @@ fn time_independent_predictor_is_not_cint_impulse_equation_fourteen_or_coefficie ); } +#[test] +fn initial_time_independent_predictor_is_not_process_increment_cint_or_impulse() { + let effect = 0.4_f64; + let predictor = 3.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let shift = + recover_initial_time_independent_predictor_effect(effect, predictor).expect("t0-tipred"); + let carry = recover_initial_time_independent_predictor_carry( + effect, + predictor, + drift, + delta, + LagClock::EventTime, + ) + .expect("t0-carry"); + let increment = recover_discrete_time_independent_predictor_effect( + effect, + predictor, + drift, + delta, + LagClock::EventTime, + ) + .expect("tipred"); + let intercept_effect = + recover_discrete_continuous_intercept_effect(effect, drift, delta, LagClock::EventTime) + .expect("cint"); + let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("tdpred"); + let evolved = + recover_discrete_latent_mean(1.0, drift, 0.3, delta, LagClock::EventTime).expect("mu-t"); + let composed = recover_discrete_latent_mean_with_initial_time_independent_predictor( + 1.0, + drift, + 0.3, + effect, + predictor, + delta, + LagClock::EventTime, + ) + .expect("eq3-t0tipred"); + assert!( + (shift - effect).abs() > 1e-3, + "Driver et al. (2017, Table 3 p. 13): T0TIPREDEFFECT is not t0_b z" + ); + assert!( + (shift - increment).abs() > 1e-3, + "Driver et al. (2017, Table 3 / Eq. 3): t0_b z is not A^{{-1}}[e^{{A Δt}} − I] B z" + ); + assert!( + (carry - shift).abs() > 1e-3, + "Driver et al. (2017, Eq. 3): e^{{A Δt}} t0_b z is not t0_b z" + ); + assert!( + (carry - increment).abs() > 1e-3, + "Driver et al. (2017, Eq. 3): e^{{A Δt}} t0_b z is not A^{{-1}}[e^{{A Δt}} − I] B z" + ); + assert!( + (shift - intercept_effect).abs() > 1e-3, + "Driver et al. (2017, Table 3): t0_b z is not CINT" + ); + assert!( + (composed - evolved).abs() > 1e-3, + "Driver et al. (2017, Eq. 3): μ_t is not μ_t + e^{{A Δt}} t0_b z" + ); + assert_eq!( + refuse_initial_time_independent_effect_as_process_increment(shift, increment), + Err( + psychometric_core::PsychometricError::InitialTimeIndependentEffectIsNotProcessIncrement + ) + ); + assert_eq!( + refuse_initial_time_independent_carry_as_initial_effect(carry, shift), + Err(psychometric_core::PsychometricError::InitialTimeIndependentCarryIsNotInitialEffect) + ); + assert_eq!( + refuse_initial_time_independent_effect_as_continuous_intercept(shift, effect), + Err( + psychometric_core::PsychometricError::InitialTimeIndependentEffectIsNotContinuousIntercept + ) + ); + assert_eq!( + refuse_initial_time_independent_effect_as_time_dependent_impulse(shift, impulse), + Err( + psychometric_core::PsychometricError::InitialTimeIndependentEffectIsNotTimeDependentImpulse + ) + ); + assert_eq!( + refuse_initial_time_independent_coefficient_as_initial_effect(effect, shift), + Err( + psychometric_core::PsychometricError::InitialTimeIndependentCoefficientIsNotInitialEffect + ) + ); +} + #[test] fn time_dependent_impulse_carry_is_not_contemporaneous_cint_tipred_or_equation_fourteen() { let effect = 0.4_f64; diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index 1dc779c0..06bf6745 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -1,7 +1,7 @@ # ADR 0005 — Posterior-aware ESEM/DSEM and structural interpretation **Decision status:** Accepted -**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), exact scalar contemporaneous `TDPREDEFFECT` impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; the §7.2 level-change form is not that impulse), exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of that Eq. 3 composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar time-independent `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle Eq. 14; `B` is not that discrete increment), exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of that Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of that carried latent mean; `τ + λ μ_t` is not that observed mean), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target +**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), exact scalar contemporaneous `TDPREDEFFECT` impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; the §7.2 level-change form is not that impulse), exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of that Eq. 3 composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar time-independent `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle Eq. 14; `B` is not that discrete increment), exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of that Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of that carried latent mean; `τ + λ μ_t` is not that observed mean), exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; `T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target **Date:** 2026-08-05 **Supersedes:** None. ADR 0012 governs upstream topic measurement/network coordinates; this ADR governs higher-order psychometric structure and longitudinal interpretation. @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), recovers the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z; `TDPREDEFFECT` is `M`, not `CINT`; `M x` is not `A^{-1}[e^{A Δt} − I] B z`; `M x` is not Voelkle et al., 2012, Eq. 14; the level-change form is not that impulse), recovers the exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition; JSS PDF re-opened 2026-08-20T09:01Z; the evolved map `τ + λ μ_t` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-impulse latent mean is not `E(y_t)`), recovers the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z; `TIPREDEFFECT` is `B`, not `κ`; form `B z` first, then the discrete intercept map; a zero drift is `B z Δt`; `B` is not the discrete increment; `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle Eq. 14), recovers the exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; JSS PDF re-opened 2026-08-20T12:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-increment latent mean is not `E(y_t)`), recovers the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z; form `m x` first, then `e^{a(t−u)} m x`; a zero drift is `m x`; underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept; `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; an impulse at `u = t` is the contemporaneous map; an impulse at `u ≤ t0` is already in `η(t0)`), recovers the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; JSS PDF re-opened 2026-08-20T05:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the carried latent mean is not `E(y_t)`), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), recovers the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z; `TDPREDEFFECT` is `M`, not `CINT`; `M x` is not `A^{-1}[e^{A Δt} − I] B z`; `M x` is not Voelkle et al., 2012, Eq. 14; the level-change form is not that impulse), recovers the exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition; JSS PDF re-opened 2026-08-20T09:01Z; the evolved map `τ + λ μ_t` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-impulse latent mean is not `E(y_t)`), recovers the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z; `TIPREDEFFECT` is `B`, not `κ`; form `B z` first, then the discrete intercept map; a zero drift is `B z Δt`; `B` is not the discrete increment; `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle Eq. 14), recovers the exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; JSS PDF re-opened 2026-08-20T12:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-increment latent mean is not `E(y_t)`), recovers the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z; form `m x` first, then `e^{a(t−u)} m x`; a zero drift is `m x`; underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept; `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; an impulse at `u = t` is the contemporaneous map; an impulse at `u ≤ t0` is already in `η(t0)`), recovers the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; JSS PDF re-opened 2026-08-20T05:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the carried latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and its Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF opened 2026-08-20T15:14Z; form `t0_b z` first, then `e^{a Δt} t0_b z`; a zero drift is `t0_b z`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; `e^{A Δt} t0_b z` is not `t0_b z`; `T0TIPREDEFFECT` is the coefficient, not the shift), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. 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 f1c08720..a47c1ee4 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -28,15 +28,16 @@ This slice stays inside `psychometric_core`. It does not add a second invariance 22. recover the exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`) and refuse treating `τ + λ μ_t`, `τ + λ(μ_t + m x)`, or `τ + λ(μ_t + e^{a(t−u)} m x)` as that observed mean; 23. recover the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; the printed Eq. 3 fourth summand is the contemporaneous Dirac) and refuse treating that carry as the contemporaneous impulse, `CINT`, `TIPREDEFFECT`, or Voelkle et al. (2012, Eq. 14); 24. recover the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`) and refuse treating `τ + λ μ_t` or `τ + λ(μ_t + m x)` as that observed mean; -25. refuse pooling discrete lags from unequal event intervals as one coefficient; -26. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); -27. refuse the difference quotient as a continuous-time rate; -28. apply the same event-time map to CWC residuals (still not DSEM); -29. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). +25. recover the exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and its Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF opened 2026-08-20T15:14Z) and refuse treating `t0_b z` as `A^{-1}[e^{A Δt} − I] B z`, as `κ`, or as `M x`; refuse treating `e^{A Δt} t0_b z` as `t0_b z`; refuse treating the coefficient as the shift; +26. refuse pooling discrete lags from unequal event intervals as one coefficient; +27. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); +28. refuse the difference quotient as a continuous-time rate; +29. apply the same event-time map to CWC residuals (still not DSEM); +30. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). ## Claim boundary -This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. The contemporaneous `TDPREDEFFECT` impulse is not the continuous intercept, not the time-independent discrete effect, and not Voelkle et al. (2012, Eq. 14). The time-independent `TIPREDEFFECT` increment is not the continuous intercept, not the contemporaneous impulse, not Voelkle et al. (2012, Eq. 14), and not the coefficient `B`. The within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). The evolved observed mean `τ + λ μ_t` is not the contemporaneous-impulse observed mean `τ + λ(μ_t + m x)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not the impulse-carry observed mean `τ + λ(μ_t + e^{a(t−u)} m x)` when `u ≠ t`. The evolved observed mean `τ + λ μ_t` is not the impulse-carry observed mean. The carried latent mean is not `E(y_t)`. The evolved-plus-impulse latent mean is not `E(y_t)`. The evolved observed mean `τ + λ μ_t` is not the time-independent-predictor observed mean `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not that time-independent-predictor observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that time-independent-predictor observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. +This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. The contemporaneous `TDPREDEFFECT` impulse is not the continuous intercept, not the time-independent discrete effect, and not Voelkle et al. (2012, Eq. 14). The time-independent `TIPREDEFFECT` increment is not the continuous intercept, not the contemporaneous impulse, not Voelkle et al. (2012, Eq. 14), and not the coefficient `B`. The within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). The evolved observed mean `τ + λ μ_t` is not the contemporaneous-impulse observed mean `τ + λ(μ_t + m x)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not the impulse-carry observed mean `τ + λ(μ_t + e^{a(t−u)} m x)` when `u ≠ t`. The evolved observed mean `τ + λ μ_t` is not the impulse-carry observed mean. The carried latent mean is not `E(y_t)`. The evolved-plus-impulse latent mean is not `E(y_t)`. The evolved observed mean `τ + λ μ_t` is not the time-independent-predictor observed mean `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not that time-independent-predictor observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that time-independent-predictor observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TIPREDEFFECT` shift `t0_b z` is not the Eq. 3 process increment `A^{-1}[e^{A Δt} − I] B z`, not `κ`, and not `M x`. The Eq. 3 first-summand carry `e^{A Δt} t0_b z` is not `t0_b z` and is not that process increment. `T0TIPREDEFFECT` is the coefficient, not the first-occasion shift. ## Authoritative sources @@ -54,7 +55,7 @@ Kish, L. (1965). *Survey sampling*. John Wiley & Sons. Oud, J. H. L., & Jansen, R. A. R. G. (2000). Continuous time state space modeling of panel data by means of SEM. *Psychometrika, 65*(2), 199–215. https://doi.org/10.1007/BF02294374 (cited by Voelkle et al., 2012, Eq. 14 discussion; PDF not opened). -The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:07Z (https://www.zora.uzh.ch/handle/20.500.14742/72792; bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`). Page 16 writes discrete auto-effects as \(A^{*}(\Delta t)=\exp(A\Delta t)\) (Eq. 7). Introducing Intercepts (manuscript p. 20, Eq. 12) adds a continuous-time intercept \(b\) and writes the expected-value solution whose discrete increment is \(A^{-1}(\exp(A\Delta t)-I)b\); the scalar constant-predictor effect is \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). The next paragraph (manuscript p. 21, Eq. 14) writes the discrete effect of a **time-varying** predictor, when the sampling interval equals the constancy interval, as \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product does not depend on the predictor auto-effect. The manuscript calls Eq. 14 a first-order approximation that deteriorates as \(\Delta t\) grows, and defers unmatched sampling/constancy intervals to Oud and Jansen (2000), which is unread. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4; JSS PDF re-opened 2026-08-19T04:10Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\), the discrete intercept \(b_{\Delta t}=A^{-1}[A_{\Delta t}-I]b\), and the discrete process-noise covariance \(Q_{\Delta t}=\int_{0}^{\Delta t}\operatorname{expm}(A(\Delta t-\tau))LGG^{\top}L^{\top}\operatorname{expm}(A(\Delta t-\tau))^{\top}\,d\tau\). Equation 4 (p. 5) writes that the integral exhibits covariance \(Q_{\Delta t}=\operatorname{irow}(A^{\#^{-1}}[e^{A^{\#}\Delta t}-I]\operatorname{row}(Q))\) with \(A^{\#}=A\otimes I+I\otimes A\). The homogeneous-process consequence (`ξ`, `z` given) is \(Q_{\Delta t}=\operatorname{cov}(\eta_{ti}\mid\eta_{t-1,i})\) and \(\operatorname{cov}(\eta_{ti},\eta_{t-1,i})=A_{\Delta t}\operatorname{cov}(\eta_{t-1,i})\). The law of total variance on that pair is \(\operatorname{Var}(\eta_{ti})=A_{\Delta t}\operatorname{Var}(\eta_{t-1,i})A_{\Delta t}^{\top}+Q_{\Delta t}\). As \(\Delta t\to\infty\) with stable \(a<0\), Eq. 4 becomes \(-q/(2a)\). The JSS summary names that limit `asymDIFFUSION` and takes it as the total within-subject variance (p. 16). Section 4.3 (pp. 9–10) constrains a stationary `T0VAR` to that model-predicted variance and distinguishes it from a predetermined first occasion. The same section (p. 9) adds a stable trait process with `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is that time-invariant between-subject variance. The JSS article has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). This slice takes scalar \(L=1\) (every latent subject to system noise); it does not implement the 0/1 selector matrix and is not a Kalman filter. Discrete auto-effects are strictly positive for finite real drift and interval; the inverse \(a=\ln\varphi/\Delta t\) therefore requires \(\varphi>0\). Binary64 `exp` of a large negative argument is `+0` and is refused as a discrete lag; the same underflow is a vanishing lagged covariance and is kept. Equations 3–4 of Voelkle et al. (2012) are the discouraged difference-quotient approximation. Meredith (1993) remains unread (Unpaywall/OpenAlex 2026-08-20T05:12Z: closed; Springer `content/pdf` is HTML 200, not a PDF; archive.org title search empty). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex 2026-08-20T05:12Z: closed; Springer `content/pdf` is HTML 200; ETS landing page is HTML). ERIC ED334221 is Singer and Willett (1991), *From whether to when*, not the 1991 journal article. ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title is not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex 2026-08-18T21:07Z: closed; Radboud landing and bitstream 403). +The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:07Z (https://www.zora.uzh.ch/handle/20.500.14742/72792; bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`). Page 16 writes discrete auto-effects as \(A^{*}(\Delta t)=\exp(A\Delta t)\) (Eq. 7). Introducing Intercepts (manuscript p. 20, Eq. 12) adds a continuous-time intercept \(b\) and writes the expected-value solution whose discrete increment is \(A^{-1}(\exp(A\Delta t)-I)b\); the scalar constant-predictor effect is \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). The next paragraph (manuscript p. 21, Eq. 14) writes the discrete effect of a **time-varying** predictor, when the sampling interval equals the constancy interval, as \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product does not depend on the predictor auto-effect. The manuscript calls Eq. 14 a first-order approximation that deteriorates as \(\Delta t\) grows, and defers unmatched sampling/constancy intervals to Oud and Jansen (2000), which is unread. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4; JSS PDF opened 2026-08-20T15:14Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\), the discrete intercept \(b_{\Delta t}=A^{-1}[A_{\Delta t}-I]b\), and the discrete process-noise covariance \(Q_{\Delta t}=\int_{0}^{\Delta t}\operatorname{expm}(A(\Delta t-\tau))LGG^{\top}L^{\top}\operatorname{expm}(A(\Delta t-\tau))^{\top}\,d\tau\). Equation 4 (p. 5) writes that the integral exhibits covariance \(Q_{\Delta t}=\operatorname{irow}(A^{\#^{-1}}[e^{A^{\#}\Delta t}-I]\operatorname{row}(Q))\) with \(A^{\#}=A\otimes I+I\otimes A\). The homogeneous-process consequence (`ξ`, `z` given) is \(Q_{\Delta t}=\operatorname{cov}(\eta_{ti}\mid\eta_{t-1,i})\) and \(\operatorname{cov}(\eta_{ti},\eta_{t-1,i})=A_{\Delta t}\operatorname{cov}(\eta_{t-1,i})\). The law of total variance on that pair is \(\operatorname{Var}(\eta_{ti})=A_{\Delta t}\operatorname{Var}(\eta_{t-1,i})A_{\Delta t}^{\top}+Q_{\Delta t}\). As \(\Delta t\to\infty\) with stable \(a<0\), Eq. 4 becomes \(-q/(2a)\). The JSS summary names that limit `asymDIFFUSION` and takes it as the total within-subject variance (p. 16). Section 4.3 (pp. 9–10) constrains a stationary `T0VAR` to that model-predicted variance and distinguishes it from a predetermined first occasion. The same section (p. 9) adds a stable trait process with `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is that time-invariant between-subject variance. Table 3 (p. 13) names `T0TIPREDEFFECT` the effect of time-independent predictors on latents at `T0` and names `TIPREDEFFECT` `B` separately. The JSS article has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). This slice takes scalar \(L=1\) (every latent subject to system noise); it does not implement the 0/1 selector matrix and is not a Kalman filter. Discrete auto-effects are strictly positive for finite real drift and interval; the inverse \(a=\ln\varphi/\Delta t\) therefore requires \(\varphi>0\). Binary64 `exp` of a large negative argument is `+0` and is refused as a discrete lag; the same underflow is a vanishing lagged covariance and is kept. Equations 3–4 of Voelkle et al. (2012) are the discouraged difference-quotient approximation. Meredith (1993) remains unread (Unpaywall/OpenAlex 2026-08-20T15:14Z: closed; Springer `content/pdf` is HTML 200, not a PDF; archive.org title search empty). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex 2026-08-20T15:14Z: closed; Springer `content/pdf` is HTML 200; ETS landing page is HTML). ERIC ED334221 is Singer and Willett (1991), *From whether to when*, not the 1991 journal article. ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title is not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex 2026-08-18T21:07Z: closed; Radboud landing and bitstream 403). ## Formula notes @@ -81,6 +82,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - **Time-independent-predictor observed-indicator mean.** Driver et al. (2017, Eq. 5, p. 5; Eq. 3, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T12:12Z): `y_i(t)=Γ+Λη_i(t)+ζ_i(t)` with `ζ∼N(0,Θ)` and `Γ∼N(τ,Ψ)`. Equation 3 prints the `TIPREDEFFECT` increment as the addend `A^{-1}[e^{A(t−t0)}−I]Bz_i` after the `T0MEANS` carry and the `CINT` increment. The scalar composition is `E(y_t)=τ+λ(μ_t+A^{-1}[e^{AΔt}−I]Bz)`. Form the evolved-plus-increment latent mean first, then `τ+λ` of that mean. A zero loading is exactly `τ`. A zero evolved-plus-increment latent mean is exactly `τ`. A zero intercept is exactly `λ(μ_t+increment)`. The evolved observed mean `τ+λμ_t` is not this composition. The contemporaneous map `τ+λ(μ_t+mx)` is not this composition. The carry map `τ+λ(μ_t+e^{a(t−u)}mx)` is not this composition when `u≠t`. `MANIFESTMEANS` is not `E(y_t)`. The evolved-plus-increment latent mean is not `E(y_t)`. `TIPREDEFFECT` is `B`, not that observed mean. An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **Time-dependent predictor impulse carry.** Driver et al. (2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z): Equation 1 writes `dη=(Aη+ξ+Bz+Mχ(t))dt+GdW`. Equation 2 writes `χ_i(t)=Σ x_{i,u} δ(t−u)`. The Green-function integral of that Dirac on `(t0,t)` is `e^{A(t−u)}Mx`. The printed Eq. 3 fourth summand is the contemporaneous jump `Mx` at `u=t`. This map is the strictly within-interval case `t0 Date: Thu, 20 Aug 2026 15:34:52 +0000 Subject: [PATCH 45/62] feat(psychometric): recover Driver Eq. 5 of T0TIPREDEFFECT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Map E(y_t) = τ + λ(μ_t + e^{a Δt} t0_b z). Form the evolved-plus-carry latent mean first. Refuse evolved, TIPREDEFFECT, impulse, and carry observed means as that composition. Fail closed on overflow. --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 1 + CLAUDE.md | 2 +- crates/psychometric_core/src/error.rs | 54 ++ crates/psychometric_core/src/event_time.rs | 483 +++++++++++++++++- crates/psychometric_core/src/lib.rs | 17 +- ...multilevel_event_time_recovery_contract.rs | 376 ++++++++++++++ .../scientific_claim_boundary_contract.rs | 155 ++++++ docs/adr/0005-posterior-esem-dsem.md | 4 +- .../multilevel-event-time-recovery.md | 15 +- 10 files changed, 1097 insertions(+), 12 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 4b491d00..d28734d7 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -61,7 +61,7 @@ boundaries above remain the target modular MSA architecture. | `tepp_simulation` | known-truth temporal/event data generation | | `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics | | `tepp_api` | versioned DTO, schema, and export contracts | -| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; first-occasion `T0TIPREDEFFECT` shift is `t0_b z` and Eq. 3 first-summand carry is `e^{A Δt} t0_b z` (`T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | +| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; first-occasion `T0TIPREDEFFECT` shift is `t0_b z` and Eq. 3 first-summand carry is `e^{A Δt} t0_b z` (`T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_b z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | No crate exposes placeholder production behavior in Task 1. This prevents an empty façade from becoming a de facto public API before its invariants and tests diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e140ed6..5fe98439 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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, Eq. 5, p. 5; Eq. 3 first summand, p. 5; Table 3, p. 13; JSS PDF re-opened 2026-08-20T15:28Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar observed-indicator mean of a first-occasion time-independent predictor. Equation 5 writes `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and `Γ ~ N(τ, Ψ)`. Table 3 names `T0TIPREDEFFECT` the effect of time-independent predictors on latents at `T0`. Equation 3's first summand carries that shift as `e^{A Δt} t0_b z`. The scalar composition is `E(y_t) = τ + λ(μ_t + e^{a Δt} t0_b z)`. Form the evolved-plus-carry latent mean first, then `τ + λ` of that mean. The evolved observed mean `τ + λ μ_t` is not this composition. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not this composition. The contemporaneous map `τ + λ(μ_t + m x)` is not this composition. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not this composition when `u ≠ t0`. `MANIFESTMEANS` is not `E(y_t)`. The evolved-plus-carry latent mean is not `E(y_t)`. `T0TIPREDEFFECT` is the coefficient, not that observed mean. A zero loading is exactly `τ`. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T15:14Z: `is_oa: false`; Springer `content/pdf` is HTML 200). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Table 3, p. 13; Eq. 3 first summand, p. 5; JSS PDF opened 2026-08-20T15:14Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar first-occasion time-independent predictor shift and its carry. Table 3 names `T0TIPREDEFFECT` the effect of time-independent predictors on latents at `T0`. Table 2 / Table 3 name `TIPREDEFFECT` `B`, which enters Equation 3 as `A^{-1}[e^{A(t−t0)} − I] B z`. Those are not the same matrix. The scalar first-occasion shift is `t0_b z`. Equation 3's first summand carries that shift as `e^{A Δt} t0_b z`. Form `t0_b z` first, then `e^{a Δt} t0_b z`. Form `μ_t` first, then add that carry. A zero drift is `t0_b z` with no dissipation. Binary64 underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `κ`, and not `M x`. `e^{A Δt} t0_b z` is not `t0_b z`. `T0TIPREDEFFECT` is the coefficient, not `t0_b z`. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T15:14Z: `is_oa: false`; Springer `content/pdf` is HTML 200). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Eq. 3, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T12:12Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar observed-indicator mean of a time-independent predictor. Equation 5 writes `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and `Γ ~ N(τ, Ψ)`. Equation 3 prints the `TIPREDEFFECT` increment as the addend `A^{-1}[e^{A(t−t0)} − I] B z_i` after the `T0MEANS` carry and the `CINT` increment. Table 2 names `B` `TIPREDEFFECT`. The scalar composition is `E(y_t) = τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. Form the evolved-plus-increment latent mean first, then `τ + λ` of that mean. The evolved observed mean `τ + λ μ_t` is not this composition. The contemporaneous map `τ + λ(μ_t + m x)` is not this composition. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not this composition when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The evolved-plus-increment latent mean is not `E(y_t)`. `TIPREDEFFECT` is `B`, not that observed mean. A zero loading is exactly `τ`. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993), Vandenberg and Lance (2000), and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T05:12Z: `is_oa: false`). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T09:01Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar observed-indicator mean of a contemporaneous time-dependent impulse. Equation 5 writes `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and `Γ ~ N(τ, Ψ)`. The latent process at `t` after a contemporaneous Dirac (`u = t`) is `μ_t + m x`. The scalar composition is `E(y_t) = τ + λ(μ_t + m x)`. Form the evolved-plus-impulse latent mean first, then `τ + λ` of that mean. The evolved observed mean `τ + λ μ_t` is not this composition. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not this composition when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The evolved-plus-impulse latent mean is not `E(y_t)`. A zero loading is exactly `τ`. The §7.2 level-change form is a different specification and is not this map. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993), Vandenberg and Lance (2000), and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T05:12Z: `is_oa: false`). diff --git a/CLAUDE.md b/CLAUDE.md index a278da6a..370fe3b7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. Putnick and Bornstein (2016, PMC5145197 opened 2026-08-19T22:15Z) require scalar invariance before latent-mean comparison; residual invariance is not a prerequisite. Two-observation series have no residual degrees of freedom (`ordinary_least_squares_fit` returns residual variance `0`) and cap at strong/scalar; they still license means. This is two-group OLS, not MGCFA. Meredith (1993) names remain unread labels. -- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. The contemporaneous time-dependent predictor impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`). Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). The §7.2 level-change form is not that impulse. The observed mean of that contemporaneous impulse is `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-impulse latent mean is not `E(y_t)`. The time-independent predictor increment is `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`). Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. `TIPREDEFFECT` is `B`, not that discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle et al. (2012, Eq. 14). The observed mean of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The within-interval time-dependent impulse carry is `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation). Form `m x` first, then `e^{a(t−u)} m x`. A zero drift is `m x` with no dissipation. Underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept. `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). An impulse at `u = t` is the contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. The observed mean of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The carried latent mean is not `E(y_t)`. The first-occasion time-independent predictor shift is `t0_b z` (Driver et al., 2017, Table 3 `T0TIPREDEFFECT`; Eq. 3 first summand). Form `t0_b z` first, then `e^{a Δt} t0_b z`. Form `μ_t` first, then add that carry. A zero drift is `t0_b z`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. `e^{A Δt} t0_b z` is not `t0_b z`. `T0TIPREDEFFECT` is the coefficient, not the shift. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. The contemporaneous time-dependent predictor impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`). Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). The §7.2 level-change form is not that impulse. The observed mean of that contemporaneous impulse is `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-impulse latent mean is not `E(y_t)`. The time-independent predictor increment is `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`). Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. `TIPREDEFFECT` is `B`, not that discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle et al. (2012, Eq. 14). The observed mean of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The within-interval time-dependent impulse carry is `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation). Form `m x` first, then `e^{a(t−u)} m x`. A zero drift is `m x` with no dissipation. Underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept. `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). An impulse at `u = t` is the contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. The observed mean of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The carried latent mean is not `E(y_t)`. The first-occasion time-independent predictor shift is `t0_b z` (Driver et al., 2017, Table 3 `T0TIPREDEFFECT`; Eq. 3 first summand). Form `t0_b z` first, then `e^{a Δt} t0_b z`. Form `μ_t` first, then add that carry. A zero drift is `t0_b z`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. `e^{A Δt} t0_b z` is not `t0_b z`. `T0TIPREDEFFECT` is the coefficient, not the shift. The observed mean of that first-occasion carry is `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The evolved-plus-carry latent mean is not `E(y_t)`. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. diff --git a/crates/psychometric_core/src/error.rs b/crates/psychometric_core/src/error.rs index abedd79d..1ba6d907 100644 --- a/crates/psychometric_core/src/error.rs +++ b/crates/psychometric_core/src/error.rs @@ -181,6 +181,24 @@ pub enum PsychometricError { /// Driver Table 3 `T0TIPREDEFFECT` was treated as the first-occasion /// shift. The coefficient is not `t0_b z`. InitialTimeIndependentCoefficientIsNotInitialEffect, + /// Driver Eq. 5 of the Eq. 3 evolved mean was treated as + /// Equation 5 of the Table 3 first-occasion TI predictor. + /// `τ + λ μ_t` is not `τ + λ(μ_t + e^{a Δt} t0_b z)`. + EvolvedObservedMeanIsNotInitialTimeIndependentObservedMean, + /// Driver Eq. 5 of the Eq. 3 process increment was treated as + /// Equation 5 of the Table 3 first-occasion TI predictor. + /// `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not + /// `τ + λ(μ_t + e^{a Δt} t0_b z)`. + TimeIndependentObservedMeanIsNotInitialTimeIndependentObservedMean, + /// Driver Eq. 5 of the contemporaneous impulse was treated as + /// Equation 5 of the Table 3 first-occasion TI predictor. + /// `τ + λ(μ_t + m x)` is not `τ + λ(μ_t + e^{a Δt} t0_b z)`. + ImpulseObservedMeanIsNotInitialTimeIndependentObservedMean, + /// Driver Eq. 5 of the Eq. 1–2 carried latent mean was treated as + /// Equation 5 of the Table 3 first-occasion TI predictor. + /// `τ + λ(μ_t + e^{a(t−u)} m x)` is not + /// `τ + λ(μ_t + e^{a Δt} t0_b z)`. + ImpulseCarryObservedMeanIsNotInitialTimeIndependentObservedMean, } impl fmt::Display for PsychometricError { @@ -334,6 +352,18 @@ impl fmt::Display for PsychometricError { Self::InitialTimeIndependentCoefficientIsNotInitialEffect => { "first-occasion time-independent predictor coefficient is not the first-occasion shift" } + Self::EvolvedObservedMeanIsNotInitialTimeIndependentObservedMean => { + "evolved observed mean is not the first-occasion time-independent-predictor observed mean" + } + Self::TimeIndependentObservedMeanIsNotInitialTimeIndependentObservedMean => { + "time-independent-predictor observed mean is not the first-occasion time-independent-predictor observed mean" + } + Self::ImpulseObservedMeanIsNotInitialTimeIndependentObservedMean => { + "contemporaneous-impulse observed mean is not the first-occasion time-independent-predictor observed mean" + } + Self::ImpulseCarryObservedMeanIsNotInitialTimeIndependentObservedMean => { + "impulse-carry observed mean is not the first-occasion time-independent-predictor observed mean" + } }; formatter.write_str(message) } @@ -576,4 +606,28 @@ mod tests { "first-occasion time-independent predictor coefficient is not the first-occasion shift" ); } + + #[test] + fn initial_time_independent_observed_mean_boundary_messages_are_stable() { + assert_eq!( + PsychometricError::EvolvedObservedMeanIsNotInitialTimeIndependentObservedMean + .to_string(), + "evolved observed mean is not the first-occasion time-independent-predictor observed mean" + ); + assert_eq!( + PsychometricError::TimeIndependentObservedMeanIsNotInitialTimeIndependentObservedMean + .to_string(), + "time-independent-predictor observed mean is not the first-occasion time-independent-predictor observed mean" + ); + assert_eq!( + PsychometricError::ImpulseObservedMeanIsNotInitialTimeIndependentObservedMean + .to_string(), + "contemporaneous-impulse observed mean is not the first-occasion time-independent-predictor observed mean" + ); + assert_eq!( + PsychometricError::ImpulseCarryObservedMeanIsNotInitialTimeIndependentObservedMean + .to_string(), + "impulse-carry observed mean is not the first-occasion time-independent-predictor observed mean" + ); + } } diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index 39179946..ddfa587e 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -56,7 +56,10 @@ //! `T0`. The scalar first-occasion shift is `t0_b z`. Equation 3's //! first summand carries that shift as `e^{A Δt} t0_b z`. That carry //! is not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and -//! not `M x`. The JSS article +//! not `M x`. Equation 5 of that carried first-occasion shift is +//! `τ + λ(μ_t + e^{a Δt} t0_b z)` (`τ + λ μ_t` is not that observed +//! mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that +//! observed mean). The JSS article //! has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). //! The difference quotient `(x(t+Δt) − x(t)) / Δt` (their //! Eqs. 3–4) is refused. This is not DSEM and not a matrix `expm`. @@ -2047,6 +2050,154 @@ pub fn refuse_initial_time_independent_coefficient_as_initial_effect( Err(PsychometricError::InitialTimeIndependentCoefficientIsNotInitialEffect) } +/// Exact scalar observed mean of a first-occasion time-independent +/// predictor. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Eq. 3 first summand, +/// p. 5; Table 3, p. 13; JSS PDF re-opened 2026-08-20T15:28Z from +/// ) +/// write `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and +/// `Γ ~ N(τ, Ψ)`. Table 3 names `T0TIPREDEFFECT` the effect of +/// time-independent predictors on latents at `T0`. Equation 3's +/// first summand carries that shift as `e^{A Δt} t0_b z`. The +/// expected intercept is `τ`. The latent process at `t` after that +/// carry is `μ_t + e^{a Δt} t0_b z`. The scalar composition is +/// `E(y_t) = τ + λ(μ_t + e^{a Δt} t0_b z)`. Form the +/// evolved-plus-carry latent mean first, then `τ + λ` of that mean. +/// A zero loading is exactly `τ`. A zero evolved-plus-carry latent +/// mean is exactly `τ`. A zero intercept is exactly +/// `λ(μ_t + e^{a Δt} t0_b z)`. The evolved observed mean +/// `τ + λ μ_t` is not this composition when the carry is nonzero. +/// The process-increment map +/// `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not this composition. +/// The contemporaneous map `τ + λ(μ_t + m x)` is not this +/// composition. The impulse-carry map +/// `τ + λ(μ_t + e^{a(t−u)} m x)` is not this composition when +/// `u ≠ t0`. `MANIFESTMEANS` is not `E(y_t)`. The +/// evolved-plus-carry latent mean is not `E(y_t)`. +/// `T0TIPREDEFFECT` is the coefficient, not that observed mean. +/// This is not a Kalman filter and not ctsem estimation. +/// +/// # Errors +/// +/// Propagates +/// [`recover_discrete_latent_mean_with_initial_time_independent_predictor`] +/// and [`recover_manifest_observed_mean`]. +#[allow(clippy::too_many_arguments)] +pub fn recover_discrete_observed_mean_with_initial_time_independent_predictor( + loading: f64, + initial_latent_mean: f64, + log_rate: f64, + continuous_intercept: f64, + initial_time_independent_effect: f64, + time_independent_predictor: f64, + manifest_mean: f64, + event_delta: f64, + clock: LagClock, +) -> Result { + let composed_latent_mean = + recover_discrete_latent_mean_with_initial_time_independent_predictor( + initial_latent_mean, + log_rate, + continuous_intercept, + initial_time_independent_effect, + time_independent_predictor, + event_delta, + clock, + )?; + recover_manifest_observed_mean(loading, composed_latent_mean, manifest_mean) +} + +/// Refuse treating the evolved observed mean as the first-occasion +/// time-independent-predictor observed mean. +/// +/// Equation 5 of the Eq. 3 evolved mean is `τ + λ μ_t`. Equation 5 +/// of the Table 3 first-occasion TI predictor is +/// `τ + λ(μ_t + e^{a Δt} t0_b z)`. Those are not the same map. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::EvolvedObservedMeanIsNotInitialTimeIndependentObservedMean`]. +pub fn refuse_evolved_observed_mean_as_initial_time_independent_observed_mean( + evolved_observed_mean: f64, + initial_time_independent_observed_mean: f64, +) -> Result { + let _ = ( + evolved_observed_mean, + initial_time_independent_observed_mean, + ); + Err(PsychometricError::EvolvedObservedMeanIsNotInitialTimeIndependentObservedMean) +} + +/// Refuse treating the process-increment observed mean as the +/// first-occasion time-independent-predictor observed mean. +/// +/// Equation 5 of the Eq. 3 `TIPREDEFFECT` increment is +/// `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. Equation 5 of the +/// Table 3 first-occasion TI predictor is +/// `τ + λ(μ_t + e^{a Δt} t0_b z)`. Those are not the same map. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::TimeIndependentObservedMeanIsNotInitialTimeIndependentObservedMean`]. +pub fn refuse_time_independent_observed_mean_as_initial_time_independent_observed_mean( + time_independent_observed_mean: f64, + initial_time_independent_observed_mean: f64, +) -> Result { + let _ = ( + time_independent_observed_mean, + initial_time_independent_observed_mean, + ); + Err(PsychometricError::TimeIndependentObservedMeanIsNotInitialTimeIndependentObservedMean) +} + +/// Refuse treating the contemporaneous-impulse observed mean as the +/// first-occasion time-independent-predictor observed mean. +/// +/// Equation 5 of the contemporaneous Dirac is `τ + λ(μ_t + m x)`. +/// Equation 5 of the Table 3 first-occasion TI predictor is +/// `τ + λ(μ_t + e^{a Δt} t0_b z)`. Those are not the same map. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::ImpulseObservedMeanIsNotInitialTimeIndependentObservedMean`]. +pub fn refuse_impulse_observed_mean_as_initial_time_independent_observed_mean( + impulse_observed_mean: f64, + initial_time_independent_observed_mean: f64, +) -> Result { + let _ = ( + impulse_observed_mean, + initial_time_independent_observed_mean, + ); + Err(PsychometricError::ImpulseObservedMeanIsNotInitialTimeIndependentObservedMean) +} + +/// Refuse treating the impulse-carry observed mean as the +/// first-occasion time-independent-predictor observed mean. +/// +/// Equation 5 of the Eq. 1–2 carried latent mean is +/// `τ + λ(μ_t + e^{a(t−u)} m x)`. Equation 5 of the Table 3 +/// first-occasion TI predictor is `τ + λ(μ_t + e^{a Δt} t0_b z)`. +/// Those are not the same map. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::ImpulseCarryObservedMeanIsNotInitialTimeIndependentObservedMean`]. +pub fn refuse_impulse_carry_observed_mean_as_initial_time_independent_observed_mean( + impulse_carry_observed_mean: f64, + initial_time_independent_observed_mean: f64, +) -> Result { + let _ = ( + impulse_carry_observed_mean, + initial_time_independent_observed_mean, + ); + Err(PsychometricError::ImpulseCarryObservedMeanIsNotInitialTimeIndependentObservedMean) +} + /// Exact scalar within-interval time-dependent impulse carry from /// Driver Equations 1–2. /// @@ -2621,6 +2772,7 @@ mod tests { recover_discrete_latent_variance, recover_discrete_observed_mean, recover_discrete_observed_mean_with_impulse, recover_discrete_observed_mean_with_impulse_carry, + recover_discrete_observed_mean_with_initial_time_independent_predictor, recover_discrete_observed_mean_with_time_independent_predictor, recover_discrete_process_noise, recover_discrete_time_independent_predictor_effect, recover_discrete_time_varying_predictor_effect, recover_event_series_mean_log_rate, @@ -2638,10 +2790,13 @@ mod tests { refuse_continuous_intercept_as_manifest_means, refuse_difference_quotient_as_local_rate, refuse_evolved_observed_mean_as_impulse_carry_observed_mean, refuse_evolved_observed_mean_as_impulse_observed_mean, + refuse_evolved_observed_mean_as_initial_time_independent_observed_mean, refuse_evolved_observed_mean_as_time_independent_observed_mean, refuse_finite_interval_process_noise_as_stationary_variance, + refuse_impulse_carry_observed_mean_as_initial_time_independent_observed_mean, refuse_impulse_carry_observed_mean_as_time_independent_observed_mean, refuse_impulse_observed_mean_as_impulse_carry_observed_mean, + refuse_impulse_observed_mean_as_initial_time_independent_observed_mean, refuse_impulse_observed_mean_as_time_independent_observed_mean, refuse_initial_latent_mean_as_evolved_mean, refuse_initial_observed_mean_as_evolved_observed_mean, @@ -2669,6 +2824,7 @@ mod tests { refuse_time_independent_effect_as_continuous_intercept, refuse_time_independent_effect_as_time_dependent_impulse, refuse_time_independent_effect_as_time_varying_discrete_effect, + refuse_time_independent_observed_mean_as_initial_time_independent_observed_mean, refuse_trait_variance_as_process_noise, refuse_trait_variance_as_stationary_within_subject, refuse_unmatched_time_varying_predictor_interval, }; @@ -6514,4 +6670,329 @@ mod tests { Err(PsychometricError::InvalidNumericInput) ); } + + #[test] + fn discrete_observed_mean_with_initial_time_independent_predictor_recovers_driver_equation_five() + { + let loading = 2.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let effect = 0.4_f64; + let predictor = 3.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let recovered = recover_discrete_observed_mean_with_initial_time_independent_predictor( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-t0tipred-mean"); + let composed = recover_discrete_latent_mean_with_initial_time_independent_predictor( + initial, + drift, + intercept, + effect, + predictor, + delta, + LagClock::EventTime, + ) + .expect("eq3-t0tipred"); + let expected = manifest_mean + loading * composed; + assert!((recovered - expected).abs() < 1e-15); + let evolved_observed = recover_discrete_observed_mean( + loading, + initial, + drift, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq3-eq5-mean"); + let process_observed = recover_discrete_observed_mean_with_time_independent_predictor( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-tipred-mean"); + let impulse_observed = recover_discrete_observed_mean_with_impulse( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-impulse-mean"); + let carried_observed = recover_discrete_observed_mean_with_impulse_carry( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + 1.0, + LagClock::EventTime, + ) + .expect("eq5-carry-mean"); + assert!((evolved_observed - recovered).abs() > 1e-3); + assert!((process_observed - recovered).abs() > 1e-3); + assert!((impulse_observed - recovered).abs() > 1e-3); + assert!((carried_observed - recovered).abs() > 1e-3); + assert_eq!( + recover_discrete_observed_mean_with_initial_time_independent_predictor( + 0.0, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime + ), + Ok(manifest_mean) + ); + } + + #[test] + fn discrete_observed_mean_with_initial_time_independent_predictor_is_not_evolved_or_zero_carry() + { + let loading = 2.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let effect = 0.4_f64; + let predictor = 3.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let recovered = recover_discrete_observed_mean_with_initial_time_independent_predictor( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-t0tipred-mean"); + let evolved_observed = recover_discrete_observed_mean( + loading, + initial, + drift, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq3-eq5-mean"); + let zero_carry = recover_discrete_observed_mean_with_initial_time_independent_predictor( + loading, + initial, + drift, + intercept, + 0.0, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("zero-carry"); + assert!((zero_carry - evolved_observed).abs() < 1e-15); + assert!((recovered - evolved_observed).abs() > 1e-3); + } + + #[test] + fn discrete_observed_mean_with_initial_time_independent_predictor_refuses_evolved_mean_and_overflow() + { + let loading = 2.0_f64; + let recovered = recover_discrete_observed_mean_with_initial_time_independent_predictor( + loading, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, + LagClock::EventTime, + ) + .expect("eq5-t0tipred-mean"); + let evolved_observed = + recover_discrete_observed_mean(loading, 1.0, -0.5, 0.3, 0.5, 2.0, LagClock::EventTime) + .expect("eq3-eq5-mean"); + let process_observed = recover_discrete_observed_mean_with_time_independent_predictor( + loading, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, + LagClock::EventTime, + ) + .expect("eq5-tipred-mean"); + let impulse_observed = recover_discrete_observed_mean_with_impulse( + loading, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, + LagClock::EventTime, + ) + .expect("eq5-impulse-mean"); + let carried_observed = recover_discrete_observed_mean_with_impulse_carry( + loading, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, + 1.0, + LagClock::EventTime, + ) + .expect("eq5-carry-mean"); + assert_eq!( + refuse_evolved_observed_mean_as_initial_time_independent_observed_mean( + evolved_observed, + recovered + ), + Err(PsychometricError::EvolvedObservedMeanIsNotInitialTimeIndependentObservedMean) + ); + assert_eq!( + refuse_time_independent_observed_mean_as_initial_time_independent_observed_mean( + process_observed, + recovered + ), + Err( + PsychometricError::TimeIndependentObservedMeanIsNotInitialTimeIndependentObservedMean + ) + ); + assert_eq!( + refuse_impulse_observed_mean_as_initial_time_independent_observed_mean( + impulse_observed, + recovered + ), + Err(PsychometricError::ImpulseObservedMeanIsNotInitialTimeIndependentObservedMean) + ); + assert_eq!( + refuse_impulse_carry_observed_mean_as_initial_time_independent_observed_mean( + carried_observed, + recovered + ), + Err(PsychometricError::ImpulseCarryObservedMeanIsNotInitialTimeIndependentObservedMean) + ); + } + + #[test] + fn discrete_observed_mean_with_initial_time_independent_predictor_invalid_inputs_fail_closed() { + let scaled = recover_discrete_observed_mean_with_initial_time_independent_predictor( + 1e308, + 1e-308, + 0.0, + 0.0, + 0.0, + 3.0, + 0.0, + 1.0, + LagClock::EventTime, + ) + .expect("scale"); + assert!((scaled - 1.0).abs() < 1e-15); + let finite_loaded = recover_discrete_observed_mean_with_initial_time_independent_predictor( + 1e308, + 0.0, + 0.0, + 0.0, + 0.0, + 3.0, + 0.0, + 1.0, + LagClock::EventTime, + ) + .expect("lambda-mu0"); + assert!((finite_loaded - 0.0).abs() < 1e-15); + assert_eq!( + recover_discrete_observed_mean_with_initial_time_independent_predictor( + 1e308, + 2.0, + 0.0, + 0.0, + 0.0, + 3.0, + 0.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_observed_mean_with_initial_time_independent_predictor( + 2.0, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_discrete_observed_mean_with_initial_time_independent_predictor( + 2.0, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_observed_mean_with_initial_time_independent_predictor( + 1e308, + 0.0, + 0.0, + 0.0, + 1e308, + 1.0, + 0.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + } } diff --git a/crates/psychometric_core/src/lib.rs b/crates/psychometric_core/src/lib.rs index 46bf76ed..1161d70d 100644 --- a/crates/psychometric_core/src/lib.rs +++ b/crates/psychometric_core/src/lib.rs @@ -56,7 +56,12 @@ //! `T0TIPREDEFFECT` shift `t0_b z` and its Eq. 3 first-summand carry //! `e^{A Δt} t0_b z` (`T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; //! `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is -//! not `t0_b z`), +//! not `t0_b z`), recovers the Driver Eq. 5 of that first-occasion +//! carry as `τ + λ(μ_t + e^{a Δt} t0_b z)` (`τ + λ μ_t` is not that +//! observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not +//! that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; +//! `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when +//! `u ≠ t0`), //! and refuses //! latent-mean comparison below strong invariance. @@ -133,6 +138,8 @@ pub use event_time::recover_discrete_observed_mean; pub use event_time::recover_discrete_observed_mean_with_impulse; /// Exact scalar discrete observed mean of a within-interval impulse carry. pub use event_time::recover_discrete_observed_mean_with_impulse_carry; +/// Exact scalar discrete observed mean of a first-occasion TI predictor. +pub use event_time::recover_discrete_observed_mean_with_initial_time_independent_predictor; /// Exact scalar discrete observed mean of a time-independent predictor. pub use event_time::recover_discrete_observed_mean_with_time_independent_predictor; /// Exact scalar discrete process noise `Q_Δt` on event time. @@ -185,14 +192,20 @@ pub use event_time::refuse_difference_quotient_as_local_rate; pub use event_time::refuse_evolved_observed_mean_as_impulse_carry_observed_mean; /// Refuse treating evolved `τ + λ μ_t` as the contemporaneous-impulse observed mean. pub use event_time::refuse_evolved_observed_mean_as_impulse_observed_mean; +/// Refuse treating evolved `τ + λ μ_t` as the first-occasion TI-predictor observed mean. +pub use event_time::refuse_evolved_observed_mean_as_initial_time_independent_observed_mean; /// Refuse treating evolved `τ + λ μ_t` as the time-independent-predictor observed mean. pub use event_time::refuse_evolved_observed_mean_as_time_independent_observed_mean; /// Refuse treating finite-interval `Q_Δt` as `asymDIFFUSION`. pub use event_time::refuse_finite_interval_process_noise_as_stationary_variance; +/// Refuse treating impulse-carry `τ + λ(μ_t + e^{a(t−u)} m x)` as the first-occasion TI-predictor observed mean. +pub use event_time::refuse_impulse_carry_observed_mean_as_initial_time_independent_observed_mean; /// Refuse treating impulse-carry `τ + λ(μ_t + e^{a(t−u)} m x)` as the time-independent-predictor observed mean. pub use event_time::refuse_impulse_carry_observed_mean_as_time_independent_observed_mean; /// Refuse treating contemporaneous `τ + λ(μ_t + m x)` as the impulse-carry observed mean. pub use event_time::refuse_impulse_observed_mean_as_impulse_carry_observed_mean; +/// Refuse treating contemporaneous `τ + λ(μ_t + m x)` as the first-occasion TI-predictor observed mean. +pub use event_time::refuse_impulse_observed_mean_as_initial_time_independent_observed_mean; /// Refuse treating contemporaneous `τ + λ(μ_t + m x)` as the time-independent-predictor observed mean. pub use event_time::refuse_impulse_observed_mean_as_time_independent_observed_mean; /// Refuse treating Driver Table 2 `T0MEANS` as the evolved latent mean. @@ -249,6 +262,8 @@ pub use event_time::refuse_time_independent_effect_as_continuous_intercept; pub use event_time::refuse_time_independent_effect_as_time_dependent_impulse; /// Refuse treating Driver Eq. 3 `TIPREDEFFECT` increment as Voelkle Eq. 14. pub use event_time::refuse_time_independent_effect_as_time_varying_discrete_effect; +/// Refuse treating process-increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` as the first-occasion TI-predictor observed mean. +pub use event_time::refuse_time_independent_observed_mean_as_initial_time_independent_observed_mean; /// Refuse treating Driver §4.3 trait variance as process noise. pub use event_time::refuse_trait_variance_as_process_noise; /// Refuse treating Driver §4.3 trait variance as `asymDIFFUSION`. 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 30ceb58d..539ac8f1 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -13,6 +13,7 @@ use psychometric_core::{ recover_discrete_latent_mean_with_time_independent_predictor, recover_discrete_latent_variance, recover_discrete_observed_mean, recover_discrete_observed_mean_with_impulse, recover_discrete_observed_mean_with_impulse_carry, + recover_discrete_observed_mean_with_initial_time_independent_predictor, recover_discrete_observed_mean_with_time_independent_predictor, recover_discrete_process_noise, recover_discrete_time_independent_predictor_effect, recover_discrete_time_varying_predictor_effect, recover_event_series_mean_log_rate, @@ -29,10 +30,13 @@ use psychometric_core::{ refuse_continuous_intercept_as_manifest_means, refuse_difference_quotient_as_local_rate, refuse_evolved_observed_mean_as_impulse_carry_observed_mean, refuse_evolved_observed_mean_as_impulse_observed_mean, + refuse_evolved_observed_mean_as_initial_time_independent_observed_mean, refuse_evolved_observed_mean_as_time_independent_observed_mean, refuse_finite_interval_process_noise_as_stationary_variance, + refuse_impulse_carry_observed_mean_as_initial_time_independent_observed_mean, refuse_impulse_carry_observed_mean_as_time_independent_observed_mean, refuse_impulse_observed_mean_as_impulse_carry_observed_mean, + refuse_impulse_observed_mean_as_initial_time_independent_observed_mean, refuse_impulse_observed_mean_as_time_independent_observed_mean, refuse_initial_latent_mean_as_evolved_mean, refuse_initial_observed_mean_as_evolved_observed_mean, @@ -59,6 +63,7 @@ use psychometric_core::{ refuse_time_independent_effect_as_continuous_intercept, refuse_time_independent_effect_as_time_dependent_impulse, refuse_time_independent_effect_as_time_varying_discrete_effect, + refuse_time_independent_observed_mean_as_initial_time_independent_observed_mean, refuse_trait_variance_as_process_noise, refuse_trait_variance_as_stationary_within_subject, refuse_unmatched_time_varying_predictor_interval, }; @@ -1797,6 +1802,377 @@ fn initial_time_independent_predictor_refuses_overflow_and_non_event_clocks() { ); } +#[test] +fn discrete_observed_mean_with_initial_time_independent_predictor_recovers_driver_equation_five() { + let loading = 2.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let effect = 0.4_f64; + let predictor = 3.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let observed = recover_discrete_observed_mean_with_initial_time_independent_predictor( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-t0tipred-mean"); + let composed = recover_discrete_latent_mean_with_initial_time_independent_predictor( + initial, + drift, + intercept, + effect, + predictor, + delta, + LagClock::EventTime, + ) + .expect("eq3-t0tipred"); + let expected = manifest_mean + loading * composed; + let error = rmse(&[expected], &[observed]); + assert!( + error < 1e-15, + "Driver Eq. 5 of Table 3 T0TIPREDEFFECT RMSE {error}" + ); + let evolved_observed = recover_discrete_observed_mean( + loading, + initial, + drift, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq3-eq5-mean"); + let evolved_error = rmse(&[expected], &[evolved_observed]); + assert!( + evolved_error > error, + "τ + λ μ_t is not T0TIPREDEFFECT E(y_t): RMSE {evolved_error} must exceed {error}" + ); + let process_observed = recover_discrete_observed_mean_with_time_independent_predictor( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-tipred-mean"); + let process_error = rmse(&[expected], &[process_observed]); + assert!( + process_error > error, + "τ + λ(μ_t + increment) is not T0TIPREDEFFECT E(y_t): RMSE {process_error} must exceed {error}" + ); +} + +#[test] +fn discrete_observed_mean_with_initial_time_independent_predictor_is_not_impulse_or_carry() { + let loading = 2.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let effect = 0.4_f64; + let predictor = 3.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let observed = recover_discrete_observed_mean_with_initial_time_independent_predictor( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-t0tipred-mean"); + let composed = recover_discrete_latent_mean_with_initial_time_independent_predictor( + initial, + drift, + intercept, + effect, + predictor, + delta, + LagClock::EventTime, + ) + .expect("eq3-t0tipred"); + let expected = manifest_mean + loading * composed; + let error = rmse(&[expected], &[observed]); + let impulse_observed = recover_discrete_observed_mean_with_impulse( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-impulse-mean"); + let impulse_error = rmse(&[expected], &[impulse_observed]); + assert!( + impulse_error > error, + "τ + λ(μ_t + m x) is not T0TIPREDEFFECT E(y_t): RMSE {impulse_error} must exceed {error}" + ); + let carried_observed = recover_discrete_observed_mean_with_impulse_carry( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + 1.0, + LagClock::EventTime, + ) + .expect("eq5-carry-mean"); + let carry_error = rmse(&[expected], &[carried_observed]); + assert!( + carry_error > error, + "τ + λ(μ_t + carry) is not T0TIPREDEFFECT E(y_t): RMSE {carry_error} must exceed {error}" + ); + let intercept_error = rmse(&[expected], &[manifest_mean]); + assert!( + intercept_error > error, + "MANIFESTMEANS is not T0TIPREDEFFECT E(y_t): RMSE {intercept_error} must exceed {error}" + ); + let latent_error = rmse(&[expected], &[composed]); + assert!( + latent_error > error, + "evolved-plus-T0TIPRED latent mean is not E(y_t): RMSE {latent_error} must exceed {error}" + ); +} + +#[test] +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; + let effect = 0.4_f64; + let predictor = 3.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let observed = recover_discrete_observed_mean_with_initial_time_independent_predictor( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-t0tipred-mean"); + let evolved_observed = recover_discrete_observed_mean( + loading, + initial, + drift, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq3-eq5-mean"); + let process_observed = recover_discrete_observed_mean_with_time_independent_predictor( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-tipred-mean"); + let impulse_observed = recover_discrete_observed_mean_with_impulse( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-impulse-mean"); + let carried_observed = recover_discrete_observed_mean_with_impulse_carry( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + 1.0, + LagClock::EventTime, + ) + .expect("eq5-carry-mean"); + assert_eq!( + refuse_evolved_observed_mean_as_initial_time_independent_observed_mean( + evolved_observed, + observed + ), + Err(PsychometricError::EvolvedObservedMeanIsNotInitialTimeIndependentObservedMean) + ); + assert_eq!( + refuse_time_independent_observed_mean_as_initial_time_independent_observed_mean( + process_observed, + observed + ), + Err(PsychometricError::TimeIndependentObservedMeanIsNotInitialTimeIndependentObservedMean) + ); + assert_eq!( + refuse_impulse_observed_mean_as_initial_time_independent_observed_mean( + impulse_observed, + observed + ), + Err(PsychometricError::ImpulseObservedMeanIsNotInitialTimeIndependentObservedMean) + ); + assert_eq!( + refuse_impulse_carry_observed_mean_as_initial_time_independent_observed_mean( + carried_observed, + observed + ), + Err(PsychometricError::ImpulseCarryObservedMeanIsNotInitialTimeIndependentObservedMean) + ); +} + +#[test] +fn discrete_observed_mean_with_initial_time_independent_predictor_zero_loading_is_manifest_mean() { + let loading = 2.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let effect = 0.4_f64; + let predictor = 3.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let observed = recover_discrete_observed_mean_with_initial_time_independent_predictor( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-t0tipred-mean"); + let composed = recover_discrete_latent_mean_with_initial_time_independent_predictor( + initial, + drift, + intercept, + effect, + predictor, + delta, + LagClock::EventTime, + ) + .expect("eq3-t0tipred"); + assert_eq!( + refuse_latent_mean_as_observed_mean(composed, observed), + Err(PsychometricError::LatentMeanIsNotObservedMean) + ); + assert_eq!( + refuse_manifest_means_as_observed_mean(manifest_mean, observed), + Err(PsychometricError::ManifestMeansIsNotObservedMean) + ); + let zero_loading = recover_discrete_observed_mean_with_initial_time_independent_predictor( + 0.0, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("lambda0"); + assert!((zero_loading - manifest_mean).abs() < 1e-15); +} + +#[test] +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, + 2.0, + 0.0, + 0.0, + 0.0, + 3.0, + 0.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_observed_mean_with_initial_time_independent_predictor( + 2.0, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_discrete_observed_mean_with_initial_time_independent_predictor( + 2.0, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + let scaled = recover_discrete_observed_mean_with_initial_time_independent_predictor( + 1e308, + 1e-308, + 0.0, + 0.0, + 0.0, + 3.0, + 0.0, + 1.0, + LagClock::EventTime, + ) + .expect("scale"); + assert!( + (scaled - 1.0).abs() < 1e-15, + "Driver Eq. 5 of Table 3 T0TIPREDEFFECT must keep λ=1e308, μ=1e-308: got {scaled}" + ); +} + #[test] fn discrete_observed_mean_with_time_independent_predictor_recovers_driver_equation_five() { let loading = 2.0_f64; diff --git a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs index aa6eb90e..23609bed 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -11,6 +11,7 @@ use psychometric_core::{ recover_discrete_latent_mean_with_time_independent_predictor, recover_discrete_latent_variance, recover_discrete_observed_mean, recover_discrete_observed_mean_with_impulse, recover_discrete_observed_mean_with_impulse_carry, + recover_discrete_observed_mean_with_initial_time_independent_predictor, recover_discrete_observed_mean_with_time_independent_predictor, recover_discrete_process_noise, recover_discrete_time_independent_predictor_effect, recover_discrete_time_varying_predictor_effect, @@ -27,10 +28,13 @@ use psychometric_core::{ refuse_continuous_intercept_as_manifest_means, refuse_evolved_observed_mean_as_impulse_carry_observed_mean, refuse_evolved_observed_mean_as_impulse_observed_mean, + refuse_evolved_observed_mean_as_initial_time_independent_observed_mean, refuse_evolved_observed_mean_as_time_independent_observed_mean, refuse_finite_interval_process_noise_as_stationary_variance, + refuse_impulse_carry_observed_mean_as_initial_time_independent_observed_mean, refuse_impulse_carry_observed_mean_as_time_independent_observed_mean, refuse_impulse_observed_mean_as_impulse_carry_observed_mean, + refuse_impulse_observed_mean_as_initial_time_independent_observed_mean, refuse_impulse_observed_mean_as_time_independent_observed_mean, refuse_initial_latent_mean_as_evolved_mean, refuse_initial_observed_mean_as_evolved_observed_mean, @@ -55,6 +59,7 @@ use psychometric_core::{ refuse_time_independent_effect_as_continuous_intercept, refuse_time_independent_effect_as_time_dependent_impulse, refuse_time_independent_effect_as_time_varying_discrete_effect, + refuse_time_independent_observed_mean_as_initial_time_independent_observed_mean, refuse_trait_variance_as_process_noise, refuse_trait_variance_as_stationary_within_subject, }; @@ -868,6 +873,156 @@ fn initial_time_independent_predictor_is_not_process_increment_cint_or_impulse() ); } +#[test] +#[allow(clippy::too_many_lines)] +fn evolved_and_process_observed_mean_are_not_initial_time_independent_observed_mean() { + let loading = 2.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let effect = 0.4_f64; + let predictor = 3.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let initial_observed = recover_discrete_observed_mean_with_initial_time_independent_predictor( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-t0tipred-mean"); + let evolved_observed = recover_discrete_observed_mean( + loading, + initial, + drift, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq3-eq5-mean"); + let process_observed = recover_discrete_observed_mean_with_time_independent_predictor( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-tipred-mean"); + let impulse_observed = recover_discrete_observed_mean_with_impulse( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-impulse-mean"); + let carried_observed = recover_discrete_observed_mean_with_impulse_carry( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + 1.0, + LagClock::EventTime, + ) + .expect("eq5-carry-mean"); + let composed = recover_discrete_latent_mean_with_initial_time_independent_predictor( + initial, + drift, + intercept, + effect, + predictor, + delta, + LagClock::EventTime, + ) + .expect("eq3-t0tipred"); + assert!( + (evolved_observed - initial_observed).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 of Table 3 T0TIPREDEFFECT): τ + λ μ_t is not T0TIPREDEFFECT E(y_t)" + ); + assert!( + (process_observed - initial_observed).abs() > 1e-3, + "Driver et al. (2017, Eq. 5): TIPREDEFFECT E(y_t) is not T0TIPREDEFFECT E(y_t)" + ); + assert!( + (impulse_observed - initial_observed).abs() > 1e-3, + "Driver et al. (2017, Eq. 5): τ + λ(μ_t + m x) is not T0TIPREDEFFECT E(y_t)" + ); + assert!( + (carried_observed - initial_observed).abs() > 1e-3, + "Driver et al. (2017, Eq. 5): τ + λ(μ_t + carry) is not T0TIPREDEFFECT E(y_t)" + ); + assert!( + (composed - initial_observed).abs() > 1e-3, + "Driver et al. (2017, Eq. 5): evolved-plus-T0TIPRED latent mean is not E(y_t)" + ); + assert!( + (manifest_mean - initial_observed).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 / Table 2 p. 12): MANIFESTMEANS is not T0TIPREDEFFECT E(y_t)" + ); + assert_eq!( + refuse_evolved_observed_mean_as_initial_time_independent_observed_mean( + evolved_observed, + initial_observed + ), + Err( + psychometric_core::PsychometricError::EvolvedObservedMeanIsNotInitialTimeIndependentObservedMean + ) + ); + assert_eq!( + refuse_time_independent_observed_mean_as_initial_time_independent_observed_mean( + process_observed, + initial_observed + ), + Err( + psychometric_core::PsychometricError::TimeIndependentObservedMeanIsNotInitialTimeIndependentObservedMean + ) + ); + assert_eq!( + refuse_impulse_observed_mean_as_initial_time_independent_observed_mean( + impulse_observed, + initial_observed + ), + Err( + psychometric_core::PsychometricError::ImpulseObservedMeanIsNotInitialTimeIndependentObservedMean + ) + ); + assert_eq!( + refuse_impulse_carry_observed_mean_as_initial_time_independent_observed_mean( + carried_observed, + initial_observed + ), + Err( + psychometric_core::PsychometricError::ImpulseCarryObservedMeanIsNotInitialTimeIndependentObservedMean + ) + ); + assert_eq!( + refuse_latent_mean_as_observed_mean(composed, initial_observed), + Err(psychometric_core::PsychometricError::LatentMeanIsNotObservedMean) + ); + assert_eq!( + refuse_manifest_means_as_observed_mean(manifest_mean, initial_observed), + Err(psychometric_core::PsychometricError::ManifestMeansIsNotObservedMean) + ); +} + #[test] fn time_dependent_impulse_carry_is_not_contemporaneous_cint_tipred_or_equation_fourteen() { let effect = 0.4_f64; diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index 06bf6745..20317fc0 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -1,7 +1,7 @@ # ADR 0005 — Posterior-aware ESEM/DSEM and structural interpretation **Decision status:** Accepted -**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), exact scalar contemporaneous `TDPREDEFFECT` impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; the §7.2 level-change form is not that impulse), exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of that Eq. 3 composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar time-independent `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle Eq. 14; `B` is not that discrete increment), exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of that Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of that carried latent mean; `τ + λ μ_t` is not that observed mean), exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; `T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target +**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), exact scalar contemporaneous `TDPREDEFFECT` impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; the §7.2 level-change form is not that impulse), exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of that Eq. 3 composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar time-independent `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle Eq. 14; `B` is not that discrete increment), exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of that Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of that carried latent mean; `τ + λ μ_t` is not that observed mean), exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; `T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`), exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target **Date:** 2026-08-05 **Supersedes:** None. ADR 0012 governs upstream topic measurement/network coordinates; this ADR governs higher-order psychometric structure and longitudinal interpretation. @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), recovers the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z; `TDPREDEFFECT` is `M`, not `CINT`; `M x` is not `A^{-1}[e^{A Δt} − I] B z`; `M x` is not Voelkle et al., 2012, Eq. 14; the level-change form is not that impulse), recovers the exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition; JSS PDF re-opened 2026-08-20T09:01Z; the evolved map `τ + λ μ_t` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-impulse latent mean is not `E(y_t)`), recovers the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z; `TIPREDEFFECT` is `B`, not `κ`; form `B z` first, then the discrete intercept map; a zero drift is `B z Δt`; `B` is not the discrete increment; `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle Eq. 14), recovers the exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; JSS PDF re-opened 2026-08-20T12:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-increment latent mean is not `E(y_t)`), recovers the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z; form `m x` first, then `e^{a(t−u)} m x`; a zero drift is `m x`; underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept; `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; an impulse at `u = t` is the contemporaneous map; an impulse at `u ≤ t0` is already in `η(t0)`), recovers the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; JSS PDF re-opened 2026-08-20T05:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the carried latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and its Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF opened 2026-08-20T15:14Z; form `t0_b z` first, then `e^{a Δt} t0_b z`; a zero drift is `t0_b z`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; `e^{A Δt} t0_b z` is not `t0_b z`; `T0TIPREDEFFECT` is the coefficient, not the shift), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), recovers the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z; `TDPREDEFFECT` is `M`, not `CINT`; `M x` is not `A^{-1}[e^{A Δt} − I] B z`; `M x` is not Voelkle et al., 2012, Eq. 14; the level-change form is not that impulse), recovers the exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition; JSS PDF re-opened 2026-08-20T09:01Z; the evolved map `τ + λ μ_t` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-impulse latent mean is not `E(y_t)`), recovers the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z; `TIPREDEFFECT` is `B`, not `κ`; form `B z` first, then the discrete intercept map; a zero drift is `B z Δt`; `B` is not the discrete increment; `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle Eq. 14), recovers the exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; JSS PDF re-opened 2026-08-20T12:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-increment latent mean is not `E(y_t)`), recovers the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z; form `m x` first, then `e^{a(t−u)} m x`; a zero drift is `m x`; underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept; `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; an impulse at `u = t` is the contemporaneous map; an impulse at `u ≤ t0` is already in `η(t0)`), recovers the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; JSS PDF re-opened 2026-08-20T05:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the carried latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and its Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF opened 2026-08-20T15:14Z; form `t0_b z` first, then `e^{a Δt} t0_b z`; a zero drift is `t0_b z`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; `e^{A Δt} t0_b z` is not `t0_b z`; `T0TIPREDEFFECT` is the coefficient, not the shift), recovers the exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition; JSS PDF re-opened 2026-08-20T15:28Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. 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 a47c1ee4..1f003680 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -29,15 +29,16 @@ This slice stays inside `psychometric_core`. It does not add a second invariance 23. recover the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; the printed Eq. 3 fourth summand is the contemporaneous Dirac) and refuse treating that carry as the contemporaneous impulse, `CINT`, `TIPREDEFFECT`, or Voelkle et al. (2012, Eq. 14); 24. recover the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`) and refuse treating `τ + λ μ_t` or `τ + λ(μ_t + m x)` as that observed mean; 25. recover the exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and its Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF opened 2026-08-20T15:14Z) and refuse treating `t0_b z` as `A^{-1}[e^{A Δt} − I] B z`, as `κ`, or as `M x`; refuse treating `e^{A Δt} t0_b z` as `t0_b z`; refuse treating the coefficient as the shift; -26. refuse pooling discrete lags from unequal event intervals as one coefficient; -27. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); -28. refuse the difference quotient as a continuous-time rate; -29. apply the same event-time map to CWC residuals (still not DSEM); -30. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). +26. recover the exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition; JSS PDF re-opened 2026-08-20T15:28Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`) and refuse treating `τ + λ μ_t`, `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, `τ + λ(μ_t + m x)`, or `τ + λ(μ_t + e^{a(t−u)} m x)` as that observed mean; +27. refuse pooling discrete lags from unequal event intervals as one coefficient; +28. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); +29. refuse the difference quotient as a continuous-time rate; +30. apply the same event-time map to CWC residuals (still not DSEM); +31. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). ## Claim boundary -This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. The contemporaneous `TDPREDEFFECT` impulse is not the continuous intercept, not the time-independent discrete effect, and not Voelkle et al. (2012, Eq. 14). The time-independent `TIPREDEFFECT` increment is not the continuous intercept, not the contemporaneous impulse, not Voelkle et al. (2012, Eq. 14), and not the coefficient `B`. The within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). The evolved observed mean `τ + λ μ_t` is not the contemporaneous-impulse observed mean `τ + λ(μ_t + m x)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not the impulse-carry observed mean `τ + λ(μ_t + e^{a(t−u)} m x)` when `u ≠ t`. The evolved observed mean `τ + λ μ_t` is not the impulse-carry observed mean. The carried latent mean is not `E(y_t)`. The evolved-plus-impulse latent mean is not `E(y_t)`. The evolved observed mean `τ + λ μ_t` is not the time-independent-predictor observed mean `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not that time-independent-predictor observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that time-independent-predictor observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TIPREDEFFECT` shift `t0_b z` is not the Eq. 3 process increment `A^{-1}[e^{A Δt} − I] B z`, not `κ`, and not `M x`. The Eq. 3 first-summand carry `e^{A Δt} t0_b z` is not `t0_b z` and is not that process increment. `T0TIPREDEFFECT` is the coefficient, not the first-occasion shift. +This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. The contemporaneous `TDPREDEFFECT` impulse is not the continuous intercept, not the time-independent discrete effect, and not Voelkle et al. (2012, Eq. 14). The time-independent `TIPREDEFFECT` increment is not the continuous intercept, not the contemporaneous impulse, not Voelkle et al. (2012, Eq. 14), and not the coefficient `B`. The within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). The evolved observed mean `τ + λ μ_t` is not the contemporaneous-impulse observed mean `τ + λ(μ_t + m x)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not the impulse-carry observed mean `τ + λ(μ_t + e^{a(t−u)} m x)` when `u ≠ t`. The evolved observed mean `τ + λ μ_t` is not the impulse-carry observed mean. The carried latent mean is not `E(y_t)`. The evolved-plus-impulse latent mean is not `E(y_t)`. The evolved observed mean `τ + λ μ_t` is not the time-independent-predictor observed mean `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not that time-independent-predictor observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that time-independent-predictor observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TIPREDEFFECT` shift `t0_b z` is not the Eq. 3 process increment `A^{-1}[e^{A Δt} − I] B z`, not `κ`, and not `M x`. The Eq. 3 first-summand carry `e^{A Δt} t0_b z` is not `t0_b z` and is not that process increment. `T0TIPREDEFFECT` is the coefficient, not the first-occasion shift. The evolved observed mean `τ + λ μ_t` is not the first-occasion TI-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_b z)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion observed mean when `u ≠ t0`. The evolved-plus-T0TIPRED latent mean is not `E(y_t)`. ## Authoritative sources @@ -83,6 +84,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - **Time-dependent predictor impulse carry.** Driver et al. (2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z): Equation 1 writes `dη=(Aη+ξ+Bz+Mχ(t))dt+GdW`. Equation 2 writes `χ_i(t)=Σ x_{i,u} δ(t−u)`. The Green-function integral of that Dirac on `(t0,t)` is `e^{A(t−u)}Mx`. The printed Eq. 3 fourth summand is the contemporaneous jump `Mx` at `u=t`. This map is the strictly within-interval case `t0 Date: Thu, 20 Aug 2026 19:19:16 +0000 Subject: [PATCH 46/62] feat(psychometric): recover Driver Table 3 T0TDPREDEFFECT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Map the first-occasion time-dependent predictor as t0_m x0 and carry it as e^{A Δt} t0_m x0. Table 3 names T0TDPREDEFFECT for latents at T0; TDPREDEFFECT M is the process impulse. Fail closed on overflow. --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 1 + CLAUDE.md | 2 +- crates/psychometric_core/src/error.rs | 73 +++ crates/psychometric_core/src/event_time.rs | 583 +++++++++++++++++- crates/psychometric_core/src/lib.rs | 34 +- ...multilevel_event_time_recovery_contract.rs | 143 ++++- .../scientific_claim_boundary_contract.rs | 125 +++- docs/adr/0005-posterior-esem-dsem.md | 4 +- .../multilevel-event-time-recovery.md | 15 +- 10 files changed, 965 insertions(+), 17 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index d28734d7..b85e42a9 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -61,7 +61,7 @@ boundaries above remain the target modular MSA architecture. | `tepp_simulation` | known-truth temporal/event data generation | | `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics | | `tepp_api` | versioned DTO, schema, and export contracts | -| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; first-occasion `T0TIPREDEFFECT` shift is `t0_b z` and Eq. 3 first-summand carry is `e^{A Δt} t0_b z` (`T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_b z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | +| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; first-occasion `T0TIPREDEFFECT` shift is `t0_b z` and Eq. 3 first-summand carry is `e^{A Δt} t0_b z` (`T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_b z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), first-occasion `T0TDPREDEFFECT` shift is `t0_m x0` and Eq. 3 first-summand carry is `e^{A Δt} t0_m x0` (`T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | No crate exposes placeholder production behavior in Task 1. This prevents an empty façade from becoming a de facto public API before its invariants and tests diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fe98439..0e6a18a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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, Table 3, p. 13; Eq. 3 first summand, p. 5; JSS PDF re-opened 2026-08-20T19:10Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar first-occasion time-dependent predictor shift and its carry. Table 3 names `T0TDPREDEFFECT` the effect of time-dependent predictors on latents at `T0`. Table 2 / Table 3 name `TDPREDEFFECT` `M`, which enters Equation 3 as the printed fourth-summand Dirac `M x` at `u = t`. Those are not the same matrix. The scalar first-occasion shift is `t0_m x0`. Equation 3's first summand carries that shift as `e^{A Δt} t0_m x0`. Form `t0_m x0` first, then `e^{a Δt} t0_m x0`. Form `μ_t` first, then add that carry. A zero drift is `t0_m x0` with no dissipation. Binary64 underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `κ`. `e^{A Δt} t0_m x0` is not `t0_m x0`. `T0TDPREDEFFECT` is the coefficient, not `t0_m x0`. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T19:09Z: `is_oa: false`). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Eq. 3 first summand, p. 5; Table 3, p. 13; JSS PDF re-opened 2026-08-20T15:28Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar observed-indicator mean of a first-occasion time-independent predictor. Equation 5 writes `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and `Γ ~ N(τ, Ψ)`. Table 3 names `T0TIPREDEFFECT` the effect of time-independent predictors on latents at `T0`. Equation 3's first summand carries that shift as `e^{A Δt} t0_b z`. The scalar composition is `E(y_t) = τ + λ(μ_t + e^{a Δt} t0_b z)`. Form the evolved-plus-carry latent mean first, then `τ + λ` of that mean. The evolved observed mean `τ + λ μ_t` is not this composition. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not this composition. The contemporaneous map `τ + λ(μ_t + m x)` is not this composition. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not this composition when `u ≠ t0`. `MANIFESTMEANS` is not `E(y_t)`. The evolved-plus-carry latent mean is not `E(y_t)`. `T0TIPREDEFFECT` is the coefficient, not that observed mean. A zero loading is exactly `τ`. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T15:14Z: `is_oa: false`; Springer `content/pdf` is HTML 200). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Table 3, p. 13; Eq. 3 first summand, p. 5; JSS PDF opened 2026-08-20T15:14Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar first-occasion time-independent predictor shift and its carry. Table 3 names `T0TIPREDEFFECT` the effect of time-independent predictors on latents at `T0`. Table 2 / Table 3 name `TIPREDEFFECT` `B`, which enters Equation 3 as `A^{-1}[e^{A(t−t0)} − I] B z`. Those are not the same matrix. The scalar first-occasion shift is `t0_b z`. Equation 3's first summand carries that shift as `e^{A Δt} t0_b z`. Form `t0_b z` first, then `e^{a Δt} t0_b z`. Form `μ_t` first, then add that carry. A zero drift is `t0_b z` with no dissipation. Binary64 underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `κ`, and not `M x`. `e^{A Δt} t0_b z` is not `t0_b z`. `T0TIPREDEFFECT` is the coefficient, not `t0_b z`. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T15:14Z: `is_oa: false`; Springer `content/pdf` is HTML 200). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Eq. 3, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T12:12Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar observed-indicator mean of a time-independent predictor. Equation 5 writes `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and `Γ ~ N(τ, Ψ)`. Equation 3 prints the `TIPREDEFFECT` increment as the addend `A^{-1}[e^{A(t−t0)} − I] B z_i` after the `T0MEANS` carry and the `CINT` increment. Table 2 names `B` `TIPREDEFFECT`. The scalar composition is `E(y_t) = τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. Form the evolved-plus-increment latent mean first, then `τ + λ` of that mean. The evolved observed mean `τ + λ μ_t` is not this composition. The contemporaneous map `τ + λ(μ_t + m x)` is not this composition. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not this composition when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The evolved-plus-increment latent mean is not `E(y_t)`. `TIPREDEFFECT` is `B`, not that observed mean. A zero loading is exactly `τ`. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993), Vandenberg and Lance (2000), and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T05:12Z: `is_oa: false`). diff --git a/CLAUDE.md b/CLAUDE.md index 370fe3b7..a78f9dce 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. Putnick and Bornstein (2016, PMC5145197 opened 2026-08-19T22:15Z) require scalar invariance before latent-mean comparison; residual invariance is not a prerequisite. Two-observation series have no residual degrees of freedom (`ordinary_least_squares_fit` returns residual variance `0`) and cap at strong/scalar; they still license means. This is two-group OLS, not MGCFA. Meredith (1993) names remain unread labels. -- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. The contemporaneous time-dependent predictor impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`). Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). The §7.2 level-change form is not that impulse. The observed mean of that contemporaneous impulse is `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-impulse latent mean is not `E(y_t)`. The time-independent predictor increment is `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`). Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. `TIPREDEFFECT` is `B`, not that discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle et al. (2012, Eq. 14). The observed mean of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The within-interval time-dependent impulse carry is `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation). Form `m x` first, then `e^{a(t−u)} m x`. A zero drift is `m x` with no dissipation. Underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept. `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). An impulse at `u = t` is the contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. The observed mean of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The carried latent mean is not `E(y_t)`. The first-occasion time-independent predictor shift is `t0_b z` (Driver et al., 2017, Table 3 `T0TIPREDEFFECT`; Eq. 3 first summand). Form `t0_b z` first, then `e^{a Δt} t0_b z`. Form `μ_t` first, then add that carry. A zero drift is `t0_b z`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. `e^{A Δt} t0_b z` is not `t0_b z`. `T0TIPREDEFFECT` is the coefficient, not the shift. The observed mean of that first-occasion carry is `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The evolved-plus-carry latent mean is not `E(y_t)`. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. The contemporaneous time-dependent predictor impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`). Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). The §7.2 level-change form is not that impulse. The observed mean of that contemporaneous impulse is `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-impulse latent mean is not `E(y_t)`. The time-independent predictor increment is `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`). Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. `TIPREDEFFECT` is `B`, not that discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle et al. (2012, Eq. 14). The observed mean of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The within-interval time-dependent impulse carry is `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation). Form `m x` first, then `e^{a(t−u)} m x`. A zero drift is `m x` with no dissipation. Underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept. `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). An impulse at `u = t` is the contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. The observed mean of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The carried latent mean is not `E(y_t)`. The first-occasion time-independent predictor shift is `t0_b z` (Driver et al., 2017, Table 3 `T0TIPREDEFFECT`; Eq. 3 first summand). Form `t0_b z` first, then `e^{a Δt} t0_b z`. Form `μ_t` first, then add that carry. A zero drift is `t0_b z`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. `e^{A Δt} t0_b z` is not `t0_b z`. `T0TIPREDEFFECT` is the coefficient, not the shift. The observed mean of that first-occasion carry is `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The evolved-plus-carry latent mean is not `E(y_t)`. The first-occasion time-dependent predictor shift is `t0_m x0` (Driver et al., 2017, Table 3 `T0TDPREDEFFECT`; Eq. 3 first summand; JSS PDF re-opened 2026-08-20T19:10Z). Form `t0_m x0` first, then `e^{a Δt} t0_m x0`. Form `μ_t` first, then add that carry. A zero drift is `t0_m x0`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`. `e^{A Δt} t0_m x0` is not `t0_m x0`. `T0TDPREDEFFECT` is the coefficient, not the shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. diff --git a/crates/psychometric_core/src/error.rs b/crates/psychometric_core/src/error.rs index 1ba6d907..be3c61b3 100644 --- a/crates/psychometric_core/src/error.rs +++ b/crates/psychometric_core/src/error.rs @@ -199,6 +199,29 @@ pub enum PsychometricError { /// `τ + λ(μ_t + e^{a(t−u)} m x)` is not /// `τ + λ(μ_t + e^{a Δt} t0_b z)`. ImpulseCarryObservedMeanIsNotInitialTimeIndependentObservedMean, + /// Driver Table 3 `T0TDPREDEFFECT` first-occasion shift was treated + /// as the contemporaneous Dirac. `t0_m x0` is not `M x`. + InitialTimeDependentEffectIsNotContemporaneousImpulse, + /// Driver Eq. 3 carry of `T0TDPREDEFFECT` was treated as the + /// first-occasion shift. `e^{A Δt} t0_m x0` is not `t0_m x0`. + InitialTimeDependentCarryIsNotInitialEffect, + /// Driver Table 3 `T0TDPREDEFFECT` first-occasion shift was treated + /// as `CINT`. `t0_m x0` is not `κ`. + InitialTimeDependentEffectIsNotContinuousIntercept, + /// Driver Table 3 `T0TDPREDEFFECT` first-occasion shift was treated + /// as the Eq. 3 process increment. `t0_m x0` is not + /// `A^{-1}[e^{A Δt} − I] B z`. + InitialTimeDependentEffectIsNotProcessIncrement, + /// Driver Table 3 `T0TDPREDEFFECT` first-occasion shift was treated + /// as the Table 3 `T0TIPREDEFFECT` shift. `t0_m x0` is not `t0_b z`. + InitialTimeDependentEffectIsNotInitialTimeIndependentEffect, + /// Driver Table 3 `T0TDPREDEFFECT` was treated as the first-occasion + /// shift. The coefficient is not `t0_m x0`. + InitialTimeDependentCoefficientIsNotInitialEffect, + /// Driver Eq. 3 carry of `T0TDPREDEFFECT` was treated as the + /// within-interval impulse carry. `e^{A Δt} t0_m x0` is not + /// `e^{A(t−u)} M x` for `t0 < u < t`. + InitialTimeDependentCarryIsNotImpulseCarry, } impl fmt::Display for PsychometricError { @@ -364,6 +387,27 @@ impl fmt::Display for PsychometricError { Self::ImpulseCarryObservedMeanIsNotInitialTimeIndependentObservedMean => { "impulse-carry observed mean is not the first-occasion time-independent-predictor observed mean" } + Self::InitialTimeDependentEffectIsNotContemporaneousImpulse => { + "first-occasion time-dependent predictor shift is not the contemporaneous impulse" + } + Self::InitialTimeDependentCarryIsNotInitialEffect => { + "carried first-occasion time-dependent predictor is not the first-occasion shift" + } + Self::InitialTimeDependentEffectIsNotContinuousIntercept => { + "first-occasion time-dependent predictor shift is not the continuous intercept" + } + Self::InitialTimeDependentEffectIsNotProcessIncrement => { + "first-occasion time-dependent predictor shift is not the process increment" + } + Self::InitialTimeDependentEffectIsNotInitialTimeIndependentEffect => { + "first-occasion time-dependent predictor shift is not the first-occasion time-independent predictor shift" + } + Self::InitialTimeDependentCoefficientIsNotInitialEffect => { + "first-occasion time-dependent predictor coefficient is not the first-occasion shift" + } + Self::InitialTimeDependentCarryIsNotImpulseCarry => { + "carried first-occasion time-dependent predictor is not the impulse carry" + } }; formatter.write_str(message) } @@ -629,5 +673,34 @@ mod tests { .to_string(), "impulse-carry observed mean is not the first-occasion time-independent-predictor observed mean" ); + assert_eq!( + PsychometricError::InitialTimeDependentEffectIsNotContemporaneousImpulse.to_string(), + "first-occasion time-dependent predictor shift is not the contemporaneous impulse" + ); + assert_eq!( + PsychometricError::InitialTimeDependentCarryIsNotInitialEffect.to_string(), + "carried first-occasion time-dependent predictor is not the first-occasion shift" + ); + assert_eq!( + PsychometricError::InitialTimeDependentEffectIsNotContinuousIntercept.to_string(), + "first-occasion time-dependent predictor shift is not the continuous intercept" + ); + assert_eq!( + PsychometricError::InitialTimeDependentEffectIsNotProcessIncrement.to_string(), + "first-occasion time-dependent predictor shift is not the process increment" + ); + assert_eq!( + PsychometricError::InitialTimeDependentEffectIsNotInitialTimeIndependentEffect + .to_string(), + "first-occasion time-dependent predictor shift is not the first-occasion time-independent predictor shift" + ); + assert_eq!( + PsychometricError::InitialTimeDependentCoefficientIsNotInitialEffect.to_string(), + "first-occasion time-dependent predictor coefficient is not the first-occasion shift" + ); + assert_eq!( + PsychometricError::InitialTimeDependentCarryIsNotImpulseCarry.to_string(), + "carried first-occasion time-dependent predictor is not the impulse carry" + ); } } diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index ddfa587e..efdf53a6 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -59,7 +59,14 @@ //! not `M x`. Equation 5 of that carried first-occasion shift is //! `τ + λ(μ_t + e^{a Δt} t0_b z)` (`τ + λ μ_t` is not that observed //! mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that -//! observed mean). The JSS article +//! observed mean). Table 3 also names a different matrix +//! `T0TDPREDEFFECT` for time-dependent predictors on latents at +//! `T0`. The scalar first-occasion shift is `t0_m x0`. Equation 3's +//! first summand carries that shift as `e^{A Δt} t0_m x0`. That +//! carry is not `t0_m x0`, not `M x`, not `e^{A(t−u)} M x` for +//! `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and +//! not `CINT`. An impulse at `u ≤ t0` that used `M` is already in +//! `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The JSS article //! has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). //! The difference quotient `(x(t+Δt) − x(t)) / Δt` (their //! Eqs. 3–4) is refused. This is not DSEM and not a matrix `expm`. @@ -2198,6 +2205,277 @@ pub fn refuse_impulse_carry_observed_mean_as_initial_time_independent_observed_m Err(PsychometricError::ImpulseCarryObservedMeanIsNotInitialTimeIndependentObservedMean) } +/// Exact scalar first-occasion time-dependent predictor shift. +/// +/// Driver, Oud, and Voelkle (2017, Table 3, p. 13; Eq. 3 first +/// summand, p. 5; JSS PDF re-opened 2026-08-20T19:10Z from +/// ) +/// name `T0TDPREDEFFECT` the effect of time-dependent predictors on +/// latents at `T0`. Table 2 / Table 3 name `TDPREDEFFECT` `M`, which +/// enters Equation 3 as the printed fourth-summand Dirac `M x` at +/// `u = t`. Those are not the same matrix. The scalar first-occasion +/// shift is `t0_m x0`. It is not `M`, not `M x`, not +/// `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not +/// `A^{-1}[e^{A Δt} − I] B z`, and not `κ`. An impulse at `u ≤ t0` +/// that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as +/// `T0TDPREDEFFECT`. A zero effect or zero predictor is exactly +/// zero. This is not a Kalman filter and not ctsem estimation. +/// +/// # Errors +/// +/// Returns [`PsychometricError::InvalidNumericInput`] when the effect +/// or predictor is non-finite or the product overflows. +pub fn recover_initial_time_dependent_predictor_effect( + initial_time_dependent_effect: f64, + time_dependent_predictor: f64, +) -> Result { + if !initial_time_dependent_effect.is_finite() || !time_dependent_predictor.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + if initial_time_dependent_effect == 0.0 || time_dependent_predictor == 0.0 { + return Ok(0.0); + } + require_finite(initial_time_dependent_effect * time_dependent_predictor) +} + +/// Exact scalar carried first-occasion time-dependent predictor. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 3, p. 5; Table 3, p. 13; JSS +/// PDF re-opened 2026-08-20T19:10Z) write the first summand as +/// `e^{A(t−t0)} η_i(t0)`. A Table 3 `T0TDPREDEFFECT` shift that is +/// already in `η(t0)` therefore appears at `t` as `e^{A Δt} t0_m x0`. +/// Form `t0_m x0` first, then `e^{a Δt} t0_m x0`. A zero drift is +/// `t0_m x0` with no dissipation of the first-occasion shift. +/// Binary64 underflow of `e^{a Δt}` to `+0` is a vanishing carry of +/// that shift and is kept. This carry is not the first-occasion +/// shift, not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not +/// `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`. When +/// `exp` overflows at a finite `a Δt`, rewrite as +/// `sign(t0_m x0) exp(ln|t0_m x0| + a Δt)`. An overflowing rewrite +/// fails closed. This is not a Kalman filter and not ctsem +/// estimation. +/// +/// # Errors +/// +/// Returns [`PsychometricError::EventTimeRequired`] for any non-event +/// clock, [`PsychometricError::NonPositiveInterval`] when +/// `event_delta` is not strictly positive, and +/// [`PsychometricError::InvalidNumericInput`] when an input is +/// non-finite or the mapped carry overflows. +pub fn recover_initial_time_dependent_predictor_carry( + initial_time_dependent_effect: f64, + time_dependent_predictor: f64, + log_rate: f64, + event_delta: f64, + clock: LagClock, +) -> Result { + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + if !event_delta.is_finite() || event_delta <= 0.0 { + return Err(PsychometricError::NonPositiveInterval); + } + if !log_rate.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + let initial_shift = recover_initial_time_dependent_predictor_effect( + initial_time_dependent_effect, + time_dependent_predictor, + )?; + if initial_shift == 0.0 { + return Ok(0.0); + } + let drift_interval = log_rate * event_delta; + let auto_effect = drift_interval.exp(); + if auto_effect.is_finite() { + // +0 underflow is a vanishing carry of the T0 TD shift. + return require_finite(auto_effect * initial_shift); + } + if !drift_interval.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + // Finite a Δt, overflowed exp. + // e^{a Δt} t0_m x0 = sign(t0_m x0) exp(ln|t0_m x0| + a Δt). + require_finite(initial_shift.signum() * (initial_shift.abs().ln() + drift_interval).exp()) +} + +/// Exact scalar evolved latent mean plus a first-occasion TD predictor. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 3, p. 5; Table 3, p. 13) write +/// the first summand as the carried `T0MEANS`, which includes any +/// `T0TDPREDEFFECT` shift already in `η(t0)`. Form `μ_t` first, then +/// add `e^{a Δt} t0_m x0`. A zero carry is exactly `μ_t`. A zero +/// evolved mean is exactly the carry. Adding `t0_m x0` without the +/// exponential is not this composition when `a Δt ≠ 0`. Adding +/// `M x` or `e^{A(t−u)} M x` is not this composition. +/// +/// # Errors +/// +/// Propagates [`recover_discrete_latent_mean`] and +/// [`recover_initial_time_dependent_predictor_carry`], and returns +/// [`PsychometricError::InvalidNumericInput`] when the sum overflows. +#[allow(clippy::too_many_arguments)] +pub fn recover_discrete_latent_mean_with_initial_time_dependent_predictor( + initial_latent_mean: f64, + log_rate: f64, + continuous_intercept: f64, + initial_time_dependent_effect: f64, + time_dependent_predictor: f64, + event_delta: f64, + clock: LagClock, +) -> Result { + let evolved_latent_mean = recover_discrete_latent_mean( + initial_latent_mean, + log_rate, + continuous_intercept, + event_delta, + clock, + )?; + let initial_carry = recover_initial_time_dependent_predictor_carry( + initial_time_dependent_effect, + time_dependent_predictor, + log_rate, + event_delta, + clock, + )?; + if initial_carry == 0.0 { + return Ok(evolved_latent_mean); + } + if evolved_latent_mean == 0.0 { + return Ok(initial_carry); + } + require_finite(evolved_latent_mean + initial_carry) +} + +/// Refuse treating the Table 3 first-occasion TD shift as `M x`. +/// +/// `T0TDPREDEFFECT` shifts `η(t0)`. `TDPREDEFFECT` `M` enters the +/// SDE as the contemporaneous Dirac `M x` at `u = t`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::InitialTimeDependentEffectIsNotContemporaneousImpulse`]. +pub fn refuse_initial_time_dependent_effect_as_contemporaneous_impulse( + initial_time_dependent_effect: f64, + time_dependent_impulse: f64, +) -> Result { + let _ = (initial_time_dependent_effect, time_dependent_impulse); + Err(PsychometricError::InitialTimeDependentEffectIsNotContemporaneousImpulse) +} + +/// Refuse treating the Eq. 3 carry of `T0TDPREDEFFECT` as the +/// first-occasion shift. +/// +/// `e^{A Δt} t0_m x0` is the first summand's contribution at `t`. +/// `t0_m x0` is the shift at `T0`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::InitialTimeDependentCarryIsNotInitialEffect`]. +pub fn refuse_initial_time_dependent_carry_as_initial_effect( + initial_time_dependent_carry: f64, + initial_time_dependent_effect: f64, +) -> Result { + let _ = (initial_time_dependent_carry, initial_time_dependent_effect); + Err(PsychometricError::InitialTimeDependentCarryIsNotInitialEffect) +} + +/// Refuse treating the Table 3 first-occasion TD shift as `CINT`. +/// +/// `t0_m x0` is an initial-mean shift. `κ` is the continuous intercept. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::InitialTimeDependentEffectIsNotContinuousIntercept`]. +pub fn refuse_initial_time_dependent_effect_as_continuous_intercept( + initial_time_dependent_effect: f64, + continuous_intercept: f64, +) -> Result { + let _ = (initial_time_dependent_effect, continuous_intercept); + Err(PsychometricError::InitialTimeDependentEffectIsNotContinuousIntercept) +} + +/// Refuse treating the Table 3 first-occasion TD shift as the Eq. 3 +/// process increment. +/// +/// `t0_m x0` shifts `η(t0)`. `TIPREDEFFECT` `B` maps as +/// `A^{-1}[e^{A Δt} − I] B z`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::InitialTimeDependentEffectIsNotProcessIncrement`]. +pub fn refuse_initial_time_dependent_effect_as_process_increment( + initial_time_dependent_effect: f64, + time_independent_increment: f64, +) -> Result { + let _ = (initial_time_dependent_effect, time_independent_increment); + Err(PsychometricError::InitialTimeDependentEffectIsNotProcessIncrement) +} + +/// Refuse treating the Table 3 first-occasion TD shift as the Table 3 +/// first-occasion TI shift. +/// +/// `T0TDPREDEFFECT` and `T0TIPREDEFFECT` are different Table 3 +/// matrices. `t0_m x0` is not `t0_b z`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::InitialTimeDependentEffectIsNotInitialTimeIndependentEffect`]. +pub fn refuse_initial_time_dependent_effect_as_initial_time_independent_effect( + initial_time_dependent_effect: f64, + initial_time_independent_effect: f64, +) -> Result { + let _ = ( + initial_time_dependent_effect, + initial_time_independent_effect, + ); + Err(PsychometricError::InitialTimeDependentEffectIsNotInitialTimeIndependentEffect) +} + +/// Refuse treating Driver Table 3 `T0TDPREDEFFECT` as the +/// first-occasion shift. +/// +/// `T0TDPREDEFFECT` is the coefficient. The shift is `t0_m x0`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::InitialTimeDependentCoefficientIsNotInitialEffect`]. +pub fn refuse_initial_time_dependent_coefficient_as_initial_effect( + initial_time_dependent_coefficient: f64, + initial_time_dependent_effect: f64, +) -> Result { + let _ = ( + initial_time_dependent_coefficient, + initial_time_dependent_effect, + ); + Err(PsychometricError::InitialTimeDependentCoefficientIsNotInitialEffect) +} + +/// Refuse treating the Eq. 3 carry of `T0TDPREDEFFECT` as the +/// within-interval impulse carry. +/// +/// `e^{A Δt} t0_m x0` carries a Table 3 first-occasion TD shift. +/// `e^{A(t−u)} M x` for `t0 < u < t` carries a Table 2 Dirac that +/// occurred inside the interval. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::InitialTimeDependentCarryIsNotImpulseCarry`]. +pub fn refuse_initial_time_dependent_carry_as_impulse_carry( + initial_time_dependent_carry: f64, + impulse_carry: f64, +) -> Result { + let _ = (initial_time_dependent_carry, impulse_carry); + Err(PsychometricError::InitialTimeDependentCarryIsNotImpulseCarry) +} + /// Exact scalar within-interval time-dependent impulse carry from /// Driver Equations 1–2. /// @@ -2767,6 +3045,7 @@ mod tests { recover_discrete_lag_one, recover_discrete_lagged_latent_covariance, recover_discrete_latent_mean, recover_discrete_latent_mean_with_impulse, recover_discrete_latent_mean_with_impulse_carry, + recover_discrete_latent_mean_with_initial_time_dependent_predictor, recover_discrete_latent_mean_with_initial_time_independent_predictor, recover_discrete_latent_mean_with_time_independent_predictor, recover_discrete_latent_variance, recover_discrete_observed_mean, @@ -2777,6 +3056,8 @@ mod tests { recover_discrete_process_noise, recover_discrete_time_independent_predictor_effect, recover_discrete_time_varying_predictor_effect, recover_event_series_mean_log_rate, recover_event_time_discrete_lag_and_log_rate, + recover_initial_time_dependent_predictor_carry, + recover_initial_time_dependent_predictor_effect, recover_initial_time_independent_predictor_carry, recover_initial_time_independent_predictor_effect, recover_irregular_centered_residual_log_rate, recover_local_log_rate, @@ -2800,6 +3081,13 @@ mod tests { refuse_impulse_observed_mean_as_time_independent_observed_mean, refuse_initial_latent_mean_as_evolved_mean, refuse_initial_observed_mean_as_evolved_observed_mean, + refuse_initial_time_dependent_carry_as_impulse_carry, + refuse_initial_time_dependent_carry_as_initial_effect, + refuse_initial_time_dependent_coefficient_as_initial_effect, + refuse_initial_time_dependent_effect_as_contemporaneous_impulse, + refuse_initial_time_dependent_effect_as_continuous_intercept, + refuse_initial_time_dependent_effect_as_initial_time_independent_effect, + refuse_initial_time_dependent_effect_as_process_increment, refuse_initial_time_independent_carry_as_initial_effect, refuse_initial_time_independent_coefficient_as_initial_effect, refuse_initial_time_independent_effect_as_continuous_intercept, @@ -6995,4 +7283,297 @@ mod tests { Err(PsychometricError::InvalidNumericInput) ); } + + #[test] + fn initial_time_dependent_predictor_recovers_table_three_t0_shift_and_carry() { + let effect = 0.4_f64; + let predictor = 3.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let shift = + recover_initial_time_dependent_predictor_effect(effect, predictor).expect("t0-tdpred"); + assert!((shift - 1.2).abs() < 1e-15); + assert_eq!( + recover_initial_time_dependent_predictor_effect(0.0, predictor), + Ok(0.0) + ); + assert_eq!( + recover_initial_time_dependent_predictor_effect(effect, 0.0), + Ok(0.0) + ); + let carry = recover_initial_time_dependent_predictor_carry( + effect, + predictor, + drift, + delta, + LagClock::EventTime, + ) + .expect("t0-td-carry"); + let expected = 1.2 * (drift * delta).exp(); + assert!((carry - expected).abs() < 1e-15); + let zero_drift = recover_initial_time_dependent_predictor_carry( + effect, + predictor, + 0.0, + delta, + LagClock::EventTime, + ) + .expect("zero-drift"); + assert!((zero_drift - 1.2).abs() < 1e-15); + let vanished = recover_initial_time_dependent_predictor_carry( + effect, + predictor, + -800.0, + 1.0, + LagClock::EventTime, + ) + .expect("underflow"); + assert_eq!(vanished.to_bits(), 0.0_f64.to_bits()); + let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("tdpred"); + let increment = recover_discrete_time_independent_predictor_effect( + effect, + predictor, + drift, + delta, + LagClock::EventTime, + ) + .expect("tipred"); + let tipred_shift = recover_initial_time_independent_predictor_effect(effect, predictor) + .expect("t0-tipred"); + let impulse_carry = recover_time_dependent_predictor_impulse_carry( + effect, + predictor, + drift, + delta, + 1.0, + LagClock::EventTime, + ) + .expect("td-carry"); + assert!((carry - shift).abs() > 1e-3); + assert!((carry - increment).abs() > 1e-3); + assert!((shift - increment).abs() > 1e-3); + assert!((shift - effect).abs() > 1e-3); + assert!((carry - impulse_carry).abs() > 1e-3); + // Algebraically a product, like M x and t0_b z, but Table 3 names a different matrix. + assert!((shift - impulse).abs() < 1e-15); + assert!((shift - tipred_shift).abs() < 1e-15); + } + + #[test] + fn initial_time_dependent_predictor_composes_evolved_mean_and_keeps_scale() { + let effect = 0.4_f64; + let predictor = 3.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let carry = recover_initial_time_dependent_predictor_carry( + effect, + predictor, + drift, + delta, + LagClock::EventTime, + ) + .expect("t0-td-carry"); + let initial = 1.0_f64; + let intercept = 0.3_f64; + let composed = recover_discrete_latent_mean_with_initial_time_dependent_predictor( + initial, + drift, + intercept, + effect, + predictor, + delta, + LagClock::EventTime, + ) + .expect("eq3-t0tdpred"); + let evolved = + recover_discrete_latent_mean(initial, drift, intercept, delta, LagClock::EventTime) + .expect("mu-t"); + assert!((composed - (evolved + carry)).abs() < 1e-15); + assert_eq!( + recover_discrete_latent_mean_with_initial_time_dependent_predictor( + initial, + drift, + intercept, + 0.0, + predictor, + delta, + LagClock::EventTime + ), + Ok(evolved) + ); + assert_eq!( + recover_discrete_latent_mean_with_initial_time_dependent_predictor( + 0.0, + drift, + 0.0, + effect, + predictor, + delta, + LagClock::EventTime + ), + Ok(carry) + ); + let scaled = recover_initial_time_dependent_predictor_carry( + 1e308, + 1e-308, + 0.0, + 1.0, + LagClock::EventTime, + ) + .expect("scale"); + assert!((scaled - 1.0).abs() < 1e-15); + let rewritten = recover_initial_time_dependent_predictor_carry( + 2.0, + 0.5, + 710.0, + 1.0, + LagClock::EventTime, + ); + assert_eq!(rewritten, Err(PsychometricError::InvalidNumericInput)); + let finite_rewrite = recover_initial_time_dependent_predictor_carry( + 1e-308, + 1.0, + 700.0, + 1.0, + LagClock::EventTime, + ) + .expect("log-rewrite"); + let expected_rewrite = (1e-308_f64.ln() + 700.0).exp(); + assert!((finite_rewrite - expected_rewrite).abs() / expected_rewrite < 1e-12); + } + + #[test] + fn initial_time_dependent_predictor_refuses_impulse_cint_process_and_coefficient() { + let effect = 0.4_f64; + let predictor = 3.0_f64; + let shift = + recover_initial_time_dependent_predictor_effect(effect, predictor).expect("t0-tdpred"); + let carry = recover_initial_time_dependent_predictor_carry( + effect, + predictor, + -0.5, + 2.0, + LagClock::EventTime, + ) + .expect("t0-td-carry"); + let increment = recover_discrete_time_independent_predictor_effect( + effect, + predictor, + -0.5, + 2.0, + LagClock::EventTime, + ) + .expect("tipred"); + let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("tdpred"); + let tipred_shift = recover_initial_time_independent_predictor_effect(effect, predictor) + .expect("t0-tipred"); + let impulse_carry = recover_time_dependent_predictor_impulse_carry( + effect, + predictor, + -0.5, + 2.0, + 1.0, + LagClock::EventTime, + ) + .expect("td-carry"); + assert_eq!( + refuse_initial_time_dependent_effect_as_contemporaneous_impulse(shift, impulse), + Err(PsychometricError::InitialTimeDependentEffectIsNotContemporaneousImpulse) + ); + assert_eq!( + refuse_initial_time_dependent_carry_as_initial_effect(carry, shift), + Err(PsychometricError::InitialTimeDependentCarryIsNotInitialEffect) + ); + assert_eq!( + refuse_initial_time_dependent_effect_as_continuous_intercept(shift, 0.4), + Err(PsychometricError::InitialTimeDependentEffectIsNotContinuousIntercept) + ); + assert_eq!( + refuse_initial_time_dependent_effect_as_process_increment(shift, increment), + Err(PsychometricError::InitialTimeDependentEffectIsNotProcessIncrement) + ); + assert_eq!( + refuse_initial_time_dependent_effect_as_initial_time_independent_effect( + shift, + tipred_shift + ), + Err(PsychometricError::InitialTimeDependentEffectIsNotInitialTimeIndependentEffect) + ); + assert_eq!( + refuse_initial_time_dependent_coefficient_as_initial_effect(effect, shift), + Err(PsychometricError::InitialTimeDependentCoefficientIsNotInitialEffect) + ); + assert_eq!( + refuse_initial_time_dependent_carry_as_impulse_carry(carry, impulse_carry), + Err(PsychometricError::InitialTimeDependentCarryIsNotImpulseCarry) + ); + } + + #[test] + fn initial_time_dependent_predictor_invalid_inputs_fail_closed() { + assert_eq!( + recover_initial_time_dependent_predictor_effect(f64::NAN, 1.0), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_initial_time_dependent_predictor_effect(1.0, f64::INFINITY), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_initial_time_dependent_predictor_effect(1e308, 2.0), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_initial_time_dependent_predictor_carry( + 0.4, + 3.0, + f64::NAN, + 2.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_initial_time_dependent_predictor_carry( + 0.4, + 3.0, + -0.5, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_initial_time_dependent_predictor_carry( + 0.4, + 3.0, + -0.5, + 2.0, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_discrete_latent_mean_with_initial_time_dependent_predictor( + 1e308, + 0.0, + 0.0, + 1e308, + 1.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_initial_time_dependent_predictor_carry( + 1.0, + 1.0, + f64::INFINITY, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + } } diff --git a/crates/psychometric_core/src/lib.rs b/crates/psychometric_core/src/lib.rs index 1161d70d..6a84d121 100644 --- a/crates/psychometric_core/src/lib.rs +++ b/crates/psychometric_core/src/lib.rs @@ -61,7 +61,13 @@ //! observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not //! that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; //! `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when -//! `u ≠ t0`), +//! `u ≠ t0`), recovers the Driver Table 3 first-occasion +//! `T0TDPREDEFFECT` shift `t0_m x0` and its Eq. 3 first-summand carry +//! `e^{A Δt} t0_m x0` (`T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; +//! `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; +//! `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; +//! `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` +//! is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), //! and refuses //! latent-mean comparison below strong invariance. @@ -126,6 +132,8 @@ pub use event_time::recover_discrete_latent_mean; pub use event_time::recover_discrete_latent_mean_with_impulse; /// Exact scalar evolved latent mean plus a within-interval impulse carry. pub use event_time::recover_discrete_latent_mean_with_impulse_carry; +/// Exact scalar evolved latent mean plus a first-occasion TD predictor. +pub use event_time::recover_discrete_latent_mean_with_initial_time_dependent_predictor; /// Exact scalar evolved latent mean plus a first-occasion TI predictor. pub use event_time::recover_discrete_latent_mean_with_initial_time_independent_predictor; /// Exact scalar evolved latent mean plus a time-independent predictor. @@ -152,6 +160,10 @@ pub use event_time::recover_discrete_time_varying_predictor_effect; pub use event_time::recover_event_series_mean_log_rate; /// Exact scalar pair `(φ, a)` on event time. pub use event_time::recover_event_time_discrete_lag_and_log_rate; +/// Exact scalar carried first-occasion `T0TDPREDEFFECT` `e^{A Δt} t0_m x0`. +pub use event_time::recover_initial_time_dependent_predictor_carry; +/// Exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0`. +pub use event_time::recover_initial_time_dependent_predictor_effect; /// Exact scalar carried first-occasion `T0TIPREDEFFECT` `e^{A Δt} t0_b z`. pub use event_time::recover_initial_time_independent_predictor_carry; /// Exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z`. @@ -212,15 +224,29 @@ pub use event_time::refuse_impulse_observed_mean_as_time_independent_observed_me pub use event_time::refuse_initial_latent_mean_as_evolved_mean; /// Refuse treating first-occasion `τ + λ μ_0` as `E(y_t)`. pub use event_time::refuse_initial_observed_mean_as_evolved_observed_mean; +/// Refuse treating the Eq. 3 `T0TDPREDEFFECT` carry as the within-interval impulse carry. +pub use event_time::refuse_initial_time_dependent_carry_as_impulse_carry; +/// Refuse treating the Eq. 3 `T0TDPREDEFFECT` carry as the first-occasion shift. +pub use event_time::refuse_initial_time_dependent_carry_as_initial_effect; +/// Refuse treating Driver Table 3 `T0TDPREDEFFECT` as the first-occasion shift. +pub use event_time::refuse_initial_time_dependent_coefficient_as_initial_effect; +/// Refuse treating the Table 3 first-occasion TD shift as `M x`. +pub use event_time::refuse_initial_time_dependent_effect_as_contemporaneous_impulse; +/// Refuse treating the Table 3 first-occasion TD shift as `CINT`. +pub use event_time::refuse_initial_time_dependent_effect_as_continuous_intercept; +/// Refuse treating the Table 3 first-occasion TD shift as the Table 3 TI shift. +pub use event_time::refuse_initial_time_dependent_effect_as_initial_time_independent_effect; +/// Refuse treating the Table 3 first-occasion TD shift as the Eq. 3 process increment. +pub use event_time::refuse_initial_time_dependent_effect_as_process_increment; /// Refuse treating the Eq. 3 `T0TIPREDEFFECT` carry as the first-occasion shift. pub use event_time::refuse_initial_time_independent_carry_as_initial_effect; /// Refuse treating Driver Table 3 `T0TIPREDEFFECT` as the first-occasion shift. pub use event_time::refuse_initial_time_independent_coefficient_as_initial_effect; -/// Refuse treating the Table 3 first-occasion shift as `CINT`. +/// Refuse treating the Table 3 first-occasion TI shift as `CINT`. pub use event_time::refuse_initial_time_independent_effect_as_continuous_intercept; -/// Refuse treating the Table 3 first-occasion shift as the Eq. 3 process increment. +/// Refuse treating the Table 3 first-occasion TI shift as the Eq. 3 process increment. pub use event_time::refuse_initial_time_independent_effect_as_process_increment; -/// Refuse treating the Table 3 first-occasion shift as `M x`. +/// Refuse treating the Table 3 first-occasion TI shift as `M x`. pub use event_time::refuse_initial_time_independent_effect_as_time_dependent_impulse; /// Refuse treating Driver Eq. 3–4 lagged latent covariance as `cov(y_t, y_{t-1})`. pub use event_time::refuse_latent_lagged_covariance_as_observed_covariance; 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 539ac8f1..0dea933f 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -9,6 +9,7 @@ use psychometric_core::{ recover_discrete_lag_from_log_rate, recover_discrete_lagged_latent_covariance, recover_discrete_latent_mean, recover_discrete_latent_mean_with_impulse, recover_discrete_latent_mean_with_impulse_carry, + recover_discrete_latent_mean_with_initial_time_dependent_predictor, recover_discrete_latent_mean_with_initial_time_independent_predictor, recover_discrete_latent_mean_with_time_independent_predictor, recover_discrete_latent_variance, recover_discrete_observed_mean, recover_discrete_observed_mean_with_impulse, @@ -17,7 +18,9 @@ use psychometric_core::{ recover_discrete_observed_mean_with_time_independent_predictor, recover_discrete_process_noise, recover_discrete_time_independent_predictor_effect, recover_discrete_time_varying_predictor_effect, recover_event_series_mean_log_rate, - recover_event_time_discrete_lag_and_log_rate, recover_initial_time_independent_predictor_carry, + recover_event_time_discrete_lag_and_log_rate, recover_initial_time_dependent_predictor_carry, + recover_initial_time_dependent_predictor_effect, + recover_initial_time_independent_predictor_carry, recover_initial_time_independent_predictor_effect, recover_irregular_centered_residual_log_rate, recover_kish_weighted_slope, recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, @@ -40,6 +43,13 @@ use psychometric_core::{ refuse_impulse_observed_mean_as_time_independent_observed_mean, refuse_initial_latent_mean_as_evolved_mean, refuse_initial_observed_mean_as_evolved_observed_mean, + refuse_initial_time_dependent_carry_as_impulse_carry, + refuse_initial_time_dependent_carry_as_initial_effect, + refuse_initial_time_dependent_coefficient_as_initial_effect, + refuse_initial_time_dependent_effect_as_contemporaneous_impulse, + refuse_initial_time_dependent_effect_as_continuous_intercept, + refuse_initial_time_dependent_effect_as_initial_time_independent_effect, + refuse_initial_time_dependent_effect_as_process_increment, refuse_initial_time_independent_carry_as_initial_effect, refuse_initial_time_independent_coefficient_as_initial_effect, refuse_initial_time_independent_effect_as_continuous_intercept, @@ -1802,6 +1812,137 @@ fn initial_time_independent_predictor_refuses_overflow_and_non_event_clocks() { ); } +#[test] +#[allow(clippy::too_many_lines)] +fn initial_time_dependent_predictor_recovers_driver_table_three_t0_shift() { + let effect = 0.4_f64; + let predictor = 3.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let shift = + recover_initial_time_dependent_predictor_effect(effect, predictor).expect("t0-tdpred"); + let error = rmse(&[1.2], &[shift]); + assert!( + error < 1e-15, + "Driver Table 3 T0TDPREDEFFECT RMSE {error}: got {shift}" + ); + let carry = recover_initial_time_dependent_predictor_carry( + effect, + predictor, + drift, + delta, + LagClock::EventTime, + ) + .expect("t0-td-carry"); + let expected_carry = 1.2 * (drift * delta).exp(); + let carry_error = rmse(&[expected_carry], &[carry]); + assert!( + carry_error < 1e-15, + "Driver Eq. 3 first-summand T0TDPREDEFFECT carry RMSE {carry_error}: got {carry}" + ); + let increment = recover_discrete_time_independent_predictor_effect( + effect, + predictor, + drift, + delta, + LagClock::EventTime, + ) + .expect("tipred"); + let intercept_effect = + recover_discrete_continuous_intercept_effect(effect, drift, delta, LagClock::EventTime) + .expect("cint"); + let impulse_carry = recover_time_dependent_predictor_impulse_carry( + effect, + predictor, + drift, + delta, + 1.0, + LagClock::EventTime, + ) + .expect("td-carry"); + assert!(rmse(&[carry], &[shift]) > rmse(&[expected_carry], &[carry])); + assert!(rmse(&[carry], &[increment]) > rmse(&[expected_carry], &[carry])); + assert!(rmse(&[shift], &[increment]) > rmse(&[1.2], &[shift])); + assert!(rmse(&[shift], &[intercept_effect]) > rmse(&[1.2], &[shift])); + assert!(rmse(&[shift], &[effect]) > rmse(&[1.2], &[shift])); + assert!(rmse(&[carry], &[impulse_carry]) > rmse(&[expected_carry], &[carry])); + let composed = recover_discrete_latent_mean_with_initial_time_dependent_predictor( + 1.0, + drift, + 0.3, + effect, + predictor, + delta, + LagClock::EventTime, + ) + .expect("eq3-t0tdpred"); + let evolved = + recover_discrete_latent_mean(1.0, drift, 0.3, delta, LagClock::EventTime).expect("mu-t"); + let composed_error = rmse(&[evolved + carry], &[composed]); + assert!( + composed_error < 1e-15, + "Driver Eq. 3 μ_t + e^{{A Δt}} t0_m x0 RMSE {composed_error}: got {composed}" + ); + let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("tdpred"); + let tipred_shift = + recover_initial_time_independent_predictor_effect(effect, predictor).expect("t0-tipred"); + assert_eq!( + refuse_initial_time_dependent_effect_as_contemporaneous_impulse(shift, impulse), + Err(PsychometricError::InitialTimeDependentEffectIsNotContemporaneousImpulse) + ); + assert_eq!( + refuse_initial_time_dependent_carry_as_initial_effect(carry, shift), + Err(PsychometricError::InitialTimeDependentCarryIsNotInitialEffect) + ); + assert_eq!( + refuse_initial_time_dependent_effect_as_continuous_intercept(shift, effect), + Err(PsychometricError::InitialTimeDependentEffectIsNotContinuousIntercept) + ); + assert_eq!( + refuse_initial_time_dependent_effect_as_process_increment(shift, increment), + Err(PsychometricError::InitialTimeDependentEffectIsNotProcessIncrement) + ); + assert_eq!( + refuse_initial_time_dependent_effect_as_initial_time_independent_effect( + shift, + tipred_shift + ), + Err(PsychometricError::InitialTimeDependentEffectIsNotInitialTimeIndependentEffect) + ); + assert_eq!( + refuse_initial_time_dependent_coefficient_as_initial_effect(effect, shift), + Err(PsychometricError::InitialTimeDependentCoefficientIsNotInitialEffect) + ); + assert_eq!( + refuse_initial_time_dependent_carry_as_impulse_carry(carry, impulse_carry), + Err(PsychometricError::InitialTimeDependentCarryIsNotImpulseCarry) + ); +} + +#[test] +fn initial_time_dependent_predictor_refuses_overflow_and_non_event_clocks() { + assert_eq!( + recover_initial_time_dependent_predictor_effect(1e308, 2.0), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_initial_time_dependent_predictor_carry(0.4, 3.0, -0.5, 2.0, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_discrete_latent_mean_with_initial_time_dependent_predictor( + 1e308, + 0.0, + 0.0, + 1e308, + 1.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); +} + #[test] fn discrete_observed_mean_with_initial_time_independent_predictor_recovers_driver_equation_five() { let loading = 2.0_f64; diff --git a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs index 23609bed..23538c76 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -7,6 +7,7 @@ use psychometric_core::{ recover_discrete_continuous_intercept_effect, recover_discrete_lagged_latent_covariance, recover_discrete_latent_mean, recover_discrete_latent_mean_with_impulse, recover_discrete_latent_mean_with_impulse_carry, + recover_discrete_latent_mean_with_initial_time_dependent_predictor, recover_discrete_latent_mean_with_initial_time_independent_predictor, recover_discrete_latent_mean_with_time_independent_predictor, recover_discrete_latent_variance, recover_discrete_observed_mean, recover_discrete_observed_mean_with_impulse, @@ -14,7 +15,8 @@ use psychometric_core::{ recover_discrete_observed_mean_with_initial_time_independent_predictor, recover_discrete_observed_mean_with_time_independent_predictor, recover_discrete_process_noise, recover_discrete_time_independent_predictor_effect, - recover_discrete_time_varying_predictor_effect, + recover_discrete_time_varying_predictor_effect, recover_initial_time_dependent_predictor_carry, + recover_initial_time_dependent_predictor_effect, recover_initial_time_independent_predictor_carry, recover_initial_time_independent_predictor_effect, recover_irregular_centered_residual_log_rate, recover_loading_point_estimate_mean, @@ -38,6 +40,13 @@ use psychometric_core::{ refuse_impulse_observed_mean_as_time_independent_observed_mean, refuse_initial_latent_mean_as_evolved_mean, refuse_initial_observed_mean_as_evolved_observed_mean, + refuse_initial_time_dependent_carry_as_impulse_carry, + refuse_initial_time_dependent_carry_as_initial_effect, + refuse_initial_time_dependent_coefficient_as_initial_effect, + refuse_initial_time_dependent_effect_as_contemporaneous_impulse, + refuse_initial_time_dependent_effect_as_continuous_intercept, + refuse_initial_time_dependent_effect_as_initial_time_independent_effect, + refuse_initial_time_dependent_effect_as_process_increment, refuse_initial_time_independent_carry_as_initial_effect, refuse_initial_time_independent_coefficient_as_initial_effect, refuse_initial_time_independent_effect_as_continuous_intercept, @@ -873,6 +882,120 @@ fn initial_time_independent_predictor_is_not_process_increment_cint_or_impulse() ); } +#[test] +#[allow(clippy::too_many_lines)] +fn initial_time_dependent_predictor_is_not_impulse_cint_process_or_t0_tipred() { + let effect = 0.4_f64; + let predictor = 3.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let shift = + recover_initial_time_dependent_predictor_effect(effect, predictor).expect("t0-tdpred"); + let carry = recover_initial_time_dependent_predictor_carry( + effect, + predictor, + drift, + delta, + LagClock::EventTime, + ) + .expect("t0-td-carry"); + let increment = recover_discrete_time_independent_predictor_effect( + effect, + predictor, + drift, + delta, + LagClock::EventTime, + ) + .expect("tipred"); + let intercept_effect = + recover_discrete_continuous_intercept_effect(effect, drift, delta, LagClock::EventTime) + .expect("cint"); + let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("tdpred"); + let tipred_shift = + recover_initial_time_independent_predictor_effect(effect, predictor).expect("t0-tipred"); + let impulse_carry = recover_time_dependent_predictor_impulse_carry( + effect, + predictor, + drift, + delta, + 1.0, + LagClock::EventTime, + ) + .expect("td-carry"); + let evolved = + recover_discrete_latent_mean(1.0, drift, 0.3, delta, LagClock::EventTime).expect("mu-t"); + let composed = recover_discrete_latent_mean_with_initial_time_dependent_predictor( + 1.0, + drift, + 0.3, + effect, + predictor, + delta, + LagClock::EventTime, + ) + .expect("eq3-t0tdpred"); + assert!( + (shift - effect).abs() > 1e-3, + "Driver et al. (2017, Table 3 p. 13): T0TDPREDEFFECT is not t0_m x0" + ); + assert!( + (shift - increment).abs() > 1e-3, + "Driver et al. (2017, Table 3 / Eq. 3): t0_m x0 is not A^{{-1}}[e^{{A Δt}} − I] B z" + ); + assert!( + (carry - shift).abs() > 1e-3, + "Driver et al. (2017, Eq. 3): e^{{A Δt}} t0_m x0 is not t0_m x0" + ); + assert!( + (carry - impulse_carry).abs() > 1e-3, + "Driver et al. (2017, Eq. 3): e^{{A Δt}} t0_m x0 is not e^{{A(t−u)}} M x" + ); + assert!( + (shift - intercept_effect).abs() > 1e-3, + "Driver et al. (2017, Table 3): t0_m x0 is not CINT" + ); + assert!( + (composed - evolved).abs() > 1e-3, + "Driver et al. (2017, Eq. 3): μ_t is not μ_t + e^{{A Δt}} t0_m x0" + ); + assert_eq!( + refuse_initial_time_dependent_effect_as_contemporaneous_impulse(shift, impulse), + Err( + psychometric_core::PsychometricError::InitialTimeDependentEffectIsNotContemporaneousImpulse + ) + ); + assert_eq!( + refuse_initial_time_dependent_carry_as_initial_effect(carry, shift), + Err(psychometric_core::PsychometricError::InitialTimeDependentCarryIsNotInitialEffect) + ); + assert_eq!( + refuse_initial_time_dependent_effect_as_continuous_intercept(shift, effect), + Err( + psychometric_core::PsychometricError::InitialTimeDependentEffectIsNotContinuousIntercept + ) + ); + assert_eq!( + refuse_initial_time_dependent_effect_as_process_increment(shift, increment), + Err(psychometric_core::PsychometricError::InitialTimeDependentEffectIsNotProcessIncrement) + ); + assert_eq!( + refuse_initial_time_dependent_effect_as_initial_time_independent_effect(shift, tipred_shift), + Err( + psychometric_core::PsychometricError::InitialTimeDependentEffectIsNotInitialTimeIndependentEffect + ) + ); + assert_eq!( + refuse_initial_time_dependent_coefficient_as_initial_effect(effect, shift), + Err( + psychometric_core::PsychometricError::InitialTimeDependentCoefficientIsNotInitialEffect + ) + ); + assert_eq!( + refuse_initial_time_dependent_carry_as_impulse_carry(carry, impulse_carry), + Err(psychometric_core::PsychometricError::InitialTimeDependentCarryIsNotImpulseCarry) + ); +} + #[test] #[allow(clippy::too_many_lines)] fn evolved_and_process_observed_mean_are_not_initial_time_independent_observed_mean() { diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index 20317fc0..d734a56c 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -1,7 +1,7 @@ # ADR 0005 — Posterior-aware ESEM/DSEM and structural interpretation **Decision status:** Accepted -**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), exact scalar contemporaneous `TDPREDEFFECT` impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; the §7.2 level-change form is not that impulse), exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of that Eq. 3 composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar time-independent `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle Eq. 14; `B` is not that discrete increment), exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of that Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of that carried latent mean; `τ + λ μ_t` is not that observed mean), exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; `T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`), exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target +**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), exact scalar contemporaneous `TDPREDEFFECT` impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; the §7.2 level-change form is not that impulse), exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of that Eq. 3 composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar time-independent `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle Eq. 14; `B` is not that discrete increment), exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of that Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of that carried latent mean; `τ + λ μ_t` is not that observed mean), exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; `T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`), exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; `T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target **Date:** 2026-08-05 **Supersedes:** None. ADR 0012 governs upstream topic measurement/network coordinates; this ADR governs higher-order psychometric structure and longitudinal interpretation. @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), recovers the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z; `TDPREDEFFECT` is `M`, not `CINT`; `M x` is not `A^{-1}[e^{A Δt} − I] B z`; `M x` is not Voelkle et al., 2012, Eq. 14; the level-change form is not that impulse), recovers the exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition; JSS PDF re-opened 2026-08-20T09:01Z; the evolved map `τ + λ μ_t` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-impulse latent mean is not `E(y_t)`), recovers the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z; `TIPREDEFFECT` is `B`, not `κ`; form `B z` first, then the discrete intercept map; a zero drift is `B z Δt`; `B` is not the discrete increment; `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle Eq. 14), recovers the exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; JSS PDF re-opened 2026-08-20T12:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-increment latent mean is not `E(y_t)`), recovers the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z; form `m x` first, then `e^{a(t−u)} m x`; a zero drift is `m x`; underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept; `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; an impulse at `u = t` is the contemporaneous map; an impulse at `u ≤ t0` is already in `η(t0)`), recovers the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; JSS PDF re-opened 2026-08-20T05:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the carried latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and its Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF opened 2026-08-20T15:14Z; form `t0_b z` first, then `e^{a Δt} t0_b z`; a zero drift is `t0_b z`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; `e^{A Δt} t0_b z` is not `t0_b z`; `T0TIPREDEFFECT` is the coefficient, not the shift), recovers the exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition; JSS PDF re-opened 2026-08-20T15:28Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), recovers the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z; `TDPREDEFFECT` is `M`, not `CINT`; `M x` is not `A^{-1}[e^{A Δt} − I] B z`; `M x` is not Voelkle et al., 2012, Eq. 14; the level-change form is not that impulse), recovers the exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition; JSS PDF re-opened 2026-08-20T09:01Z; the evolved map `τ + λ μ_t` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-impulse latent mean is not `E(y_t)`), recovers the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z; `TIPREDEFFECT` is `B`, not `κ`; form `B z` first, then the discrete intercept map; a zero drift is `B z Δt`; `B` is not the discrete increment; `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle Eq. 14), recovers the exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; JSS PDF re-opened 2026-08-20T12:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-increment latent mean is not `E(y_t)`), recovers the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z; form `m x` first, then `e^{a(t−u)} m x`; a zero drift is `m x`; underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept; `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; an impulse at `u = t` is the contemporaneous map; an impulse at `u ≤ t0` is already in `η(t0)`), recovers the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; JSS PDF re-opened 2026-08-20T05:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the carried latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and its Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF opened 2026-08-20T15:14Z; form `t0_b z` first, then `e^{a Δt} t0_b z`; a zero drift is `t0_b z`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; `e^{A Δt} t0_b z` is not `t0_b z`; `T0TIPREDEFFECT` is the coefficient, not the shift), recovers the exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition; JSS PDF re-opened 2026-08-20T15:28Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and its Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; form `t0_m x0` first, then `e^{a Δt} t0_m x0`; a zero drift is `t0_m x0`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `T0TDPREDEFFECT` is the coefficient, not the shift; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. 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 1f003680..e38b3995 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -30,15 +30,16 @@ This slice stays inside `psychometric_core`. It does not add a second invariance 24. recover the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`) and refuse treating `τ + λ μ_t` or `τ + λ(μ_t + m x)` as that observed mean; 25. recover the exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and its Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF opened 2026-08-20T15:14Z) and refuse treating `t0_b z` as `A^{-1}[e^{A Δt} − I] B z`, as `κ`, or as `M x`; refuse treating `e^{A Δt} t0_b z` as `t0_b z`; refuse treating the coefficient as the shift; 26. recover the exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition; JSS PDF re-opened 2026-08-20T15:28Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`) and refuse treating `τ + λ μ_t`, `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, `τ + λ(μ_t + m x)`, or `τ + λ(μ_t + e^{a(t−u)} m x)` as that observed mean; -27. refuse pooling discrete lags from unequal event intervals as one coefficient; -28. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); -29. refuse the difference quotient as a continuous-time rate; -30. apply the same event-time map to CWC residuals (still not DSEM); -31. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). +27. recover the exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and its Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z) and refuse treating `t0_m x0` as `M x`, as `e^{A(t−u)} M x` for `t0 < u < t`, as `t0_b z`, as `A^{-1}[e^{A Δt} − I] B z`, or as `κ`; refuse treating `e^{A Δt} t0_m x0` as `t0_m x0` or as `e^{A(t−u)} M x`; refuse treating the coefficient as the shift; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`; +28. refuse pooling discrete lags from unequal event intervals as one coefficient; +29. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); +30. refuse the difference quotient as a continuous-time rate; +31. apply the same event-time map to CWC residuals (still not DSEM); +32. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). ## Claim boundary -This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. The contemporaneous `TDPREDEFFECT` impulse is not the continuous intercept, not the time-independent discrete effect, and not Voelkle et al. (2012, Eq. 14). The time-independent `TIPREDEFFECT` increment is not the continuous intercept, not the contemporaneous impulse, not Voelkle et al. (2012, Eq. 14), and not the coefficient `B`. The within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). The evolved observed mean `τ + λ μ_t` is not the contemporaneous-impulse observed mean `τ + λ(μ_t + m x)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not the impulse-carry observed mean `τ + λ(μ_t + e^{a(t−u)} m x)` when `u ≠ t`. The evolved observed mean `τ + λ μ_t` is not the impulse-carry observed mean. The carried latent mean is not `E(y_t)`. The evolved-plus-impulse latent mean is not `E(y_t)`. The evolved observed mean `τ + λ μ_t` is not the time-independent-predictor observed mean `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not that time-independent-predictor observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that time-independent-predictor observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TIPREDEFFECT` shift `t0_b z` is not the Eq. 3 process increment `A^{-1}[e^{A Δt} − I] B z`, not `κ`, and not `M x`. The Eq. 3 first-summand carry `e^{A Δt} t0_b z` is not `t0_b z` and is not that process increment. `T0TIPREDEFFECT` is the coefficient, not the first-occasion shift. The evolved observed mean `τ + λ μ_t` is not the first-occasion TI-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_b z)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion observed mean when `u ≠ t0`. The evolved-plus-T0TIPRED latent mean is not `E(y_t)`. +This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. The contemporaneous `TDPREDEFFECT` impulse is not the continuous intercept, not the time-independent discrete effect, and not Voelkle et al. (2012, Eq. 14). The time-independent `TIPREDEFFECT` increment is not the continuous intercept, not the contemporaneous impulse, not Voelkle et al. (2012, Eq. 14), and not the coefficient `B`. The within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). The evolved observed mean `τ + λ μ_t` is not the contemporaneous-impulse observed mean `τ + λ(μ_t + m x)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not the impulse-carry observed mean `τ + λ(μ_t + e^{a(t−u)} m x)` when `u ≠ t`. The evolved observed mean `τ + λ μ_t` is not the impulse-carry observed mean. The carried latent mean is not `E(y_t)`. The evolved-plus-impulse latent mean is not `E(y_t)`. The evolved observed mean `τ + λ μ_t` is not the time-independent-predictor observed mean `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not that time-independent-predictor observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that time-independent-predictor observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TIPREDEFFECT` shift `t0_b z` is not the Eq. 3 process increment `A^{-1}[e^{A Δt} − I] B z`, not `κ`, and not `M x`. The Eq. 3 first-summand carry `e^{A Δt} t0_b z` is not `t0_b z` and is not that process increment. `T0TIPREDEFFECT` is the coefficient, not the first-occasion shift. The evolved observed mean `τ + λ μ_t` is not the first-occasion TI-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_b z)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion observed mean when `u ≠ t0`. The evolved-plus-T0TIPRED latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TDPREDEFFECT` shift `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `κ`. The Eq. 3 first-summand carry `e^{A Δt} t0_m x0` is not `t0_m x0` and is not that within-interval impulse carry. `T0TDPREDEFFECT` is the coefficient, not the first-occasion shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The Table 3 first-occasion `T0TDPREDEFFECT` shift `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `κ`. The Eq. 3 first-summand carry `e^{A Δt} t0_m x0` is not `t0_m x0` and is not that within-interval impulse carry. `T0TDPREDEFFECT` is the coefficient, not the first-occasion shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. ## Authoritative sources @@ -85,6 +86,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - **Impulse-carry observed-indicator mean.** Driver et al. (2017, Eq. 5, p. 5; Eq. 1–2, pp. 4–5; Eq. 3 exponential map; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T05:12Z): `y_i(t)=Γ+Λη_i(t)+ζ_i(t)` with `ζ∼N(0,Θ)` and `Γ∼N(τ,Ψ)`. The latent process at `t` after a Dirac that occurred strictly inside `(t0,t)` is `μ_t+e^{a(t−u)}mx`. The scalar composition is `E(y_t)=τ+λ(μ_t+e^{a(t−u)}mx)`. Form the carried latent mean first, then `τ+λ` of that mean. A zero loading is exactly `τ`. A zero evolved-plus-carry latent mean is exactly `τ`. A zero intercept is exactly `λ(μ_t+carry)`. The evolved observed mean `τ+λμ_t` is not this composition. The contemporaneous map `τ+λ(μ_t+mx)` is not this composition when `u≠t`. `MANIFESTMEANS` is not `E(y_t)`. The carried latent mean is not `E(y_t)`. The §7.2 level-change form is not this map. An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **First-occasion time-independent predictor.** Driver et al. (2017, Table 3, p. 13; Eq. 3 first summand, p. 5; JSS PDF opened 2026-08-20T15:14Z): Table 3 names `T0TIPREDEFFECT` the effect of time-independent predictors on latents at `T0`. Table 2 / Table 3 name `TIPREDEFFECT` `B`, which enters Equation 3 as `A^{-1}[e^{A(t−t0)}−I]Bz`. The scalar first-occasion shift is `t0_b z`. Equation 3's first summand carries that shift as `e^{AΔt}t0_b z`. Form `t0_b z` first, then `e^{aΔt}t0_b z`. Form `μ_t` first, then add that carry. A zero drift is `t0_b z`. Binary64 underflow of `e^{aΔt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. A zero effect or zero predictor is exactly zero. `t0_b z` is not `A^{-1}[e^{AΔt}−I]Bz`, not `κ`, and not `M x`. `e^{AΔt}t0_b z` is not `t0_b z`. `T0TIPREDEFFECT` is the coefficient, not the shift. An overflowing product, rewrite, or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **First-occasion time-independent-predictor observed-indicator mean.** Driver et al. (2017, Eq. 5, p. 5; Eq. 3 first summand, p. 5; Table 3, p. 13; JSS PDF re-opened 2026-08-20T15:28Z): `y_i(t)=Γ+Λη_i(t)+ζ_i(t)` with `ζ∼N(0,Θ)` and `Γ∼N(τ,Ψ)`. The latent process at `t` after the Table 3 first-occasion carry is `μ_t+e^{aΔt}t0_b z`. The scalar composition is `E(y_t)=τ+λ(μ_t+e^{aΔt}t0_b z)`. Form the evolved-plus-carry latent mean first, then `τ+λ` of that mean. A zero loading is exactly `τ`. A zero evolved-plus-carry latent mean is exactly `τ`. A zero intercept is exactly `λ(μ_t+carry)`. The evolved observed mean `τ+λμ_t` is not this composition. The process-increment map `τ+λ(μ_t+A^{-1}[e^{AΔt}−I]Bz)` is not this composition. The contemporaneous map `τ+λ(μ_t+mx)` is not this composition. The impulse-carry map `τ+λ(μ_t+e^{a(t−u)}mx)` is not this composition when `u≠t0`. `MANIFESTMEANS` is not `E(y_t)`. The evolved-plus-carry latent mean is not `E(y_t)`. `T0TIPREDEFFECT` is the coefficient, not that observed mean. An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. +- **First-occasion time-dependent predictor.** Driver et al. (2017, Table 3, p. 13; Eq. 3 first summand, p. 5; JSS PDF re-opened 2026-08-20T19:10Z): Table 3 names `T0TDPREDEFFECT` the effect of time-dependent predictors on latents at `T0`. Table 2 / Table 3 name `TDPREDEFFECT` `M`, which enters Equation 3 as the printed fourth-summand Dirac `M x` at `u = t`. The scalar first-occasion shift is `t0_m x0`. Equation 3's first summand carries that shift as `e^{AΔt}t0_m x0`. Form `t0_m x0` first, then `e^{aΔt}t0_m x0`. Form `μ_t` first, then add that carry. A zero drift is `t0_m x0`. Binary64 underflow of `e^{aΔt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. A zero effect or zero predictor is exactly zero. `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{AΔt}−I]Bz`, and not `κ`. `e^{AΔt}t0_m x0` is not `t0_m x0`. `T0TDPREDEFFECT` is the coefficient, not the shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. An overflowing product, rewrite, or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **CWC-then-lag.** Sample cluster means are removed first. Consecutive within residuals are then fitted by least squares to \(r_{t+\Delta t}\approx\exp(a\Delta t)\,r_{t}\) on event time. Same-sign pair-wise logs initialize the scalar Newton step. Sign-flipping \(T=2\) CWC pairs have no real logarithm and fail closed. Curran and Bauer (2011, pp. 607–608) show that this person-mean subtraction on a raw autoregressive series does **not** isolate the lagged within-person effect; the helper therefore does not claim to recover the raw-process drift. - **Already-centered irregular residual.** The caller supplies lagged within residuals. The mean of \(a=\ln(r_{t+\Delta t}/r_t)/\Delta t\) is the exact scalar map. Intervals may be irregular. The helper does not center again. This is not DSEM. @@ -115,6 +117,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - Driver et al. (2017, Eq. 5 of the Eq. 1–2 carried latent mean; Table 2, p. 12) recovers a known \(E(y_t)=\tau+\lambda(\mu_t+e^{a(t-u)}mx)\) at machine-scale RMSE, and that RMSE is smaller than treating \(\tau+\lambda\mu_t\), \(\tau+\lambda(\mu_t+mx)\), `MANIFESTMEANS`, or the carried latent mean as \(E(y_t)\); a zero loading is \(\tau\); a zero carry recovers \(\tau+\lambda\mu_t\); a non-event clock, a non-positive or non-interior interval, and an overflowing product or sum fail closed; - Driver et al. (2017, Table 3, p. 13; Eq. 3 first summand) recovers a known first-occasion shift \(t0_b z\) and carry \(e^{a\Delta t}t0_b z\) at machine-scale RMSE, and those RMSEs are smaller than treating `TIPREDEFFECT`, `CINT`, the coefficient, or the un-carried shift as the carry; composing \(\mu_t\) plus that carry recovers the known sum; a zero drift is \(t0_b z\); underflow of \(e^{a\Delta t}\) to `+0` is a vanishing carry and is kept; a zero effect or zero predictor is exactly zero; a non-event clock, a non-positive interval, and an overflowing product or rewrite fail closed; - Driver et al. (2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition; Table 3, p. 13) recovers a known \(E(y_t)=\tau+\lambda(\mu_t+e^{a\Delta t}t0_b z)\) at machine-scale RMSE, and that RMSE is smaller than treating \(\tau+\lambda\mu_t\), \(\tau+\lambda(\mu_t+A^{-1}[e^{A\Delta t}-I]Bz)\), \(\tau+\lambda(\mu_t+mx)\), \(\tau+\lambda(\mu_t+e^{a(t-u)}mx)\), `MANIFESTMEANS`, or the evolved-plus-carry latent mean as \(E(y_t)\); a zero loading is \(\tau\); a non-event clock, a non-positive interval, and an overflowing product or sum fail closed; +- Driver et al. (2017, Table 3, p. 13; Eq. 3 first summand) recovers a known first-occasion time-dependent shift \(t0_m x0\) and carry \(e^{a\Delta t}t0_m x0\) at machine-scale RMSE, and those RMSEs are smaller than treating `TDPREDEFFECT` `M x`, the within-interval impulse carry, `T0TIPREDEFFECT`, `TIPREDEFFECT`, `CINT`, the coefficient, or the un-carried shift as the carry; composing \(\mu_t\) plus that carry recovers the known sum; a zero drift is \(t0_m x0\); underflow of \(e^{a\Delta t}\) to `+0` is a vanishing carry and is kept; a zero effect or zero predictor is exactly zero; a non-event clock, a non-positive interval, and an overflowing product or rewrite fail closed; - pooling discrete lags from unequal intervals fails closed; - CWC-then-lag on a two-cluster decaying series has smaller computed RMSE than a level-pooled series when the latter is identified; - already-centered irregular residuals recover a known drift at machine-scale RMSE, and that RMSE is smaller than CWC of the corresponding raw autoregressive series (Curran & Bauer, 2011, pp. 607–608); From 3df5d16870ee3cf5ebb51adcd7ea35efe9037ad6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 19:33:04 +0000 Subject: [PATCH 47/62] feat(psychometric): recover Driver Eq. 5 of T0TDPREDEFFECT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Map E(y_t) = τ + λ(μ_t + e^{a Δt} t0_m x0). Form the evolved-plus-carry latent mean first. Refuse evolved, TIPREDEFFECT, impulse, carry, and T0TIPREDEFFECT observed means as that composition. Fail closed on overflow. --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 1 + CLAUDE.md | 2 +- crates/psychometric_core/src/error.rs | 61 +++ crates/psychometric_core/src/event_time.rs | 472 +++++++++++++++++- crates/psychometric_core/src/lib.rs | 19 + ...multilevel_event_time_recovery_contract.rs | 397 +++++++++++++++ .../scientific_claim_boundary_contract.rs | 179 +++++++ docs/adr/0005-posterior-esem-dsem.md | 4 +- .../multilevel-event-time-recovery.md | 15 +- 10 files changed, 1141 insertions(+), 11 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index b85e42a9..79e4caab 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -61,7 +61,7 @@ boundaries above remain the target modular MSA architecture. | `tepp_simulation` | known-truth temporal/event data generation | | `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics | | `tepp_api` | versioned DTO, schema, and export contracts | -| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; first-occasion `T0TIPREDEFFECT` shift is `t0_b z` and Eq. 3 first-summand carry is `e^{A Δt} t0_b z` (`T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_b z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), first-occasion `T0TDPREDEFFECT` shift is `t0_m x0` and Eq. 3 first-summand carry is `e^{A Δt} t0_m x0` (`T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | +| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; first-occasion `T0TIPREDEFFECT` shift is `t0_b z` and Eq. 3 first-summand carry is `e^{A Δt} t0_b z` (`T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_b z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), first-occasion `T0TDPREDEFFECT` shift is `t0_m x0` and Eq. 3 first-summand carry is `e^{A Δt} t0_m x0` (`T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | No crate exposes placeholder production behavior in Task 1. This prevents an empty façade from becoming a de facto public API before its invariants and tests diff --git a/CHANGELOG.md b/CHANGELOG.md index 5df1e0ea..5f7a110a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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, Eq. 5, p. 5; Eq. 3 first summand, p. 5; Table 3, p. 13; JSS PDF re-opened 2026-08-20T19:07Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar observed-indicator mean of a first-occasion time-dependent predictor. Equation 5 writes `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and `Γ ~ N(τ, Ψ)`. Table 3 names `T0TDPREDEFFECT` the effect of time-dependent predictors on latents at `T0`. Equation 3's first summand carries that shift as `e^{A Δt} t0_m x0`. The scalar composition is `E(y_t) = τ + λ(μ_t + e^{a Δt} t0_m x0)`. Form the evolved-plus-carry latent mean first, then `τ + λ` of that mean. The evolved observed mean `τ + λ μ_t` is not this composition. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not this composition. The contemporaneous map `τ + λ(μ_t + m x)` is not this composition. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not this composition when `u ≠ t0`. The first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not this composition. `MANIFESTMEANS` is not `E(y_t)`. The evolved-plus-carry latent mean is not `E(y_t)`. `T0TDPREDEFFECT` is the coefficient, not that observed mean. Same numbers as `T0TIPREDEFFECT` yield the same product; Table 3 names a different matrix. A zero loading is exactly `τ`. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T19:09Z: `is_oa: false`). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Table 3, p. 13; Eq. 3 first summand, p. 5; JSS PDF re-opened 2026-08-20T19:10Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar first-occasion time-dependent predictor shift and its carry. Table 3 names `T0TDPREDEFFECT` the effect of time-dependent predictors on latents at `T0`. Table 2 / Table 3 name `TDPREDEFFECT` `M`, which enters Equation 3 as the printed fourth-summand Dirac `M x` at `u = t`. Those are not the same matrix. The scalar first-occasion shift is `t0_m x0`. Equation 3's first summand carries that shift as `e^{A Δt} t0_m x0`. Form `t0_m x0` first, then `e^{a Δt} t0_m x0`. Form `μ_t` first, then add that carry. A zero drift is `t0_m x0` with no dissipation. Binary64 underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `κ`. `e^{A Δt} t0_m x0` is not `t0_m x0`. `T0TDPREDEFFECT` is the coefficient, not `t0_m x0`. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T19:09Z: `is_oa: false`). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Eq. 3 first summand, p. 5; Table 3, p. 13; JSS PDF re-opened 2026-08-20T15:28Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar observed-indicator mean of a first-occasion time-independent predictor. Equation 5 writes `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and `Γ ~ N(τ, Ψ)`. Table 3 names `T0TIPREDEFFECT` the effect of time-independent predictors on latents at `T0`. Equation 3's first summand carries that shift as `e^{A Δt} t0_b z`. The scalar composition is `E(y_t) = τ + λ(μ_t + e^{a Δt} t0_b z)`. Form the evolved-plus-carry latent mean first, then `τ + λ` of that mean. The evolved observed mean `τ + λ μ_t` is not this composition. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not this composition. The contemporaneous map `τ + λ(μ_t + m x)` is not this composition. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not this composition when `u ≠ t0`. `MANIFESTMEANS` is not `E(y_t)`. The evolved-plus-carry latent mean is not `E(y_t)`. `T0TIPREDEFFECT` is the coefficient, not that observed mean. A zero loading is exactly `τ`. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T15:14Z: `is_oa: false`; Springer `content/pdf` is HTML 200). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Table 3, p. 13; Eq. 3 first summand, p. 5; JSS PDF opened 2026-08-20T15:14Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar first-occasion time-independent predictor shift and its carry. Table 3 names `T0TIPREDEFFECT` the effect of time-independent predictors on latents at `T0`. Table 2 / Table 3 name `TIPREDEFFECT` `B`, which enters Equation 3 as `A^{-1}[e^{A(t−t0)} − I] B z`. Those are not the same matrix. The scalar first-occasion shift is `t0_b z`. Equation 3's first summand carries that shift as `e^{A Δt} t0_b z`. Form `t0_b z` first, then `e^{a Δt} t0_b z`. Form `μ_t` first, then add that carry. A zero drift is `t0_b z` with no dissipation. Binary64 underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `κ`, and not `M x`. `e^{A Δt} t0_b z` is not `t0_b z`. `T0TIPREDEFFECT` is the coefficient, not `t0_b z`. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T15:14Z: `is_oa: false`; Springer `content/pdf` is HTML 200). diff --git a/CLAUDE.md b/CLAUDE.md index a78f9dce..53bac69a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. Putnick and Bornstein (2016, PMC5145197 opened 2026-08-19T22:15Z) require scalar invariance before latent-mean comparison; residual invariance is not a prerequisite. Two-observation series have no residual degrees of freedom (`ordinary_least_squares_fit` returns residual variance `0`) and cap at strong/scalar; they still license means. This is two-group OLS, not MGCFA. Meredith (1993) names remain unread labels. -- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. The contemporaneous time-dependent predictor impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`). Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). The §7.2 level-change form is not that impulse. The observed mean of that contemporaneous impulse is `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-impulse latent mean is not `E(y_t)`. The time-independent predictor increment is `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`). Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. `TIPREDEFFECT` is `B`, not that discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle et al. (2012, Eq. 14). The observed mean of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The within-interval time-dependent impulse carry is `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation). Form `m x` first, then `e^{a(t−u)} m x`. A zero drift is `m x` with no dissipation. Underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept. `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). An impulse at `u = t` is the contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. The observed mean of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The carried latent mean is not `E(y_t)`. The first-occasion time-independent predictor shift is `t0_b z` (Driver et al., 2017, Table 3 `T0TIPREDEFFECT`; Eq. 3 first summand). Form `t0_b z` first, then `e^{a Δt} t0_b z`. Form `μ_t` first, then add that carry. A zero drift is `t0_b z`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. `e^{A Δt} t0_b z` is not `t0_b z`. `T0TIPREDEFFECT` is the coefficient, not the shift. The observed mean of that first-occasion carry is `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The evolved-plus-carry latent mean is not `E(y_t)`. The first-occasion time-dependent predictor shift is `t0_m x0` (Driver et al., 2017, Table 3 `T0TDPREDEFFECT`; Eq. 3 first summand; JSS PDF re-opened 2026-08-20T19:10Z). Form `t0_m x0` first, then `e^{a Δt} t0_m x0`. Form `μ_t` first, then add that carry. A zero drift is `t0_m x0`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`. `e^{A Δt} t0_m x0` is not `t0_m x0`. `T0TDPREDEFFECT` is the coefficient, not the shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. The contemporaneous time-dependent predictor impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`). Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). The §7.2 level-change form is not that impulse. The observed mean of that contemporaneous impulse is `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-impulse latent mean is not `E(y_t)`. The time-independent predictor increment is `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`). Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. `TIPREDEFFECT` is `B`, not that discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle et al. (2012, Eq. 14). The observed mean of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The within-interval time-dependent impulse carry is `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation). Form `m x` first, then `e^{a(t−u)} m x`. A zero drift is `m x` with no dissipation. Underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept. `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). An impulse at `u = t` is the contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. The observed mean of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The carried latent mean is not `E(y_t)`. The first-occasion time-independent predictor shift is `t0_b z` (Driver et al., 2017, Table 3 `T0TIPREDEFFECT`; Eq. 3 first summand). Form `t0_b z` first, then `e^{a Δt} t0_b z`. Form `μ_t` first, then add that carry. A zero drift is `t0_b z`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. `e^{A Δt} t0_b z` is not `t0_b z`. `T0TIPREDEFFECT` is the coefficient, not the shift. The observed mean of that first-occasion carry is `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The evolved-plus-carry latent mean is not `E(y_t)`. The first-occasion time-dependent predictor shift is `t0_m x0` (Driver et al., 2017, Table 3 `T0TDPREDEFFECT`; Eq. 3 first summand; JSS PDF re-opened 2026-08-20T19:10Z). Form `t0_m x0` first, then `e^{a Δt} t0_m x0`. Form `μ_t` first, then add that carry. A zero drift is `t0_m x0`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`. `e^{A Δt} t0_m x0` is not `t0_m x0`. `T0TDPREDEFFECT` is the coefficient, not the shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The observed mean of that first-occasion TD carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean. The evolved-plus-carry latent mean is not `E(y_t)`. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. diff --git a/crates/psychometric_core/src/error.rs b/crates/psychometric_core/src/error.rs index be3c61b3..263abe6f 100644 --- a/crates/psychometric_core/src/error.rs +++ b/crates/psychometric_core/src/error.rs @@ -222,6 +222,29 @@ pub enum PsychometricError { /// within-interval impulse carry. `e^{A Δt} t0_m x0` is not /// `e^{A(t−u)} M x` for `t0 < u < t`. InitialTimeDependentCarryIsNotImpulseCarry, + /// Driver Eq. 5 of the Eq. 3 evolved mean was treated as + /// Equation 5 of the Table 3 first-occasion TD predictor. + /// `τ + λ μ_t` is not `τ + λ(μ_t + e^{a Δt} t0_m x0)`. + EvolvedObservedMeanIsNotInitialTimeDependentObservedMean, + /// Driver Eq. 5 of the Eq. 3 process increment was treated as + /// Equation 5 of the Table 3 first-occasion TD predictor. + /// `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not + /// `τ + λ(μ_t + e^{a Δt} t0_m x0)`. + TimeIndependentObservedMeanIsNotInitialTimeDependentObservedMean, + /// Driver Eq. 5 of the contemporaneous impulse was treated as + /// Equation 5 of the Table 3 first-occasion TD predictor. + /// `τ + λ(μ_t + m x)` is not `τ + λ(μ_t + e^{a Δt} t0_m x0)`. + ImpulseObservedMeanIsNotInitialTimeDependentObservedMean, + /// Driver Eq. 5 of the Eq. 1–2 carried latent mean was treated as + /// Equation 5 of the Table 3 first-occasion TD predictor. + /// `τ + λ(μ_t + e^{a(t−u)} m x)` is not + /// `τ + λ(μ_t + e^{a Δt} t0_m x0)`. + ImpulseCarryObservedMeanIsNotInitialTimeDependentObservedMean, + /// Driver Eq. 5 of Table 3 `T0TIPREDEFFECT` was treated as + /// Equation 5 of Table 3 `T0TDPREDEFFECT`. + /// `τ + λ(μ_t + e^{a Δt} t0_b z)` is not + /// `τ + λ(μ_t + e^{a Δt} t0_m x0)`. + InitialTimeIndependentObservedMeanIsNotInitialTimeDependentObservedMean, } impl fmt::Display for PsychometricError { @@ -408,6 +431,21 @@ impl fmt::Display for PsychometricError { Self::InitialTimeDependentCarryIsNotImpulseCarry => { "carried first-occasion time-dependent predictor is not the impulse carry" } + Self::EvolvedObservedMeanIsNotInitialTimeDependentObservedMean => { + "evolved observed mean is not the first-occasion time-dependent-predictor observed mean" + } + Self::TimeIndependentObservedMeanIsNotInitialTimeDependentObservedMean => { + "time-independent-predictor observed mean is not the first-occasion time-dependent-predictor observed mean" + } + Self::ImpulseObservedMeanIsNotInitialTimeDependentObservedMean => { + "contemporaneous-impulse observed mean is not the first-occasion time-dependent-predictor observed mean" + } + Self::ImpulseCarryObservedMeanIsNotInitialTimeDependentObservedMean => { + "impulse-carry observed mean is not the first-occasion time-dependent-predictor observed mean" + } + Self::InitialTimeIndependentObservedMeanIsNotInitialTimeDependentObservedMean => { + "first-occasion time-independent-predictor observed mean is not the first-occasion time-dependent-predictor observed mean" + } }; formatter.write_str(message) } @@ -702,5 +740,28 @@ mod tests { PsychometricError::InitialTimeDependentCarryIsNotImpulseCarry.to_string(), "carried first-occasion time-dependent predictor is not the impulse carry" ); + assert_eq!( + PsychometricError::EvolvedObservedMeanIsNotInitialTimeDependentObservedMean.to_string(), + "evolved observed mean is not the first-occasion time-dependent-predictor observed mean" + ); + assert_eq!( + PsychometricError::TimeIndependentObservedMeanIsNotInitialTimeDependentObservedMean + .to_string(), + "time-independent-predictor observed mean is not the first-occasion time-dependent-predictor observed mean" + ); + assert_eq!( + PsychometricError::ImpulseObservedMeanIsNotInitialTimeDependentObservedMean.to_string(), + "contemporaneous-impulse observed mean is not the first-occasion time-dependent-predictor observed mean" + ); + assert_eq!( + PsychometricError::ImpulseCarryObservedMeanIsNotInitialTimeDependentObservedMean + .to_string(), + "impulse-carry observed mean is not the first-occasion time-dependent-predictor observed mean" + ); + assert_eq!( + PsychometricError::InitialTimeIndependentObservedMeanIsNotInitialTimeDependentObservedMean + .to_string(), + "first-occasion time-independent-predictor observed mean is not the first-occasion time-dependent-predictor observed mean" + ); } } diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index efdf53a6..c5494a41 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -66,7 +66,11 @@ //! carry is not `t0_m x0`, not `M x`, not `e^{A(t−u)} M x` for //! `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and //! not `CINT`. An impulse at `u ≤ t0` that used `M` is already in -//! `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The JSS article +//! `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. Equation 5 of +//! that carried first-occasion TD shift is +//! `τ + λ(μ_t + e^{a Δt} t0_m x0)` (`τ + λ μ_t` is not that +//! observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that +//! observed mean). The JSS article //! has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). //! The difference quotient `(x(t+Δt) − x(t)) / Δt` (their //! Eqs. 3–4) is refused. This is not DSEM and not a matrix `expm`. @@ -2476,6 +2480,172 @@ pub fn refuse_initial_time_dependent_carry_as_impulse_carry( Err(PsychometricError::InitialTimeDependentCarryIsNotImpulseCarry) } +/// Exact scalar observed mean of a first-occasion time-dependent +/// predictor. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Eq. 3 first summand, +/// p. 5; Table 3, p. 13; JSS PDF re-opened 2026-08-20T19:20Z from +/// ) +/// write `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and +/// `Γ ~ N(τ, Ψ)`. Table 3 names `T0TDPREDEFFECT` the effect of +/// time-dependent predictors on latents at `T0`. Equation 3's first +/// summand carries that shift as `e^{A Δt} t0_m x0`. The expected +/// intercept is `τ`. The latent process at `t` after that carry is +/// `μ_t + e^{a Δt} t0_m x0`. The scalar composition is +/// `E(y_t) = τ + λ(μ_t + e^{a Δt} t0_m x0)`. Form the +/// evolved-plus-carry latent mean first, then `τ + λ` of that mean. +/// A zero loading is exactly `τ`. A zero evolved-plus-carry latent +/// mean is exactly `τ`. A zero intercept is exactly +/// `λ(μ_t + e^{a Δt} t0_m x0)`. The evolved observed mean +/// `τ + λ μ_t` is not this composition when the carry is nonzero. +/// The process-increment map +/// `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not this composition. +/// The contemporaneous map `τ + λ(μ_t + m x)` is not this +/// composition. The impulse-carry map +/// `τ + λ(μ_t + e^{a(t−u)} m x)` is not this composition when +/// `u ≠ t0`. The first-occasion TI map +/// `τ + λ(μ_t + e^{a Δt} t0_b z)` is not this composition. +/// `MANIFESTMEANS` is not `E(y_t)`. The evolved-plus-carry latent +/// mean is not `E(y_t)`. `T0TDPREDEFFECT` is the coefficient, not +/// that observed mean. This is not a Kalman filter and not ctsem +/// estimation. +/// +/// # Errors +/// +/// Propagates +/// [`recover_discrete_latent_mean_with_initial_time_dependent_predictor`] +/// and [`recover_manifest_observed_mean`]. +#[allow(clippy::too_many_arguments)] +pub fn recover_discrete_observed_mean_with_initial_time_dependent_predictor( + loading: f64, + initial_latent_mean: f64, + log_rate: f64, + continuous_intercept: f64, + initial_time_dependent_effect: f64, + time_dependent_predictor: f64, + manifest_mean: f64, + event_delta: f64, + clock: LagClock, +) -> Result { + let composed_latent_mean = recover_discrete_latent_mean_with_initial_time_dependent_predictor( + initial_latent_mean, + log_rate, + continuous_intercept, + initial_time_dependent_effect, + time_dependent_predictor, + event_delta, + clock, + )?; + recover_manifest_observed_mean(loading, composed_latent_mean, manifest_mean) +} + +/// Refuse treating the evolved observed mean as the first-occasion +/// time-dependent-predictor observed mean. +/// +/// Equation 5 of the Eq. 3 evolved mean is `τ + λ μ_t`. Equation 5 +/// of the Table 3 first-occasion TD predictor is +/// `τ + λ(μ_t + e^{a Δt} t0_m x0)`. Those are not the same map. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::EvolvedObservedMeanIsNotInitialTimeDependentObservedMean`]. +pub fn refuse_evolved_observed_mean_as_initial_time_dependent_observed_mean( + evolved_observed_mean: f64, + initial_time_dependent_observed_mean: f64, +) -> Result { + let _ = (evolved_observed_mean, initial_time_dependent_observed_mean); + Err(PsychometricError::EvolvedObservedMeanIsNotInitialTimeDependentObservedMean) +} + +/// Refuse treating the process-increment observed mean as the +/// first-occasion time-dependent-predictor observed mean. +/// +/// Equation 5 of the Eq. 3 `TIPREDEFFECT` increment is +/// `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. Equation 5 of the +/// Table 3 first-occasion TD predictor is +/// `τ + λ(μ_t + e^{a Δt} t0_m x0)`. Those are not the same map. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::TimeIndependentObservedMeanIsNotInitialTimeDependentObservedMean`]. +pub fn refuse_time_independent_observed_mean_as_initial_time_dependent_observed_mean( + time_independent_observed_mean: f64, + initial_time_dependent_observed_mean: f64, +) -> Result { + let _ = ( + time_independent_observed_mean, + initial_time_dependent_observed_mean, + ); + Err(PsychometricError::TimeIndependentObservedMeanIsNotInitialTimeDependentObservedMean) +} + +/// Refuse treating the contemporaneous-impulse observed mean as the +/// first-occasion time-dependent-predictor observed mean. +/// +/// Equation 5 of the contemporaneous Dirac is `τ + λ(μ_t + m x)`. +/// Equation 5 of the Table 3 first-occasion TD predictor is +/// `τ + λ(μ_t + e^{a Δt} t0_m x0)`. Those are not the same map. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::ImpulseObservedMeanIsNotInitialTimeDependentObservedMean`]. +pub fn refuse_impulse_observed_mean_as_initial_time_dependent_observed_mean( + impulse_observed_mean: f64, + initial_time_dependent_observed_mean: f64, +) -> Result { + let _ = (impulse_observed_mean, initial_time_dependent_observed_mean); + Err(PsychometricError::ImpulseObservedMeanIsNotInitialTimeDependentObservedMean) +} + +/// Refuse treating the impulse-carry observed mean as the +/// first-occasion time-dependent-predictor observed mean. +/// +/// Equation 5 of the Eq. 1–2 carried latent mean is +/// `τ + λ(μ_t + e^{a(t−u)} m x)`. Equation 5 of the Table 3 +/// first-occasion TD predictor is `τ + λ(μ_t + e^{a Δt} t0_m x0)`. +/// Those are not the same map. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::ImpulseCarryObservedMeanIsNotInitialTimeDependentObservedMean`]. +pub fn refuse_impulse_carry_observed_mean_as_initial_time_dependent_observed_mean( + impulse_carry_observed_mean: f64, + initial_time_dependent_observed_mean: f64, +) -> Result { + let _ = ( + impulse_carry_observed_mean, + initial_time_dependent_observed_mean, + ); + Err(PsychometricError::ImpulseCarryObservedMeanIsNotInitialTimeDependentObservedMean) +} + +/// Refuse treating the first-occasion TI observed mean as the +/// first-occasion TD observed mean. +/// +/// Equation 5 of Table 3 `T0TIPREDEFFECT` is +/// `τ + λ(μ_t + e^{a Δt} t0_b z)`. Equation 5 of Table 3 +/// `T0TDPREDEFFECT` is `τ + λ(μ_t + e^{a Δt} t0_m x0)`. Those are +/// not the same map. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::InitialTimeIndependentObservedMeanIsNotInitialTimeDependentObservedMean`]. +pub fn refuse_initial_time_independent_observed_mean_as_initial_time_dependent_observed_mean( + initial_time_independent_observed_mean: f64, + initial_time_dependent_observed_mean: f64, +) -> Result { + let _ = ( + initial_time_independent_observed_mean, + initial_time_dependent_observed_mean, + ); + Err(PsychometricError::InitialTimeIndependentObservedMeanIsNotInitialTimeDependentObservedMean) +} + /// Exact scalar within-interval time-dependent impulse carry from /// Driver Equations 1–2. /// @@ -3051,6 +3221,7 @@ mod tests { recover_discrete_latent_variance, recover_discrete_observed_mean, recover_discrete_observed_mean_with_impulse, recover_discrete_observed_mean_with_impulse_carry, + recover_discrete_observed_mean_with_initial_time_dependent_predictor, recover_discrete_observed_mean_with_initial_time_independent_predictor, recover_discrete_observed_mean_with_time_independent_predictor, recover_discrete_process_noise, recover_discrete_time_independent_predictor_effect, @@ -3071,12 +3242,15 @@ mod tests { refuse_continuous_intercept_as_manifest_means, refuse_difference_quotient_as_local_rate, refuse_evolved_observed_mean_as_impulse_carry_observed_mean, refuse_evolved_observed_mean_as_impulse_observed_mean, + refuse_evolved_observed_mean_as_initial_time_dependent_observed_mean, refuse_evolved_observed_mean_as_initial_time_independent_observed_mean, refuse_evolved_observed_mean_as_time_independent_observed_mean, refuse_finite_interval_process_noise_as_stationary_variance, + refuse_impulse_carry_observed_mean_as_initial_time_dependent_observed_mean, refuse_impulse_carry_observed_mean_as_initial_time_independent_observed_mean, refuse_impulse_carry_observed_mean_as_time_independent_observed_mean, refuse_impulse_observed_mean_as_impulse_carry_observed_mean, + refuse_impulse_observed_mean_as_initial_time_dependent_observed_mean, refuse_impulse_observed_mean_as_initial_time_independent_observed_mean, refuse_impulse_observed_mean_as_time_independent_observed_mean, refuse_initial_latent_mean_as_evolved_mean, @@ -3093,6 +3267,7 @@ mod tests { refuse_initial_time_independent_effect_as_continuous_intercept, refuse_initial_time_independent_effect_as_process_increment, refuse_initial_time_independent_effect_as_time_dependent_impulse, + refuse_initial_time_independent_observed_mean_as_initial_time_dependent_observed_mean, refuse_latent_lagged_covariance_as_observed_covariance, refuse_latent_mean_as_observed_mean, refuse_latent_variance_as_observed_variance, refuse_manifest_means_as_observed_mean, @@ -3112,6 +3287,7 @@ mod tests { refuse_time_independent_effect_as_continuous_intercept, refuse_time_independent_effect_as_time_dependent_impulse, refuse_time_independent_effect_as_time_varying_discrete_effect, + refuse_time_independent_observed_mean_as_initial_time_dependent_observed_mean, refuse_time_independent_observed_mean_as_initial_time_independent_observed_mean, refuse_trait_variance_as_process_noise, refuse_trait_variance_as_stationary_within_subject, refuse_unmatched_time_varying_predictor_interval, @@ -7576,4 +7752,298 @@ mod tests { Err(PsychometricError::InvalidNumericInput) ); } + + #[test] + #[allow(clippy::too_many_lines)] + fn discrete_observed_mean_with_initial_time_dependent_predictor_recovers_driver_equation_five() + { + let loading = 2.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let effect = 0.4_f64; + let predictor = 3.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let recovered = recover_discrete_observed_mean_with_initial_time_dependent_predictor( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-t0tdpred-mean"); + let composed = recover_discrete_latent_mean_with_initial_time_dependent_predictor( + initial, + drift, + intercept, + effect, + predictor, + delta, + LagClock::EventTime, + ) + .expect("eq3-t0tdpred"); + let expected = manifest_mean + loading * composed; + assert!((recovered - expected).abs() < 1e-15); + let evolved_observed = recover_discrete_observed_mean( + loading, + initial, + drift, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq3-eq5-mean"); + let process_observed = recover_discrete_observed_mean_with_time_independent_predictor( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-tipred"); + let impulse_observed = recover_discrete_observed_mean_with_impulse( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-impulse"); + let carry_observed = recover_discrete_observed_mean_with_impulse_carry( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + 1.0, + LagClock::EventTime, + ) + .expect("eq5-carry"); + let tipred_observed = + recover_discrete_observed_mean_with_initial_time_independent_predictor( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-t0tipred"); + assert!((recovered - evolved_observed).abs() > 1e-3); + assert!((recovered - process_observed).abs() > 1e-3); + assert!((recovered - impulse_observed).abs() > 1e-3); + assert!((recovered - carry_observed).abs() > 1e-3); + // Same numbers as T0TIPRED yield the same product, but Table 3 names a different matrix. + assert!((recovered - tipred_observed).abs() < 1e-15); + assert_eq!( + recover_discrete_observed_mean_with_initial_time_dependent_predictor( + 0.0, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime + ), + Ok(manifest_mean) + ); + assert!((recovered - composed).abs() > 1e-3); + assert!((recovered - manifest_mean).abs() > 1e-3); + } + + #[test] + fn discrete_observed_mean_with_initial_time_dependent_predictor_refuses_evolved_mean_and_overflow() + { + let recovered = recover_discrete_observed_mean_with_initial_time_dependent_predictor( + 2.0, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, + LagClock::EventTime, + ) + .expect("eq5-t0tdpred"); + let evolved = + recover_discrete_observed_mean(2.0, 1.0, -0.5, 0.3, 0.5, 2.0, LagClock::EventTime) + .expect("evolved"); + let process = recover_discrete_observed_mean_with_time_independent_predictor( + 2.0, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, + LagClock::EventTime, + ) + .expect("tipred"); + let impulse = recover_discrete_observed_mean_with_impulse( + 2.0, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, + LagClock::EventTime, + ) + .expect("impulse"); + let carry = recover_discrete_observed_mean_with_impulse_carry( + 2.0, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, + 1.0, + LagClock::EventTime, + ) + .expect("carry"); + let tipred = recover_discrete_observed_mean_with_initial_time_independent_predictor( + 2.0, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, + LagClock::EventTime, + ) + .expect("t0tipred"); + assert_eq!( + refuse_evolved_observed_mean_as_initial_time_dependent_observed_mean( + evolved, recovered + ), + Err(PsychometricError::EvolvedObservedMeanIsNotInitialTimeDependentObservedMean) + ); + assert_eq!( + refuse_time_independent_observed_mean_as_initial_time_dependent_observed_mean( + process, recovered + ), + Err( + PsychometricError::TimeIndependentObservedMeanIsNotInitialTimeDependentObservedMean + ) + ); + assert_eq!( + refuse_impulse_observed_mean_as_initial_time_dependent_observed_mean( + impulse, recovered + ), + Err(PsychometricError::ImpulseObservedMeanIsNotInitialTimeDependentObservedMean) + ); + assert_eq!( + refuse_impulse_carry_observed_mean_as_initial_time_dependent_observed_mean( + carry, recovered + ), + Err(PsychometricError::ImpulseCarryObservedMeanIsNotInitialTimeDependentObservedMean) + ); + assert_eq!( + refuse_initial_time_independent_observed_mean_as_initial_time_dependent_observed_mean( + tipred, recovered + ), + Err(PsychometricError::InitialTimeIndependentObservedMeanIsNotInitialTimeDependentObservedMean) + ); + } + + #[test] + fn discrete_observed_mean_with_initial_time_dependent_predictor_invalid_inputs_fail_closed() { + let scaled = recover_discrete_observed_mean_with_initial_time_dependent_predictor( + 1e308, + 1e-308, + 0.0, + 0.0, + 0.0, + 3.0, + 0.0, + 1.0, + LagClock::EventTime, + ) + .expect("scale"); + assert!((scaled - 1.0).abs() < 1e-15); + assert_eq!( + recover_discrete_observed_mean_with_initial_time_dependent_predictor( + 1e308, + 2.0, + 0.0, + 0.0, + 0.0, + 3.0, + 0.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_observed_mean_with_initial_time_dependent_predictor( + 2.0, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_discrete_observed_mean_with_initial_time_dependent_predictor( + 2.0, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_observed_mean_with_initial_time_dependent_predictor( + 1e308, + 0.0, + 0.0, + 0.0, + 1e308, + 1.0, + 0.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + } } diff --git a/crates/psychometric_core/src/lib.rs b/crates/psychometric_core/src/lib.rs index 6a84d121..a7f2665b 100644 --- a/crates/psychometric_core/src/lib.rs +++ b/crates/psychometric_core/src/lib.rs @@ -68,6 +68,13 @@ //! `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; //! `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` //! is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), +//! recovers the Driver Eq. 5 of that first-occasion TD carry as +//! `τ + λ(μ_t + e^{a Δt} t0_m x0)` (`τ + λ μ_t` is not that observed +//! mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that +//! observed mean; `τ + λ(μ_t + m x)` is not that observed mean; +//! `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when +//! `u ≠ t0`; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed +//! mean), //! and refuses //! latent-mean comparison below strong invariance. @@ -146,6 +153,8 @@ pub use event_time::recover_discrete_observed_mean; pub use event_time::recover_discrete_observed_mean_with_impulse; /// Exact scalar discrete observed mean of a within-interval impulse carry. pub use event_time::recover_discrete_observed_mean_with_impulse_carry; +/// Exact scalar discrete observed mean of a first-occasion TD predictor. +pub use event_time::recover_discrete_observed_mean_with_initial_time_dependent_predictor; /// Exact scalar discrete observed mean of a first-occasion TI predictor. pub use event_time::recover_discrete_observed_mean_with_initial_time_independent_predictor; /// Exact scalar discrete observed mean of a time-independent predictor. @@ -204,18 +213,24 @@ pub use event_time::refuse_difference_quotient_as_local_rate; pub use event_time::refuse_evolved_observed_mean_as_impulse_carry_observed_mean; /// Refuse treating evolved `τ + λ μ_t` as the contemporaneous-impulse observed mean. pub use event_time::refuse_evolved_observed_mean_as_impulse_observed_mean; +/// Refuse treating evolved `τ + λ μ_t` as the first-occasion TD-predictor observed mean. +pub use event_time::refuse_evolved_observed_mean_as_initial_time_dependent_observed_mean; /// Refuse treating evolved `τ + λ μ_t` as the first-occasion TI-predictor observed mean. pub use event_time::refuse_evolved_observed_mean_as_initial_time_independent_observed_mean; /// Refuse treating evolved `τ + λ μ_t` as the time-independent-predictor observed mean. pub use event_time::refuse_evolved_observed_mean_as_time_independent_observed_mean; /// Refuse treating finite-interval `Q_Δt` as `asymDIFFUSION`. pub use event_time::refuse_finite_interval_process_noise_as_stationary_variance; +/// Refuse treating impulse-carry `τ + λ(μ_t + e^{a(t−u)} m x)` as the first-occasion TD-predictor observed mean. +pub use event_time::refuse_impulse_carry_observed_mean_as_initial_time_dependent_observed_mean; /// Refuse treating impulse-carry `τ + λ(μ_t + e^{a(t−u)} m x)` as the first-occasion TI-predictor observed mean. pub use event_time::refuse_impulse_carry_observed_mean_as_initial_time_independent_observed_mean; /// Refuse treating impulse-carry `τ + λ(μ_t + e^{a(t−u)} m x)` as the time-independent-predictor observed mean. pub use event_time::refuse_impulse_carry_observed_mean_as_time_independent_observed_mean; /// Refuse treating contemporaneous `τ + λ(μ_t + m x)` as the impulse-carry observed mean. pub use event_time::refuse_impulse_observed_mean_as_impulse_carry_observed_mean; +/// Refuse treating contemporaneous `τ + λ(μ_t + m x)` as the first-occasion TD-predictor observed mean. +pub use event_time::refuse_impulse_observed_mean_as_initial_time_dependent_observed_mean; /// Refuse treating contemporaneous `τ + λ(μ_t + m x)` as the first-occasion TI-predictor observed mean. pub use event_time::refuse_impulse_observed_mean_as_initial_time_independent_observed_mean; /// Refuse treating contemporaneous `τ + λ(μ_t + m x)` as the time-independent-predictor observed mean. @@ -248,6 +263,8 @@ pub use event_time::refuse_initial_time_independent_effect_as_continuous_interce pub use event_time::refuse_initial_time_independent_effect_as_process_increment; /// Refuse treating the Table 3 first-occasion TI shift as `M x`. pub use event_time::refuse_initial_time_independent_effect_as_time_dependent_impulse; +/// Refuse treating first-occasion TI observed mean as the first-occasion TD observed mean. +pub use event_time::refuse_initial_time_independent_observed_mean_as_initial_time_dependent_observed_mean; /// Refuse treating Driver Eq. 3–4 lagged latent covariance as `cov(y_t, y_{t-1})`. pub use event_time::refuse_latent_lagged_covariance_as_observed_covariance; /// Refuse treating Driver Eq. 5 latent mean as `E(y)`. @@ -288,6 +305,8 @@ pub use event_time::refuse_time_independent_effect_as_continuous_intercept; pub use event_time::refuse_time_independent_effect_as_time_dependent_impulse; /// Refuse treating Driver Eq. 3 `TIPREDEFFECT` increment as Voelkle Eq. 14. pub use event_time::refuse_time_independent_effect_as_time_varying_discrete_effect; +/// Refuse treating process-increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` as the first-occasion TD-predictor observed mean. +pub use event_time::refuse_time_independent_observed_mean_as_initial_time_dependent_observed_mean; /// Refuse treating process-increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` as the first-occasion TI-predictor observed mean. pub use event_time::refuse_time_independent_observed_mean_as_initial_time_independent_observed_mean; /// Refuse treating Driver §4.3 trait variance as process noise. 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 0dea933f..2eeba1b9 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -14,6 +14,7 @@ use psychometric_core::{ recover_discrete_latent_mean_with_time_independent_predictor, recover_discrete_latent_variance, recover_discrete_observed_mean, recover_discrete_observed_mean_with_impulse, recover_discrete_observed_mean_with_impulse_carry, + recover_discrete_observed_mean_with_initial_time_dependent_predictor, recover_discrete_observed_mean_with_initial_time_independent_predictor, recover_discrete_observed_mean_with_time_independent_predictor, recover_discrete_process_noise, recover_discrete_time_independent_predictor_effect, @@ -33,12 +34,15 @@ use psychometric_core::{ refuse_continuous_intercept_as_manifest_means, refuse_difference_quotient_as_local_rate, refuse_evolved_observed_mean_as_impulse_carry_observed_mean, refuse_evolved_observed_mean_as_impulse_observed_mean, + refuse_evolved_observed_mean_as_initial_time_dependent_observed_mean, refuse_evolved_observed_mean_as_initial_time_independent_observed_mean, refuse_evolved_observed_mean_as_time_independent_observed_mean, refuse_finite_interval_process_noise_as_stationary_variance, + refuse_impulse_carry_observed_mean_as_initial_time_dependent_observed_mean, refuse_impulse_carry_observed_mean_as_initial_time_independent_observed_mean, refuse_impulse_carry_observed_mean_as_time_independent_observed_mean, refuse_impulse_observed_mean_as_impulse_carry_observed_mean, + refuse_impulse_observed_mean_as_initial_time_dependent_observed_mean, refuse_impulse_observed_mean_as_initial_time_independent_observed_mean, refuse_impulse_observed_mean_as_time_independent_observed_mean, refuse_initial_latent_mean_as_evolved_mean, @@ -55,6 +59,7 @@ use psychometric_core::{ refuse_initial_time_independent_effect_as_continuous_intercept, refuse_initial_time_independent_effect_as_process_increment, refuse_initial_time_independent_effect_as_time_dependent_impulse, + refuse_initial_time_independent_observed_mean_as_initial_time_dependent_observed_mean, refuse_latent_lagged_covariance_as_observed_covariance, refuse_latent_mean_as_observed_mean, refuse_latent_variance_as_observed_variance, refuse_manifest_means_as_observed_mean, refuse_manifest_trait_variance_as_measurement_error, @@ -73,6 +78,7 @@ use psychometric_core::{ refuse_time_independent_effect_as_continuous_intercept, refuse_time_independent_effect_as_time_dependent_impulse, refuse_time_independent_effect_as_time_varying_discrete_effect, + refuse_time_independent_observed_mean_as_initial_time_dependent_observed_mean, refuse_time_independent_observed_mean_as_initial_time_independent_observed_mean, refuse_trait_variance_as_process_noise, refuse_trait_variance_as_stationary_within_subject, refuse_unmatched_time_varying_predictor_interval, @@ -3028,3 +3034,394 @@ fn admitted_coordinates_still_required_for_multilevel_weights() { ); let _ = IndicatorKind::AdditiveLogRatio; } + +#[test] +fn discrete_observed_mean_with_initial_time_dependent_predictor_recovers_driver_equation_five() { + let loading = 2.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let effect = 0.4_f64; + let predictor = 3.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let observed = recover_discrete_observed_mean_with_initial_time_dependent_predictor( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-t0tdpred-mean"); + let composed = recover_discrete_latent_mean_with_initial_time_dependent_predictor( + initial, + drift, + intercept, + effect, + predictor, + delta, + LagClock::EventTime, + ) + .expect("eq3-t0tdpred"); + let expected = manifest_mean + loading * composed; + let error = rmse(&[expected], &[observed]); + assert!( + error < 1e-15, + "Driver Eq. 5 of Table 3 T0TDPREDEFFECT RMSE {error}" + ); + let evolved_observed = recover_discrete_observed_mean( + loading, + initial, + drift, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq3-eq5-mean"); + let evolved_error = rmse(&[expected], &[evolved_observed]); + assert!( + evolved_error > error, + "τ + λ μ_t is not T0TDPREDEFFECT E(y_t): RMSE {evolved_error} must exceed {error}" + ); + let process_observed = recover_discrete_observed_mean_with_time_independent_predictor( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-tipred-mean"); + let process_error = rmse(&[expected], &[process_observed]); + assert!( + process_error > error, + "τ + λ(μ_t + increment) is not T0TDPREDEFFECT E(y_t): RMSE {process_error} must exceed {error}" + ); +} + +#[test] +fn discrete_observed_mean_with_initial_time_dependent_predictor_is_not_impulse_or_carry() { + let loading = 2.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let effect = 0.4_f64; + let predictor = 3.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let observed = recover_discrete_observed_mean_with_initial_time_dependent_predictor( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-t0tdpred-mean"); + let composed = recover_discrete_latent_mean_with_initial_time_dependent_predictor( + initial, + drift, + intercept, + effect, + predictor, + delta, + LagClock::EventTime, + ) + .expect("eq3-t0tdpred"); + let expected = manifest_mean + loading * composed; + let error = rmse(&[expected], &[observed]); + let impulse_observed = recover_discrete_observed_mean_with_impulse( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-impulse-mean"); + let impulse_error = rmse(&[expected], &[impulse_observed]); + assert!( + impulse_error > error, + "τ + λ(μ_t + m x) is not T0TDPREDEFFECT E(y_t): RMSE {impulse_error} must exceed {error}" + ); + let carried_observed = recover_discrete_observed_mean_with_impulse_carry( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + 1.0, + LagClock::EventTime, + ) + .expect("eq5-carry-mean"); + let carry_error = rmse(&[expected], &[carried_observed]); + assert!( + carry_error > error, + "τ + λ(μ_t + carry) is not T0TDPREDEFFECT E(y_t): RMSE {carry_error} must exceed {error}" + ); + let intercept_error = rmse(&[expected], &[manifest_mean]); + assert!( + intercept_error > error, + "MANIFESTMEANS is not T0TDPREDEFFECT E(y_t): RMSE {intercept_error} must exceed {error}" + ); + let latent_error = rmse(&[expected], &[composed]); + assert!( + latent_error > error, + "evolved-plus-T0TDPRED latent mean is not E(y_t): RMSE {latent_error} must exceed {error}" + ); +} + +#[test] +#[allow(clippy::too_many_lines)] +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; + let effect = 0.4_f64; + let predictor = 3.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let observed = recover_discrete_observed_mean_with_initial_time_dependent_predictor( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-t0tdpred-mean"); + let evolved_observed = recover_discrete_observed_mean( + loading, + initial, + drift, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq3-eq5-mean"); + let process_observed = recover_discrete_observed_mean_with_time_independent_predictor( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-tipred-mean"); + let impulse_observed = recover_discrete_observed_mean_with_impulse( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-impulse-mean"); + let carried_observed = recover_discrete_observed_mean_with_impulse_carry( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + 1.0, + LagClock::EventTime, + ) + .expect("eq5-carry-mean"); + let tipred_observed = recover_discrete_observed_mean_with_initial_time_independent_predictor( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-t0tipred-mean"); + assert_eq!( + refuse_evolved_observed_mean_as_initial_time_dependent_observed_mean( + evolved_observed, + observed + ), + Err(PsychometricError::EvolvedObservedMeanIsNotInitialTimeDependentObservedMean) + ); + assert_eq!( + refuse_time_independent_observed_mean_as_initial_time_dependent_observed_mean( + process_observed, + observed + ), + Err(PsychometricError::TimeIndependentObservedMeanIsNotInitialTimeDependentObservedMean) + ); + assert_eq!( + refuse_impulse_observed_mean_as_initial_time_dependent_observed_mean( + impulse_observed, + observed + ), + Err(PsychometricError::ImpulseObservedMeanIsNotInitialTimeDependentObservedMean) + ); + assert_eq!( + refuse_impulse_carry_observed_mean_as_initial_time_dependent_observed_mean( + carried_observed, + observed + ), + Err(PsychometricError::ImpulseCarryObservedMeanIsNotInitialTimeDependentObservedMean) + ); + assert_eq!( + refuse_initial_time_independent_observed_mean_as_initial_time_dependent_observed_mean( + tipred_observed, + observed + ), + Err(PsychometricError::InitialTimeIndependentObservedMeanIsNotInitialTimeDependentObservedMean) + ); +} + +#[test] +fn discrete_observed_mean_with_initial_time_dependent_predictor_zero_loading_is_manifest_mean() { + let loading = 2.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let effect = 0.4_f64; + let predictor = 3.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let observed = recover_discrete_observed_mean_with_initial_time_dependent_predictor( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-t0tdpred-mean"); + let composed = recover_discrete_latent_mean_with_initial_time_dependent_predictor( + initial, + drift, + intercept, + effect, + predictor, + delta, + LagClock::EventTime, + ) + .expect("eq3-t0tdpred"); + assert_eq!( + refuse_latent_mean_as_observed_mean(composed, observed), + Err(PsychometricError::LatentMeanIsNotObservedMean) + ); + assert_eq!( + refuse_manifest_means_as_observed_mean(manifest_mean, observed), + Err(PsychometricError::ManifestMeansIsNotObservedMean) + ); + let zero_loading = recover_discrete_observed_mean_with_initial_time_dependent_predictor( + 0.0, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("lambda0"); + assert!((zero_loading - manifest_mean).abs() < 1e-15); +} + +#[test] +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, + 2.0, + 0.0, + 0.0, + 0.0, + 3.0, + 0.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_observed_mean_with_initial_time_dependent_predictor( + 2.0, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_discrete_observed_mean_with_initial_time_dependent_predictor( + 2.0, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + let scaled = recover_discrete_observed_mean_with_initial_time_dependent_predictor( + 1e308, + 1e-308, + 0.0, + 0.0, + 0.0, + 3.0, + 0.0, + 1.0, + LagClock::EventTime, + ) + .expect("scale"); + assert!( + (scaled - 1.0).abs() < 1e-15, + "Driver Eq. 5 of Table 3 T0TDPREDEFFECT must keep λ=1e308, μ=1e-308: got {scaled}" + ); +} diff --git a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs index 23538c76..b7fe953b 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -12,6 +12,7 @@ use psychometric_core::{ recover_discrete_latent_mean_with_time_independent_predictor, recover_discrete_latent_variance, recover_discrete_observed_mean, recover_discrete_observed_mean_with_impulse, recover_discrete_observed_mean_with_impulse_carry, + recover_discrete_observed_mean_with_initial_time_dependent_predictor, recover_discrete_observed_mean_with_initial_time_independent_predictor, recover_discrete_observed_mean_with_time_independent_predictor, recover_discrete_process_noise, recover_discrete_time_independent_predictor_effect, @@ -30,12 +31,15 @@ use psychometric_core::{ refuse_continuous_intercept_as_manifest_means, refuse_evolved_observed_mean_as_impulse_carry_observed_mean, refuse_evolved_observed_mean_as_impulse_observed_mean, + refuse_evolved_observed_mean_as_initial_time_dependent_observed_mean, refuse_evolved_observed_mean_as_initial_time_independent_observed_mean, refuse_evolved_observed_mean_as_time_independent_observed_mean, refuse_finite_interval_process_noise_as_stationary_variance, + refuse_impulse_carry_observed_mean_as_initial_time_dependent_observed_mean, refuse_impulse_carry_observed_mean_as_initial_time_independent_observed_mean, refuse_impulse_carry_observed_mean_as_time_independent_observed_mean, refuse_impulse_observed_mean_as_impulse_carry_observed_mean, + refuse_impulse_observed_mean_as_initial_time_dependent_observed_mean, refuse_impulse_observed_mean_as_initial_time_independent_observed_mean, refuse_impulse_observed_mean_as_time_independent_observed_mean, refuse_initial_latent_mean_as_evolved_mean, @@ -52,6 +56,7 @@ use psychometric_core::{ refuse_initial_time_independent_effect_as_continuous_intercept, refuse_initial_time_independent_effect_as_process_increment, refuse_initial_time_independent_effect_as_time_dependent_impulse, + refuse_initial_time_independent_observed_mean_as_initial_time_dependent_observed_mean, refuse_latent_lagged_covariance_as_observed_covariance, refuse_latent_mean_as_observed_mean, refuse_latent_variance_as_observed_variance, refuse_manifest_means_as_observed_mean, refuse_manifest_trait_variance_as_measurement_error, @@ -68,6 +73,7 @@ use psychometric_core::{ refuse_time_independent_effect_as_continuous_intercept, refuse_time_independent_effect_as_time_dependent_impulse, refuse_time_independent_effect_as_time_varying_discrete_effect, + refuse_time_independent_observed_mean_as_initial_time_dependent_observed_mean, refuse_time_independent_observed_mean_as_initial_time_independent_observed_mean, refuse_trait_variance_as_process_noise, refuse_trait_variance_as_stationary_within_subject, }; @@ -1146,6 +1152,179 @@ fn evolved_and_process_observed_mean_are_not_initial_time_independent_observed_m ); } +#[test] +#[allow(clippy::too_many_lines)] +fn evolved_and_process_observed_mean_are_not_initial_time_dependent_observed_mean() { + let loading = 2.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let effect = 0.4_f64; + let predictor = 3.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let initial_observed = recover_discrete_observed_mean_with_initial_time_dependent_predictor( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-t0tdpred-mean"); + let evolved_observed = recover_discrete_observed_mean( + loading, + initial, + drift, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq3-eq5-mean"); + let process_observed = recover_discrete_observed_mean_with_time_independent_predictor( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-tipred-mean"); + let impulse_observed = recover_discrete_observed_mean_with_impulse( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-impulse-mean"); + let carried_observed = recover_discrete_observed_mean_with_impulse_carry( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + 1.0, + LagClock::EventTime, + ) + .expect("eq5-carry-mean"); + let tipred_observed = recover_discrete_observed_mean_with_initial_time_independent_predictor( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-t0tipred-mean"); + let composed = recover_discrete_latent_mean_with_initial_time_dependent_predictor( + initial, + drift, + intercept, + effect, + predictor, + delta, + LagClock::EventTime, + ) + .expect("eq3-t0tdpred"); + assert!( + (evolved_observed - initial_observed).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 of Table 3 T0TDPREDEFFECT): τ + λ μ_t is not T0TDPREDEFFECT E(y_t)" + ); + assert!( + (process_observed - initial_observed).abs() > 1e-3, + "Driver et al. (2017, Eq. 5): TIPREDEFFECT E(y_t) is not T0TDPREDEFFECT E(y_t)" + ); + assert!( + (impulse_observed - initial_observed).abs() > 1e-3, + "Driver et al. (2017, Eq. 5): τ + λ(μ_t + m x) is not T0TDPREDEFFECT E(y_t)" + ); + assert!( + (carried_observed - initial_observed).abs() > 1e-3, + "Driver et al. (2017, Eq. 5): τ + λ(μ_t + carry) is not T0TDPREDEFFECT E(y_t)" + ); + assert!( + (composed - initial_observed).abs() > 1e-3, + "Driver et al. (2017, Eq. 5): evolved-plus-T0TDPRED latent mean is not E(y_t)" + ); + assert!( + (manifest_mean - initial_observed).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 / Table 2 p. 12): MANIFESTMEANS is not T0TDPREDEFFECT E(y_t)" + ); + // Same numbers as T0TIPRED yield the same product; Table 3 names a different matrix. + assert!((tipred_observed - initial_observed).abs() < 1e-15); + assert_eq!( + refuse_evolved_observed_mean_as_initial_time_dependent_observed_mean( + evolved_observed, + initial_observed + ), + Err( + psychometric_core::PsychometricError::EvolvedObservedMeanIsNotInitialTimeDependentObservedMean + ) + ); + assert_eq!( + refuse_time_independent_observed_mean_as_initial_time_dependent_observed_mean( + process_observed, + initial_observed + ), + Err( + psychometric_core::PsychometricError::TimeIndependentObservedMeanIsNotInitialTimeDependentObservedMean + ) + ); + assert_eq!( + refuse_impulse_observed_mean_as_initial_time_dependent_observed_mean( + impulse_observed, + initial_observed + ), + Err( + psychometric_core::PsychometricError::ImpulseObservedMeanIsNotInitialTimeDependentObservedMean + ) + ); + assert_eq!( + refuse_impulse_carry_observed_mean_as_initial_time_dependent_observed_mean( + carried_observed, + initial_observed + ), + Err( + psychometric_core::PsychometricError::ImpulseCarryObservedMeanIsNotInitialTimeDependentObservedMean + ) + ); + assert_eq!( + refuse_initial_time_independent_observed_mean_as_initial_time_dependent_observed_mean( + tipred_observed, + initial_observed + ), + Err( + psychometric_core::PsychometricError::InitialTimeIndependentObservedMeanIsNotInitialTimeDependentObservedMean + ) + ); + assert_eq!( + refuse_latent_mean_as_observed_mean(composed, initial_observed), + Err(psychometric_core::PsychometricError::LatentMeanIsNotObservedMean) + ); + assert_eq!( + refuse_manifest_means_as_observed_mean(manifest_mean, initial_observed), + Err(psychometric_core::PsychometricError::ManifestMeansIsNotObservedMean) + ); +} + #[test] fn time_dependent_impulse_carry_is_not_contemporaneous_cint_tipred_or_equation_fourteen() { let effect = 0.4_f64; diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index d734a56c..a068e49b 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -1,7 +1,7 @@ # ADR 0005 — Posterior-aware ESEM/DSEM and structural interpretation **Decision status:** Accepted -**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), exact scalar contemporaneous `TDPREDEFFECT` impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; the §7.2 level-change form is not that impulse), exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of that Eq. 3 composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar time-independent `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle Eq. 14; `B` is not that discrete increment), exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of that Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of that carried latent mean; `τ + λ μ_t` is not that observed mean), exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; `T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`), exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; `T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target +**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), exact scalar contemporaneous `TDPREDEFFECT` impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; the §7.2 level-change form is not that impulse), exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of that Eq. 3 composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar time-independent `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle Eq. 14; `B` is not that discrete increment), exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of that Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of that carried latent mean; `τ + λ μ_t` is not that observed mean), exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; `T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`), exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; `T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand TD composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target **Date:** 2026-08-05 **Supersedes:** None. ADR 0012 governs upstream topic measurement/network coordinates; this ADR governs higher-order psychometric structure and longitudinal interpretation. @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), recovers the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z; `TDPREDEFFECT` is `M`, not `CINT`; `M x` is not `A^{-1}[e^{A Δt} − I] B z`; `M x` is not Voelkle et al., 2012, Eq. 14; the level-change form is not that impulse), recovers the exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition; JSS PDF re-opened 2026-08-20T09:01Z; the evolved map `τ + λ μ_t` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-impulse latent mean is not `E(y_t)`), recovers the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z; `TIPREDEFFECT` is `B`, not `κ`; form `B z` first, then the discrete intercept map; a zero drift is `B z Δt`; `B` is not the discrete increment; `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle Eq. 14), recovers the exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; JSS PDF re-opened 2026-08-20T12:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-increment latent mean is not `E(y_t)`), recovers the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z; form `m x` first, then `e^{a(t−u)} m x`; a zero drift is `m x`; underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept; `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; an impulse at `u = t` is the contemporaneous map; an impulse at `u ≤ t0` is already in `η(t0)`), recovers the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; JSS PDF re-opened 2026-08-20T05:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the carried latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and its Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF opened 2026-08-20T15:14Z; form `t0_b z` first, then `e^{a Δt} t0_b z`; a zero drift is `t0_b z`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; `e^{A Δt} t0_b z` is not `t0_b z`; `T0TIPREDEFFECT` is the coefficient, not the shift), recovers the exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition; JSS PDF re-opened 2026-08-20T15:28Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and its Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; form `t0_m x0` first, then `e^{a Δt} t0_m x0`; a zero drift is `t0_m x0`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `T0TDPREDEFFECT` is the coefficient, not the shift; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), recovers the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z; `TDPREDEFFECT` is `M`, not `CINT`; `M x` is not `A^{-1}[e^{A Δt} − I] B z`; `M x` is not Voelkle et al., 2012, Eq. 14; the level-change form is not that impulse), recovers the exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition; JSS PDF re-opened 2026-08-20T09:01Z; the evolved map `τ + λ μ_t` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-impulse latent mean is not `E(y_t)`), recovers the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z; `TIPREDEFFECT` is `B`, not `κ`; form `B z` first, then the discrete intercept map; a zero drift is `B z Δt`; `B` is not the discrete increment; `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle Eq. 14), recovers the exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; JSS PDF re-opened 2026-08-20T12:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-increment latent mean is not `E(y_t)`), recovers the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z; form `m x` first, then `e^{a(t−u)} m x`; a zero drift is `m x`; underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept; `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; an impulse at `u = t` is the contemporaneous map; an impulse at `u ≤ t0` is already in `η(t0)`), recovers the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; JSS PDF re-opened 2026-08-20T05:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the carried latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and its Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF opened 2026-08-20T15:14Z; form `t0_b z` first, then `e^{a Δt} t0_b z`; a zero drift is `t0_b z`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; `e^{A Δt} t0_b z` is not `t0_b z`; `T0TIPREDEFFECT` is the coefficient, not the shift), recovers the exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition; JSS PDF re-opened 2026-08-20T15:28Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and its Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; form `t0_m x0` first, then `e^{a Δt} t0_m x0`; a zero drift is `t0_m x0`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `T0TDPREDEFFECT` is the coefficient, not the shift; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), recovers the exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; the first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. 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 e38b3995..0ea7a689 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -31,15 +31,16 @@ This slice stays inside `psychometric_core`. It does not add a second invariance 25. recover the exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and its Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF opened 2026-08-20T15:14Z) and refuse treating `t0_b z` as `A^{-1}[e^{A Δt} − I] B z`, as `κ`, or as `M x`; refuse treating `e^{A Δt} t0_b z` as `t0_b z`; refuse treating the coefficient as the shift; 26. recover the exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition; JSS PDF re-opened 2026-08-20T15:28Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`) and refuse treating `τ + λ μ_t`, `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, `τ + λ(μ_t + m x)`, or `τ + λ(μ_t + e^{a(t−u)} m x)` as that observed mean; 27. recover the exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and its Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z) and refuse treating `t0_m x0` as `M x`, as `e^{A(t−u)} M x` for `t0 < u < t`, as `t0_b z`, as `A^{-1}[e^{A Δt} − I] B z`, or as `κ`; refuse treating `e^{A Δt} t0_m x0` as `t0_m x0` or as `e^{A(t−u)} M x`; refuse treating the coefficient as the shift; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`; -28. refuse pooling discrete lags from unequal event intervals as one coefficient; -29. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); -30. refuse the difference quotient as a continuous-time rate; -31. apply the same event-time map to CWC residuals (still not DSEM); -32. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). +28. recover the exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; the first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean) and refuse treating `τ + λ μ_t`, `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, `τ + λ(μ_t + m x)`, `τ + λ(μ_t + e^{a(t−u)} m x)`, or `τ + λ(μ_t + e^{a Δt} t0_b z)` as that observed mean; +29. refuse pooling discrete lags from unequal event intervals as one coefficient; +30. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); +31. refuse the difference quotient as a continuous-time rate; +32. apply the same event-time map to CWC residuals (still not DSEM); +33. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). ## Claim boundary -This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. The contemporaneous `TDPREDEFFECT` impulse is not the continuous intercept, not the time-independent discrete effect, and not Voelkle et al. (2012, Eq. 14). The time-independent `TIPREDEFFECT` increment is not the continuous intercept, not the contemporaneous impulse, not Voelkle et al. (2012, Eq. 14), and not the coefficient `B`. The within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). The evolved observed mean `τ + λ μ_t` is not the contemporaneous-impulse observed mean `τ + λ(μ_t + m x)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not the impulse-carry observed mean `τ + λ(μ_t + e^{a(t−u)} m x)` when `u ≠ t`. The evolved observed mean `τ + λ μ_t` is not the impulse-carry observed mean. The carried latent mean is not `E(y_t)`. The evolved-plus-impulse latent mean is not `E(y_t)`. The evolved observed mean `τ + λ μ_t` is not the time-independent-predictor observed mean `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not that time-independent-predictor observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that time-independent-predictor observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TIPREDEFFECT` shift `t0_b z` is not the Eq. 3 process increment `A^{-1}[e^{A Δt} − I] B z`, not `κ`, and not `M x`. The Eq. 3 first-summand carry `e^{A Δt} t0_b z` is not `t0_b z` and is not that process increment. `T0TIPREDEFFECT` is the coefficient, not the first-occasion shift. The evolved observed mean `τ + λ μ_t` is not the first-occasion TI-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_b z)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion observed mean when `u ≠ t0`. The evolved-plus-T0TIPRED latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TDPREDEFFECT` shift `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `κ`. The Eq. 3 first-summand carry `e^{A Δt} t0_m x0` is not `t0_m x0` and is not that within-interval impulse carry. `T0TDPREDEFFECT` is the coefficient, not the first-occasion shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The Table 3 first-occasion `T0TDPREDEFFECT` shift `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `κ`. The Eq. 3 first-summand carry `e^{A Δt} t0_m x0` is not `t0_m x0` and is not that within-interval impulse carry. `T0TDPREDEFFECT` is the coefficient, not the first-occasion shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. +This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. The contemporaneous `TDPREDEFFECT` impulse is not the continuous intercept, not the time-independent discrete effect, and not Voelkle et al. (2012, Eq. 14). The time-independent `TIPREDEFFECT` increment is not the continuous intercept, not the contemporaneous impulse, not Voelkle et al. (2012, Eq. 14), and not the coefficient `B`. The within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). The evolved observed mean `τ + λ μ_t` is not the contemporaneous-impulse observed mean `τ + λ(μ_t + m x)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not the impulse-carry observed mean `τ + λ(μ_t + e^{a(t−u)} m x)` when `u ≠ t`. The evolved observed mean `τ + λ μ_t` is not the impulse-carry observed mean. The carried latent mean is not `E(y_t)`. The evolved-plus-impulse latent mean is not `E(y_t)`. The evolved observed mean `τ + λ μ_t` is not the time-independent-predictor observed mean `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not that time-independent-predictor observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that time-independent-predictor observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TIPREDEFFECT` shift `t0_b z` is not the Eq. 3 process increment `A^{-1}[e^{A Δt} − I] B z`, not `κ`, and not `M x`. The Eq. 3 first-summand carry `e^{A Δt} t0_b z` is not `t0_b z` and is not that process increment. `T0TIPREDEFFECT` is the coefficient, not the first-occasion shift. The evolved observed mean `τ + λ μ_t` is not the first-occasion TI-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_b z)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion observed mean when `u ≠ t0`. The evolved-plus-T0TIPRED latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TDPREDEFFECT` shift `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `κ`. The Eq. 3 first-summand carry `e^{A Δt} t0_m x0` is not `t0_m x0` and is not that within-interval impulse carry. `T0TDPREDEFFECT` is the coefficient, not the first-occasion shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The evolved observed mean `τ + λ μ_t` is not the first-occasion TD-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_m x0)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion TD observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion TD observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion TD observed mean when `u ≠ t0`. The first-occasion TI composition `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that first-occasion TD observed mean. Same numbers as `T0TIPREDEFFECT` yield the same product; Table 3 names a different matrix. The evolved-plus-T0TDPRED latent mean is not `E(y_t)`. ## Authoritative sources @@ -87,6 +88,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - **First-occasion time-independent predictor.** Driver et al. (2017, Table 3, p. 13; Eq. 3 first summand, p. 5; JSS PDF opened 2026-08-20T15:14Z): Table 3 names `T0TIPREDEFFECT` the effect of time-independent predictors on latents at `T0`. Table 2 / Table 3 name `TIPREDEFFECT` `B`, which enters Equation 3 as `A^{-1}[e^{A(t−t0)}−I]Bz`. The scalar first-occasion shift is `t0_b z`. Equation 3's first summand carries that shift as `e^{AΔt}t0_b z`. Form `t0_b z` first, then `e^{aΔt}t0_b z`. Form `μ_t` first, then add that carry. A zero drift is `t0_b z`. Binary64 underflow of `e^{aΔt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. A zero effect or zero predictor is exactly zero. `t0_b z` is not `A^{-1}[e^{AΔt}−I]Bz`, not `κ`, and not `M x`. `e^{AΔt}t0_b z` is not `t0_b z`. `T0TIPREDEFFECT` is the coefficient, not the shift. An overflowing product, rewrite, or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **First-occasion time-independent-predictor observed-indicator mean.** Driver et al. (2017, Eq. 5, p. 5; Eq. 3 first summand, p. 5; Table 3, p. 13; JSS PDF re-opened 2026-08-20T15:28Z): `y_i(t)=Γ+Λη_i(t)+ζ_i(t)` with `ζ∼N(0,Θ)` and `Γ∼N(τ,Ψ)`. The latent process at `t` after the Table 3 first-occasion carry is `μ_t+e^{aΔt}t0_b z`. The scalar composition is `E(y_t)=τ+λ(μ_t+e^{aΔt}t0_b z)`. Form the evolved-plus-carry latent mean first, then `τ+λ` of that mean. A zero loading is exactly `τ`. A zero evolved-plus-carry latent mean is exactly `τ`. A zero intercept is exactly `λ(μ_t+carry)`. The evolved observed mean `τ+λμ_t` is not this composition. The process-increment map `τ+λ(μ_t+A^{-1}[e^{AΔt}−I]Bz)` is not this composition. The contemporaneous map `τ+λ(μ_t+mx)` is not this composition. The impulse-carry map `τ+λ(μ_t+e^{a(t−u)}mx)` is not this composition when `u≠t0`. `MANIFESTMEANS` is not `E(y_t)`. The evolved-plus-carry latent mean is not `E(y_t)`. `T0TIPREDEFFECT` is the coefficient, not that observed mean. An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **First-occasion time-dependent predictor.** Driver et al. (2017, Table 3, p. 13; Eq. 3 first summand, p. 5; JSS PDF re-opened 2026-08-20T19:10Z): Table 3 names `T0TDPREDEFFECT` the effect of time-dependent predictors on latents at `T0`. Table 2 / Table 3 name `TDPREDEFFECT` `M`, which enters Equation 3 as the printed fourth-summand Dirac `M x` at `u = t`. The scalar first-occasion shift is `t0_m x0`. Equation 3's first summand carries that shift as `e^{AΔt}t0_m x0`. Form `t0_m x0` first, then `e^{aΔt}t0_m x0`. Form `μ_t` first, then add that carry. A zero drift is `t0_m x0`. Binary64 underflow of `e^{aΔt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. A zero effect or zero predictor is exactly zero. `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{AΔt}−I]Bz`, and not `κ`. `e^{AΔt}t0_m x0` is not `t0_m x0`. `T0TDPREDEFFECT` is the coefficient, not the shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. An overflowing product, rewrite, or sum fails closed. This is not a Kalman filter and not ctsem estimation. +- **First-occasion time-dependent-predictor observed-indicator mean.** Driver et al. (2017, Eq. 5, p. 5; Eq. 3 first summand, p. 5; Table 3, p. 13; JSS PDF re-opened 2026-08-20T19:07Z): `y_i(t)=Γ+Λη_i(t)+ζ_i(t)` with `ζ∼N(0,Θ)` and `Γ∼N(τ,Ψ)`. The latent process at `t` after the Table 3 first-occasion TD carry is `μ_t+e^{aΔt}t0_m x0`. The scalar composition is `E(y_t)=τ+λ(μ_t+e^{aΔt}t0_m x0)`. Form the evolved-plus-carry latent mean first, then `τ+λ` of that mean. A zero loading is exactly `τ`. A zero evolved-plus-carry latent mean is exactly `τ`. A zero intercept is exactly `λ(μ_t+carry)`. The evolved observed mean `τ+λμ_t` is not this composition. The process-increment map `τ+λ(μ_t+A^{-1}[e^{AΔt}−I]Bz)` is not this composition. The contemporaneous map `τ+λ(μ_t+mx)` is not this composition. The impulse-carry map `τ+λ(μ_t+e^{a(t−u)}mx)` is not this composition when `u≠t0`. The first-occasion TI map `τ+λ(μ_t+e^{aΔt}t0_b z)` is not this composition. Same numbers as `T0TIPREDEFFECT` yield the same product; Table 3 names a different matrix. `MANIFESTMEANS` is not `E(y_t)`. The evolved-plus-carry latent mean is not `E(y_t)`. `T0TDPREDEFFECT` is the coefficient, not that observed mean. An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **CWC-then-lag.** Sample cluster means are removed first. Consecutive within residuals are then fitted by least squares to \(r_{t+\Delta t}\approx\exp(a\Delta t)\,r_{t}\) on event time. Same-sign pair-wise logs initialize the scalar Newton step. Sign-flipping \(T=2\) CWC pairs have no real logarithm and fail closed. Curran and Bauer (2011, pp. 607–608) show that this person-mean subtraction on a raw autoregressive series does **not** isolate the lagged within-person effect; the helper therefore does not claim to recover the raw-process drift. - **Already-centered irregular residual.** The caller supplies lagged within residuals. The mean of \(a=\ln(r_{t+\Delta t}/r_t)/\Delta t\) is the exact scalar map. Intervals may be irregular. The helper does not center again. This is not DSEM. @@ -118,6 +120,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - Driver et al. (2017, Table 3, p. 13; Eq. 3 first summand) recovers a known first-occasion shift \(t0_b z\) and carry \(e^{a\Delta t}t0_b z\) at machine-scale RMSE, and those RMSEs are smaller than treating `TIPREDEFFECT`, `CINT`, the coefficient, or the un-carried shift as the carry; composing \(\mu_t\) plus that carry recovers the known sum; a zero drift is \(t0_b z\); underflow of \(e^{a\Delta t}\) to `+0` is a vanishing carry and is kept; a zero effect or zero predictor is exactly zero; a non-event clock, a non-positive interval, and an overflowing product or rewrite fail closed; - Driver et al. (2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition; Table 3, p. 13) recovers a known \(E(y_t)=\tau+\lambda(\mu_t+e^{a\Delta t}t0_b z)\) at machine-scale RMSE, and that RMSE is smaller than treating \(\tau+\lambda\mu_t\), \(\tau+\lambda(\mu_t+A^{-1}[e^{A\Delta t}-I]Bz)\), \(\tau+\lambda(\mu_t+mx)\), \(\tau+\lambda(\mu_t+e^{a(t-u)}mx)\), `MANIFESTMEANS`, or the evolved-plus-carry latent mean as \(E(y_t)\); a zero loading is \(\tau\); a non-event clock, a non-positive interval, and an overflowing product or sum fail closed; - Driver et al. (2017, Table 3, p. 13; Eq. 3 first summand) recovers a known first-occasion time-dependent shift \(t0_m x0\) and carry \(e^{a\Delta t}t0_m x0\) at machine-scale RMSE, and those RMSEs are smaller than treating `TDPREDEFFECT` `M x`, the within-interval impulse carry, `T0TIPREDEFFECT`, `TIPREDEFFECT`, `CINT`, the coefficient, or the un-carried shift as the carry; composing \(\mu_t\) plus that carry recovers the known sum; a zero drift is \(t0_m x0\); underflow of \(e^{a\Delta t}\) to `+0` is a vanishing carry and is kept; a zero effect or zero predictor is exactly zero; a non-event clock, a non-positive interval, and an overflowing product or rewrite fail closed; +- Driver et al. (2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; Table 3, p. 13) recovers a known \(E(y_t)=\tau+\lambda(\mu_t+e^{a\Delta t}t0_m x0)\) at machine-scale RMSE, and that RMSE is smaller than treating \(\tau+\lambda\mu_t\), \(\tau+\lambda(\mu_t+A^{-1}[e^{A\Delta t}-I]Bz)\), \(\tau+\lambda(\mu_t+mx)\), \(\tau+\lambda(\mu_t+e^{a(t-u)}mx)\), `MANIFESTMEANS`, or the evolved-plus-carry latent mean as \(E(y_t)\); same numbers as `T0TIPREDEFFECT` yield the same product, but Table 3 names a different matrix and the refuse is fail-closed; a zero loading is \(\tau\); a non-event clock, a non-positive interval, and an overflowing product or sum fail closed; - pooling discrete lags from unequal intervals fails closed; - CWC-then-lag on a two-cluster decaying series has smaller computed RMSE than a level-pooled series when the latter is identified; - already-centered irregular residuals recover a known drift at machine-scale RMSE, and that RMSE is smaller than CWC of the corresponding raw autoregressive series (Curran & Bauer, 2011, pp. 607–608); From 7a1a25b6426e24829424c7c6dab61e34b3b47ac9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 19:48:13 +0000 Subject: [PATCH 48/62] =?UTF-8?q?feat(psychometric):=20recover=20Driver=20?= =?UTF-8?q?=C2=A77.2=20level-change=20CINT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Map κ=−a m x. Form m x first. Refuse impulse, free CINT, and TIPREDEFFECT. Fail closed on a≥0 and overflow. Keep PR #144 draft. Never self-approve. --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 1 + CLAUDE.md | 2 +- crates/psychometric_core/src/error.rs | 46 ++++ crates/psychometric_core/src/event_time.rs | 205 +++++++++++++++++- crates/psychometric_core/src/lib.rs | 12 + ...multilevel_event_time_recovery_contract.rs | 82 ++++++- .../scientific_claim_boundary_contract.rs | 60 ++++- docs/adr/0005-posterior-esem-dsem.md | 4 +- .../multilevel-event-time-recovery.md | 17 +- 10 files changed, 393 insertions(+), 38 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 79e4caab..e074b6a1 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -61,7 +61,7 @@ boundaries above remain the target modular MSA architecture. | `tepp_simulation` | known-truth temporal/event data generation | | `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics | | `tepp_api` | versioned DTO, schema, and export contracts | -| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; first-occasion `T0TIPREDEFFECT` shift is `t0_b z` and Eq. 3 first-summand carry is `e^{A Δt} t0_b z` (`T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_b z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), first-occasion `T0TDPREDEFFECT` shift is `t0_m x0` and Eq. 3 first-summand carry is `e^{A Δt} t0_m x0` (`T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | +| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; first-occasion `T0TIPREDEFFECT` shift is `t0_b z` and Eq. 3 first-summand carry is `e^{A Δt} t0_b z` (`T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_b z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), first-occasion `T0TDPREDEFFECT` shift is `t0_m x0` and Eq. 3 first-summand carry is `e^{A Δt} t0_m x0` (`T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; §7.2 level-change `CINT` is `κ = −a m x` with `a < 0` so `−κ / a = m x` (`−a m x` is not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | No crate exposes placeholder production behavior in Task 1. This prevents an empty façade from becoming a de facto public API before its invariants and tests diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f7a110a..351e638d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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, §7.2, pp. 20–21; Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T19:45Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar lasting level-change `CINT`. Section 7.2 contrasts a sudden Dirac that dissipates back to the process mean with a lasting level change. To generate that lasting change, `CINT` is set to `TDPREDEFFECT * −DRIFT`. The scalar setting is `κ = −a m x`. Form `m x` first, then multiply by `−a`. Stable `a < 0` is required so `−κ / a = m x` is an equilibrium offset. `a ≥ 0` cannot hold a new process mean. `−a m x` is not the contemporaneous jump `m x`. `−a m x` is not a free `CINT`. `−a m x` is not `A^{-1}[e^{A Δt} − I] B z`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not this `CINT` setting. A zero effect or zero predictor is exactly zero. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T19:45Z: `is_oa: false`; Springer `content/pdf` is HTML 200). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Eq. 3 first summand, p. 5; Table 3, p. 13; JSS PDF re-opened 2026-08-20T19:07Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar observed-indicator mean of a first-occasion time-dependent predictor. Equation 5 writes `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and `Γ ~ N(τ, Ψ)`. Table 3 names `T0TDPREDEFFECT` the effect of time-dependent predictors on latents at `T0`. Equation 3's first summand carries that shift as `e^{A Δt} t0_m x0`. The scalar composition is `E(y_t) = τ + λ(μ_t + e^{a Δt} t0_m x0)`. Form the evolved-plus-carry latent mean first, then `τ + λ` of that mean. The evolved observed mean `τ + λ μ_t` is not this composition. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not this composition. The contemporaneous map `τ + λ(μ_t + m x)` is not this composition. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not this composition when `u ≠ t0`. The first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not this composition. `MANIFESTMEANS` is not `E(y_t)`. The evolved-plus-carry latent mean is not `E(y_t)`. `T0TDPREDEFFECT` is the coefficient, not that observed mean. Same numbers as `T0TIPREDEFFECT` yield the same product; Table 3 names a different matrix. A zero loading is exactly `τ`. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T19:09Z: `is_oa: false`). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Table 3, p. 13; Eq. 3 first summand, p. 5; JSS PDF re-opened 2026-08-20T19:10Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar first-occasion time-dependent predictor shift and its carry. Table 3 names `T0TDPREDEFFECT` the effect of time-dependent predictors on latents at `T0`. Table 2 / Table 3 name `TDPREDEFFECT` `M`, which enters Equation 3 as the printed fourth-summand Dirac `M x` at `u = t`. Those are not the same matrix. The scalar first-occasion shift is `t0_m x0`. Equation 3's first summand carries that shift as `e^{A Δt} t0_m x0`. Form `t0_m x0` first, then `e^{a Δt} t0_m x0`. Form `μ_t` first, then add that carry. A zero drift is `t0_m x0` with no dissipation. Binary64 underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `κ`. `e^{A Δt} t0_m x0` is not `t0_m x0`. `T0TDPREDEFFECT` is the coefficient, not `t0_m x0`. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T19:09Z: `is_oa: false`). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Eq. 3 first summand, p. 5; Table 3, p. 13; JSS PDF re-opened 2026-08-20T15:28Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar observed-indicator mean of a first-occasion time-independent predictor. Equation 5 writes `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and `Γ ~ N(τ, Ψ)`. Table 3 names `T0TIPREDEFFECT` the effect of time-independent predictors on latents at `T0`. Equation 3's first summand carries that shift as `e^{A Δt} t0_b z`. The scalar composition is `E(y_t) = τ + λ(μ_t + e^{a Δt} t0_b z)`. Form the evolved-plus-carry latent mean first, then `τ + λ` of that mean. The evolved observed mean `τ + λ μ_t` is not this composition. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not this composition. The contemporaneous map `τ + λ(μ_t + m x)` is not this composition. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not this composition when `u ≠ t0`. `MANIFESTMEANS` is not `E(y_t)`. The evolved-plus-carry latent mean is not `E(y_t)`. `T0TIPREDEFFECT` is the coefficient, not that observed mean. A zero loading is exactly `τ`. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T15:14Z: `is_oa: false`; Springer `content/pdf` is HTML 200). diff --git a/CLAUDE.md b/CLAUDE.md index 53bac69a..18dd37dc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. Putnick and Bornstein (2016, PMC5145197 opened 2026-08-19T22:15Z) require scalar invariance before latent-mean comparison; residual invariance is not a prerequisite. Two-observation series have no residual degrees of freedom (`ordinary_least_squares_fit` returns residual variance `0`) and cap at strong/scalar; they still license means. This is two-group OLS, not MGCFA. Meredith (1993) names remain unread labels. -- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. The contemporaneous time-dependent predictor impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`). Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). The §7.2 level-change form is not that impulse. The observed mean of that contemporaneous impulse is `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-impulse latent mean is not `E(y_t)`. The time-independent predictor increment is `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`). Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. `TIPREDEFFECT` is `B`, not that discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle et al. (2012, Eq. 14). The observed mean of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The within-interval time-dependent impulse carry is `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation). Form `m x` first, then `e^{a(t−u)} m x`. A zero drift is `m x` with no dissipation. Underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept. `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). An impulse at `u = t` is the contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. The observed mean of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The carried latent mean is not `E(y_t)`. The first-occasion time-independent predictor shift is `t0_b z` (Driver et al., 2017, Table 3 `T0TIPREDEFFECT`; Eq. 3 first summand). Form `t0_b z` first, then `e^{a Δt} t0_b z`. Form `μ_t` first, then add that carry. A zero drift is `t0_b z`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. `e^{A Δt} t0_b z` is not `t0_b z`. `T0TIPREDEFFECT` is the coefficient, not the shift. The observed mean of that first-occasion carry is `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The evolved-plus-carry latent mean is not `E(y_t)`. The first-occasion time-dependent predictor shift is `t0_m x0` (Driver et al., 2017, Table 3 `T0TDPREDEFFECT`; Eq. 3 first summand; JSS PDF re-opened 2026-08-20T19:10Z). Form `t0_m x0` first, then `e^{a Δt} t0_m x0`. Form `μ_t` first, then add that carry. A zero drift is `t0_m x0`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`. `e^{A Δt} t0_m x0` is not `t0_m x0`. `T0TDPREDEFFECT` is the coefficient, not the shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The observed mean of that first-occasion TD carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean. The evolved-plus-carry latent mean is not `E(y_t)`. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. The contemporaneous time-dependent predictor impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`). Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). The §7.2 level-change form is not that impulse. The observed mean of that contemporaneous impulse is `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-impulse latent mean is not `E(y_t)`. The time-independent predictor increment is `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`). Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. `TIPREDEFFECT` is `B`, not that discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle et al. (2012, Eq. 14). The observed mean of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The within-interval time-dependent impulse carry is `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation). Form `m x` first, then `e^{a(t−u)} m x`. A zero drift is `m x` with no dissipation. Underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept. `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). An impulse at `u = t` is the contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. The observed mean of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The carried latent mean is not `E(y_t)`. The first-occasion time-independent predictor shift is `t0_b z` (Driver et al., 2017, Table 3 `T0TIPREDEFFECT`; Eq. 3 first summand). Form `t0_b z` first, then `e^{a Δt} t0_b z`. Form `μ_t` first, then add that carry. A zero drift is `t0_b z`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. `e^{A Δt} t0_b z` is not `t0_b z`. `T0TIPREDEFFECT` is the coefficient, not the shift. The observed mean of that first-occasion carry is `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The evolved-plus-carry latent mean is not `E(y_t)`. The first-occasion time-dependent predictor shift is `t0_m x0` (Driver et al., 2017, Table 3 `T0TDPREDEFFECT`; Eq. 3 first summand; JSS PDF re-opened 2026-08-20T19:10Z). Form `t0_m x0` first, then `e^{a Δt} t0_m x0`. Form `μ_t` first, then add that carry. A zero drift is `t0_m x0`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`. `e^{A Δt} t0_m x0` is not `t0_m x0`. `T0TDPREDEFFECT` is the coefficient, not the shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The observed mean of that first-occasion TD carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean. The evolved-plus-carry latent mean is not `E(y_t)`. The lasting level-change `CINT` is `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z). Form `m x` first, then multiply by `−a`. Stable `a < 0` is required so `−κ / a = m x` is an equilibrium offset. `a ≥ 0` cannot hold a new process mean. `−a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not this `CINT` setting. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. diff --git a/crates/psychometric_core/src/error.rs b/crates/psychometric_core/src/error.rs index 263abe6f..646b235d 100644 --- a/crates/psychometric_core/src/error.rs +++ b/crates/psychometric_core/src/error.rs @@ -245,6 +245,20 @@ pub enum PsychometricError { /// `τ + λ(μ_t + e^{a Δt} t0_b z)` is not /// `τ + λ(μ_t + e^{a Δt} t0_m x0)`. InitialTimeIndependentObservedMeanIsNotInitialTimeDependentObservedMean, + /// Driver §7.2 level-change `CINT` was requested for a non-stable + /// drift. Lasting level change via `CINT = TDPREDEFFECT × (−DRIFT)` + /// requires `a < 0` so `−κ / a = m x` is an equilibrium offset. + LevelChangeRequiresStableDrift, + /// Driver §7.2 level-change `CINT` was treated as the + /// contemporaneous Dirac. `−a m x` is not `m x`. + LevelChangeInterceptIsNotImpulse, + /// Driver §7.2 level-change `CINT` was treated as a free `CINT`. + /// `−a m x` is not an arbitrary `κ`. + LevelChangeInterceptIsNotFreeContinuousIntercept, + /// Driver §7.2 level-change `CINT` was treated as the Eq. 3 + /// process increment. `−a m x` is not + /// `A^{-1}[e^{A Δt} − I] B z`. + LevelChangeInterceptIsNotProcessIncrement, } impl fmt::Display for PsychometricError { @@ -446,6 +460,18 @@ impl fmt::Display for PsychometricError { Self::InitialTimeIndependentObservedMeanIsNotInitialTimeDependentObservedMean => { "first-occasion time-independent-predictor observed mean is not the first-occasion time-dependent-predictor observed mean" } + Self::LevelChangeRequiresStableDrift => { + "lasting level-change CINT requires stable negative drift" + } + Self::LevelChangeInterceptIsNotImpulse => { + "level-change CINT is not the contemporaneous impulse" + } + Self::LevelChangeInterceptIsNotFreeContinuousIntercept => { + "level-change CINT is not a free continuous intercept" + } + Self::LevelChangeInterceptIsNotProcessIncrement => { + "level-change CINT is not the time-independent process increment" + } }; formatter.write_str(message) } @@ -764,4 +790,24 @@ mod tests { "first-occasion time-independent-predictor observed mean is not the first-occasion time-dependent-predictor observed mean" ); } + + #[test] + fn level_change_boundary_messages_are_stable() { + assert_eq!( + PsychometricError::LevelChangeRequiresStableDrift.to_string(), + "lasting level-change CINT requires stable negative drift" + ); + assert_eq!( + PsychometricError::LevelChangeInterceptIsNotImpulse.to_string(), + "level-change CINT is not the contemporaneous impulse" + ); + assert_eq!( + PsychometricError::LevelChangeInterceptIsNotFreeContinuousIntercept.to_string(), + "level-change CINT is not a free continuous intercept" + ); + assert_eq!( + PsychometricError::LevelChangeInterceptIsNotProcessIncrement.to_string(), + "level-change CINT is not the time-independent process increment" + ); + } } diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index c5494a41..d9ef20a1 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -70,7 +70,11 @@ //! that carried first-occasion TD shift is //! `τ + λ(μ_t + e^{a Δt} t0_m x0)` (`τ + λ μ_t` is not that //! observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that -//! observed mean). The JSS article +//! observed mean). The JSS §7.2 lasting level change sets +//! `CINT` to `TDPREDEFFECT * −DRIFT` (`κ = −a m x`; `a < 0` so +//! `−κ / a = m x`). That `CINT` setting is not the dissipating +//! Dirac, not a free `CINT`, and not the extra near-zero-drift +//! latent process also named in §7.2. The JSS article //! has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). //! The difference quotient `(x(t+Δt) − x(t)) / Δt` (their //! Eqs. 3–4) is refused. This is not DSEM and not a matrix `expm`. @@ -1335,10 +1339,11 @@ pub fn refuse_initial_observed_mean_as_evolved_observed_mean( /// the second-summand `CINT` map `A^{-1}[e^{A Δt} − I] κ`, not the /// third-summand time-independent map `A^{-1}[e^{A Δt} − I] B z`, /// and not Voelkle et al. (2012, Eq. 14) `a_{yx} Δt`. The §7.2 -/// level-change form is a different specification (an extra latent -/// process with near-zero drift) and is not this map. A zero effect -/// or zero predictor is exactly zero. This is not a Kalman filter -/// and not ctsem estimation. +/// lasting level change sets `CINT` to `TDPREDEFFECT * −DRIFT` +/// (`κ = −a m x`) and is not this jump. The extra near-zero-drift +/// latent process also named in §7.2 is a third specification. A +/// zero effect or zero predictor is exactly zero. This is not a +/// Kalman filter and not ctsem estimation. /// /// # Errors /// @@ -1357,6 +1362,100 @@ pub fn recover_time_dependent_predictor_impulse( require_finite(time_dependent_effect * time_dependent_predictor) } +/// Exact scalar level-change `CINT` from Driver Section 7.2. +/// +/// Driver, Oud, and Voelkle (2017, §7.2, pp. 20–21; Eq. 1–3, pp. 4–5; +/// Table 2, p. 12; JSS PDF re-opened 2026-08-20T19:45Z from +/// ) +/// contrast a sudden Dirac that dissipates back to the process mean +/// with a lasting level change. To generate that lasting change, +/// `CINT` is set to `TDPREDEFFECT * −DRIFT`. The scalar setting is +/// `κ = −a m x`. Form `m x` first, then multiply by `−a`. A zero +/// effect or zero predictor is exactly zero. Stable `a < 0` is +/// required so `−κ / a = m x` is an equilibrium offset. `a ≥ 0` +/// cannot hold a new process mean. `−a m x` is not the +/// contemporaneous jump `m x`. `−a m x` is not a free `CINT`. +/// `−a m x` is not `A^{-1}[e^{A Δt} − I] B z`. The extra +/// near-zero-drift latent process also named in §7.2 is a different +/// specification and is not this `CINT` setting. This is not a +/// Kalman filter and not ctsem estimation. +/// +/// # Errors +/// +/// Returns [`PsychometricError::InvalidNumericInput`] when an input +/// is non-finite or a product overflows, and +/// [`PsychometricError::LevelChangeRequiresStableDrift`] when the +/// drift is not strictly negative and the impulse is nonzero. +pub fn recover_level_change_continuous_intercept( + time_dependent_effect: f64, + time_dependent_predictor: f64, + log_rate: f64, +) -> Result { + if !time_dependent_effect.is_finite() + || !time_dependent_predictor.is_finite() + || !log_rate.is_finite() + { + return Err(PsychometricError::InvalidNumericInput); + } + if time_dependent_effect == 0.0 || time_dependent_predictor == 0.0 { + return Ok(0.0); + } + if log_rate >= 0.0 { + return Err(PsychometricError::LevelChangeRequiresStableDrift); + } + let impulse = require_finite(time_dependent_effect * time_dependent_predictor)?; + require_finite(-log_rate * impulse) +} + +/// Refuse treating the §7.2 level-change `CINT` as the +/// contemporaneous Dirac. +/// +/// `κ = −a m x` is not the jump `m x`. +/// +/// # Errors +/// +/// Always returns [`PsychometricError::LevelChangeInterceptIsNotImpulse`]. +pub fn refuse_level_change_intercept_as_impulse( + level_change_intercept: f64, + time_dependent_impulse: f64, +) -> Result { + let _ = (level_change_intercept, time_dependent_impulse); + Err(PsychometricError::LevelChangeInterceptIsNotImpulse) +} + +/// Refuse treating the §7.2 level-change `CINT` as a free `CINT`. +/// +/// `κ = −a m x` is not an arbitrary continuous intercept. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::LevelChangeInterceptIsNotFreeContinuousIntercept`]. +pub fn refuse_level_change_intercept_as_free_continuous_intercept( + level_change_intercept: f64, + continuous_intercept: f64, +) -> Result { + let _ = (level_change_intercept, continuous_intercept); + Err(PsychometricError::LevelChangeInterceptIsNotFreeContinuousIntercept) +} + +/// Refuse treating the §7.2 level-change `CINT` as the Eq. 3 +/// process increment. +/// +/// `κ = −a m x` is not `A^{-1}[e^{A Δt} − I] B z`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::LevelChangeInterceptIsNotProcessIncrement`]. +pub fn refuse_level_change_intercept_as_process_increment( + level_change_intercept: f64, + time_independent_increment: f64, +) -> Result { + let _ = (level_change_intercept, time_independent_increment); + Err(PsychometricError::LevelChangeInterceptIsNotProcessIncrement) +} + /// Exact scalar evolved latent mean plus a contemporaneous impulse. /// /// Driver, Oud, and Voelkle (2017, Eq. 3, p. 5) write the first two @@ -3231,12 +3330,13 @@ mod tests { recover_initial_time_dependent_predictor_effect, recover_initial_time_independent_predictor_carry, recover_initial_time_independent_predictor_effect, - recover_irregular_centered_residual_log_rate, recover_local_log_rate, - recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, - recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, - recover_stationary_latent_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_irregular_centered_residual_log_rate, recover_level_change_continuous_intercept, + recover_local_log_rate, recover_manifest_lagged_observed_covariance, + recover_manifest_observed_mean, recover_manifest_observed_variance, + recover_manifest_trait_plus_state_observed_variance, recover_stationary_latent_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_continuous_intercept_as_discrete_mean_increment, refuse_continuous_intercept_as_initial_latent_mean, refuse_continuous_intercept_as_manifest_means, refuse_difference_quotient_as_local_rate, @@ -3270,7 +3370,9 @@ mod tests { refuse_initial_time_independent_observed_mean_as_initial_time_dependent_observed_mean, refuse_latent_lagged_covariance_as_observed_covariance, refuse_latent_mean_as_observed_mean, refuse_latent_variance_as_observed_variance, - refuse_manifest_means_as_observed_mean, + refuse_level_change_intercept_as_free_continuous_intercept, + refuse_level_change_intercept_as_impulse, + refuse_level_change_intercept_as_process_increment, refuse_manifest_means_as_observed_mean, refuse_manifest_trait_variance_as_measurement_error, refuse_measurement_error_as_lagged_observed_covariance, refuse_measurement_error_as_observed_variance, @@ -5314,6 +5416,85 @@ mod tests { ); } + #[test] + fn level_change_continuous_intercept_recovers_driver_section_seven_point_two() { + let effect = 0.4_f64; + let predictor = 3.0_f64; + let drift = -0.5_f64; + let intercept = recover_level_change_continuous_intercept(effect, predictor, drift) + .expect("level-change"); + let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("impulse"); + assert!((intercept - 0.6).abs() < 1e-15); + assert!((impulse - 1.2).abs() < 1e-15); + let equilibrium = intercept / (-drift); + assert!((equilibrium - impulse).abs() < 1e-15); + assert_eq!( + recover_level_change_continuous_intercept(0.0, predictor, drift), + Ok(0.0) + ); + assert_eq!( + recover_level_change_continuous_intercept(effect, 0.0, drift), + Ok(0.0) + ); + assert_eq!( + recover_level_change_continuous_intercept(effect, predictor, 0.0), + Err(PsychometricError::LevelChangeRequiresStableDrift) + ); + assert_eq!( + recover_level_change_continuous_intercept(effect, predictor, 0.5), + Err(PsychometricError::LevelChangeRequiresStableDrift) + ); + let increment = recover_discrete_time_independent_predictor_effect( + effect, + predictor, + drift, + 2.0, + LagClock::EventTime, + ) + .expect("tipred"); + assert_eq!( + refuse_level_change_intercept_as_impulse(intercept, impulse), + Err(PsychometricError::LevelChangeInterceptIsNotImpulse) + ); + assert_eq!( + refuse_level_change_intercept_as_free_continuous_intercept(intercept, 0.3), + Err(PsychometricError::LevelChangeInterceptIsNotFreeContinuousIntercept) + ); + assert_eq!( + refuse_level_change_intercept_as_process_increment(intercept, increment), + Err(PsychometricError::LevelChangeInterceptIsNotProcessIncrement) + ); + } + + #[test] + fn level_change_continuous_intercept_invalid_inputs_fail_closed() { + assert_eq!( + recover_level_change_continuous_intercept(f64::NAN, 1.0, -0.5), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_level_change_continuous_intercept(1.0, f64::INFINITY, -0.5), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_level_change_continuous_intercept(0.4, 3.0, f64::NAN), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_level_change_continuous_intercept(1e308, 2.0, -0.5), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_level_change_continuous_intercept(1.0, 2.0, -1e308), + Err(PsychometricError::InvalidNumericInput) + ); + let scaled = recover_level_change_continuous_intercept(1e-308, 1.0, -1.0).expect("scale"); + assert!((scaled - 1e-308).abs() < 1e-320); + let rewritten = + recover_level_change_continuous_intercept(1e-308, 1.0, -1e308).expect("rewrite"); + assert!((rewritten - 1.0).abs() < 1e-12); + } + #[test] fn discrete_observed_mean_with_impulse_recovers_driver_equation_five() { let loading = 2.0_f64; diff --git a/crates/psychometric_core/src/lib.rs b/crates/psychometric_core/src/lib.rs index a7f2665b..66476413 100644 --- a/crates/psychometric_core/src/lib.rs +++ b/crates/psychometric_core/src/lib.rs @@ -75,6 +75,10 @@ //! `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when //! `u ≠ t0`; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed //! mean), +//! recovers the Driver §7.2 level-change `CINT` setting `κ = −a m x` +//! (`a < 0` so `−κ / a = m x`; not the dissipating Dirac `m x`, not +//! a free `CINT`, not `A^{-1}[e^{A Δt} − I] B z`, and not the extra +//! near-zero-drift latent process also named in §7.2), //! and refuses //! latent-mean comparison below strong invariance. @@ -179,6 +183,8 @@ pub use event_time::recover_initial_time_independent_predictor_carry; pub use event_time::recover_initial_time_independent_predictor_effect; /// Mean exact log-rate on already-centered irregular residuals. pub use event_time::recover_irregular_centered_residual_log_rate; +/// Exact scalar §7.2 level-change `CINT` `κ = −a m x`. +pub use event_time::recover_level_change_continuous_intercept; /// Exact scalar inverse `a = ln(φ) / Δt`. pub use event_time::recover_local_log_rate; /// Exact scalar lagged observed-indicator covariance `λ² cov(η) + ψ`. @@ -271,6 +277,12 @@ pub use event_time::refuse_latent_lagged_covariance_as_observed_covariance; pub use event_time::refuse_latent_mean_as_observed_mean; /// Refuse treating Driver Eq. 5 latent variance as `Var(y)`. pub use event_time::refuse_latent_variance_as_observed_variance; +/// Refuse treating Driver §7.2 level-change `CINT` as a free `CINT`. +pub use event_time::refuse_level_change_intercept_as_free_continuous_intercept; +/// Refuse treating Driver §7.2 level-change `CINT` as the contemporaneous Dirac. +pub use event_time::refuse_level_change_intercept_as_impulse; +/// Refuse treating Driver §7.2 level-change `CINT` as the Eq. 3 process increment. +pub use event_time::refuse_level_change_intercept_as_process_increment; /// Refuse treating Driver Eq. 5 `MANIFESTMEANS` as `E(y)`. pub use event_time::refuse_manifest_means_as_observed_mean; /// Refuse treating Driver Eq. 5 `MANIFESTTRAITVAR` as `MANIFESTVAR`. 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 2eeba1b9..d260ef0d 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -24,11 +24,12 @@ use psychometric_core::{ recover_initial_time_independent_predictor_carry, recover_initial_time_independent_predictor_effect, recover_irregular_centered_residual_log_rate, recover_kish_weighted_slope, - recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, - recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, - recover_stationary_latent_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_level_change_continuous_intercept, recover_manifest_lagged_observed_covariance, + recover_manifest_observed_mean, recover_manifest_observed_variance, + recover_manifest_trait_plus_state_observed_variance, recover_stationary_latent_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_continuous_intercept_as_discrete_mean_increment, refuse_continuous_intercept_as_initial_latent_mean, refuse_continuous_intercept_as_manifest_means, refuse_difference_quotient_as_local_rate, @@ -61,8 +62,10 @@ use psychometric_core::{ refuse_initial_time_independent_effect_as_time_dependent_impulse, refuse_initial_time_independent_observed_mean_as_initial_time_dependent_observed_mean, refuse_latent_lagged_covariance_as_observed_covariance, refuse_latent_mean_as_observed_mean, - refuse_latent_variance_as_observed_variance, refuse_manifest_means_as_observed_mean, - refuse_manifest_trait_variance_as_measurement_error, + refuse_latent_variance_as_observed_variance, + refuse_level_change_intercept_as_free_continuous_intercept, + refuse_level_change_intercept_as_impulse, refuse_level_change_intercept_as_process_increment, + refuse_manifest_means_as_observed_mean, refuse_manifest_trait_variance_as_measurement_error, refuse_measurement_error_as_lagged_observed_covariance, refuse_measurement_error_as_observed_variance, refuse_pooled_discrete_lag_across_unequal_intervals, @@ -3425,3 +3428,68 @@ fn discrete_observed_mean_with_initial_time_dependent_predictor_refuses_overflow "Driver Eq. 5 of Table 3 T0TDPREDEFFECT must keep λ=1e308, μ=1e-308: got {scaled}" ); } + +#[test] +fn level_change_continuous_intercept_recovers_driver_section_seven_point_two() { + let effect = 0.4_f64; + let predictor = 3.0_f64; + let drift = -0.5_f64; + let intercept = + recover_level_change_continuous_intercept(effect, predictor, drift).expect("level-change"); + let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("impulse"); + let error = rmse(&[0.6], &[intercept]); + assert!( + error < 1e-15, + "Driver §7.2 level-change CINT RMSE {error}: got {intercept}" + ); + let equilibrium_error = rmse(&[impulse], &[intercept / (-drift)]); + assert!( + equilibrium_error < 1e-15, + "Driver §7.2 −κ/a must recover m x: RMSE {equilibrium_error}" + ); + assert!(rmse(&[intercept], &[impulse]) > error); + let increment = recover_discrete_time_independent_predictor_effect( + effect, + predictor, + drift, + 2.0, + LagClock::EventTime, + ) + .expect("tipred"); + assert_eq!( + refuse_level_change_intercept_as_impulse(intercept, impulse), + Err(PsychometricError::LevelChangeInterceptIsNotImpulse) + ); + assert_eq!( + refuse_level_change_intercept_as_free_continuous_intercept(intercept, 0.3), + Err(PsychometricError::LevelChangeInterceptIsNotFreeContinuousIntercept) + ); + assert_eq!( + refuse_level_change_intercept_as_process_increment(intercept, increment), + Err(PsychometricError::LevelChangeInterceptIsNotProcessIncrement) + ); +} + +#[test] +fn level_change_continuous_intercept_refuses_unstable_drift_and_overflow() { + assert_eq!( + recover_level_change_continuous_intercept(0.4, 3.0, 0.0), + Err(PsychometricError::LevelChangeRequiresStableDrift) + ); + assert_eq!( + recover_level_change_continuous_intercept(0.4, 3.0, 0.5), + Err(PsychometricError::LevelChangeRequiresStableDrift) + ); + assert_eq!( + recover_level_change_continuous_intercept(1e308, 2.0, -0.5), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_level_change_continuous_intercept(1.0, 2.0, -1e308), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_level_change_continuous_intercept(0.0, 3.0, 0.0), + Ok(0.0) + ); +} diff --git a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs index b7fe953b..f8eca40a 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -20,12 +20,12 @@ use psychometric_core::{ recover_initial_time_dependent_predictor_effect, recover_initial_time_independent_predictor_carry, recover_initial_time_independent_predictor_effect, - recover_irregular_centered_residual_log_rate, recover_loading_point_estimate_mean, - recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, - recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, - recover_stationary_latent_variance, recover_time_dependent_predictor_impulse, - recover_time_dependent_predictor_impulse_carry, recover_trait_plus_state_latent_variance, - recover_within_residual_event_time_log_rate, + recover_irregular_centered_residual_log_rate, recover_level_change_continuous_intercept, + recover_loading_point_estimate_mean, recover_manifest_lagged_observed_covariance, + recover_manifest_observed_mean, recover_manifest_observed_variance, + recover_manifest_trait_plus_state_observed_variance, recover_stationary_latent_variance, + recover_time_dependent_predictor_impulse, recover_time_dependent_predictor_impulse_carry, + recover_trait_plus_state_latent_variance, recover_within_residual_event_time_log_rate, refuse_continuous_intercept_as_discrete_mean_increment, refuse_continuous_intercept_as_initial_latent_mean, refuse_continuous_intercept_as_manifest_means, @@ -58,8 +58,10 @@ use psychometric_core::{ refuse_initial_time_independent_effect_as_time_dependent_impulse, refuse_initial_time_independent_observed_mean_as_initial_time_dependent_observed_mean, refuse_latent_lagged_covariance_as_observed_covariance, refuse_latent_mean_as_observed_mean, - refuse_latent_variance_as_observed_variance, refuse_manifest_means_as_observed_mean, - refuse_manifest_trait_variance_as_measurement_error, + refuse_latent_variance_as_observed_variance, + refuse_level_change_intercept_as_free_continuous_intercept, + refuse_level_change_intercept_as_impulse, refuse_level_change_intercept_as_process_increment, + refuse_manifest_means_as_observed_mean, refuse_manifest_trait_variance_as_measurement_error, refuse_measurement_error_as_lagged_observed_covariance, refuse_measurement_error_as_observed_variance, refuse_process_noise_as_unconditional_variance, refuse_time_dependent_impulse_as_continuous_intercept, @@ -1661,3 +1663,45 @@ fn impulse_and_carry_observed_mean_are_not_time_independent_observed_mean() { ) ); } + +#[test] +fn level_change_cint_is_not_impulse_free_cint_or_process_increment() { + let effect = 0.4_f64; + let predictor = 3.0_f64; + let drift = -0.5_f64; + let intercept = + recover_level_change_continuous_intercept(effect, predictor, drift).expect("level-change"); + let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("impulse"); + let increment = recover_discrete_time_independent_predictor_effect( + effect, + predictor, + drift, + 2.0, + LagClock::EventTime, + ) + .expect("tipred"); + assert!( + (intercept - impulse).abs() > 1e-3, + "Driver et al. (2017, §7.2): −a m x is not the dissipating Dirac m x" + ); + assert!( + (intercept - 0.3).abs() > 1e-3, + "Driver et al. (2017, §7.2): −a m x is not a free CINT" + ); + assert!( + (intercept - increment).abs() > 1e-3, + "Driver et al. (2017, §7.2): −a m x is not TIPREDEFFECT increment" + ); + assert_eq!( + refuse_level_change_intercept_as_impulse(intercept, impulse), + Err(psychometric_core::PsychometricError::LevelChangeInterceptIsNotImpulse) + ); + assert_eq!( + refuse_level_change_intercept_as_free_continuous_intercept(intercept, 0.3), + Err(psychometric_core::PsychometricError::LevelChangeInterceptIsNotFreeContinuousIntercept) + ); + assert_eq!( + refuse_level_change_intercept_as_process_increment(intercept, increment), + Err(psychometric_core::PsychometricError::LevelChangeInterceptIsNotProcessIncrement) + ); +} diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index a068e49b..6264f533 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -1,7 +1,7 @@ # ADR 0005 — Posterior-aware ESEM/DSEM and structural interpretation **Decision status:** Accepted -**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), exact scalar contemporaneous `TDPREDEFFECT` impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; the §7.2 level-change form is not that impulse), exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of that Eq. 3 composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar time-independent `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle Eq. 14; `B` is not that discrete increment), exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of that Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of that carried latent mean; `τ + λ μ_t` is not that observed mean), exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; `T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`), exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; `T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand TD composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target +**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), exact scalar contemporaneous `TDPREDEFFECT` impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; the §7.2 level-change form is not that impulse), exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of that Eq. 3 composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar time-independent `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle Eq. 14; `B` is not that discrete increment), exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of that Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of that carried latent mean; `τ + λ μ_t` is not that observed mean), exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; `T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`), exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; `T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand TD composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean), exact scalar §7.2 level-change `CINT` `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; `a < 0` so `−κ / a = m x`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target **Date:** 2026-08-05 **Supersedes:** None. ADR 0012 governs upstream topic measurement/network coordinates; this ADR governs higher-order psychometric structure and longitudinal interpretation. @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), recovers the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z; `TDPREDEFFECT` is `M`, not `CINT`; `M x` is not `A^{-1}[e^{A Δt} − I] B z`; `M x` is not Voelkle et al., 2012, Eq. 14; the level-change form is not that impulse), recovers the exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition; JSS PDF re-opened 2026-08-20T09:01Z; the evolved map `τ + λ μ_t` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-impulse latent mean is not `E(y_t)`), recovers the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z; `TIPREDEFFECT` is `B`, not `κ`; form `B z` first, then the discrete intercept map; a zero drift is `B z Δt`; `B` is not the discrete increment; `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle Eq. 14), recovers the exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; JSS PDF re-opened 2026-08-20T12:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-increment latent mean is not `E(y_t)`), recovers the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z; form `m x` first, then `e^{a(t−u)} m x`; a zero drift is `m x`; underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept; `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; an impulse at `u = t` is the contemporaneous map; an impulse at `u ≤ t0` is already in `η(t0)`), recovers the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; JSS PDF re-opened 2026-08-20T05:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the carried latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and its Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF opened 2026-08-20T15:14Z; form `t0_b z` first, then `e^{a Δt} t0_b z`; a zero drift is `t0_b z`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; `e^{A Δt} t0_b z` is not `t0_b z`; `T0TIPREDEFFECT` is the coefficient, not the shift), recovers the exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition; JSS PDF re-opened 2026-08-20T15:28Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and its Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; form `t0_m x0` first, then `e^{a Δt} t0_m x0`; a zero drift is `t0_m x0`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `T0TDPREDEFFECT` is the coefficient, not the shift; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), recovers the exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; the first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), recovers the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z; `TDPREDEFFECT` is `M`, not `CINT`; `M x` is not `A^{-1}[e^{A Δt} − I] B z`; `M x` is not Voelkle et al., 2012, Eq. 14; the level-change form is not that impulse), recovers the exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition; JSS PDF re-opened 2026-08-20T09:01Z; the evolved map `τ + λ μ_t` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-impulse latent mean is not `E(y_t)`), recovers the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z; `TIPREDEFFECT` is `B`, not `κ`; form `B z` first, then the discrete intercept map; a zero drift is `B z Δt`; `B` is not the discrete increment; `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle Eq. 14), recovers the exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; JSS PDF re-opened 2026-08-20T12:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-increment latent mean is not `E(y_t)`), recovers the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z; form `m x` first, then `e^{a(t−u)} m x`; a zero drift is `m x`; underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept; `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; an impulse at `u = t` is the contemporaneous map; an impulse at `u ≤ t0` is already in `η(t0)`), recovers the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; JSS PDF re-opened 2026-08-20T05:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the carried latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and its Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF opened 2026-08-20T15:14Z; form `t0_b z` first, then `e^{a Δt} t0_b z`; a zero drift is `t0_b z`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; `e^{A Δt} t0_b z` is not `t0_b z`; `T0TIPREDEFFECT` is the coefficient, not the shift), recovers the exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition; JSS PDF re-opened 2026-08-20T15:28Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and its Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; form `t0_m x0` first, then `e^{a Δt} t0_m x0`; a zero drift is `t0_m x0`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `T0TDPREDEFFECT` is the coefficient, not the shift; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), recovers the exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; the first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar §7.2 level-change `CINT` setting `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z; form `m x` first, then multiply by `−a`; `a < 0` so `−κ / a = m x`; `a ≥ 0` cannot hold a new process mean; `−a m x` is not the dissipating Dirac, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`; the extra near-zero-drift latent process also named in §7.2 is a different specification), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. 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 0ea7a689..37696e98 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -32,15 +32,16 @@ This slice stays inside `psychometric_core`. It does not add a second invariance 26. recover the exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition; JSS PDF re-opened 2026-08-20T15:28Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`) and refuse treating `τ + λ μ_t`, `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, `τ + λ(μ_t + m x)`, or `τ + λ(μ_t + e^{a(t−u)} m x)` as that observed mean; 27. recover the exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and its Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z) and refuse treating `t0_m x0` as `M x`, as `e^{A(t−u)} M x` for `t0 < u < t`, as `t0_b z`, as `A^{-1}[e^{A Δt} − I] B z`, or as `κ`; refuse treating `e^{A Δt} t0_m x0` as `t0_m x0` or as `e^{A(t−u)} M x`; refuse treating the coefficient as the shift; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`; 28. recover the exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; the first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean) and refuse treating `τ + λ μ_t`, `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, `τ + λ(μ_t + m x)`, `τ + λ(μ_t + e^{a(t−u)} m x)`, or `τ + λ(μ_t + e^{a Δt} t0_b z)` as that observed mean; -29. refuse pooling discrete lags from unequal event intervals as one coefficient; -30. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); -31. refuse the difference quotient as a continuous-time rate; -32. apply the same event-time map to CWC residuals (still not DSEM); -33. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). +29. recover the exact scalar §7.2 level-change `CINT` `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z; form `m x` first, then multiply by `−a`; `a < 0` so `−κ / a = m x`) and refuse treating `−a m x` as the dissipating Dirac `m x`, as a free `CINT`, or as `A^{-1}[e^{A Δt} − I] B z`; `a ≥ 0` cannot hold a new process mean; the extra near-zero-drift latent process also named in §7.2 is a different specification; +30. refuse pooling discrete lags from unequal event intervals as one coefficient; +31. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); +32. refuse the difference quotient as a continuous-time rate; +33. apply the same event-time map to CWC residuals (still not DSEM); +34. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). ## Claim boundary -This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. The contemporaneous `TDPREDEFFECT` impulse is not the continuous intercept, not the time-independent discrete effect, and not Voelkle et al. (2012, Eq. 14). The time-independent `TIPREDEFFECT` increment is not the continuous intercept, not the contemporaneous impulse, not Voelkle et al. (2012, Eq. 14), and not the coefficient `B`. The within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). The evolved observed mean `τ + λ μ_t` is not the contemporaneous-impulse observed mean `τ + λ(μ_t + m x)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not the impulse-carry observed mean `τ + λ(μ_t + e^{a(t−u)} m x)` when `u ≠ t`. The evolved observed mean `τ + λ μ_t` is not the impulse-carry observed mean. The carried latent mean is not `E(y_t)`. The evolved-plus-impulse latent mean is not `E(y_t)`. The evolved observed mean `τ + λ μ_t` is not the time-independent-predictor observed mean `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not that time-independent-predictor observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that time-independent-predictor observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TIPREDEFFECT` shift `t0_b z` is not the Eq. 3 process increment `A^{-1}[e^{A Δt} − I] B z`, not `κ`, and not `M x`. The Eq. 3 first-summand carry `e^{A Δt} t0_b z` is not `t0_b z` and is not that process increment. `T0TIPREDEFFECT` is the coefficient, not the first-occasion shift. The evolved observed mean `τ + λ μ_t` is not the first-occasion TI-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_b z)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion observed mean when `u ≠ t0`. The evolved-plus-T0TIPRED latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TDPREDEFFECT` shift `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `κ`. The Eq. 3 first-summand carry `e^{A Δt} t0_m x0` is not `t0_m x0` and is not that within-interval impulse carry. `T0TDPREDEFFECT` is the coefficient, not the first-occasion shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The evolved observed mean `τ + λ μ_t` is not the first-occasion TD-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_m x0)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion TD observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion TD observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion TD observed mean when `u ≠ t0`. The first-occasion TI composition `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that first-occasion TD observed mean. Same numbers as `T0TIPREDEFFECT` yield the same product; Table 3 names a different matrix. The evolved-plus-T0TDPRED latent mean is not `E(y_t)`. +This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. The contemporaneous `TDPREDEFFECT` impulse is not the continuous intercept, not the time-independent discrete effect, and not Voelkle et al. (2012, Eq. 14). The time-independent `TIPREDEFFECT` increment is not the continuous intercept, not the contemporaneous impulse, not Voelkle et al. (2012, Eq. 14), and not the coefficient `B`. The within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). The evolved observed mean `τ + λ μ_t` is not the contemporaneous-impulse observed mean `τ + λ(μ_t + m x)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not the impulse-carry observed mean `τ + λ(μ_t + e^{a(t−u)} m x)` when `u ≠ t`. The evolved observed mean `τ + λ μ_t` is not the impulse-carry observed mean. The carried latent mean is not `E(y_t)`. The evolved-plus-impulse latent mean is not `E(y_t)`. The evolved observed mean `τ + λ μ_t` is not the time-independent-predictor observed mean `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not that time-independent-predictor observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that time-independent-predictor observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TIPREDEFFECT` shift `t0_b z` is not the Eq. 3 process increment `A^{-1}[e^{A Δt} − I] B z`, not `κ`, and not `M x`. The Eq. 3 first-summand carry `e^{A Δt} t0_b z` is not `t0_b z` and is not that process increment. `T0TIPREDEFFECT` is the coefficient, not the first-occasion shift. The evolved observed mean `τ + λ μ_t` is not the first-occasion TI-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_b z)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion observed mean when `u ≠ t0`. The evolved-plus-T0TIPRED latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TDPREDEFFECT` shift `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `κ`. The Eq. 3 first-summand carry `e^{A Δt} t0_m x0` is not `t0_m x0` and is not that within-interval impulse carry. `T0TDPREDEFFECT` is the coefficient, not the first-occasion shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The evolved observed mean `τ + λ μ_t` is not the first-occasion TD-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_m x0)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion TD observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion TD observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion TD observed mean when `u ≠ t0`. The first-occasion TI composition `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that first-occasion TD observed mean. Same numbers as `T0TIPREDEFFECT` yield the same product; Table 3 names a different matrix. The evolved-plus-T0TDPRED latent mean is not `E(y_t)`. The §7.2 level-change `CINT` `κ = −a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. Lasting level change via that `CINT` setting requires `a < 0`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not that `CINT` setting. ## Authoritative sources @@ -79,7 +80,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - **Observed-indicator mean.** Driver et al. (2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z): \(y_i(t)=\Gamma+\Lambda\eta_i(t)+\zeta_i(t)\) with \(\zeta\sim N(0,\Theta)\) and \(\Gamma\sim N(\tau,\Psi)\). Table 2 names \(\tau\) `MANIFESTMEANS`, \(\kappa\) `CINT`, and the first-occasion latent mean `T0MEANS`. The scalar map is \(E(y)=\tau+\lambda\mu\). Form \(\lambda\mu\) then add \(\tau\). A zero loading or zero latent mean is exactly \(\tau\). A zero intercept is exactly \(\lambda\mu\). `MANIFESTMEANS` is not \(E(y)\). \(E(\eta)\) is not \(E(y)\). `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not \(E(y)\). An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **Discrete latent mean.** Driver et al. (2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z): \(\eta(t)=\exp(A\Delta t)\eta(t_0)+\int\exp(A(t-s))(b+\cdots)\,ds\) plus a stochastic integral of mean zero. Table 2 names the first-occasion latent mean `T0MEANS` and \(\kappa\) `CINT`. The scalar map is \(\mu_t=\exp(a\Delta t)\mu_0+(\exp(a\Delta t)-1)/a\,\kappa\). Form the `CINT` increment first, then add the carried `T0MEANS` term. A zero drift is the Eq. 3 integral \(\kappa\Delta t\) (\(A=0\) has no inverse). A zero intercept is exactly \(\exp(a\Delta t)\mu_0\). A zero initial mean is exactly the increment. As \(\Delta t\to\infty\) with stable \(a<0\), \(\mu_t\to-\kappa/a\). Binary64 underflow of \(\exp(a\Delta t)\) to `+0` drops the carried `T0MEANS` and keeps that equilibrium increment. `T0MEANS` is not \(\mu_t\). `CINT` is not the discrete increment. `CINT` is not `T0MEANS`. An overflowing exponential, product, or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **Discrete observed-indicator mean.** Driver et al. (2017, Eq. 3, p. 5; Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T22:10Z): `η_i(t)=exp(AΔt)η_i(t0)+A^{-1}[exp(AΔt)−I]ξ_i+…` with `ξ_i∼N(κ,φ_ξ)` and a stochastic integral of mean zero, then `y_i(t)=Γ_i+Λη_i(t)+ζ_i(t)` with `Γ∼N(τ,Ψ)`. The scalar composition is `E(y_t)=τ+λμ_t`. Form `μ_t` first, then `τ+λμ_t`. A zero loading or zero evolved latent mean is exactly `τ`. A zero intercept is exactly `λμ_t`. A zero drift is `τ+λ(μ_0+κΔt)`. Underflow of `exp(aΔt)` to `+0` keeps `τ+λ(−κ/a)`. The first-occasion map `τ+λμ_0` is not `E(y_t)`. `MANIFESTMEANS` is not `E(y_t)`. `T0MEANS` is not `E(y_t)`. `μ_t` is not `E(y_t)`. An overflowing exponential, product, or sum fails closed. This is not a Kalman filter and not ctsem estimation. -- **Time-dependent predictor impulse.** Driver et al. (2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z): `χ_i(t)=Σ x_{i,u} δ(t−u)` and the fourth summand is `M Σ x_{i,u} δ(t−u)`. Table 2 names `M` `TDPREDEFFECT`. Section 7.2 calls this a sudden impulse that dissipates back to the process mean. The scalar contemporaneous jump is `m x`. Form `μ_t` first, then add `m x`. A zero effect or zero predictor is exactly zero. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{AΔt}−I] B z` (`TIPREDEFFECT`). `M x` is not Voelkle et al. (2012, Eq. 14) `a_{yx}Δt`. The §7.2 level-change form is an extra latent process with near-zero drift and is not this map. An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. +- **Time-dependent predictor impulse.** Driver et al. (2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z): `χ_i(t)=Σ x_{i,u} δ(t−u)` and the fourth summand is `M Σ x_{i,u} δ(t−u)`. Table 2 names `M` `TDPREDEFFECT`. Section 7.2 calls this a sudden impulse that dissipates back to the process mean. The scalar contemporaneous jump is `m x`. Form `μ_t` first, then add `m x`. A zero effect or zero predictor is exactly zero. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{AΔt}−I] B z` (`TIPREDEFFECT`). `M x` is not Voelkle et al. (2012, Eq. 14) `a_{yx}Δt`. The §7.2 lasting level change sets `CINT` to `TDPREDEFFECT * −DRIFT` (`κ=−a m x`) and is not this jump. The extra near-zero-drift latent process also named in §7.2 is a third specification. An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **Contemporaneous-impulse observed-indicator mean.** Driver et al. (2017, Eq. 5, p. 5; Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T09:01Z): `y_i(t)=Γ+Λη_i(t)+ζ_i(t)` with `ζ∼N(0,Θ)` and `Γ∼N(τ,Ψ)`. The latent process at `t` after a contemporaneous Dirac (`u=t`) is `μ_t+mx`. The scalar composition is `E(y_t)=τ+λ(μ_t+mx)`. Form the evolved-plus-impulse latent mean first, then `τ+λ` of that mean. A zero loading is exactly `τ`. A zero evolved-plus-impulse latent mean is exactly `τ`. A zero intercept is exactly `λ(μ_t+mx)`. The evolved observed mean `τ+λμ_t` is not this composition. The carry map `τ+λ(μ_t+e^{a(t−u)}mx)` is not this composition when `u≠t`. `MANIFESTMEANS` is not `E(y_t)`. The evolved-plus-impulse latent mean is not `E(y_t)`. The §7.2 level-change form is not this map. An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **Time-independent predictor effect.** Driver et al. (2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z): Equation 1 writes `dη=(Aη+b+A_{ηξ}ξ+Bz)dt+GdW+Mdχ`. Equation 3's second summand is `A^{-1}[e^{AΔt}−I](b+A_{ηξ}ξ+Bz)`. Table 2 names `B` `TIPREDEFFECT`. The scalar map is `(e^{aΔt}−1)/a·Bz` for `a≠0`. Form `Bz` first, then the discrete intercept map. A zero drift is `BzΔt`. Form `μ_t` first, then add that increment. A zero effect or zero predictor is exactly zero. `TIPREDEFFECT` is `B`, not the discrete increment. `A^{-1}[e^{AΔt}−I]Bz` is not `CINT`, not `Mx`, and not Voelkle et al. (2012, Eq. 14) `a_{yx}Δt`. An overflowing product, increment, or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **Time-independent-predictor observed-indicator mean.** Driver et al. (2017, Eq. 5, p. 5; Eq. 3, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T12:12Z): `y_i(t)=Γ+Λη_i(t)+ζ_i(t)` with `ζ∼N(0,Θ)` and `Γ∼N(τ,Ψ)`. Equation 3 prints the `TIPREDEFFECT` increment as the addend `A^{-1}[e^{A(t−t0)}−I]Bz_i` after the `T0MEANS` carry and the `CINT` increment. The scalar composition is `E(y_t)=τ+λ(μ_t+A^{-1}[e^{AΔt}−I]Bz)`. Form the evolved-plus-increment latent mean first, then `τ+λ` of that mean. A zero loading is exactly `τ`. A zero evolved-plus-increment latent mean is exactly `τ`. A zero intercept is exactly `λ(μ_t+increment)`. The evolved observed mean `τ+λμ_t` is not this composition. The contemporaneous map `τ+λ(μ_t+mx)` is not this composition. The carry map `τ+λ(μ_t+e^{a(t−u)}mx)` is not this composition when `u≠t`. `MANIFESTMEANS` is not `E(y_t)`. The evolved-plus-increment latent mean is not `E(y_t)`. `TIPREDEFFECT` is `B`, not that observed mean. An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. @@ -89,6 +90,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - **First-occasion time-independent-predictor observed-indicator mean.** Driver et al. (2017, Eq. 5, p. 5; Eq. 3 first summand, p. 5; Table 3, p. 13; JSS PDF re-opened 2026-08-20T15:28Z): `y_i(t)=Γ+Λη_i(t)+ζ_i(t)` with `ζ∼N(0,Θ)` and `Γ∼N(τ,Ψ)`. The latent process at `t` after the Table 3 first-occasion carry is `μ_t+e^{aΔt}t0_b z`. The scalar composition is `E(y_t)=τ+λ(μ_t+e^{aΔt}t0_b z)`. Form the evolved-plus-carry latent mean first, then `τ+λ` of that mean. A zero loading is exactly `τ`. A zero evolved-plus-carry latent mean is exactly `τ`. A zero intercept is exactly `λ(μ_t+carry)`. The evolved observed mean `τ+λμ_t` is not this composition. The process-increment map `τ+λ(μ_t+A^{-1}[e^{AΔt}−I]Bz)` is not this composition. The contemporaneous map `τ+λ(μ_t+mx)` is not this composition. The impulse-carry map `τ+λ(μ_t+e^{a(t−u)}mx)` is not this composition when `u≠t0`. `MANIFESTMEANS` is not `E(y_t)`. The evolved-plus-carry latent mean is not `E(y_t)`. `T0TIPREDEFFECT` is the coefficient, not that observed mean. An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **First-occasion time-dependent predictor.** Driver et al. (2017, Table 3, p. 13; Eq. 3 first summand, p. 5; JSS PDF re-opened 2026-08-20T19:10Z): Table 3 names `T0TDPREDEFFECT` the effect of time-dependent predictors on latents at `T0`. Table 2 / Table 3 name `TDPREDEFFECT` `M`, which enters Equation 3 as the printed fourth-summand Dirac `M x` at `u = t`. The scalar first-occasion shift is `t0_m x0`. Equation 3's first summand carries that shift as `e^{AΔt}t0_m x0`. Form `t0_m x0` first, then `e^{aΔt}t0_m x0`. Form `μ_t` first, then add that carry. A zero drift is `t0_m x0`. Binary64 underflow of `e^{aΔt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. A zero effect or zero predictor is exactly zero. `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{AΔt}−I]Bz`, and not `κ`. `e^{AΔt}t0_m x0` is not `t0_m x0`. `T0TDPREDEFFECT` is the coefficient, not the shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. An overflowing product, rewrite, or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **First-occasion time-dependent-predictor observed-indicator mean.** Driver et al. (2017, Eq. 5, p. 5; Eq. 3 first summand, p. 5; Table 3, p. 13; JSS PDF re-opened 2026-08-20T19:07Z): `y_i(t)=Γ+Λη_i(t)+ζ_i(t)` with `ζ∼N(0,Θ)` and `Γ∼N(τ,Ψ)`. The latent process at `t` after the Table 3 first-occasion TD carry is `μ_t+e^{aΔt}t0_m x0`. The scalar composition is `E(y_t)=τ+λ(μ_t+e^{aΔt}t0_m x0)`. Form the evolved-plus-carry latent mean first, then `τ+λ` of that mean. A zero loading is exactly `τ`. A zero evolved-plus-carry latent mean is exactly `τ`. A zero intercept is exactly `λ(μ_t+carry)`. The evolved observed mean `τ+λμ_t` is not this composition. The process-increment map `τ+λ(μ_t+A^{-1}[e^{AΔt}−I]Bz)` is not this composition. The contemporaneous map `τ+λ(μ_t+mx)` is not this composition. The impulse-carry map `τ+λ(μ_t+e^{a(t−u)}mx)` is not this composition when `u≠t0`. The first-occasion TI map `τ+λ(μ_t+e^{aΔt}t0_b z)` is not this composition. Same numbers as `T0TIPREDEFFECT` yield the same product; Table 3 names a different matrix. `MANIFESTMEANS` is not `E(y_t)`. The evolved-plus-carry latent mean is not `E(y_t)`. `T0TDPREDEFFECT` is the coefficient, not that observed mean. An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. +- **Level-change continuous intercept.** Driver et al. (2017, §7.2, pp. 20–21; Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T19:45Z): a sudden Dirac dissipates back to the process mean. To generate a lasting level change, `CINT` is set to `TDPREDEFFECT * −DRIFT`. The scalar setting is `κ=−a m x`. Form `m x` first, then multiply by `−a`. Stable `a<0` is required so `−κ/a=m x` is an equilibrium offset. `a≥0` cannot hold a new process mean. A zero effect or zero predictor is exactly zero. `−a m x` is not `m x`, not a free `CINT`, and not `A^{-1}[e^{AΔt}−I]Bz`. The extra near-zero-drift latent process also named in §7.2 is a different specification. An overflowing product fails closed. This is not a Kalman filter and not ctsem estimation. - **CWC-then-lag.** Sample cluster means are removed first. Consecutive within residuals are then fitted by least squares to \(r_{t+\Delta t}\approx\exp(a\Delta t)\,r_{t}\) on event time. Same-sign pair-wise logs initialize the scalar Newton step. Sign-flipping \(T=2\) CWC pairs have no real logarithm and fail closed. Curran and Bauer (2011, pp. 607–608) show that this person-mean subtraction on a raw autoregressive series does **not** isolate the lagged within-person effect; the helper therefore does not claim to recover the raw-process drift. - **Already-centered irregular residual.** The caller supplies lagged within residuals. The mean of \(a=\ln(r_{t+\Delta t}/r_t)/\Delta t\) is the exact scalar map. Intervals may be irregular. The helper does not center again. This is not DSEM. @@ -121,6 +123,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - Driver et al. (2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition; Table 3, p. 13) recovers a known \(E(y_t)=\tau+\lambda(\mu_t+e^{a\Delta t}t0_b z)\) at machine-scale RMSE, and that RMSE is smaller than treating \(\tau+\lambda\mu_t\), \(\tau+\lambda(\mu_t+A^{-1}[e^{A\Delta t}-I]Bz)\), \(\tau+\lambda(\mu_t+mx)\), \(\tau+\lambda(\mu_t+e^{a(t-u)}mx)\), `MANIFESTMEANS`, or the evolved-plus-carry latent mean as \(E(y_t)\); a zero loading is \(\tau\); a non-event clock, a non-positive interval, and an overflowing product or sum fail closed; - Driver et al. (2017, Table 3, p. 13; Eq. 3 first summand) recovers a known first-occasion time-dependent shift \(t0_m x0\) and carry \(e^{a\Delta t}t0_m x0\) at machine-scale RMSE, and those RMSEs are smaller than treating `TDPREDEFFECT` `M x`, the within-interval impulse carry, `T0TIPREDEFFECT`, `TIPREDEFFECT`, `CINT`, the coefficient, or the un-carried shift as the carry; composing \(\mu_t\) plus that carry recovers the known sum; a zero drift is \(t0_m x0\); underflow of \(e^{a\Delta t}\) to `+0` is a vanishing carry and is kept; a zero effect or zero predictor is exactly zero; a non-event clock, a non-positive interval, and an overflowing product or rewrite fail closed; - Driver et al. (2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; Table 3, p. 13) recovers a known \(E(y_t)=\tau+\lambda(\mu_t+e^{a\Delta t}t0_m x0)\) at machine-scale RMSE, and that RMSE is smaller than treating \(\tau+\lambda\mu_t\), \(\tau+\lambda(\mu_t+A^{-1}[e^{A\Delta t}-I]Bz)\), \(\tau+\lambda(\mu_t+mx)\), \(\tau+\lambda(\mu_t+e^{a(t-u)}mx)\), `MANIFESTMEANS`, or the evolved-plus-carry latent mean as \(E(y_t)\); same numbers as `T0TIPREDEFFECT` yield the same product, but Table 3 names a different matrix and the refuse is fail-closed; a zero loading is \(\tau\); a non-event clock, a non-positive interval, and an overflowing product or sum fail closed; +- Driver et al. (2017, §7.2) recovers a known level-change `CINT` \(\kappa=-a\,mx\) at machine-scale RMSE, and that RMSE is smaller than treating the dissipating Dirac \(mx\), a free `CINT`, or `TIPREDEFFECT` as that setting; \(-\kappa/a\) recovers \(mx\) when \(a<0\); \(a\ge 0\) with a nonzero impulse fails closed; a zero effect or zero predictor is exactly zero; an overflowing product fails closed; - pooling discrete lags from unequal intervals fails closed; - CWC-then-lag on a two-cluster decaying series has smaller computed RMSE than a level-pooled series when the latter is identified; - already-centered irregular residuals recover a known drift at machine-scale RMSE, and that RMSE is smaller than CWC of the corresponding raw autoregressive series (Curran & Bauer, 2011, pp. 607–608); From 1101cfb3af7a0e59deac1d38e42a98686569ef64 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 19:55:09 +0000 Subject: [PATCH 49/62] =?UTF-8?q?feat(psychometric):=20recover=20Driver=20?= =?UTF-8?q?Eq.=203=20of=20=C2=A77.2=20CINT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Map (1−e^{aΔt}) m x. Form κ first. Refuse impulse, CINT, and TIPREDEFFECT. Fail closed on a≥0, non-event clocks, and overflow. Keep PR #144 draft. --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 1 + CLAUDE.md | 2 +- crates/psychometric_core/src/error.rs | 31 +++ crates/psychometric_core/src/event_time.rs | 246 +++++++++++++++++- crates/psychometric_core/src/lib.rs | 11 + ...multilevel_event_time_recovery_contract.rs | 102 +++++++- .../scientific_claim_boundary_contract.rs | 65 ++++- docs/adr/0005-posterior-esem-dsem.md | 4 +- .../multilevel-event-time-recovery.md | 15 +- 10 files changed, 449 insertions(+), 30 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index e074b6a1..c0a69b22 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -61,7 +61,7 @@ boundaries above remain the target modular MSA architecture. | `tepp_simulation` | known-truth temporal/event data generation | | `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics | | `tepp_api` | versioned DTO, schema, and export contracts | -| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; first-occasion `T0TIPREDEFFECT` shift is `t0_b z` and Eq. 3 first-summand carry is `e^{A Δt} t0_b z` (`T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_b z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), first-occasion `T0TDPREDEFFECT` shift is `t0_m x0` and Eq. 3 first-summand carry is `e^{A Δt} t0_m x0` (`T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; §7.2 level-change `CINT` is `κ = −a m x` with `a < 0` so `−κ / a = m x` (`−a m x` is not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | +| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; first-occasion `T0TIPREDEFFECT` shift is `t0_b z` and Eq. 3 first-summand carry is `e^{A Δt} t0_b z` (`T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_b z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), first-occasion `T0TDPREDEFFECT` shift is `t0_m x0` and Eq. 3 first-summand carry is `e^{A Δt} t0_m x0` (`T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; §7.2 level-change `CINT` is `κ = −a m x` with `a < 0` so `−κ / a = m x` (`−a m x` is not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2; Eq. 3 of that setting is `(1 − e^{a Δt}) m x`, which is not `m x`, not `κ`, and not `TIPREDEFFECT`), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | No crate exposes placeholder production behavior in Task 1. This prevents an empty façade from becoming a de facto public API before its invariants and tests diff --git a/CHANGELOG.md b/CHANGELOG.md index 351e638d..f3a60c5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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, §7.2, pp. 20–21; Eq. 3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T19:50Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar discrete increment of the lasting level-change `CINT`. Section 7.2 sets `CINT` to `TDPREDEFFECT * −DRIFT` (`κ = −a m x`). Equation 3 maps that intercept through `A^{-1}[e^{A Δt} − I] κ`. With `κ = −a m x` the scalar increment is `(e^{a Δt} − 1)/a · (−a m x) = (1 − e^{a Δt}) m x`. Form the level-change `CINT` first, then the discrete intercept map. Underflow of `e^{a Δt}` to `+0` keeps the equilibrium offset `m x`. `(1 − e^{a Δt}) m x` is not the contemporaneous jump `m x`. `(1 − e^{a Δt}) m x` is not `κ`. `(1 − e^{a Δt}) m x` is not `A^{-1}[e^{A Δt} − I] B z`. Stable `a < 0` is required. A zero effect or zero predictor is exactly zero. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T19:45Z: `is_oa: false`; Springer `content/pdf` is HTML 200). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, §7.2, pp. 20–21; Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T19:45Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar lasting level-change `CINT`. Section 7.2 contrasts a sudden Dirac that dissipates back to the process mean with a lasting level change. To generate that lasting change, `CINT` is set to `TDPREDEFFECT * −DRIFT`. The scalar setting is `κ = −a m x`. Form `m x` first, then multiply by `−a`. Stable `a < 0` is required so `−κ / a = m x` is an equilibrium offset. `a ≥ 0` cannot hold a new process mean. `−a m x` is not the contemporaneous jump `m x`. `−a m x` is not a free `CINT`. `−a m x` is not `A^{-1}[e^{A Δt} − I] B z`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not this `CINT` setting. A zero effect or zero predictor is exactly zero. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T19:45Z: `is_oa: false`; Springer `content/pdf` is HTML 200). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Eq. 3 first summand, p. 5; Table 3, p. 13; JSS PDF re-opened 2026-08-20T19:07Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar observed-indicator mean of a first-occasion time-dependent predictor. Equation 5 writes `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and `Γ ~ N(τ, Ψ)`. Table 3 names `T0TDPREDEFFECT` the effect of time-dependent predictors on latents at `T0`. Equation 3's first summand carries that shift as `e^{A Δt} t0_m x0`. The scalar composition is `E(y_t) = τ + λ(μ_t + e^{a Δt} t0_m x0)`. Form the evolved-plus-carry latent mean first, then `τ + λ` of that mean. The evolved observed mean `τ + λ μ_t` is not this composition. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not this composition. The contemporaneous map `τ + λ(μ_t + m x)` is not this composition. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not this composition when `u ≠ t0`. The first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not this composition. `MANIFESTMEANS` is not `E(y_t)`. The evolved-plus-carry latent mean is not `E(y_t)`. `T0TDPREDEFFECT` is the coefficient, not that observed mean. Same numbers as `T0TIPREDEFFECT` yield the same product; Table 3 names a different matrix. A zero loading is exactly `τ`. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T19:09Z: `is_oa: false`). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Table 3, p. 13; Eq. 3 first summand, p. 5; JSS PDF re-opened 2026-08-20T19:10Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar first-occasion time-dependent predictor shift and its carry. Table 3 names `T0TDPREDEFFECT` the effect of time-dependent predictors on latents at `T0`. Table 2 / Table 3 name `TDPREDEFFECT` `M`, which enters Equation 3 as the printed fourth-summand Dirac `M x` at `u = t`. Those are not the same matrix. The scalar first-occasion shift is `t0_m x0`. Equation 3's first summand carries that shift as `e^{A Δt} t0_m x0`. Form `t0_m x0` first, then `e^{a Δt} t0_m x0`. Form `μ_t` first, then add that carry. A zero drift is `t0_m x0` with no dissipation. Binary64 underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `κ`. `e^{A Δt} t0_m x0` is not `t0_m x0`. `T0TDPREDEFFECT` is the coefficient, not `t0_m x0`. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T19:09Z: `is_oa: false`). diff --git a/CLAUDE.md b/CLAUDE.md index 18dd37dc..0426e8d0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. Putnick and Bornstein (2016, PMC5145197 opened 2026-08-19T22:15Z) require scalar invariance before latent-mean comparison; residual invariance is not a prerequisite. Two-observation series have no residual degrees of freedom (`ordinary_least_squares_fit` returns residual variance `0`) and cap at strong/scalar; they still license means. This is two-group OLS, not MGCFA. Meredith (1993) names remain unread labels. -- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. The contemporaneous time-dependent predictor impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`). Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). The §7.2 level-change form is not that impulse. The observed mean of that contemporaneous impulse is `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-impulse latent mean is not `E(y_t)`. The time-independent predictor increment is `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`). Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. `TIPREDEFFECT` is `B`, not that discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle et al. (2012, Eq. 14). The observed mean of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The within-interval time-dependent impulse carry is `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation). Form `m x` first, then `e^{a(t−u)} m x`. A zero drift is `m x` with no dissipation. Underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept. `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). An impulse at `u = t` is the contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. The observed mean of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The carried latent mean is not `E(y_t)`. The first-occasion time-independent predictor shift is `t0_b z` (Driver et al., 2017, Table 3 `T0TIPREDEFFECT`; Eq. 3 first summand). Form `t0_b z` first, then `e^{a Δt} t0_b z`. Form `μ_t` first, then add that carry. A zero drift is `t0_b z`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. `e^{A Δt} t0_b z` is not `t0_b z`. `T0TIPREDEFFECT` is the coefficient, not the shift. The observed mean of that first-occasion carry is `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The evolved-plus-carry latent mean is not `E(y_t)`. The first-occasion time-dependent predictor shift is `t0_m x0` (Driver et al., 2017, Table 3 `T0TDPREDEFFECT`; Eq. 3 first summand; JSS PDF re-opened 2026-08-20T19:10Z). Form `t0_m x0` first, then `e^{a Δt} t0_m x0`. Form `μ_t` first, then add that carry. A zero drift is `t0_m x0`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`. `e^{A Δt} t0_m x0` is not `t0_m x0`. `T0TDPREDEFFECT` is the coefficient, not the shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The observed mean of that first-occasion TD carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean. The evolved-plus-carry latent mean is not `E(y_t)`. The lasting level-change `CINT` is `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z). Form `m x` first, then multiply by `−a`. Stable `a < 0` is required so `−κ / a = m x` is an equilibrium offset. `a ≥ 0` cannot hold a new process mean. `−a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not this `CINT` setting. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. The contemporaneous time-dependent predictor impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`). Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). The §7.2 level-change form is not that impulse. The observed mean of that contemporaneous impulse is `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-impulse latent mean is not `E(y_t)`. The time-independent predictor increment is `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`). Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. `TIPREDEFFECT` is `B`, not that discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle et al. (2012, Eq. 14). The observed mean of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The within-interval time-dependent impulse carry is `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation). Form `m x` first, then `e^{a(t−u)} m x`. A zero drift is `m x` with no dissipation. Underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept. `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). An impulse at `u = t` is the contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. The observed mean of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The carried latent mean is not `E(y_t)`. The first-occasion time-independent predictor shift is `t0_b z` (Driver et al., 2017, Table 3 `T0TIPREDEFFECT`; Eq. 3 first summand). Form `t0_b z` first, then `e^{a Δt} t0_b z`. Form `μ_t` first, then add that carry. A zero drift is `t0_b z`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. `e^{A Δt} t0_b z` is not `t0_b z`. `T0TIPREDEFFECT` is the coefficient, not the shift. The observed mean of that first-occasion carry is `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The evolved-plus-carry latent mean is not `E(y_t)`. The first-occasion time-dependent predictor shift is `t0_m x0` (Driver et al., 2017, Table 3 `T0TDPREDEFFECT`; Eq. 3 first summand; JSS PDF re-opened 2026-08-20T19:10Z). Form `t0_m x0` first, then `e^{a Δt} t0_m x0`. Form `μ_t` first, then add that carry. A zero drift is `t0_m x0`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`. `e^{A Δt} t0_m x0` is not `t0_m x0`. `T0TDPREDEFFECT` is the coefficient, not the shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The observed mean of that first-occasion TD carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean. The evolved-plus-carry latent mean is not `E(y_t)`. The lasting level-change `CINT` is `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z). Form `m x` first, then multiply by `−a`. Stable `a < 0` is required so `−κ / a = m x` is an equilibrium offset. `a ≥ 0` cannot hold a new process mean. `−a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not this `CINT` setting. Equation 3 maps that intercept as `(1 − e^{a Δt}) m x` (JSS PDF re-opened 2026-08-20T19:50Z). Form the level-change `CINT` first, then the discrete intercept map. Underflow of `e^{a Δt}` to `+0` keeps `m x`. `(1 − e^{a Δt}) m x` is not `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. diff --git a/crates/psychometric_core/src/error.rs b/crates/psychometric_core/src/error.rs index 646b235d..e6f08eff 100644 --- a/crates/psychometric_core/src/error.rs +++ b/crates/psychometric_core/src/error.rs @@ -259,6 +259,16 @@ pub enum PsychometricError { /// process increment. `−a m x` is not /// `A^{-1}[e^{A Δt} − I] B z`. LevelChangeInterceptIsNotProcessIncrement, + /// Driver §7.2 level-change CINT increment was treated as the + /// contemporaneous Dirac. `(1 − e^{a Δt}) m x` is not `m x`. + LevelChangeIncrementIsNotImpulse, + /// Driver §7.2 level-change CINT increment was treated as `CINT`. + /// `(1 − e^{a Δt}) m x` is not `κ = −a m x`. + LevelChangeIncrementIsNotIntercept, + /// Driver §7.2 level-change CINT increment was treated as the + /// Eq. 3 process increment. `(1 − e^{a Δt}) m x` is not + /// `A^{-1}[e^{A Δt} − I] B z`. + LevelChangeIncrementIsNotProcessIncrement, } impl fmt::Display for PsychometricError { @@ -472,6 +482,15 @@ impl fmt::Display for PsychometricError { Self::LevelChangeInterceptIsNotProcessIncrement => { "level-change CINT is not the time-independent process increment" } + Self::LevelChangeIncrementIsNotImpulse => { + "level-change CINT increment is not the contemporaneous impulse" + } + Self::LevelChangeIncrementIsNotIntercept => { + "level-change CINT increment is not the level-change intercept" + } + Self::LevelChangeIncrementIsNotProcessIncrement => { + "level-change CINT increment is not the time-independent process increment" + } }; formatter.write_str(message) } @@ -809,5 +828,17 @@ mod tests { PsychometricError::LevelChangeInterceptIsNotProcessIncrement.to_string(), "level-change CINT is not the time-independent process increment" ); + assert_eq!( + PsychometricError::LevelChangeIncrementIsNotImpulse.to_string(), + "level-change CINT increment is not the contemporaneous impulse" + ); + assert_eq!( + PsychometricError::LevelChangeIncrementIsNotIntercept.to_string(), + "level-change CINT increment is not the level-change intercept" + ); + assert_eq!( + PsychometricError::LevelChangeIncrementIsNotProcessIncrement.to_string(), + "level-change CINT increment is not the time-independent process increment" + ); } } diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index d9ef20a1..2e8c1b6f 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -74,7 +74,9 @@ //! `CINT` to `TDPREDEFFECT * −DRIFT` (`κ = −a m x`; `a < 0` so //! `−κ / a = m x`). That `CINT` setting is not the dissipating //! Dirac, not a free `CINT`, and not the extra near-zero-drift -//! latent process also named in §7.2. The JSS article +//! latent process also named in §7.2. Equation 3 maps that +//! intercept as `(1 − e^{a Δt}) m x` (`(1 − e^{a Δt}) m x` is not +//! `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`). The JSS article //! has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). //! The difference quotient `(x(t+Δt) − x(t)) / Δt` (their //! Eqs. 3–4) is refused. This is not DSEM and not a matrix `expm`. @@ -1456,6 +1458,92 @@ pub fn refuse_level_change_intercept_as_process_increment( Err(PsychometricError::LevelChangeInterceptIsNotProcessIncrement) } +/// Exact scalar discrete increment of the §7.2 level-change `CINT`. +/// +/// Driver, Oud, and Voelkle (2017, §7.2, pp. 20–21; Eq. 3, pp. 4–5; +/// Table 2, p. 12; JSS PDF re-opened 2026-08-20T19:50Z from +/// ) +/// set `CINT` to `TDPREDEFFECT * −DRIFT` so a sudden impulse holds a +/// new process mean. Equation 3 maps that intercept through +/// `A^{-1}[e^{A Δt} − I] κ`. With `κ = −a m x` the scalar increment +/// is `(e^{a Δt} − 1)/a · (−a m x) = (1 − e^{a Δt}) m x`. Form the +/// level-change `CINT` first, then the discrete intercept map. +/// Underflow of `e^{a Δt}` to `+0` keeps the equilibrium offset +/// `m x`. A zero effect or zero predictor is exactly zero. Stable +/// `a < 0` is required. `(1 − e^{a Δt}) m x` is not the +/// contemporaneous jump `m x`. `(1 − e^{a Δt}) m x` is not `κ`. +/// `(1 − e^{a Δt}) m x` is not `A^{-1}[e^{A Δt} − I] B z`. The extra +/// near-zero-drift latent process also named in §7.2 is a different +/// specification. This is not a Kalman filter and not ctsem +/// estimation. +/// +/// # Errors +/// +/// Propagates [`recover_level_change_continuous_intercept`] and +/// [`recover_discrete_continuous_intercept_effect`]. +pub fn recover_level_change_discrete_increment( + time_dependent_effect: f64, + time_dependent_predictor: f64, + log_rate: f64, + event_delta: f64, + clock: LagClock, +) -> Result { + let intercept = recover_level_change_continuous_intercept( + time_dependent_effect, + time_dependent_predictor, + log_rate, + )?; + recover_discrete_continuous_intercept_effect(intercept, log_rate, event_delta, clock) +} + +/// Refuse treating the §7.2 level-change CINT increment as the +/// contemporaneous Dirac. +/// +/// `(1 − e^{a Δt}) m x` is not the jump `m x`. +/// +/// # Errors +/// +/// Always returns [`PsychometricError::LevelChangeIncrementIsNotImpulse`]. +pub fn refuse_level_change_increment_as_impulse( + level_change_increment: f64, + time_dependent_impulse: f64, +) -> Result { + let _ = (level_change_increment, time_dependent_impulse); + Err(PsychometricError::LevelChangeIncrementIsNotImpulse) +} + +/// Refuse treating the §7.2 level-change CINT increment as `CINT`. +/// +/// `(1 − e^{a Δt}) m x` is not `κ = −a m x`. +/// +/// # Errors +/// +/// Always returns [`PsychometricError::LevelChangeIncrementIsNotIntercept`]. +pub fn refuse_level_change_increment_as_intercept( + level_change_increment: f64, + level_change_intercept: f64, +) -> Result { + let _ = (level_change_increment, level_change_intercept); + Err(PsychometricError::LevelChangeIncrementIsNotIntercept) +} + +/// Refuse treating the §7.2 level-change CINT increment as the Eq. 3 +/// process increment. +/// +/// `(1 − e^{a Δt}) m x` is not `A^{-1}[e^{A Δt} − I] B z`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::LevelChangeIncrementIsNotProcessIncrement`]. +pub fn refuse_level_change_increment_as_process_increment( + level_change_increment: f64, + time_independent_increment: f64, +) -> Result { + let _ = (level_change_increment, time_independent_increment); + Err(PsychometricError::LevelChangeIncrementIsNotProcessIncrement) +} + /// Exact scalar evolved latent mean plus a contemporaneous impulse. /// /// Driver, Oud, and Voelkle (2017, Eq. 3, p. 5) write the first two @@ -3331,12 +3419,12 @@ mod tests { recover_initial_time_independent_predictor_carry, recover_initial_time_independent_predictor_effect, recover_irregular_centered_residual_log_rate, recover_level_change_continuous_intercept, - recover_local_log_rate, recover_manifest_lagged_observed_covariance, - recover_manifest_observed_mean, recover_manifest_observed_variance, - recover_manifest_trait_plus_state_observed_variance, recover_stationary_latent_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_level_change_discrete_increment, recover_local_log_rate, + recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, + recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, + recover_stationary_latent_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_continuous_intercept_as_discrete_mean_increment, refuse_continuous_intercept_as_initial_latent_mean, refuse_continuous_intercept_as_manifest_means, refuse_difference_quotient_as_local_rate, @@ -3370,6 +3458,8 @@ mod tests { refuse_initial_time_independent_observed_mean_as_initial_time_dependent_observed_mean, refuse_latent_lagged_covariance_as_observed_covariance, refuse_latent_mean_as_observed_mean, refuse_latent_variance_as_observed_variance, + refuse_level_change_increment_as_impulse, refuse_level_change_increment_as_intercept, + refuse_level_change_increment_as_process_increment, refuse_level_change_intercept_as_free_continuous_intercept, refuse_level_change_intercept_as_impulse, refuse_level_change_intercept_as_process_increment, refuse_manifest_means_as_observed_mean, @@ -5495,6 +5585,148 @@ mod tests { assert!((rewritten - 1.0).abs() < 1e-12); } + #[test] + fn level_change_discrete_increment_recovers_driver_equation_three_of_section_seven_point_two() { + let effect = 0.4_f64; + let predictor = 3.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let increment = recover_level_change_discrete_increment( + effect, + predictor, + drift, + delta, + LagClock::EventTime, + ) + .expect("level-change-increment"); + let intercept = recover_level_change_continuous_intercept(effect, predictor, drift) + .expect("level-change"); + let via_cint = recover_discrete_continuous_intercept_effect( + intercept, + drift, + delta, + LagClock::EventTime, + ) + .expect("cint-increment"); + let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("impulse"); + let expected = (1.0 - (drift * delta).exp()) * impulse; + assert!((increment - expected).abs() < 1e-15); + assert!((increment - via_cint).abs() < 1e-15); + assert!((increment - impulse).abs() > 1e-3); + assert!((increment - intercept).abs() > 1e-3); + let tipred = recover_discrete_time_independent_predictor_effect( + effect, + predictor, + drift, + delta, + LagClock::EventTime, + ) + .expect("tipred"); + assert_eq!( + refuse_level_change_increment_as_impulse(increment, impulse), + Err(PsychometricError::LevelChangeIncrementIsNotImpulse) + ); + assert_eq!( + refuse_level_change_increment_as_intercept(increment, intercept), + Err(PsychometricError::LevelChangeIncrementIsNotIntercept) + ); + assert_eq!( + refuse_level_change_increment_as_process_increment(increment, tipred), + Err(PsychometricError::LevelChangeIncrementIsNotProcessIncrement) + ); + let equilibrated = recover_level_change_discrete_increment( + effect, + predictor, + -800.0, + 1.0, + LagClock::EventTime, + ) + .expect("underflow"); + assert!((equilibrated - impulse).abs() < 1e-15); + assert_eq!( + recover_level_change_discrete_increment( + 0.0, + predictor, + drift, + delta, + LagClock::EventTime + ), + Ok(0.0) + ); + } + + #[test] + fn level_change_discrete_increment_invalid_inputs_fail_closed() { + let effect = 0.4_f64; + let predictor = 3.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + assert_eq!( + recover_level_change_discrete_increment( + effect, + predictor, + 0.0, + delta, + LagClock::EventTime + ), + Err(PsychometricError::LevelChangeRequiresStableDrift) + ); + assert_eq!( + recover_level_change_discrete_increment( + effect, + predictor, + 0.5, + delta, + LagClock::EventTime + ), + Err(PsychometricError::LevelChangeRequiresStableDrift) + ); + assert_eq!( + recover_level_change_discrete_increment( + effect, + predictor, + drift, + delta, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_level_change_discrete_increment( + effect, + predictor, + drift, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_level_change_discrete_increment(1e308, 2.0, drift, delta, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_level_change_discrete_increment( + f64::NAN, + predictor, + drift, + delta, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_level_change_discrete_increment( + 0.0, + predictor, + 0.0, + delta, + LagClock::EventTime + ), + Ok(0.0) + ); + } + #[test] fn discrete_observed_mean_with_impulse_recovers_driver_equation_five() { let loading = 2.0_f64; diff --git a/crates/psychometric_core/src/lib.rs b/crates/psychometric_core/src/lib.rs index 66476413..945d319e 100644 --- a/crates/psychometric_core/src/lib.rs +++ b/crates/psychometric_core/src/lib.rs @@ -79,6 +79,9 @@ //! (`a < 0` so `−κ / a = m x`; not the dissipating Dirac `m x`, not //! a free `CINT`, not `A^{-1}[e^{A Δt} − I] B z`, and not the extra //! near-zero-drift latent process also named in §7.2), +//! recovers the Driver Eq. 3 increment of that setting as +//! `(1 − e^{a Δt}) m x` (`(1 − e^{a Δt}) m x` is not `m x`, not `κ`, +//! and not `A^{-1}[e^{A Δt} − I] B z`), //! and refuses //! latent-mean comparison below strong invariance. @@ -185,6 +188,8 @@ pub use event_time::recover_initial_time_independent_predictor_effect; pub use event_time::recover_irregular_centered_residual_log_rate; /// Exact scalar §7.2 level-change `CINT` `κ = −a m x`. pub use event_time::recover_level_change_continuous_intercept; +/// Exact scalar Eq. 3 increment of that `CINT` `(1 − e^{a Δt}) m x`. +pub use event_time::recover_level_change_discrete_increment; /// Exact scalar inverse `a = ln(φ) / Δt`. pub use event_time::recover_local_log_rate; /// Exact scalar lagged observed-indicator covariance `λ² cov(η) + ψ`. @@ -277,6 +282,12 @@ pub use event_time::refuse_latent_lagged_covariance_as_observed_covariance; pub use event_time::refuse_latent_mean_as_observed_mean; /// Refuse treating Driver Eq. 5 latent variance as `Var(y)`. pub use event_time::refuse_latent_variance_as_observed_variance; +/// Refuse treating the §7.2 level-change CINT increment as the contemporaneous Dirac. +pub use event_time::refuse_level_change_increment_as_impulse; +/// Refuse treating the §7.2 level-change CINT increment as `CINT`. +pub use event_time::refuse_level_change_increment_as_intercept; +/// Refuse treating the §7.2 level-change CINT increment as the Eq. 3 process increment. +pub use event_time::refuse_level_change_increment_as_process_increment; /// Refuse treating Driver §7.2 level-change `CINT` as a free `CINT`. pub use event_time::refuse_level_change_intercept_as_free_continuous_intercept; /// Refuse treating Driver §7.2 level-change `CINT` as the contemporaneous Dirac. 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 d260ef0d..f1403119 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -24,12 +24,12 @@ use psychometric_core::{ recover_initial_time_independent_predictor_carry, recover_initial_time_independent_predictor_effect, recover_irregular_centered_residual_log_rate, recover_kish_weighted_slope, - recover_level_change_continuous_intercept, recover_manifest_lagged_observed_covariance, - recover_manifest_observed_mean, recover_manifest_observed_variance, - recover_manifest_trait_plus_state_observed_variance, recover_stationary_latent_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_level_change_continuous_intercept, recover_level_change_discrete_increment, + recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, + recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, + recover_stationary_latent_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_continuous_intercept_as_discrete_mean_increment, refuse_continuous_intercept_as_initial_latent_mean, refuse_continuous_intercept_as_manifest_means, refuse_difference_quotient_as_local_rate, @@ -62,7 +62,8 @@ use psychometric_core::{ refuse_initial_time_independent_effect_as_time_dependent_impulse, refuse_initial_time_independent_observed_mean_as_initial_time_dependent_observed_mean, refuse_latent_lagged_covariance_as_observed_covariance, refuse_latent_mean_as_observed_mean, - refuse_latent_variance_as_observed_variance, + refuse_latent_variance_as_observed_variance, refuse_level_change_increment_as_impulse, + refuse_level_change_increment_as_intercept, refuse_level_change_increment_as_process_increment, refuse_level_change_intercept_as_free_continuous_intercept, refuse_level_change_intercept_as_impulse, refuse_level_change_intercept_as_process_increment, refuse_manifest_means_as_observed_mean, refuse_manifest_trait_variance_as_measurement_error, @@ -3493,3 +3494,90 @@ fn level_change_continuous_intercept_refuses_unstable_drift_and_overflow() { Ok(0.0) ); } + +#[test] +fn level_change_discrete_increment_recovers_driver_equation_three_of_section_seven_point_two() { + let effect = 0.4_f64; + let predictor = 3.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let increment = recover_level_change_discrete_increment( + effect, + predictor, + drift, + delta, + LagClock::EventTime, + ) + .expect("level-change-increment"); + let intercept = + recover_level_change_continuous_intercept(effect, predictor, drift).expect("level-change"); + let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("impulse"); + let expected = (1.0 - (drift * delta).exp()) * impulse; + let error = rmse(&[expected], &[increment]); + assert!( + error < 1e-15, + "Driver §7.2 Eq. 3 level-change increment RMSE {error}: got {increment}" + ); + assert!(rmse(&[increment], &[impulse]) > error); + assert!(rmse(&[increment], &[intercept]) > error); + let tipred = recover_discrete_time_independent_predictor_effect( + effect, + predictor, + drift, + delta, + LagClock::EventTime, + ) + .expect("tipred"); + assert_eq!( + refuse_level_change_increment_as_impulse(increment, impulse), + Err(PsychometricError::LevelChangeIncrementIsNotImpulse) + ); + assert_eq!( + refuse_level_change_increment_as_intercept(increment, intercept), + Err(PsychometricError::LevelChangeIncrementIsNotIntercept) + ); + assert_eq!( + refuse_level_change_increment_as_process_increment(increment, tipred), + Err(PsychometricError::LevelChangeIncrementIsNotProcessIncrement) + ); + let equilibrated = recover_level_change_discrete_increment( + effect, + predictor, + -800.0, + 1.0, + LagClock::EventTime, + ) + .expect("underflow"); + assert!( + rmse(&[impulse], &[equilibrated]) < 1e-15, + "underflow of e^{{aΔt}} must keep m x: got {equilibrated}" + ); +} + +#[test] +fn level_change_discrete_increment_refuses_unstable_drift_clock_and_overflow() { + assert_eq!( + recover_level_change_discrete_increment(0.4, 3.0, 0.0, 2.0, LagClock::EventTime), + Err(PsychometricError::LevelChangeRequiresStableDrift) + ); + assert_eq!( + recover_level_change_discrete_increment(0.4, 3.0, 0.5, 2.0, LagClock::EventTime), + Err(PsychometricError::LevelChangeRequiresStableDrift) + ); + assert_eq!( + recover_level_change_discrete_increment(0.4, 3.0, -0.5, 2.0, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_level_change_discrete_increment(0.4, 3.0, -0.5, 0.0, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_level_change_discrete_increment(1e308, 2.0, -0.5, 2.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_level_change_discrete_increment(0.0, 3.0, 0.0, 2.0, LagClock::EventTime), + Ok(0.0) + ); +} diff --git a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs index f8eca40a..99adc575 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -21,11 +21,12 @@ use psychometric_core::{ recover_initial_time_independent_predictor_carry, recover_initial_time_independent_predictor_effect, recover_irregular_centered_residual_log_rate, recover_level_change_continuous_intercept, - recover_loading_point_estimate_mean, recover_manifest_lagged_observed_covariance, - recover_manifest_observed_mean, recover_manifest_observed_variance, - recover_manifest_trait_plus_state_observed_variance, recover_stationary_latent_variance, - recover_time_dependent_predictor_impulse, recover_time_dependent_predictor_impulse_carry, - recover_trait_plus_state_latent_variance, recover_within_residual_event_time_log_rate, + recover_level_change_discrete_increment, recover_loading_point_estimate_mean, + recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, + recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, + recover_stationary_latent_variance, recover_time_dependent_predictor_impulse, + recover_time_dependent_predictor_impulse_carry, recover_trait_plus_state_latent_variance, + recover_within_residual_event_time_log_rate, refuse_continuous_intercept_as_discrete_mean_increment, refuse_continuous_intercept_as_initial_latent_mean, refuse_continuous_intercept_as_manifest_means, @@ -58,7 +59,8 @@ use psychometric_core::{ refuse_initial_time_independent_effect_as_time_dependent_impulse, refuse_initial_time_independent_observed_mean_as_initial_time_dependent_observed_mean, refuse_latent_lagged_covariance_as_observed_covariance, refuse_latent_mean_as_observed_mean, - refuse_latent_variance_as_observed_variance, + refuse_latent_variance_as_observed_variance, refuse_level_change_increment_as_impulse, + refuse_level_change_increment_as_intercept, refuse_level_change_increment_as_process_increment, refuse_level_change_intercept_as_free_continuous_intercept, refuse_level_change_intercept_as_impulse, refuse_level_change_intercept_as_process_increment, refuse_manifest_means_as_observed_mean, refuse_manifest_trait_variance_as_measurement_error, @@ -1705,3 +1707,54 @@ fn level_change_cint_is_not_impulse_free_cint_or_process_increment() { Err(psychometric_core::PsychometricError::LevelChangeInterceptIsNotProcessIncrement) ); } + +#[test] +fn level_change_increment_is_not_impulse_intercept_or_process_increment() { + let effect = 0.4_f64; + let predictor = 3.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let intercept = + recover_level_change_continuous_intercept(effect, predictor, drift).expect("level-change"); + let increment = recover_level_change_discrete_increment( + effect, + predictor, + drift, + delta, + LagClock::EventTime, + ) + .expect("level-change-increment"); + let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("impulse"); + let tipred = recover_discrete_time_independent_predictor_effect( + effect, + predictor, + drift, + delta, + LagClock::EventTime, + ) + .expect("tipred"); + assert!( + (increment - impulse).abs() > 1e-3, + "Driver et al. (2017, §7.2 / Eq. 3): (1 − e^{{aΔt}}) m x is not the dissipating Dirac m x" + ); + assert!( + (increment - intercept).abs() > 1e-3, + "Driver et al. (2017, §7.2 / Eq. 3): (1 − e^{{aΔt}}) m x is not κ" + ); + assert!( + (increment - tipred).abs() > 1e-3, + "Driver et al. (2017, §7.2 / Eq. 3): (1 − e^{{aΔt}}) m x is not TIPREDEFFECT increment" + ); + assert_eq!( + refuse_level_change_increment_as_impulse(increment, impulse), + Err(psychometric_core::PsychometricError::LevelChangeIncrementIsNotImpulse) + ); + assert_eq!( + refuse_level_change_increment_as_intercept(increment, intercept), + Err(psychometric_core::PsychometricError::LevelChangeIncrementIsNotIntercept) + ); + assert_eq!( + refuse_level_change_increment_as_process_increment(increment, tipred), + Err(psychometric_core::PsychometricError::LevelChangeIncrementIsNotProcessIncrement) + ); +} diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index 6264f533..6a78e9ca 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -1,7 +1,7 @@ # ADR 0005 — Posterior-aware ESEM/DSEM and structural interpretation **Decision status:** Accepted -**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), exact scalar contemporaneous `TDPREDEFFECT` impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; the §7.2 level-change form is not that impulse), exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of that Eq. 3 composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar time-independent `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle Eq. 14; `B` is not that discrete increment), exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of that Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of that carried latent mean; `τ + λ μ_t` is not that observed mean), exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; `T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`), exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; `T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand TD composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean), exact scalar §7.2 level-change `CINT` `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; `a < 0` so `−κ / a = m x`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target +**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), exact scalar contemporaneous `TDPREDEFFECT` impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; the §7.2 level-change form is not that impulse), exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of that Eq. 3 composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar time-independent `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle Eq. 14; `B` is not that discrete increment), exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of that Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of that carried latent mean; `τ + λ μ_t` is not that observed mean), exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; `T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`), exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; `T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand TD composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean), exact scalar §7.2 level-change `CINT` `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; `a < 0` so `−κ / a = m x`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2), exact scalar Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` (not `m x`, not `κ`, and not `TIPREDEFFECT`), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target **Date:** 2026-08-05 **Supersedes:** None. ADR 0012 governs upstream topic measurement/network coordinates; this ADR governs higher-order psychometric structure and longitudinal interpretation. @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), recovers the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z; `TDPREDEFFECT` is `M`, not `CINT`; `M x` is not `A^{-1}[e^{A Δt} − I] B z`; `M x` is not Voelkle et al., 2012, Eq. 14; the level-change form is not that impulse), recovers the exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition; JSS PDF re-opened 2026-08-20T09:01Z; the evolved map `τ + λ μ_t` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-impulse latent mean is not `E(y_t)`), recovers the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z; `TIPREDEFFECT` is `B`, not `κ`; form `B z` first, then the discrete intercept map; a zero drift is `B z Δt`; `B` is not the discrete increment; `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle Eq. 14), recovers the exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; JSS PDF re-opened 2026-08-20T12:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-increment latent mean is not `E(y_t)`), recovers the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z; form `m x` first, then `e^{a(t−u)} m x`; a zero drift is `m x`; underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept; `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; an impulse at `u = t` is the contemporaneous map; an impulse at `u ≤ t0` is already in `η(t0)`), recovers the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; JSS PDF re-opened 2026-08-20T05:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the carried latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and its Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF opened 2026-08-20T15:14Z; form `t0_b z` first, then `e^{a Δt} t0_b z`; a zero drift is `t0_b z`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; `e^{A Δt} t0_b z` is not `t0_b z`; `T0TIPREDEFFECT` is the coefficient, not the shift), recovers the exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition; JSS PDF re-opened 2026-08-20T15:28Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and its Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; form `t0_m x0` first, then `e^{a Δt} t0_m x0`; a zero drift is `t0_m x0`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `T0TDPREDEFFECT` is the coefficient, not the shift; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), recovers the exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; the first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar §7.2 level-change `CINT` setting `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z; form `m x` first, then multiply by `−a`; `a < 0` so `−κ / a = m x`; `a ≥ 0` cannot hold a new process mean; `−a m x` is not the dissipating Dirac, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`; the extra near-zero-drift latent process also named in §7.2 is a different specification), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), recovers the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z; `TDPREDEFFECT` is `M`, not `CINT`; `M x` is not `A^{-1}[e^{A Δt} − I] B z`; `M x` is not Voelkle et al., 2012, Eq. 14; the level-change form is not that impulse), recovers the exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition; JSS PDF re-opened 2026-08-20T09:01Z; the evolved map `τ + λ μ_t` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-impulse latent mean is not `E(y_t)`), recovers the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z; `TIPREDEFFECT` is `B`, not `κ`; form `B z` first, then the discrete intercept map; a zero drift is `B z Δt`; `B` is not the discrete increment; `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle Eq. 14), recovers the exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; JSS PDF re-opened 2026-08-20T12:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-increment latent mean is not `E(y_t)`), recovers the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z; form `m x` first, then `e^{a(t−u)} m x`; a zero drift is `m x`; underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept; `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; an impulse at `u = t` is the contemporaneous map; an impulse at `u ≤ t0` is already in `η(t0)`), recovers the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; JSS PDF re-opened 2026-08-20T05:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the carried latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and its Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF opened 2026-08-20T15:14Z; form `t0_b z` first, then `e^{a Δt} t0_b z`; a zero drift is `t0_b z`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; `e^{A Δt} t0_b z` is not `t0_b z`; `T0TIPREDEFFECT` is the coefficient, not the shift), recovers the exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition; JSS PDF re-opened 2026-08-20T15:28Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and its Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; form `t0_m x0` first, then `e^{a Δt} t0_m x0`; a zero drift is `t0_m x0`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `T0TDPREDEFFECT` is the coefficient, not the shift; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), recovers the exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; the first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar §7.2 level-change `CINT` setting `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z; form `m x` first, then multiply by `−a`; `a < 0` so `−κ / a = m x`; `a ≥ 0` cannot hold a new process mean; `−a m x` is not the dissipating Dirac, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`; the extra near-zero-drift latent process also named in §7.2 is a different specification), recovers the exact scalar Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` (JSS PDF re-opened 2026-08-20T19:50Z; form the level-change `CINT` first, then the discrete intercept map; underflow of `e^{a Δt}` to `+0` keeps `m x`; `(1 − e^{a Δt}) m x` is not `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. 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 37696e98..6e8ffd6b 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -33,15 +33,16 @@ This slice stays inside `psychometric_core`. It does not add a second invariance 27. recover the exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and its Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z) and refuse treating `t0_m x0` as `M x`, as `e^{A(t−u)} M x` for `t0 < u < t`, as `t0_b z`, as `A^{-1}[e^{A Δt} − I] B z`, or as `κ`; refuse treating `e^{A Δt} t0_m x0` as `t0_m x0` or as `e^{A(t−u)} M x`; refuse treating the coefficient as the shift; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`; 28. recover the exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; the first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean) and refuse treating `τ + λ μ_t`, `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, `τ + λ(μ_t + m x)`, `τ + λ(μ_t + e^{a(t−u)} m x)`, or `τ + λ(μ_t + e^{a Δt} t0_b z)` as that observed mean; 29. recover the exact scalar §7.2 level-change `CINT` `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z; form `m x` first, then multiply by `−a`; `a < 0` so `−κ / a = m x`) and refuse treating `−a m x` as the dissipating Dirac `m x`, as a free `CINT`, or as `A^{-1}[e^{A Δt} − I] B z`; `a ≥ 0` cannot hold a new process mean; the extra near-zero-drift latent process also named in §7.2 is a different specification; -30. refuse pooling discrete lags from unequal event intervals as one coefficient; -31. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); -32. refuse the difference quotient as a continuous-time rate; -33. apply the same event-time map to CWC residuals (still not DSEM); -34. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). +30. recover the exact scalar Eq. 3 increment of that level-change `CINT` `(1 − e^{a Δt}) m x` (Driver et al., 2017, §7.2, pp. 20–21; Eq. 3, pp. 4–5; JSS PDF re-opened 2026-08-20T19:50Z; form `κ = −a m x` first, then `A^{-1}[e^{A Δt} − I] κ`; underflow of `e^{a Δt}` to `+0` keeps `m x`) and refuse treating `(1 − e^{a Δt}) m x` as `m x`, as `κ`, or as `A^{-1}[e^{A Δt} − I] B z`; +31. refuse pooling discrete lags from unequal event intervals as one coefficient; +32. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); +33. refuse the difference quotient as a continuous-time rate; +34. apply the same event-time map to CWC residuals (still not DSEM); +35. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). ## Claim boundary -This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. The contemporaneous `TDPREDEFFECT` impulse is not the continuous intercept, not the time-independent discrete effect, and not Voelkle et al. (2012, Eq. 14). The time-independent `TIPREDEFFECT` increment is not the continuous intercept, not the contemporaneous impulse, not Voelkle et al. (2012, Eq. 14), and not the coefficient `B`. The within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). The evolved observed mean `τ + λ μ_t` is not the contemporaneous-impulse observed mean `τ + λ(μ_t + m x)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not the impulse-carry observed mean `τ + λ(μ_t + e^{a(t−u)} m x)` when `u ≠ t`. The evolved observed mean `τ + λ μ_t` is not the impulse-carry observed mean. The carried latent mean is not `E(y_t)`. The evolved-plus-impulse latent mean is not `E(y_t)`. The evolved observed mean `τ + λ μ_t` is not the time-independent-predictor observed mean `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not that time-independent-predictor observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that time-independent-predictor observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TIPREDEFFECT` shift `t0_b z` is not the Eq. 3 process increment `A^{-1}[e^{A Δt} − I] B z`, not `κ`, and not `M x`. The Eq. 3 first-summand carry `e^{A Δt} t0_b z` is not `t0_b z` and is not that process increment. `T0TIPREDEFFECT` is the coefficient, not the first-occasion shift. The evolved observed mean `τ + λ μ_t` is not the first-occasion TI-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_b z)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion observed mean when `u ≠ t0`. The evolved-plus-T0TIPRED latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TDPREDEFFECT` shift `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `κ`. The Eq. 3 first-summand carry `e^{A Δt} t0_m x0` is not `t0_m x0` and is not that within-interval impulse carry. `T0TDPREDEFFECT` is the coefficient, not the first-occasion shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The evolved observed mean `τ + λ μ_t` is not the first-occasion TD-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_m x0)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion TD observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion TD observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion TD observed mean when `u ≠ t0`. The first-occasion TI composition `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that first-occasion TD observed mean. Same numbers as `T0TIPREDEFFECT` yield the same product; Table 3 names a different matrix. The evolved-plus-T0TDPRED latent mean is not `E(y_t)`. The §7.2 level-change `CINT` `κ = −a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. Lasting level change via that `CINT` setting requires `a < 0`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not that `CINT` setting. +This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. The contemporaneous `TDPREDEFFECT` impulse is not the continuous intercept, not the time-independent discrete effect, and not Voelkle et al. (2012, Eq. 14). The time-independent `TIPREDEFFECT` increment is not the continuous intercept, not the contemporaneous impulse, not Voelkle et al. (2012, Eq. 14), and not the coefficient `B`. The within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). The evolved observed mean `τ + λ μ_t` is not the contemporaneous-impulse observed mean `τ + λ(μ_t + m x)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not the impulse-carry observed mean `τ + λ(μ_t + e^{a(t−u)} m x)` when `u ≠ t`. The evolved observed mean `τ + λ μ_t` is not the impulse-carry observed mean. The carried latent mean is not `E(y_t)`. The evolved-plus-impulse latent mean is not `E(y_t)`. The evolved observed mean `τ + λ μ_t` is not the time-independent-predictor observed mean `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not that time-independent-predictor observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that time-independent-predictor observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TIPREDEFFECT` shift `t0_b z` is not the Eq. 3 process increment `A^{-1}[e^{A Δt} − I] B z`, not `κ`, and not `M x`. The Eq. 3 first-summand carry `e^{A Δt} t0_b z` is not `t0_b z` and is not that process increment. `T0TIPREDEFFECT` is the coefficient, not the first-occasion shift. The evolved observed mean `τ + λ μ_t` is not the first-occasion TI-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_b z)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion observed mean when `u ≠ t0`. The evolved-plus-T0TIPRED latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TDPREDEFFECT` shift `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `κ`. The Eq. 3 first-summand carry `e^{A Δt} t0_m x0` is not `t0_m x0` and is not that within-interval impulse carry. `T0TDPREDEFFECT` is the coefficient, not the first-occasion shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The evolved observed mean `τ + λ μ_t` is not the first-occasion TD-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_m x0)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion TD observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion TD observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion TD observed mean when `u ≠ t0`. The first-occasion TI composition `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that first-occasion TD observed mean. Same numbers as `T0TIPREDEFFECT` yield the same product; Table 3 names a different matrix. The evolved-plus-T0TDPRED latent mean is not `E(y_t)`. The §7.2 level-change `CINT` `κ = −a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. Lasting level change via that `CINT` setting requires `a < 0`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not that `CINT` setting. The Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` is not the dissipating Dirac `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`. Underflow of `e^{a Δt}` to `+0` keeps the equilibrium offset `m x`. ## Authoritative sources @@ -91,6 +92,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - **First-occasion time-dependent predictor.** Driver et al. (2017, Table 3, p. 13; Eq. 3 first summand, p. 5; JSS PDF re-opened 2026-08-20T19:10Z): Table 3 names `T0TDPREDEFFECT` the effect of time-dependent predictors on latents at `T0`. Table 2 / Table 3 name `TDPREDEFFECT` `M`, which enters Equation 3 as the printed fourth-summand Dirac `M x` at `u = t`. The scalar first-occasion shift is `t0_m x0`. Equation 3's first summand carries that shift as `e^{AΔt}t0_m x0`. Form `t0_m x0` first, then `e^{aΔt}t0_m x0`. Form `μ_t` first, then add that carry. A zero drift is `t0_m x0`. Binary64 underflow of `e^{aΔt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. A zero effect or zero predictor is exactly zero. `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{AΔt}−I]Bz`, and not `κ`. `e^{AΔt}t0_m x0` is not `t0_m x0`. `T0TDPREDEFFECT` is the coefficient, not the shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. An overflowing product, rewrite, or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **First-occasion time-dependent-predictor observed-indicator mean.** Driver et al. (2017, Eq. 5, p. 5; Eq. 3 first summand, p. 5; Table 3, p. 13; JSS PDF re-opened 2026-08-20T19:07Z): `y_i(t)=Γ+Λη_i(t)+ζ_i(t)` with `ζ∼N(0,Θ)` and `Γ∼N(τ,Ψ)`. The latent process at `t` after the Table 3 first-occasion TD carry is `μ_t+e^{aΔt}t0_m x0`. The scalar composition is `E(y_t)=τ+λ(μ_t+e^{aΔt}t0_m x0)`. Form the evolved-plus-carry latent mean first, then `τ+λ` of that mean. A zero loading is exactly `τ`. A zero evolved-plus-carry latent mean is exactly `τ`. A zero intercept is exactly `λ(μ_t+carry)`. The evolved observed mean `τ+λμ_t` is not this composition. The process-increment map `τ+λ(μ_t+A^{-1}[e^{AΔt}−I]Bz)` is not this composition. The contemporaneous map `τ+λ(μ_t+mx)` is not this composition. The impulse-carry map `τ+λ(μ_t+e^{a(t−u)}mx)` is not this composition when `u≠t0`. The first-occasion TI map `τ+λ(μ_t+e^{aΔt}t0_b z)` is not this composition. Same numbers as `T0TIPREDEFFECT` yield the same product; Table 3 names a different matrix. `MANIFESTMEANS` is not `E(y_t)`. The evolved-plus-carry latent mean is not `E(y_t)`. `T0TDPREDEFFECT` is the coefficient, not that observed mean. An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **Level-change continuous intercept.** Driver et al. (2017, §7.2, pp. 20–21; Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T19:45Z): a sudden Dirac dissipates back to the process mean. To generate a lasting level change, `CINT` is set to `TDPREDEFFECT * −DRIFT`. The scalar setting is `κ=−a m x`. Form `m x` first, then multiply by `−a`. Stable `a<0` is required so `−κ/a=m x` is an equilibrium offset. `a≥0` cannot hold a new process mean. A zero effect or zero predictor is exactly zero. `−a m x` is not `m x`, not a free `CINT`, and not `A^{-1}[e^{AΔt}−I]Bz`. The extra near-zero-drift latent process also named in §7.2 is a different specification. An overflowing product fails closed. This is not a Kalman filter and not ctsem estimation. +- **Level-change discrete increment.** Driver et al. (2017, §7.2, pp. 20–21; Eq. 3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T19:50Z): Equation 3 maps `CINT` through `A^{-1}[e^{AΔt}−I]κ`. With `κ=−a m x` the scalar increment is `(e^{aΔt}−1)/a·(−a m x)=(1−e^{aΔt})m x`. Form the level-change `CINT` first, then the discrete intercept map. Underflow of `e^{aΔt}` to `+0` keeps `m x`. A zero effect or zero predictor is exactly zero. `(1−e^{aΔt})m x` is not `m x`, not `κ`, and not `A^{-1}[e^{AΔt}−I]Bz`. An overflowing product or increment fails closed. This is not a Kalman filter and not ctsem estimation. - **CWC-then-lag.** Sample cluster means are removed first. Consecutive within residuals are then fitted by least squares to \(r_{t+\Delta t}\approx\exp(a\Delta t)\,r_{t}\) on event time. Same-sign pair-wise logs initialize the scalar Newton step. Sign-flipping \(T=2\) CWC pairs have no real logarithm and fail closed. Curran and Bauer (2011, pp. 607–608) show that this person-mean subtraction on a raw autoregressive series does **not** isolate the lagged within-person effect; the helper therefore does not claim to recover the raw-process drift. - **Already-centered irregular residual.** The caller supplies lagged within residuals. The mean of \(a=\ln(r_{t+\Delta t}/r_t)/\Delta t\) is the exact scalar map. Intervals may be irregular. The helper does not center again. This is not DSEM. @@ -124,6 +126,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - Driver et al. (2017, Table 3, p. 13; Eq. 3 first summand) recovers a known first-occasion time-dependent shift \(t0_m x0\) and carry \(e^{a\Delta t}t0_m x0\) at machine-scale RMSE, and those RMSEs are smaller than treating `TDPREDEFFECT` `M x`, the within-interval impulse carry, `T0TIPREDEFFECT`, `TIPREDEFFECT`, `CINT`, the coefficient, or the un-carried shift as the carry; composing \(\mu_t\) plus that carry recovers the known sum; a zero drift is \(t0_m x0\); underflow of \(e^{a\Delta t}\) to `+0` is a vanishing carry and is kept; a zero effect or zero predictor is exactly zero; a non-event clock, a non-positive interval, and an overflowing product or rewrite fail closed; - Driver et al. (2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; Table 3, p. 13) recovers a known \(E(y_t)=\tau+\lambda(\mu_t+e^{a\Delta t}t0_m x0)\) at machine-scale RMSE, and that RMSE is smaller than treating \(\tau+\lambda\mu_t\), \(\tau+\lambda(\mu_t+A^{-1}[e^{A\Delta t}-I]Bz)\), \(\tau+\lambda(\mu_t+mx)\), \(\tau+\lambda(\mu_t+e^{a(t-u)}mx)\), `MANIFESTMEANS`, or the evolved-plus-carry latent mean as \(E(y_t)\); same numbers as `T0TIPREDEFFECT` yield the same product, but Table 3 names a different matrix and the refuse is fail-closed; a zero loading is \(\tau\); a non-event clock, a non-positive interval, and an overflowing product or sum fail closed; - Driver et al. (2017, §7.2) recovers a known level-change `CINT` \(\kappa=-a\,mx\) at machine-scale RMSE, and that RMSE is smaller than treating the dissipating Dirac \(mx\), a free `CINT`, or `TIPREDEFFECT` as that setting; \(-\kappa/a\) recovers \(mx\) when \(a<0\); \(a\ge 0\) with a nonzero impulse fails closed; a zero effect or zero predictor is exactly zero; an overflowing product fails closed; +- Driver et al. (2017, §7.2 / Eq. 3) recovers a known level-change increment \((1-e^{a\Delta t})mx\) at machine-scale RMSE, and that RMSE is smaller than treating the dissipating Dirac \(mx\), the intercept \(\kappa\), or `TIPREDEFFECT` as that increment; underflow of \(e^{a\Delta t}\) to `+0` keeps \(mx\); \(a\ge 0\) with a nonzero impulse fails closed; a non-event clock, a non-positive interval, and an overflowing product fail closed; - pooling discrete lags from unequal intervals fails closed; - CWC-then-lag on a two-cluster decaying series has smaller computed RMSE than a level-pooled series when the latter is identified; - already-centered irregular residuals recover a known drift at machine-scale RMSE, and that RMSE is smaller than CWC of the corresponding raw autoregressive series (Curran & Bauer, 2011, pp. 607–608); From 5b18ea7145902b820b2add4e8e2f11101c54ef84 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 06:11:15 +0900 Subject: [PATCH 50/62] test(psychometric): require executable pooled rate evidence --- ...multilevel_event_time_recovery_contract.rs | 19 +++++++------------ docs/research/standards-and-literature.md | 2 ++ .../strong-invariance-latent-means.md | 10 +++++++++- 3 files changed, 18 insertions(+), 13 deletions(-) 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 0dea933f..05d2286e 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -566,18 +566,13 @@ fn within_residual_event_time_log_rate_beats_pooled_levels() { score: person_mean + start, }); } - let pooled_rate = recover_event_series_mean_log_rate(&pooled, LagClock::EventTime); - match pooled_rate { - Ok(rate) => { - let pooled_error = rmse(&[true_drift], &[rate]); - assert!( - within_error < pooled_error, - "CWC lag RMSE {within_error} should beat pooled {pooled_error}" - ); - } - Err(PsychometricError::InvalidNumericInput | PsychometricError::NonPositiveInterval) => {} - Err(other) => panic!("unexpected pooled error {other}"), - } + let pooled_rate = recover_event_series_mean_log_rate(&pooled, LagClock::EventTime) + .expect("pooled positive lag"); + let pooled_error = rmse(&[true_drift], &[pooled_rate]); + assert!( + within_error < pooled_error, + "CWC lag RMSE {within_error} should beat pooled {pooled_error}" + ); assert!(within_error < 0.25, "CWC lag RMSE {within_error} too large"); } diff --git a/docs/research/standards-and-literature.md b/docs/research/standards-and-literature.md index a82fb618..3631bc32 100644 --- a/docs/research/standards-and-literature.md +++ b/docs/research/standards-and-literature.md @@ -32,6 +32,8 @@ Mislevy, R. J. (1991). Randomization-based inference about latent variables from Meredith, W. (1993). Measurement invariance, factor analysis and factorial invariance. *Psychometrika, 58*(4), 525–543. https://doi.org/10.1007/BF02294825 +Sörbom, D. (1974). A general method for studying differences in factor means and factor structure between groups. *British Journal of Mathematical and Statistical Psychology, 27*(2), 229–239. https://doi.org/10.1111/j.2044-8317.1974.tb00543.x + Putnick, D. L., & Bornstein, M. H. (2016). Measurement invariance conventions and reporting: The state of the art and future directions for psychological research. *Developmental Review, 41*, 71–90. https://doi.org/10.1016/j.dr.2016.06.004 Holland, P. W. (1986). Statistics and causal inference. *Journal of the American Statistical Association, 81*(396), 945–960. https://doi.org/10.1080/01621459.1986.10478354 diff --git a/docs/research/strong-invariance-latent-means.md b/docs/research/strong-invariance-latent-means.md index 83a4db00..c451e048 100644 --- a/docs/research/strong-invariance-latent-means.md +++ b/docs/research/strong-invariance-latent-means.md @@ -17,6 +17,14 @@ This slice does **not** import the unpublished `measurement_invariance` crate on ## Authoritative sources used for the mean gate +Meredith, W. (1993). Measurement invariance, factor analysis and factorial invariance. *Psychometrika, 58*(4), 525–543. https://doi.org/10.1007/BF02294825 + +The original-paper record and abstract were opened on the Cambridge Core page on 2026-08-21. Meredith defines weak measurement invariance, strong factorial invariance, and strict factorial invariance and relates factorial invariance to group differences. This is the primary source for the hierarchy used by this gate; the implementation deliberately reports the narrower local `strong`/`strict` labels rather than claiming a full multiple-group CFA. + +Sörbom, D. (1974). A general method for studying differences in factor means and factor structure between groups. *British Journal of Mathematical and Statistical Psychology, 27*(2), 229–239. https://doi.org/10.1111/j.2044-8317.1974.tb00543.x + +The original article record and abstract were opened on the Wiley Online Library page on 2026-08-21. Sörbom's primary model estimates factor means, loadings, and unique variances jointly from group observed means, variances, and covariances while allowing factorial-invariance constraints. It is the direct source for the factor-mean comparison target. The formula implemented here is the scalar two-group OLS reduction obtained by subtracting the observed-mean equation under equal loading and intercept; the source is not being presented as stating this crate-specific OLS formula. + Putnick, D. L., & Bornstein, M. H. (2016). Measurement invariance conventions and reporting: The state of the art and future directions for psychological research. *Developmental Review, 41*, 71–90. https://doi.org/10.1016/j.dr.2016.06.004 PMC author manuscript (PMC5145197) opened 2026-08-19T22:15Z from https://pmc.ncbi.nlm.nih.gov/articles/PMC5145197/. The NIHMS PDF endpoints returned HTML/500 on this cycle; the PMC HTML full text is the opened copy. @@ -48,7 +56,7 @@ Per group, \(y=\nu+\lambda f+e\) is fit by OLS. Status is: - strong when loadings and intercepts match and residual variances differ, or when residual degrees of freedom are absent; - strict when both groups have residual degrees of freedom and loadings, intercepts, and residual variances match. -The latent-mean difference is \((\bar y_c-\bar y_r)/\lambda\) with \(\lambda\) the midpoint of the two loadings, and only after strong or strict. +The latent-mean difference is \((\bar y_c-\bar y_r)/\lambda\) with \(\lambda\) the midpoint of the two loadings, and only after strong or strict. Meredith (1993) supplies the invariance hierarchy and Sörbom (1974) supplies the factor-mean comparison model; the displayed expression is the explicitly stated scalar OLS derivation for this implementation. The formula follows by subtracting the group means of \(y=\nu+\lambda f+e\) after the equal-loading/equal-intercept restrictions have been accepted; the cited multi-group latent-mean study supplies the comparison target, while this document records the narrower OLS derivation. From 27506adffbea1feb00deb0c3f467a1b308e9b155 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 06:13:30 +0900 Subject: [PATCH 51/62] docs(research): record accessed invariance sources --- docs/research/standards-and-literature.md | 2 ++ docs/research/strong-invariance-latent-means.md | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/research/standards-and-literature.md b/docs/research/standards-and-literature.md index 3631bc32..86b77f9f 100644 --- a/docs/research/standards-and-literature.md +++ b/docs/research/standards-and-literature.md @@ -40,6 +40,8 @@ Holland, P. W. (1986). Statistics and causal inference. *Journal of the American TEPP applies these sources to construct definition, score interpretation, reliability, validity evidence, uncertainty, consequences, longitudinal invariance, ESEM cross-loadings, and DSEM. Topic outputs are treated as fallible indicators or components only after their construct role is evaluated. Reflective, formative, and network classes remain distinct (Bollen & Lennox, 1991). Complete-data OLS loadings across posterior indicator draws are combined with Rubin (1996) \(T_m\); the arithmetic-mean helper remains a point estimate. Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex/Semantic Scholar 2026-08-18T03:07Z: closed). The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title was opened 2026-08-17T12:04Z from archive.org; it is not the 1991 journal article and is not used as Mislevy plausible-value authority. Temporal precedence is not causal identification (Holland, 1986). Within/between OLS follows Enders and Tofighi (2007), Curran and Bauer (2011), and Hamaker et al. (2015). Enders and Tofighi (2007, Table 2, pp. 124–127; PDF opened 2026-08-17) show that the CWC cluster-mean coefficient is the contextual effect (`between − within`), not the between-cluster effect. Curran and Bauer (2011, pp. 607–608) reject person-mean subtraction on a raw autoregressive series as the lagged within-person residual; already-centered irregular residuals use the Voelkle et al. (2012, Eq. 7) / Driver et al. (2017, Eq. 3) scalar map. Discrete lags from unequal event intervals are remapped through that log-rate (Voelkle et al., 2012, ZORA accepted manuscript re-opened 2026-08-17T13:13Z) and are not pooled. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\) and restate the discrete intercept as a function of \(A\) and \(\Delta t\). A binary64 underflow of \(\exp(a\Delta t)\) to `+0` is refused because discrete auto-effects are strictly positive. The discrete effect of a constant predictor is Voelkle et al. (2012, Eq. 12; ZORA accepted manuscript re-opened 2026-08-17T14:20Z, Introducing Intercepts, manuscript p. 20), evaluated as \(a_{yx}(\operatorname{expm1}(z)/a_{xx})\) with \(z=a_{xx}\Delta t\) so a finite result is not lost when \(z\) overflows to \(-\infty\) or when \(a_{yx}\Delta t\) overflows, and in log space when `expm1(z)` overflows at a finite \(z\); a zero continuous effect is exactly zero; an overflowing \(a_{yx}/a_{xx}\) rewrite term fails closed; the first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. The discrete effect of a time-varying predictor with matched sampling and constancy intervals is Voelkle et al. (2012, Eq. 14; manuscript p. 21): \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product is not Eq. 12. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). The exact scalar discrete process noise is Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-18T14:04Z, p. 4): \(Q_{\Delta t}=0.5 q(\operatorname{expm1}(z)/a)\) with \(z=2(a\Delta t)\) for \(a\neq 0\) and \(q=GG^{\top}\ge 0\); do not form \(2a\) first; \(a=0\) recovers \(q\Delta t\); an overflowing rewrite scale \(0.5 q/a\) fails closed. This is not a Kalman filter. Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-18T14:04Z) write the same discrete intercept as \(A^{-1}[e^{A\Delta t}-I]\xi\). The lagged covariance is \(\mathrm{e}^{a\Delta t}p\) and the unconditional variance is \(\mathrm{e}^{2a\Delta t}p+Q_{\Delta t}\) (Driver et al., 2017, Eq. 3–4, pp. 4–5); a zero diffusion whose \(2(a\Delta t)\) overflows to \(+\infty\) fails closed. The stationary within-subject variance is the \(\Delta t\to\infty\) limit of Eq. 4: \(-q/(2a)\) for stable \(a<0\) (JSS p. 16 `asymDIFFUSION`; §4.3; PDF re-opened 2026-08-18T18:03Z). Finite-interval \(Q_{\Delta t}\) is not that limit. Trait-plus-state variance is \(\mathrm{trait}+\mathrm{state}\) and lagged covariance is \(\mathrm{trait}+\mathrm{e}^{a\Delta t}p\) (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z). Trait variance is not process noise and not `asymDIFFUSION`. The first-occasion map `τ + λ μ_0` is not `E(y_t)`. The contemporaneous `TDPREDEFFECT` impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2; §7.2; JSS PDF re-opened 2026-08-20T07:10Z). `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). Metric/weak invariance does not license latent-mean comparison. Putnick and Bornstein (2016, PMC author manuscript PMC5145197 opened 2026-08-19T22:15Z) require scalar invariance before latent-mean comparison and state that residual invariance is not a prerequisite. Two-observation OLS residual variance is identically `0` and is not strict. Meredith (1993) remains unread (Unpaywall/OpenAlex 2026-08-19T22:15Z: closed; Springer `content/pdf` is HTML 200). Vandenberg and Lance (2000) remains unread. Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex 2026-08-18T21:07Z: closed). ERIC ED334221 is Singer and Willett (1991), not the 1991 journal article. ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex 2026-08-18T21:07Z: closed). +For Meredith (1993), the Cambridge Core original-paper page and abstract were opened on 2026-08-21; the earlier `remains unread` note means that the full text was not available, not that the authoritative record was unverified. + ## Structural, correlated, dynamic, relational, and multilingual topic models Blei, D. M., & Lafferty, J. D. (2006). Dynamic topic models. In *Proceedings of the 23rd International Conference on Machine Learning* (pp. 113–120). Association for Computing Machinery. https://doi.org/10.1145/1143844.1143859 diff --git a/docs/research/strong-invariance-latent-means.md b/docs/research/strong-invariance-latent-means.md index c451e048..2ca2f586 100644 --- a/docs/research/strong-invariance-latent-means.md +++ b/docs/research/strong-invariance-latent-means.md @@ -13,7 +13,7 @@ This slice does **not** import the unpublished `measurement_invariance` crate on - Strict (also equal residual variance) also licenses latent means. Residual invariance is **not** required for those means. - Two-observation series have no residual degrees of freedom. OLS residual variance is then identically `0` and is not an estimated residual. Those series cap at strong/scalar and still license means. - This is two-group OLS, not MGCFA, not partial invariance, and not alignment optimization. -- The weak/strong/strict labels remain conventional labels here. Meredith (1993) is listed for terminology only; its PDF was not opened. Putnick and Bornstein (2016) cite Meredith for residual invariance as part of *full factorial invariance*; that citation is not a reading of Meredith. +- The weak/strong/strict labels remain conventional labels here. Meredith (1993) is the primary source for the hierarchy; its Cambridge Core original-paper page and abstract were opened, but its full-text PDF was not. Putnick and Bornstein (2016) cite Meredith for residual invariance as part of *full factorial invariance*; that specific claim is not a reading of Meredith's full text. ## Authoritative sources used for the mean gate From 402aba4db4c26eda551a0ccf644c278e778030a4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 06:28:38 +0900 Subject: [PATCH 52/62] docs(psychometric): refresh evidence review dates --- docs/TRACEABILITY.md | 2 +- docs/validation/temporal-event-foundation.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 3c1a2df9..7f131a83 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -1,7 +1,7 @@ # TEPP Requirements, Research, and Evidence Traceability **Status:** Accepted cross-cutting traceability baseline -**Last reviewed:** 2026-08-20 +**Last reviewed:** 2026-08-21 The full APA 7th standards/literature register remains `docs/research/standards-and-literature.md`. This matrix links durable requirements to their owning decisions and implementation/evidence maturity without duplicating the bibliography. diff --git a/docs/validation/temporal-event-foundation.md b/docs/validation/temporal-event-foundation.md index 2857e02a..fa2b3683 100644 --- a/docs/validation/temporal-event-foundation.md +++ b/docs/validation/temporal-event-foundation.md @@ -1,7 +1,7 @@ # Temporal Event Foundation — validation and release-readiness report **Status:** Living validation ledger for the Temporal/Event foundation program -**Last reviewed:** 2026-08-20 +**Last reviewed:** 2026-08-21 **Authority:** ADR 0014 (claim promotion), ADR 0007 (quality gates), AGENTS.md scientific acceptance ## Scope From e0e568da8c44acc1d30a1a2bad6d76810ee9d6e3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 23:22:58 +0000 Subject: [PATCH 53/62] =?UTF-8?q?feat(psychometric):=20recover=20Driver=20?= =?UTF-8?q?=C2=A77.2=20extra-process=20map?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Driver, Oud, and Voelkle (2017, §7.2, pp. 22–23) specify a lasting level change by an extra latent process, not by rewriting CINT. T0MEANS, CINT, T0VAR, DIFFUSION, and TRAITVAR of that process are fixed to 0; TDPREDEFFECT on it is 1; extra DRIFT is printed -0.000001. After a unit identification impulse the scalar contribution is a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a). ε = a is a_{ηξ} x Δt e^{a Δt}. ε ≥ 0 fails closed. That contribution is not κ = −a m x, not (1 − e^{a Δt}) m x, and not the dissipating Dirac m x. JSS PDF re-opened 2026-08-20T23:10Z. Still not DSEM, not a Kalman filter, and not ctsem estimation. --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 1 + CLAUDE.md | 2 +- crates/psychometric_core/src/error.rs | 45 ++ crates/psychometric_core/src/event_time.rs | 444 +++++++++++++++++- crates/psychometric_core/src/lib.rs | 13 + ...multilevel_event_time_recovery_contract.rs | 152 +++++- .../scientific_claim_boundary_contract.rs | 71 ++- docs/TRACEABILITY.md | 2 +- docs/adr/0005-posterior-esem-dsem.md | 4 +- .../multilevel-event-time-recovery.md | 15 +- docs/validation/temporal-event-foundation.md | 2 +- 12 files changed, 718 insertions(+), 35 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index c0a69b22..7ccb7849 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -61,7 +61,7 @@ boundaries above remain the target modular MSA architecture. | `tepp_simulation` | known-truth temporal/event data generation | | `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics | | `tepp_api` | versioned DTO, schema, and export contracts | -| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; first-occasion `T0TIPREDEFFECT` shift is `t0_b z` and Eq. 3 first-summand carry is `e^{A Δt} t0_b z` (`T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_b z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), first-occasion `T0TDPREDEFFECT` shift is `t0_m x0` and Eq. 3 first-summand carry is `e^{A Δt} t0_m x0` (`T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; §7.2 level-change `CINT` is `κ = −a m x` with `a < 0` so `−κ / a = m x` (`−a m x` is not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2; Eq. 3 of that setting is `(1 − e^{a Δt}) m x`, which is not `m x`, not `κ`, and not `TIPREDEFFECT`), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | +| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; first-occasion `T0TIPREDEFFECT` shift is `t0_b z` and Eq. 3 first-summand carry is `e^{A Δt} t0_b z` (`T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_b z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), first-occasion `T0TDPREDEFFECT` shift is `t0_m x0` and Eq. 3 first-summand carry is `e^{A Δt} t0_m x0` (`T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; §7.2 level-change `CINT` is `κ = −a m x` with `a < 0` so `−κ / a = m x` (`−a m x` is not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2; Eq. 3 of that setting is `(1 − e^{a Δt}) m x`, which is not `m x`, not `κ`, and not `TIPREDEFFECT`; §7.2 extra-process contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`; identification `TDPREDEFFECT` on the extra process is 1; printed extra `DRIFT` is `−0.000001`; not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`; `ε ≥ 0` fails closed), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | No crate exposes placeholder production behavior in Task 1. This prevents an empty façade from becoming a de facto public API before its invariants and tests diff --git a/CHANGELOG.md b/CHANGELOG.md index f3a60c5d..fc9b1d4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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, §7.2, pp. 22–23; Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T23:10Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar contribution of the extra near-zero-drift latent process. Section 7.2 specifies a lasting level change by that extra process: `T0MEANS`, `CINT`, `T0VAR`, `DIFFUSION`, and `TRAITVAR` of it are fixed to 0; `TDPREDEFFECT` on it is fixed to 1; its `DRIFT` diagonal is very close to 0 (printed example `−0.000001`; precisely 0 causes computational problems); and its effect on the original process is the `DRIFT` coupling `a_{ηξ}`. After a unit identification impulse the scalar contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`). Form `a_{ηξ} x` first. A zero coupling or zero predictor is exactly zero. `ε ≥ 0` fails closed. That contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T23:10Z: `is_oa: false`; Springer `content/pdf` is HTML 200). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, §7.2, pp. 20–21; Eq. 3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T19:50Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar discrete increment of the lasting level-change `CINT`. Section 7.2 sets `CINT` to `TDPREDEFFECT * −DRIFT` (`κ = −a m x`). Equation 3 maps that intercept through `A^{-1}[e^{A Δt} − I] κ`. With `κ = −a m x` the scalar increment is `(e^{a Δt} − 1)/a · (−a m x) = (1 − e^{a Δt}) m x`. Form the level-change `CINT` first, then the discrete intercept map. Underflow of `e^{a Δt}` to `+0` keeps the equilibrium offset `m x`. `(1 − e^{a Δt}) m x` is not the contemporaneous jump `m x`. `(1 − e^{a Δt}) m x` is not `κ`. `(1 − e^{a Δt}) m x` is not `A^{-1}[e^{A Δt} − I] B z`. Stable `a < 0` is required. A zero effect or zero predictor is exactly zero. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T19:45Z: `is_oa: false`; Springer `content/pdf` is HTML 200). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, §7.2, pp. 20–21; Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T19:45Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar lasting level-change `CINT`. Section 7.2 contrasts a sudden Dirac that dissipates back to the process mean with a lasting level change. To generate that lasting change, `CINT` is set to `TDPREDEFFECT * −DRIFT`. The scalar setting is `κ = −a m x`. Form `m x` first, then multiply by `−a`. Stable `a < 0` is required so `−κ / a = m x` is an equilibrium offset. `a ≥ 0` cannot hold a new process mean. `−a m x` is not the contemporaneous jump `m x`. `−a m x` is not a free `CINT`. `−a m x` is not `A^{-1}[e^{A Δt} − I] B z`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not this `CINT` setting. A zero effect or zero predictor is exactly zero. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T19:45Z: `is_oa: false`; Springer `content/pdf` is HTML 200). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Eq. 3 first summand, p. 5; Table 3, p. 13; JSS PDF re-opened 2026-08-20T19:07Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar observed-indicator mean of a first-occasion time-dependent predictor. Equation 5 writes `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and `Γ ~ N(τ, Ψ)`. Table 3 names `T0TDPREDEFFECT` the effect of time-dependent predictors on latents at `T0`. Equation 3's first summand carries that shift as `e^{A Δt} t0_m x0`. The scalar composition is `E(y_t) = τ + λ(μ_t + e^{a Δt} t0_m x0)`. Form the evolved-plus-carry latent mean first, then `τ + λ` of that mean. The evolved observed mean `τ + λ μ_t` is not this composition. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not this composition. The contemporaneous map `τ + λ(μ_t + m x)` is not this composition. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not this composition when `u ≠ t0`. The first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not this composition. `MANIFESTMEANS` is not `E(y_t)`. The evolved-plus-carry latent mean is not `E(y_t)`. `T0TDPREDEFFECT` is the coefficient, not that observed mean. Same numbers as `T0TIPREDEFFECT` yield the same product; Table 3 names a different matrix. A zero loading is exactly `τ`. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T19:09Z: `is_oa: false`). diff --git a/CLAUDE.md b/CLAUDE.md index 0426e8d0..8705df0c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. Putnick and Bornstein (2016, PMC5145197 opened 2026-08-19T22:15Z) require scalar invariance before latent-mean comparison; residual invariance is not a prerequisite. Two-observation series have no residual degrees of freedom (`ordinary_least_squares_fit` returns residual variance `0`) and cap at strong/scalar; they still license means. This is two-group OLS, not MGCFA. Meredith (1993) names remain unread labels. -- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. The contemporaneous time-dependent predictor impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`). Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). The §7.2 level-change form is not that impulse. The observed mean of that contemporaneous impulse is `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-impulse latent mean is not `E(y_t)`. The time-independent predictor increment is `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`). Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. `TIPREDEFFECT` is `B`, not that discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle et al. (2012, Eq. 14). The observed mean of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The within-interval time-dependent impulse carry is `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation). Form `m x` first, then `e^{a(t−u)} m x`. A zero drift is `m x` with no dissipation. Underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept. `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). An impulse at `u = t` is the contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. The observed mean of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The carried latent mean is not `E(y_t)`. The first-occasion time-independent predictor shift is `t0_b z` (Driver et al., 2017, Table 3 `T0TIPREDEFFECT`; Eq. 3 first summand). Form `t0_b z` first, then `e^{a Δt} t0_b z`. Form `μ_t` first, then add that carry. A zero drift is `t0_b z`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. `e^{A Δt} t0_b z` is not `t0_b z`. `T0TIPREDEFFECT` is the coefficient, not the shift. The observed mean of that first-occasion carry is `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The evolved-plus-carry latent mean is not `E(y_t)`. The first-occasion time-dependent predictor shift is `t0_m x0` (Driver et al., 2017, Table 3 `T0TDPREDEFFECT`; Eq. 3 first summand; JSS PDF re-opened 2026-08-20T19:10Z). Form `t0_m x0` first, then `e^{a Δt} t0_m x0`. Form `μ_t` first, then add that carry. A zero drift is `t0_m x0`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`. `e^{A Δt} t0_m x0` is not `t0_m x0`. `T0TDPREDEFFECT` is the coefficient, not the shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The observed mean of that first-occasion TD carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean. The evolved-plus-carry latent mean is not `E(y_t)`. The lasting level-change `CINT` is `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z). Form `m x` first, then multiply by `−a`. Stable `a < 0` is required so `−κ / a = m x` is an equilibrium offset. `a ≥ 0` cannot hold a new process mean. `−a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not this `CINT` setting. Equation 3 maps that intercept as `(1 − e^{a Δt}) m x` (JSS PDF re-opened 2026-08-20T19:50Z). Form the level-change `CINT` first, then the discrete intercept map. Underflow of `e^{a Δt}` to `+0` keeps `m x`. `(1 − e^{a Δt}) m x` is not `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. The contemporaneous time-dependent predictor impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`). Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). The §7.2 level-change form is not that impulse. The observed mean of that contemporaneous impulse is `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-impulse latent mean is not `E(y_t)`. The time-independent predictor increment is `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`). Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. `TIPREDEFFECT` is `B`, not that discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle et al. (2012, Eq. 14). The observed mean of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The within-interval time-dependent impulse carry is `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation). Form `m x` first, then `e^{a(t−u)} m x`. A zero drift is `m x` with no dissipation. Underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept. `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). An impulse at `u = t` is the contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. The observed mean of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The carried latent mean is not `E(y_t)`. The first-occasion time-independent predictor shift is `t0_b z` (Driver et al., 2017, Table 3 `T0TIPREDEFFECT`; Eq. 3 first summand). Form `t0_b z` first, then `e^{a Δt} t0_b z`. Form `μ_t` first, then add that carry. A zero drift is `t0_b z`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. `e^{A Δt} t0_b z` is not `t0_b z`. `T0TIPREDEFFECT` is the coefficient, not the shift. The observed mean of that first-occasion carry is `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The evolved-plus-carry latent mean is not `E(y_t)`. The first-occasion time-dependent predictor shift is `t0_m x0` (Driver et al., 2017, Table 3 `T0TDPREDEFFECT`; Eq. 3 first summand; JSS PDF re-opened 2026-08-20T19:10Z). Form `t0_m x0` first, then `e^{a Δt} t0_m x0`. Form `μ_t` first, then add that carry. A zero drift is `t0_m x0`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`. `e^{A Δt} t0_m x0` is not `t0_m x0`. `T0TDPREDEFFECT` is the coefficient, not the shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The observed mean of that first-occasion TD carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean. The evolved-plus-carry latent mean is not `E(y_t)`. The lasting level-change `CINT` is `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z). Form `m x` first, then multiply by `−a`. Stable `a < 0` is required so `−κ / a = m x` is an equilibrium offset. `a ≥ 0` cannot hold a new process mean. `−a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not this `CINT` setting. Equation 3 maps that intercept as `(1 − e^{a Δt}) m x` (JSS PDF re-opened 2026-08-20T19:50Z). Form the level-change `CINT` first, then the discrete intercept map. Underflow of `e^{a Δt}` to `+0` keeps `m x`. `(1 − e^{a Δt}) m x` is not `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`. The printed §7.2 lasting level change is an extra near-zero-drift latent process (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z). `T0MEANS`, `CINT`, `T0VAR`, `DIFFUSION`, and `TRAITVAR` of that process are fixed to 0; `TDPREDEFFECT` on it is fixed to 1; its `DRIFT` diagonal is very close to 0 (printed example `−0.000001`; precisely 0 causes computational problems); the original process is driven by the `DRIFT` coupling `a_{ηξ}`. After a unit identification impulse the scalar contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`). Form `a_{ηξ} x` first. A zero coupling or zero predictor is exactly zero. `ε ≥ 0` fails closed. That contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. diff --git a/crates/psychometric_core/src/error.rs b/crates/psychometric_core/src/error.rs index e6f08eff..32850b39 100644 --- a/crates/psychometric_core/src/error.rs +++ b/crates/psychometric_core/src/error.rs @@ -269,6 +269,23 @@ pub enum PsychometricError { /// Eq. 3 process increment. `(1 − e^{a Δt}) m x` is not /// `A^{-1}[e^{A Δt} − I] B z`. LevelChangeIncrementIsNotProcessIncrement, + /// Driver §7.2 extra-process contribution was requested for a + /// non-negative extra drift. Lasting level change via the extra + /// latent process requires `ε < 0`. Precisely `ε = 0` causes + /// computational problems in the printed ctsem specification. + LevelChangeExtraProcessRequiresNegativeDrift, + /// Driver §7.2 extra-process contribution was treated as the + /// contemporaneous Dirac. `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` + /// is not `m x`. + LevelChangeExtraProcessIsNotImpulse, + /// Driver §7.2 extra-process contribution was treated as the + /// level-change `CINT`. `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` + /// is not `κ = −a m x`. + LevelChangeExtraProcessIsNotIntercept, + /// Driver §7.2 extra-process contribution was treated as the + /// Eq. 3 level-change increment. `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` + /// is not `(1 − e^{a Δt}) m x`. + LevelChangeExtraProcessIsNotIncrement, } impl fmt::Display for PsychometricError { @@ -491,6 +508,18 @@ impl fmt::Display for PsychometricError { Self::LevelChangeIncrementIsNotProcessIncrement => { "level-change CINT increment is not the time-independent process increment" } + Self::LevelChangeExtraProcessRequiresNegativeDrift => { + "lasting level-change extra process requires strictly negative extra drift" + } + Self::LevelChangeExtraProcessIsNotImpulse => { + "level-change extra-process contribution is not the contemporaneous impulse" + } + Self::LevelChangeExtraProcessIsNotIntercept => { + "level-change extra-process contribution is not the level-change intercept" + } + Self::LevelChangeExtraProcessIsNotIncrement => { + "level-change extra-process contribution is not the level-change increment" + } }; formatter.write_str(message) } @@ -840,5 +869,21 @@ mod tests { PsychometricError::LevelChangeIncrementIsNotProcessIncrement.to_string(), "level-change CINT increment is not the time-independent process increment" ); + assert_eq!( + PsychometricError::LevelChangeExtraProcessRequiresNegativeDrift.to_string(), + "lasting level-change extra process requires strictly negative extra drift" + ); + assert_eq!( + PsychometricError::LevelChangeExtraProcessIsNotImpulse.to_string(), + "level-change extra-process contribution is not the contemporaneous impulse" + ); + assert_eq!( + PsychometricError::LevelChangeExtraProcessIsNotIntercept.to_string(), + "level-change extra-process contribution is not the level-change intercept" + ); + assert_eq!( + PsychometricError::LevelChangeExtraProcessIsNotIncrement.to_string(), + "level-change extra-process contribution is not the level-change increment" + ); } } diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index 2e8c1b6f..e34905ec 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -76,7 +76,18 @@ //! Dirac, not a free `CINT`, and not the extra near-zero-drift //! latent process also named in §7.2. Equation 3 maps that //! intercept as `(1 − e^{a Δt}) m x` (`(1 − e^{a Δt}) m x` is not -//! `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`). The JSS article +//! `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`). Section 7.2 +//! (pp. 22–23) then specifies a lasting level change by an extra +//! latent process: `T0MEANS`, `CINT`, `T0VAR`, `DIFFUSION`, and +//! `TRAITVAR` of that process are fixed to 0; `TDPREDEFFECT` on it +//! is fixed to 1; its `DRIFT` diagonal is very close to 0 (printed +//! example `−0.000001`; precisely 0 causes computational problems); +//! and its effect on the original process is the `DRIFT` coupling +//! `a_{ηξ}`. After a unit identification impulse the scalar +//! contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` +//! (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`). That contribution is not +//! `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating +//! Dirac `m x`. `ε ≥ 0` fails closed. The JSS article //! has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). //! The difference quotient `(x(t+Δt) − x(t)) / Δt` (their //! Eqs. 3–4) is refused. This is not DSEM and not a matrix `expm`. @@ -1544,6 +1555,148 @@ pub fn refuse_level_change_increment_as_process_increment( Err(PsychometricError::LevelChangeIncrementIsNotProcessIncrement) } +/// Exact scalar contribution of the §7.2 extra near-zero-drift process. +/// +/// Driver, Oud, and Voelkle (2017, §7.2, pp. 22–23; Eq. 1–3, pp. 4–5; +/// Table 2, p. 12; JSS PDF re-opened 2026-08-20T23:10Z from +/// ) +/// specify a lasting level change by an extra latent process, not by +/// rewriting `CINT`. `T0MEANS`, `CINT`, `T0VAR`, `DIFFUSION`, and +/// `TRAITVAR` of that process are fixed to 0. `TDPREDEFFECT` on it is +/// fixed to 1 to identify the effect. Its `DRIFT` diagonal is very +/// close to 0 (printed example `−0.000001`; precisely 0 causes +/// computational problems). The original process is driven by the +/// `DRIFT` coupling `a_{ηξ}`. After a unit identification impulse the +/// extra state is `x e^{ε t}` and the scalar contribution to the +/// original process is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`. +/// Form `a_{ηξ} x` first. When `ε = a` the contribution is +/// `a_{ηξ} x Δt e^{a Δt}`. A zero coupling or zero predictor is +/// exactly zero. `ε ≥ 0` cannot hold a lasting extra state and fails +/// closed. That contribution is not `κ = −a m x`, not +/// `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. This +/// is not a Kalman filter, not a matrix `expm`, and not ctsem +/// estimation. +/// +/// # Errors +/// +/// Returns [`PsychometricError::EventTimeRequired`] for any non-event +/// clock, [`PsychometricError::NonPositiveInterval`] when the interval +/// is not strictly positive, +/// [`PsychometricError::LevelChangeExtraProcessRequiresNegativeDrift`] +/// when the extra drift is not strictly negative and the contribution +/// is nonzero, and [`PsychometricError::InvalidNumericInput`] when an +/// input is non-finite or a product, exponential, or quotient +/// overflows. +pub fn recover_level_change_extra_process_contribution( + original_from_extra_drift: f64, + time_dependent_predictor: f64, + original_log_rate: f64, + extra_log_rate: f64, + event_delta: f64, + clock: LagClock, +) -> Result { + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + if !event_delta.is_finite() || event_delta <= 0.0 { + return Err(PsychometricError::NonPositiveInterval); + } + if !original_from_extra_drift.is_finite() + || !time_dependent_predictor.is_finite() + || !original_log_rate.is_finite() + || !extra_log_rate.is_finite() + { + return Err(PsychometricError::InvalidNumericInput); + } + if original_from_extra_drift == 0.0 || time_dependent_predictor == 0.0 { + return Ok(0.0); + } + if extra_log_rate >= 0.0 { + return Err(PsychometricError::LevelChangeExtraProcessRequiresNegativeDrift); + } + let coupling = require_finite(original_from_extra_drift * time_dependent_predictor)?; + let extra_argument = extra_log_rate * event_delta; + let extra_lag = if extra_argument == 0.0 { + 1.0 + } else { + extra_argument.exp() + }; + let original_argument = original_log_rate * event_delta; + let original_lag = if original_log_rate == 0.0 || original_argument == 0.0 { + 1.0 + } else { + let lag = original_argument.exp(); + if !lag.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + lag + }; + let rate_gap = extra_log_rate - original_log_rate; + let gap_argument = rate_gap * event_delta; + if gap_argument == 0.0 { + return require_finite(coupling * event_delta * original_lag); + } + let increment = gap_argument.exp_m1(); + if increment.is_finite() { + if original_lag == 0.0 { + return require_finite(coupling * extra_lag / rate_gap); + } + return require_finite(coupling * original_lag * (increment / rate_gap)); + } + require_finite(coupling * (extra_lag - original_lag) / rate_gap) +} + +/// Refuse treating the §7.2 extra-process contribution as the +/// contemporaneous Dirac. +/// +/// `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` is not the jump `m x`. +/// +/// # Errors +/// +/// Always returns [`PsychometricError::LevelChangeExtraProcessIsNotImpulse`]. +pub fn refuse_level_change_extra_process_as_impulse( + extra_process_contribution: f64, + time_dependent_impulse: f64, +) -> Result { + let _ = (extra_process_contribution, time_dependent_impulse); + Err(PsychometricError::LevelChangeExtraProcessIsNotImpulse) +} + +/// Refuse treating the §7.2 extra-process contribution as the +/// level-change `CINT`. +/// +/// `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` is not `κ = −a m x`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::LevelChangeExtraProcessIsNotIntercept`]. +pub fn refuse_level_change_extra_process_as_intercept( + extra_process_contribution: f64, + level_change_intercept: f64, +) -> Result { + let _ = (extra_process_contribution, level_change_intercept); + Err(PsychometricError::LevelChangeExtraProcessIsNotIntercept) +} + +/// Refuse treating the §7.2 extra-process contribution as the Eq. 3 +/// level-change increment. +/// +/// `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` is not +/// `(1 − e^{a Δt}) m x`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::LevelChangeExtraProcessIsNotIncrement`]. +pub fn refuse_level_change_extra_process_as_increment( + extra_process_contribution: f64, + level_change_increment: f64, +) -> Result { + let _ = (extra_process_contribution, level_change_increment); + Err(PsychometricError::LevelChangeExtraProcessIsNotIncrement) +} + /// Exact scalar evolved latent mean plus a contemporaneous impulse. /// /// Driver, Oud, and Voelkle (2017, Eq. 3, p. 5) write the first two @@ -3419,12 +3572,13 @@ mod tests { recover_initial_time_independent_predictor_carry, recover_initial_time_independent_predictor_effect, recover_irregular_centered_residual_log_rate, recover_level_change_continuous_intercept, - recover_level_change_discrete_increment, recover_local_log_rate, - recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, - recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, - recover_stationary_latent_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_level_change_discrete_increment, recover_level_change_extra_process_contribution, + recover_local_log_rate, recover_manifest_lagged_observed_covariance, + recover_manifest_observed_mean, recover_manifest_observed_variance, + recover_manifest_trait_plus_state_observed_variance, recover_stationary_latent_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_continuous_intercept_as_discrete_mean_increment, refuse_continuous_intercept_as_initial_latent_mean, refuse_continuous_intercept_as_manifest_means, refuse_difference_quotient_as_local_rate, @@ -3458,7 +3612,10 @@ mod tests { refuse_initial_time_independent_observed_mean_as_initial_time_dependent_observed_mean, refuse_latent_lagged_covariance_as_observed_covariance, refuse_latent_mean_as_observed_mean, refuse_latent_variance_as_observed_variance, - refuse_level_change_increment_as_impulse, refuse_level_change_increment_as_intercept, + refuse_level_change_extra_process_as_impulse, + refuse_level_change_extra_process_as_increment, + refuse_level_change_extra_process_as_intercept, refuse_level_change_increment_as_impulse, + refuse_level_change_increment_as_intercept, refuse_level_change_increment_as_process_increment, refuse_level_change_intercept_as_free_continuous_intercept, refuse_level_change_intercept_as_impulse, @@ -5727,6 +5884,277 @@ mod tests { ); } + #[test] + fn extra_process_contribution_recovers_driver_section_seven_point_two() { + let coupling = 0.569_907_f64; + let predictor = 1.0_f64; + let original = -0.1393_f64; + let extra = -0.000_001_f64; + let delta = 1.0_f64; + let recovered = recover_level_change_extra_process_contribution( + coupling, + predictor, + original, + extra, + delta, + LagClock::EventTime, + ) + .expect("extra-process"); + let expected = coupling * predictor * ((extra * delta).exp() - (original * delta).exp()) + / (extra - original); + assert!((recovered - expected).abs() < 1e-15); + let equal_rate = recover_level_change_extra_process_contribution( + coupling, + predictor, + extra, + extra, + delta, + LagClock::EventTime, + ) + .expect("equal-rate"); + let equal_expected = coupling * predictor * delta * (extra * delta).exp(); + assert!((equal_rate - equal_expected).abs() < 1e-15); + let brownian = recover_level_change_extra_process_contribution( + coupling, + predictor, + 0.0, + extra, + delta, + LagClock::EventTime, + ) + .expect("brownian-original"); + let brownian_expected = coupling * predictor * (extra * delta).exp_m1() / extra; + assert!((brownian - brownian_expected).abs() < 1e-15); + assert_eq!( + recover_level_change_extra_process_contribution( + 0.0, + predictor, + original, + extra, + delta, + LagClock::EventTime + ), + Ok(0.0) + ); + assert_eq!( + recover_level_change_extra_process_contribution( + coupling, + 0.0, + original, + extra, + delta, + LagClock::EventTime + ), + Ok(0.0) + ); + assert_eq!( + recover_level_change_extra_process_contribution( + coupling, + 0.0, + original, + 0.0, + delta, + LagClock::EventTime + ), + Ok(0.0) + ); + } + + #[test] + fn extra_process_contribution_is_not_cint_rewrite_or_impulse() { + let coupling = 0.4_f64; + let predictor = 3.0_f64; + let original = -0.5_f64; + let extra = -0.05_f64; + let delta = 2.0_f64; + let recovered = recover_level_change_extra_process_contribution( + coupling, + predictor, + original, + extra, + delta, + LagClock::EventTime, + ) + .expect("extra-process"); + let intercept = recover_level_change_continuous_intercept(coupling, predictor, original) + .expect("level-change"); + let increment = recover_level_change_discrete_increment( + coupling, + predictor, + original, + delta, + LagClock::EventTime, + ) + .expect("level-change-increment"); + let impulse = + recover_time_dependent_predictor_impulse(coupling, predictor).expect("impulse"); + assert!((recovered - intercept).abs() > 1e-3); + assert!((recovered - increment).abs() > 1e-3); + assert!((recovered - impulse).abs() > 1e-3); + assert_eq!( + refuse_level_change_extra_process_as_impulse(recovered, impulse), + Err(PsychometricError::LevelChangeExtraProcessIsNotImpulse) + ); + assert_eq!( + refuse_level_change_extra_process_as_intercept(recovered, intercept), + Err(PsychometricError::LevelChangeExtraProcessIsNotIntercept) + ); + assert_eq!( + refuse_level_change_extra_process_as_increment(recovered, increment), + Err(PsychometricError::LevelChangeExtraProcessIsNotIncrement) + ); + } + + #[test] + fn extra_process_contribution_invalid_inputs_fail_closed() { + let coupling = 0.4_f64; + let predictor = 3.0_f64; + let original = -0.5_f64; + let extra = -0.000_001_f64; + let delta = 2.0_f64; + assert_eq!( + recover_level_change_extra_process_contribution( + coupling, + predictor, + original, + 0.0, + delta, + LagClock::EventTime + ), + Err(PsychometricError::LevelChangeExtraProcessRequiresNegativeDrift) + ); + assert_eq!( + recover_level_change_extra_process_contribution( + coupling, + predictor, + original, + 0.5, + delta, + LagClock::EventTime + ), + Err(PsychometricError::LevelChangeExtraProcessRequiresNegativeDrift) + ); + assert_eq!( + recover_level_change_extra_process_contribution( + coupling, + predictor, + original, + extra, + delta, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_level_change_extra_process_contribution( + coupling, + predictor, + original, + extra, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_level_change_extra_process_contribution( + f64::NAN, + predictor, + original, + extra, + delta, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_level_change_extra_process_contribution( + 1e308, + 2.0, + original, + extra, + delta, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_level_change_extra_process_contribution( + coupling, + predictor, + 710.0, + extra, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + } + + #[test] + fn extra_process_contribution_underflow_and_overflow_paths() { + let coupling = 0.4_f64; + let predictor = 3.0_f64; + let original = -0.5_f64; + let extra = -0.000_001_f64; + let vanished = recover_level_change_extra_process_contribution( + coupling, + predictor, + -800.0, + extra, + 1.0, + LagClock::EventTime, + ) + .expect("underflow"); + let vanished_expected = coupling * predictor * (extra * 1.0).exp() / (extra - -800.0); + assert!((vanished - vanished_expected).abs() < 1e-15); + let extra_underflow = recover_level_change_extra_process_contribution( + coupling, + predictor, + original, + -f64::from_bits(1), + 0.5, + LagClock::EventTime, + ) + .expect("extra-argument-underflow"); + assert!(extra_underflow.is_finite()); + let original_underflow = recover_level_change_extra_process_contribution( + coupling, + predictor, + -2e-160_f64, + -1e-160_f64, + 1e-200_f64, + LagClock::EventTime, + ) + .expect("gap-argument-underflow"); + let original_underflow_expected = coupling * predictor * 1e-200_f64; + assert!((original_underflow - original_underflow_expected).abs() <= 1e-200_f64); + let vanished_finite_increment = recover_level_change_extra_process_contribution( + coupling, + predictor, + -800.0, + -92.0, + 1.0, + LagClock::EventTime, + ) + .expect("original-lag-underflow-finite-increment"); + let vanished_finite_expected = coupling * predictor * (-92.0_f64).exp() / (-92.0 - -800.0); + assert!((vanished_finite_increment - vanished_finite_expected).abs() < 1e-15); + let overflow_fallback = recover_level_change_extra_process_contribution( + coupling, + predictor, + -0.8, + extra, + 900.0, + LagClock::EventTime, + ) + .expect("expm1-overflow-fallback"); + let overflow_expected = + coupling * predictor * ((extra * 900.0).exp() - (-0.8_f64 * 900.0).exp()) + / (extra - -0.8); + assert!((overflow_fallback - overflow_expected).abs() < 1e-12); + } + #[test] fn discrete_observed_mean_with_impulse_recovers_driver_equation_five() { let loading = 2.0_f64; diff --git a/crates/psychometric_core/src/lib.rs b/crates/psychometric_core/src/lib.rs index 945d319e..54f758ac 100644 --- a/crates/psychometric_core/src/lib.rs +++ b/crates/psychometric_core/src/lib.rs @@ -82,6 +82,11 @@ //! recovers the Driver Eq. 3 increment of that setting as //! `(1 − e^{a Δt}) m x` (`(1 − e^{a Δt}) m x` is not `m x`, not `κ`, //! and not `A^{-1}[e^{A Δt} − I] B z`), +//! recovers the Driver §7.2 extra near-zero-drift latent process +//! contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (pp. 22–23; +//! identification `TDPREDEFFECT` on the extra process is 1; `ε < 0`; +//! printed extra `DRIFT` is `−0.000001`; not `κ = −a m x`, not +//! `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`), //! and refuses //! latent-mean comparison below strong invariance. @@ -190,6 +195,8 @@ pub use event_time::recover_irregular_centered_residual_log_rate; pub use event_time::recover_level_change_continuous_intercept; /// Exact scalar Eq. 3 increment of that `CINT` `(1 − e^{a Δt}) m x`. pub use event_time::recover_level_change_discrete_increment; +/// Exact scalar §7.2 extra-process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`. +pub use event_time::recover_level_change_extra_process_contribution; /// Exact scalar inverse `a = ln(φ) / Δt`. pub use event_time::recover_local_log_rate; /// Exact scalar lagged observed-indicator covariance `λ² cov(η) + ψ`. @@ -282,6 +289,12 @@ pub use event_time::refuse_latent_lagged_covariance_as_observed_covariance; pub use event_time::refuse_latent_mean_as_observed_mean; /// Refuse treating Driver Eq. 5 latent variance as `Var(y)`. pub use event_time::refuse_latent_variance_as_observed_variance; +/// Refuse treating the §7.2 extra-process contribution as the contemporaneous Dirac. +pub use event_time::refuse_level_change_extra_process_as_impulse; +/// Refuse treating the §7.2 extra-process contribution as the Eq. 3 level-change increment. +pub use event_time::refuse_level_change_extra_process_as_increment; +/// Refuse treating the §7.2 extra-process contribution as the level-change `CINT`. +pub use event_time::refuse_level_change_extra_process_as_intercept; /// Refuse treating the §7.2 level-change CINT increment as the contemporaneous Dirac. pub use event_time::refuse_level_change_increment_as_impulse; /// Refuse treating the §7.2 level-change CINT increment as `CINT`. 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 ec48f82c..4e1aa56a 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -25,11 +25,12 @@ use psychometric_core::{ recover_initial_time_independent_predictor_effect, recover_irregular_centered_residual_log_rate, recover_kish_weighted_slope, recover_level_change_continuous_intercept, recover_level_change_discrete_increment, - recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, - recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, - recover_stationary_latent_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_level_change_extra_process_contribution, recover_manifest_lagged_observed_covariance, + recover_manifest_observed_mean, recover_manifest_observed_variance, + recover_manifest_trait_plus_state_observed_variance, recover_stationary_latent_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_continuous_intercept_as_discrete_mean_increment, refuse_continuous_intercept_as_initial_latent_mean, refuse_continuous_intercept_as_manifest_means, refuse_difference_quotient_as_local_rate, @@ -62,8 +63,10 @@ use psychometric_core::{ refuse_initial_time_independent_effect_as_time_dependent_impulse, refuse_initial_time_independent_observed_mean_as_initial_time_dependent_observed_mean, refuse_latent_lagged_covariance_as_observed_covariance, refuse_latent_mean_as_observed_mean, - refuse_latent_variance_as_observed_variance, refuse_level_change_increment_as_impulse, - refuse_level_change_increment_as_intercept, refuse_level_change_increment_as_process_increment, + refuse_latent_variance_as_observed_variance, refuse_level_change_extra_process_as_impulse, + refuse_level_change_extra_process_as_increment, refuse_level_change_extra_process_as_intercept, + refuse_level_change_increment_as_impulse, refuse_level_change_increment_as_intercept, + refuse_level_change_increment_as_process_increment, refuse_level_change_intercept_as_free_continuous_intercept, refuse_level_change_intercept_as_impulse, refuse_level_change_intercept_as_process_increment, refuse_manifest_means_as_observed_mean, refuse_manifest_trait_variance_as_measurement_error, @@ -3576,3 +3579,138 @@ fn level_change_discrete_increment_refuses_unstable_drift_clock_and_overflow() { Ok(0.0) ); } + +#[test] +fn extra_process_contribution_recovers_driver_section_seven_point_two() { + let coupling = 0.569_907_f64; + let predictor = 1.0_f64; + let original = -0.1393_f64; + let extra = -0.000_001_f64; + let delta = 1.0_f64; + let recovered = recover_level_change_extra_process_contribution( + coupling, + predictor, + original, + extra, + delta, + LagClock::EventTime, + ) + .expect("extra-process"); + let expected = coupling * predictor * ((extra * delta).exp() - (original * delta).exp()) + / (extra - original); + assert!( + rmse(&[expected], &[recovered]) < 1e-15, + "Driver et al. (2017, §7.2 pp. 22–23) extra-process map: expected {expected}, got {recovered}" + ); + let intercept = + recover_level_change_continuous_intercept(coupling, predictor, original).expect("cint"); + let increment = recover_level_change_discrete_increment( + coupling, + predictor, + original, + delta, + LagClock::EventTime, + ) + .expect("increment"); + let impulse = recover_time_dependent_predictor_impulse(coupling, predictor).expect("impulse"); + let distinction = recover_level_change_extra_process_contribution( + 0.4, + 3.0, + -0.5, + -0.05, + 2.0, + LagClock::EventTime, + ) + .expect("distinction"); + let distinct_intercept = + recover_level_change_continuous_intercept(0.4, 3.0, -0.5).expect("distinct-cint"); + let distinct_increment = + recover_level_change_discrete_increment(0.4, 3.0, -0.5, 2.0, LagClock::EventTime) + .expect("distinct-increment"); + let distinct_impulse = recover_time_dependent_predictor_impulse(0.4, 3.0).expect("dirac"); + assert!(rmse(&[distinct_intercept], &[distinction]) > 1e-3); + assert!(rmse(&[distinct_increment], &[distinction]) > 1e-3); + assert!(rmse(&[distinct_impulse], &[distinction]) > 1e-3); + assert_eq!( + refuse_level_change_extra_process_as_impulse(recovered, impulse), + Err(PsychometricError::LevelChangeExtraProcessIsNotImpulse) + ); + assert_eq!( + refuse_level_change_extra_process_as_intercept(recovered, intercept), + Err(PsychometricError::LevelChangeExtraProcessIsNotIntercept) + ); + assert_eq!( + refuse_level_change_extra_process_as_increment(recovered, increment), + Err(PsychometricError::LevelChangeExtraProcessIsNotIncrement) + ); +} + +#[test] +fn extra_process_contribution_refuses_nonnegative_extra_drift_clock_and_overflow() { + assert_eq!( + recover_level_change_extra_process_contribution( + 0.4, + 3.0, + -0.5, + 0.0, + 2.0, + LagClock::EventTime + ), + Err(PsychometricError::LevelChangeExtraProcessRequiresNegativeDrift) + ); + assert_eq!( + recover_level_change_extra_process_contribution( + 0.4, + 3.0, + -0.5, + 0.5, + 2.0, + LagClock::EventTime + ), + Err(PsychometricError::LevelChangeExtraProcessRequiresNegativeDrift) + ); + assert_eq!( + recover_level_change_extra_process_contribution( + 0.4, + 3.0, + -0.5, + -0.000_001, + 2.0, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_level_change_extra_process_contribution( + 0.4, + 3.0, + -0.5, + -0.000_001, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_level_change_extra_process_contribution( + 1e308, + 2.0, + -0.5, + -0.000_001, + 2.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_level_change_extra_process_contribution( + 0.0, + 3.0, + -0.5, + 0.0, + 2.0, + LagClock::EventTime + ), + Ok(0.0) + ); +} diff --git a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs index 99adc575..da6179b6 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -21,12 +21,12 @@ use psychometric_core::{ recover_initial_time_independent_predictor_carry, recover_initial_time_independent_predictor_effect, recover_irregular_centered_residual_log_rate, recover_level_change_continuous_intercept, - recover_level_change_discrete_increment, recover_loading_point_estimate_mean, - recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, - recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, - recover_stationary_latent_variance, recover_time_dependent_predictor_impulse, - recover_time_dependent_predictor_impulse_carry, recover_trait_plus_state_latent_variance, - recover_within_residual_event_time_log_rate, + recover_level_change_discrete_increment, recover_level_change_extra_process_contribution, + recover_loading_point_estimate_mean, recover_manifest_lagged_observed_covariance, + recover_manifest_observed_mean, recover_manifest_observed_variance, + recover_manifest_trait_plus_state_observed_variance, recover_stationary_latent_variance, + recover_time_dependent_predictor_impulse, recover_time_dependent_predictor_impulse_carry, + recover_trait_plus_state_latent_variance, recover_within_residual_event_time_log_rate, refuse_continuous_intercept_as_discrete_mean_increment, refuse_continuous_intercept_as_initial_latent_mean, refuse_continuous_intercept_as_manifest_means, @@ -59,8 +59,10 @@ use psychometric_core::{ refuse_initial_time_independent_effect_as_time_dependent_impulse, refuse_initial_time_independent_observed_mean_as_initial_time_dependent_observed_mean, refuse_latent_lagged_covariance_as_observed_covariance, refuse_latent_mean_as_observed_mean, - refuse_latent_variance_as_observed_variance, refuse_level_change_increment_as_impulse, - refuse_level_change_increment_as_intercept, refuse_level_change_increment_as_process_increment, + refuse_latent_variance_as_observed_variance, refuse_level_change_extra_process_as_impulse, + refuse_level_change_extra_process_as_increment, refuse_level_change_extra_process_as_intercept, + refuse_level_change_increment_as_impulse, refuse_level_change_increment_as_intercept, + refuse_level_change_increment_as_process_increment, refuse_level_change_intercept_as_free_continuous_intercept, refuse_level_change_intercept_as_impulse, refuse_level_change_intercept_as_process_increment, refuse_manifest_means_as_observed_mean, refuse_manifest_trait_variance_as_measurement_error, @@ -1758,3 +1760,56 @@ fn level_change_increment_is_not_impulse_intercept_or_process_increment() { Err(psychometric_core::PsychometricError::LevelChangeIncrementIsNotProcessIncrement) ); } + +#[test] +fn extra_process_contribution_is_not_cint_rewrite_increment_or_impulse() { + let coupling = 0.4_f64; + let predictor = 3.0_f64; + let original = -0.5_f64; + let extra = -0.05_f64; + let delta = 2.0_f64; + let recovered = recover_level_change_extra_process_contribution( + coupling, + predictor, + original, + extra, + delta, + LagClock::EventTime, + ) + .expect("extra-process"); + let intercept = + recover_level_change_continuous_intercept(coupling, predictor, original).expect("cint"); + let increment = recover_level_change_discrete_increment( + coupling, + predictor, + original, + delta, + LagClock::EventTime, + ) + .expect("increment"); + let impulse = recover_time_dependent_predictor_impulse(coupling, predictor).expect("impulse"); + assert!( + (recovered - intercept).abs() > 1e-3, + "Driver et al. (2017, §7.2 pp. 22–23): extra-process contribution is not κ = −a m x" + ); + assert!( + (recovered - increment).abs() > 1e-3, + "Driver et al. (2017, §7.2 pp. 22–23): extra-process contribution is not (1 − e^{{aΔt}}) m x" + ); + assert!( + (recovered - impulse).abs() > 1e-3, + "Driver et al. (2017, §7.2 pp. 22–23): extra-process contribution is not the dissipating Dirac m x" + ); + assert_eq!( + refuse_level_change_extra_process_as_impulse(recovered, impulse), + Err(psychometric_core::PsychometricError::LevelChangeExtraProcessIsNotImpulse) + ); + assert_eq!( + refuse_level_change_extra_process_as_intercept(recovered, intercept), + Err(psychometric_core::PsychometricError::LevelChangeExtraProcessIsNotIntercept) + ); + assert_eq!( + refuse_level_change_extra_process_as_increment(recovered, increment), + Err(psychometric_core::PsychometricError::LevelChangeExtraProcessIsNotIncrement) + ); +} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 7f131a83..137c32d5 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -28,7 +28,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | report template/section/copied/style/modality method effects | ADR 0004/0012; PRD/TRD | simulation truth factors implemented; estimator-side method model remains future | partial | | candidate K statistical/Pareto gates + blinded LLM review | ADR 0012; research | future `model_selection` | accepted-target | | compositional topic correlation / stable clustering | ADR 0005/0012; research | future `network_analysis` | accepted-target | -| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z); full ESEM/DSEM remaining | partial | +| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; §7.2 level-change `CINT` is `κ = −a m x` (`a < 0`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`; Eq. 3 of that setting is `(1 − e^{a Δt}) m x`); §7.2 extra-process contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, not the dissipating Dirac; `ε ≥ 0` fails closed), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z); full ESEM/DSEM remaining | partial | | CPU bounded multithreading + GPU/VRAM streaming/parity | ADR 0001/0006 | future `compute_backend` | accepted-target | | TDT detection/tracking vs CHRONOS schema/prediction/temporal consistency | ADR 0016; PRD/research | future `event_intelligence` | accepted-target | | evidence-bounded LLM interpretation | ADR 0010/0012; PRD | `tepp_api` router plus future `interpretation_gateway` | partial | diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index 6a78e9ca..ba7c2649 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -1,7 +1,7 @@ # ADR 0005 — Posterior-aware ESEM/DSEM and structural interpretation **Decision status:** Accepted -**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), exact scalar contemporaneous `TDPREDEFFECT` impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; the §7.2 level-change form is not that impulse), exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of that Eq. 3 composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar time-independent `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle Eq. 14; `B` is not that discrete increment), exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of that Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of that carried latent mean; `τ + λ μ_t` is not that observed mean), exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; `T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`), exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; `T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand TD composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean), exact scalar §7.2 level-change `CINT` `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; `a < 0` so `−κ / a = m x`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2), exact scalar Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` (not `m x`, not `κ`, and not `TIPREDEFFECT`), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target +**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), exact scalar contemporaneous `TDPREDEFFECT` impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; the §7.2 level-change form is not that impulse), exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of that Eq. 3 composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar time-independent `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle Eq. 14; `B` is not that discrete increment), exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of that Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of that carried latent mean; `τ + λ μ_t` is not that observed mean), exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; `T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`), exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; `T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand TD composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean), exact scalar §7.2 level-change `CINT` `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; `a < 0` so `−κ / a = m x`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2), exact scalar Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` (not `m x`, not `κ`, and not `TIPREDEFFECT`), exact scalar §7.2 extra near-zero-drift latent process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z; identification `TDPREDEFFECT` on the extra process is 1; extra `DRIFT` printed as `−0.000001`; precisely 0 causes computational problems; `ε = a` is `a_{ηξ} x Δt e^{a Δt}`; not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`; `ε ≥ 0` fails closed), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target **Date:** 2026-08-05 **Supersedes:** None. ADR 0012 governs upstream topic measurement/network coordinates; this ADR governs higher-order psychometric structure and longitudinal interpretation. @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), recovers the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z; `TDPREDEFFECT` is `M`, not `CINT`; `M x` is not `A^{-1}[e^{A Δt} − I] B z`; `M x` is not Voelkle et al., 2012, Eq. 14; the level-change form is not that impulse), recovers the exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition; JSS PDF re-opened 2026-08-20T09:01Z; the evolved map `τ + λ μ_t` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-impulse latent mean is not `E(y_t)`), recovers the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z; `TIPREDEFFECT` is `B`, not `κ`; form `B z` first, then the discrete intercept map; a zero drift is `B z Δt`; `B` is not the discrete increment; `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle Eq. 14), recovers the exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; JSS PDF re-opened 2026-08-20T12:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-increment latent mean is not `E(y_t)`), recovers the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z; form `m x` first, then `e^{a(t−u)} m x`; a zero drift is `m x`; underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept; `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; an impulse at `u = t` is the contemporaneous map; an impulse at `u ≤ t0` is already in `η(t0)`), recovers the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; JSS PDF re-opened 2026-08-20T05:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the carried latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and its Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF opened 2026-08-20T15:14Z; form `t0_b z` first, then `e^{a Δt} t0_b z`; a zero drift is `t0_b z`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; `e^{A Δt} t0_b z` is not `t0_b z`; `T0TIPREDEFFECT` is the coefficient, not the shift), recovers the exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition; JSS PDF re-opened 2026-08-20T15:28Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and its Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; form `t0_m x0` first, then `e^{a Δt} t0_m x0`; a zero drift is `t0_m x0`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `T0TDPREDEFFECT` is the coefficient, not the shift; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), recovers the exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; the first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar §7.2 level-change `CINT` setting `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z; form `m x` first, then multiply by `−a`; `a < 0` so `−κ / a = m x`; `a ≥ 0` cannot hold a new process mean; `−a m x` is not the dissipating Dirac, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`; the extra near-zero-drift latent process also named in §7.2 is a different specification), recovers the exact scalar Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` (JSS PDF re-opened 2026-08-20T19:50Z; form the level-change `CINT` first, then the discrete intercept map; underflow of `e^{a Δt}` to `+0` keeps `m x`; `(1 − e^{a Δt}) m x` is not `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), recovers the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z; `TDPREDEFFECT` is `M`, not `CINT`; `M x` is not `A^{-1}[e^{A Δt} − I] B z`; `M x` is not Voelkle et al., 2012, Eq. 14; the level-change form is not that impulse), recovers the exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition; JSS PDF re-opened 2026-08-20T09:01Z; the evolved map `τ + λ μ_t` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-impulse latent mean is not `E(y_t)`), recovers the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z; `TIPREDEFFECT` is `B`, not `κ`; form `B z` first, then the discrete intercept map; a zero drift is `B z Δt`; `B` is not the discrete increment; `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle Eq. 14), recovers the exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; JSS PDF re-opened 2026-08-20T12:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-increment latent mean is not `E(y_t)`), recovers the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z; form `m x` first, then `e^{a(t−u)} m x`; a zero drift is `m x`; underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept; `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; an impulse at `u = t` is the contemporaneous map; an impulse at `u ≤ t0` is already in `η(t0)`), recovers the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; JSS PDF re-opened 2026-08-20T05:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the carried latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and its Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF opened 2026-08-20T15:14Z; form `t0_b z` first, then `e^{a Δt} t0_b z`; a zero drift is `t0_b z`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; `e^{A Δt} t0_b z` is not `t0_b z`; `T0TIPREDEFFECT` is the coefficient, not the shift), recovers the exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition; JSS PDF re-opened 2026-08-20T15:28Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and its Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; form `t0_m x0` first, then `e^{a Δt} t0_m x0`; a zero drift is `t0_m x0`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `T0TDPREDEFFECT` is the coefficient, not the shift; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), recovers the exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; the first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar §7.2 level-change `CINT` setting `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z; form `m x` first, then multiply by `−a`; `a < 0` so `−κ / a = m x`; `a ≥ 0` cannot hold a new process mean; `−a m x` is not the dissipating Dirac, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`; the extra near-zero-drift latent process also named in §7.2 is a different specification), recovers the exact scalar Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` (JSS PDF re-opened 2026-08-20T19:50Z; form the level-change `CINT` first, then the discrete intercept map; underflow of `e^{a Δt}` to `+0` keeps `m x`; `(1 − e^{a Δt}) m x` is not `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`), recovers the exact scalar §7.2 extra near-zero-drift latent process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z; form `a_{ηξ} x` first; `ε = a` is `a_{ηξ} x Δt e^{a Δt}`; a zero coupling or zero predictor is exactly zero; `ε ≥ 0` cannot hold a lasting extra state; that contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. 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 6e8ffd6b..cec730fd 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -34,15 +34,16 @@ This slice stays inside `psychometric_core`. It does not add a second invariance 28. recover the exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; the first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean) and refuse treating `τ + λ μ_t`, `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, `τ + λ(μ_t + m x)`, `τ + λ(μ_t + e^{a(t−u)} m x)`, or `τ + λ(μ_t + e^{a Δt} t0_b z)` as that observed mean; 29. recover the exact scalar §7.2 level-change `CINT` `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z; form `m x` first, then multiply by `−a`; `a < 0` so `−κ / a = m x`) and refuse treating `−a m x` as the dissipating Dirac `m x`, as a free `CINT`, or as `A^{-1}[e^{A Δt} − I] B z`; `a ≥ 0` cannot hold a new process mean; the extra near-zero-drift latent process also named in §7.2 is a different specification; 30. recover the exact scalar Eq. 3 increment of that level-change `CINT` `(1 − e^{a Δt}) m x` (Driver et al., 2017, §7.2, pp. 20–21; Eq. 3, pp. 4–5; JSS PDF re-opened 2026-08-20T19:50Z; form `κ = −a m x` first, then `A^{-1}[e^{A Δt} − I] κ`; underflow of `e^{a Δt}` to `+0` keeps `m x`) and refuse treating `(1 − e^{a Δt}) m x` as `m x`, as `κ`, or as `A^{-1}[e^{A Δt} − I] B z`; -31. refuse pooling discrete lags from unequal event intervals as one coefficient; -32. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); -33. refuse the difference quotient as a continuous-time rate; -34. apply the same event-time map to CWC residuals (still not DSEM); -35. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). +31. recover the exact scalar §7.2 extra near-zero-drift latent process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z; identification `TDPREDEFFECT` on the extra process is 1; extra `DRIFT` printed as `−0.000001`; precisely 0 causes computational problems; `ε = a` is `a_{ηξ} x Δt e^{a Δt}`) and refuse treating that contribution as `κ = −a m x`, as `(1 − e^{a Δt}) m x`, or as the dissipating Dirac `m x`; `ε ≥ 0` fails closed; +32. refuse pooling discrete lags from unequal event intervals as one coefficient; +33. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); +34. refuse the difference quotient as a continuous-time rate; +35. apply the same event-time map to CWC residuals (still not DSEM); +36. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). ## Claim boundary -This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. The contemporaneous `TDPREDEFFECT` impulse is not the continuous intercept, not the time-independent discrete effect, and not Voelkle et al. (2012, Eq. 14). The time-independent `TIPREDEFFECT` increment is not the continuous intercept, not the contemporaneous impulse, not Voelkle et al. (2012, Eq. 14), and not the coefficient `B`. The within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). The evolved observed mean `τ + λ μ_t` is not the contemporaneous-impulse observed mean `τ + λ(μ_t + m x)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not the impulse-carry observed mean `τ + λ(μ_t + e^{a(t−u)} m x)` when `u ≠ t`. The evolved observed mean `τ + λ μ_t` is not the impulse-carry observed mean. The carried latent mean is not `E(y_t)`. The evolved-plus-impulse latent mean is not `E(y_t)`. The evolved observed mean `τ + λ μ_t` is not the time-independent-predictor observed mean `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not that time-independent-predictor observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that time-independent-predictor observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TIPREDEFFECT` shift `t0_b z` is not the Eq. 3 process increment `A^{-1}[e^{A Δt} − I] B z`, not `κ`, and not `M x`. The Eq. 3 first-summand carry `e^{A Δt} t0_b z` is not `t0_b z` and is not that process increment. `T0TIPREDEFFECT` is the coefficient, not the first-occasion shift. The evolved observed mean `τ + λ μ_t` is not the first-occasion TI-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_b z)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion observed mean when `u ≠ t0`. The evolved-plus-T0TIPRED latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TDPREDEFFECT` shift `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `κ`. The Eq. 3 first-summand carry `e^{A Δt} t0_m x0` is not `t0_m x0` and is not that within-interval impulse carry. `T0TDPREDEFFECT` is the coefficient, not the first-occasion shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The evolved observed mean `τ + λ μ_t` is not the first-occasion TD-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_m x0)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion TD observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion TD observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion TD observed mean when `u ≠ t0`. The first-occasion TI composition `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that first-occasion TD observed mean. Same numbers as `T0TIPREDEFFECT` yield the same product; Table 3 names a different matrix. The evolved-plus-T0TDPRED latent mean is not `E(y_t)`. The §7.2 level-change `CINT` `κ = −a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. Lasting level change via that `CINT` setting requires `a < 0`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not that `CINT` setting. The Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` is not the dissipating Dirac `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`. Underflow of `e^{a Δt}` to `+0` keeps the equilibrium offset `m x`. +This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. The contemporaneous `TDPREDEFFECT` impulse is not the continuous intercept, not the time-independent discrete effect, and not Voelkle et al. (2012, Eq. 14). The time-independent `TIPREDEFFECT` increment is not the continuous intercept, not the contemporaneous impulse, not Voelkle et al. (2012, Eq. 14), and not the coefficient `B`. The within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). The evolved observed mean `τ + λ μ_t` is not the contemporaneous-impulse observed mean `τ + λ(μ_t + m x)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not the impulse-carry observed mean `τ + λ(μ_t + e^{a(t−u)} m x)` when `u ≠ t`. The evolved observed mean `τ + λ μ_t` is not the impulse-carry observed mean. The carried latent mean is not `E(y_t)`. The evolved-plus-impulse latent mean is not `E(y_t)`. The evolved observed mean `τ + λ μ_t` is not the time-independent-predictor observed mean `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not that time-independent-predictor observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that time-independent-predictor observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TIPREDEFFECT` shift `t0_b z` is not the Eq. 3 process increment `A^{-1}[e^{A Δt} − I] B z`, not `κ`, and not `M x`. The Eq. 3 first-summand carry `e^{A Δt} t0_b z` is not `t0_b z` and is not that process increment. `T0TIPREDEFFECT` is the coefficient, not the first-occasion shift. The evolved observed mean `τ + λ μ_t` is not the first-occasion TI-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_b z)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion observed mean when `u ≠ t0`. The evolved-plus-T0TIPRED latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TDPREDEFFECT` shift `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `κ`. The Eq. 3 first-summand carry `e^{A Δt} t0_m x0` is not `t0_m x0` and is not that within-interval impulse carry. `T0TDPREDEFFECT` is the coefficient, not the first-occasion shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The evolved observed mean `τ + λ μ_t` is not the first-occasion TD-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_m x0)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion TD observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion TD observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion TD observed mean when `u ≠ t0`. The first-occasion TI composition `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that first-occasion TD observed mean. Same numbers as `T0TIPREDEFFECT` yield the same product; Table 3 names a different matrix. The evolved-plus-T0TDPRED latent mean is not `E(y_t)`. The §7.2 level-change `CINT` `κ = −a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. Lasting level change via that `CINT` setting requires `a < 0`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not that `CINT` setting. The Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` is not the dissipating Dirac `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`. Underflow of `e^{a Δt}` to `+0` keeps the equilibrium offset `m x`. The §7.2 extra-process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. Lasting level change via that extra process requires `ε < 0`. Precisely `ε = 0` causes computational problems in the printed specification. ## Authoritative sources @@ -92,6 +93,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - **First-occasion time-dependent predictor.** Driver et al. (2017, Table 3, p. 13; Eq. 3 first summand, p. 5; JSS PDF re-opened 2026-08-20T19:10Z): Table 3 names `T0TDPREDEFFECT` the effect of time-dependent predictors on latents at `T0`. Table 2 / Table 3 name `TDPREDEFFECT` `M`, which enters Equation 3 as the printed fourth-summand Dirac `M x` at `u = t`. The scalar first-occasion shift is `t0_m x0`. Equation 3's first summand carries that shift as `e^{AΔt}t0_m x0`. Form `t0_m x0` first, then `e^{aΔt}t0_m x0`. Form `μ_t` first, then add that carry. A zero drift is `t0_m x0`. Binary64 underflow of `e^{aΔt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. A zero effect or zero predictor is exactly zero. `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{AΔt}−I]Bz`, and not `κ`. `e^{AΔt}t0_m x0` is not `t0_m x0`. `T0TDPREDEFFECT` is the coefficient, not the shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. An overflowing product, rewrite, or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **First-occasion time-dependent-predictor observed-indicator mean.** Driver et al. (2017, Eq. 5, p. 5; Eq. 3 first summand, p. 5; Table 3, p. 13; JSS PDF re-opened 2026-08-20T19:07Z): `y_i(t)=Γ+Λη_i(t)+ζ_i(t)` with `ζ∼N(0,Θ)` and `Γ∼N(τ,Ψ)`. The latent process at `t` after the Table 3 first-occasion TD carry is `μ_t+e^{aΔt}t0_m x0`. The scalar composition is `E(y_t)=τ+λ(μ_t+e^{aΔt}t0_m x0)`. Form the evolved-plus-carry latent mean first, then `τ+λ` of that mean. A zero loading is exactly `τ`. A zero evolved-plus-carry latent mean is exactly `τ`. A zero intercept is exactly `λ(μ_t+carry)`. The evolved observed mean `τ+λμ_t` is not this composition. The process-increment map `τ+λ(μ_t+A^{-1}[e^{AΔt}−I]Bz)` is not this composition. The contemporaneous map `τ+λ(μ_t+mx)` is not this composition. The impulse-carry map `τ+λ(μ_t+e^{a(t−u)}mx)` is not this composition when `u≠t0`. The first-occasion TI map `τ+λ(μ_t+e^{aΔt}t0_b z)` is not this composition. Same numbers as `T0TIPREDEFFECT` yield the same product; Table 3 names a different matrix. `MANIFESTMEANS` is not `E(y_t)`. The evolved-plus-carry latent mean is not `E(y_t)`. `T0TDPREDEFFECT` is the coefficient, not that observed mean. An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **Level-change continuous intercept.** Driver et al. (2017, §7.2, pp. 20–21; Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T19:45Z): a sudden Dirac dissipates back to the process mean. To generate a lasting level change, `CINT` is set to `TDPREDEFFECT * −DRIFT`. The scalar setting is `κ=−a m x`. Form `m x` first, then multiply by `−a`. Stable `a<0` is required so `−κ/a=m x` is an equilibrium offset. `a≥0` cannot hold a new process mean. A zero effect or zero predictor is exactly zero. `−a m x` is not `m x`, not a free `CINT`, and not `A^{-1}[e^{AΔt}−I]Bz`. The extra near-zero-drift latent process also named in §7.2 is a different specification. An overflowing product fails closed. This is not a Kalman filter and not ctsem estimation. +- **Level-change extra process.** Driver et al. (2017, §7.2, pp. 22–23; Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T23:10Z): a lasting level change is specified by an extra latent process. `T0MEANS`, `CINT`, `T0VAR`, `DIFFUSION`, and `TRAITVAR` of that process are fixed to 0. `TDPREDEFFECT` on it is fixed to 1. Its `DRIFT` diagonal is very close to 0 (printed example `−0.000001`; precisely 0 causes computational problems). The original process is driven by the `DRIFT` coupling `a_{ηξ}`. After a unit identification impulse the scalar contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`. Form `a_{ηξ} x` first. When `ε = a` the contribution is `a_{ηξ} x Δt e^{a Δt}`. A zero coupling or zero predictor is exactly zero. `ε ≥ 0` fails closed. That contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. An overflowing product, exponential, or quotient fails closed. This is not a Kalman filter, not a matrix `expm`, and not ctsem estimation. - **Level-change discrete increment.** Driver et al. (2017, §7.2, pp. 20–21; Eq. 3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T19:50Z): Equation 3 maps `CINT` through `A^{-1}[e^{AΔt}−I]κ`. With `κ=−a m x` the scalar increment is `(e^{aΔt}−1)/a·(−a m x)=(1−e^{aΔt})m x`. Form the level-change `CINT` first, then the discrete intercept map. Underflow of `e^{aΔt}` to `+0` keeps `m x`. A zero effect or zero predictor is exactly zero. `(1−e^{aΔt})m x` is not `m x`, not `κ`, and not `A^{-1}[e^{AΔt}−I]Bz`. An overflowing product or increment fails closed. This is not a Kalman filter and not ctsem estimation. - **CWC-then-lag.** Sample cluster means are removed first. Consecutive within residuals are then fitted by least squares to \(r_{t+\Delta t}\approx\exp(a\Delta t)\,r_{t}\) on event time. Same-sign pair-wise logs initialize the scalar Newton step. Sign-flipping \(T=2\) CWC pairs have no real logarithm and fail closed. Curran and Bauer (2011, pp. 607–608) show that this person-mean subtraction on a raw autoregressive series does **not** isolate the lagged within-person effect; the helper therefore does not claim to recover the raw-process drift. - **Already-centered irregular residual.** The caller supplies lagged within residuals. The mean of \(a=\ln(r_{t+\Delta t}/r_t)/\Delta t\) is the exact scalar map. Intervals may be irregular. The helper does not center again. This is not DSEM. @@ -127,6 +129,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - Driver et al. (2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; Table 3, p. 13) recovers a known \(E(y_t)=\tau+\lambda(\mu_t+e^{a\Delta t}t0_m x0)\) at machine-scale RMSE, and that RMSE is smaller than treating \(\tau+\lambda\mu_t\), \(\tau+\lambda(\mu_t+A^{-1}[e^{A\Delta t}-I]Bz)\), \(\tau+\lambda(\mu_t+mx)\), \(\tau+\lambda(\mu_t+e^{a(t-u)}mx)\), `MANIFESTMEANS`, or the evolved-plus-carry latent mean as \(E(y_t)\); same numbers as `T0TIPREDEFFECT` yield the same product, but Table 3 names a different matrix and the refuse is fail-closed; a zero loading is \(\tau\); a non-event clock, a non-positive interval, and an overflowing product or sum fail closed; - Driver et al. (2017, §7.2) recovers a known level-change `CINT` \(\kappa=-a\,mx\) at machine-scale RMSE, and that RMSE is smaller than treating the dissipating Dirac \(mx\), a free `CINT`, or `TIPREDEFFECT` as that setting; \(-\kappa/a\) recovers \(mx\) when \(a<0\); \(a\ge 0\) with a nonzero impulse fails closed; a zero effect or zero predictor is exactly zero; an overflowing product fails closed; - Driver et al. (2017, §7.2 / Eq. 3) recovers a known level-change increment \((1-e^{a\Delta t})mx\) at machine-scale RMSE, and that RMSE is smaller than treating the dissipating Dirac \(mx\), the intercept \(\kappa\), or `TIPREDEFFECT` as that increment; underflow of \(e^{a\Delta t}\) to `+0` keeps \(mx\); \(a\ge 0\) with a nonzero impulse fails closed; a non-event clock, a non-positive interval, and an overflowing product fail closed; +- Driver et al. (2017, §7.2, pp. 22–23) recovers a known extra-process contribution \(a_{\eta\xi}x(e^{\varepsilon\Delta t}-e^{a\Delta t})/(\varepsilon-a)\) at machine-scale RMSE, and that RMSE is smaller than treating \(\kappa=-amx\), \((1-e^{a\Delta t})mx\), or the dissipating Dirac \(mx\) as that contribution; \(\varepsilon=a\) is \(a_{\eta\xi}x\Delta t\,e^{a\Delta t}\); \(\varepsilon\ge 0\) with a nonzero contribution fails closed; a zero coupling or zero predictor is exactly zero; a non-event clock, a non-positive interval, and an overflowing product fail closed; - pooling discrete lags from unequal intervals fails closed; - CWC-then-lag on a two-cluster decaying series has smaller computed RMSE than a level-pooled series when the latter is identified; - already-centered irregular residuals recover a known drift at machine-scale RMSE, and that RMSE is smaller than CWC of the corresponding raw autoregressive series (Curran & Bauer, 2011, pp. 607–608); diff --git a/docs/validation/temporal-event-foundation.md b/docs/validation/temporal-event-foundation.md index fa2b3683..3cba5afa 100644 --- a/docs/validation/temporal-event-foundation.md +++ b/docs/validation/temporal-event-foundation.md @@ -23,7 +23,7 @@ This report tracks exact-head scientific and engineering evidence required befor | Truth corpora / manifests | `tepp_simulation` | implemented-main | — | deterministic generator tests | Task 10 / PR #18 | | Recovery metrics | `validation_core` | implemented-main | — | RMSE/bias/coverage/MC gates | Task 11 / PR #19 | | Versioned API/export contracts | `tepp_api` | implemented-main | naruon HTTP interchange | unknown-field/version/limit + naruon HTTPS interchange tests | Task 12 / PR #21; live HTTP service remaining | -| Psychometric structural input gates | `psychometric_core` | partial | stacked psychometric PR | construct-class refusal + ALR/ILR boundary + true-loading RMSE + posterior-draw point-estimate mean + Rubin `T` + CWC within/between + CWC contextual effect + event-time log-rate + constant- and time-varying-predictor discrete effects + exact scalar discrete process noise + lagged latent covariance and unconditional latent variance + stationary within-subject variance + trait-plus-state variance + observed-indicator variance + discrete latent mean (`T0MEANS`/`CINT`) + evolved observed mean (`τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`) + contemporaneous `TDPREDEFFECT` impulse (`m x`; not `CINT`, not `TIPREDEFFECT`, not Voelkle Eq. 14) + Eq. 5 of that contemporaneous impulse (`τ + λ(μ_t + m x)`; `τ + λ μ_t` is not that observed mean) + time-independent `TIPREDEFFECT` increment (`A^{-1}[e^{A Δt} − I] B z`; not `CINT`, not `M x`, not Voelkle Eq. 14, not the coefficient `B`) + Eq. 5 of that increment (`τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`; `τ + λ μ_t` is not that observed mean) + within-interval `TDPREDEFFECT` carry (`e^{A(t−u)} M x` for `t0 < u < t`; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, not Voelkle Eq. 14) + Eq. 5 of that carry (`τ + λ(μ_t + e^{a(t−u)} m x)`; `τ + λ μ_t` is not that observed mean) + irregular already-centered residual lag + strong-gated latent means (n=2 residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016); full ESEM/DSEM remaining | ADR 0005; `docs/research/posterior-esem-input-gates.md`; `docs/research/multilevel-event-time-recovery.md`; `docs/research/rubin-total-variance.md`; `docs/research/strong-invariance-latent-means.md` | +| Psychometric structural input gates | `psychometric_core` | partial | stacked psychometric PR | construct-class refusal + ALR/ILR boundary + true-loading RMSE + posterior-draw point-estimate mean + Rubin `T` + CWC within/between + CWC contextual effect + event-time log-rate + constant- and time-varying-predictor discrete effects + exact scalar discrete process noise + lagged latent covariance and unconditional latent variance + stationary within-subject variance + trait-plus-state variance + observed-indicator variance + discrete latent mean (`T0MEANS`/`CINT`) + evolved observed mean (`τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`) + contemporaneous `TDPREDEFFECT` impulse (`m x`; not `CINT`, not `TIPREDEFFECT`, not Voelkle Eq. 14) + Eq. 5 of that contemporaneous impulse (`τ + λ(μ_t + m x)`; `τ + λ μ_t` is not that observed mean) + time-independent `TIPREDEFFECT` increment (`A^{-1}[e^{A Δt} − I] B z`; not `CINT`, not `M x`, not Voelkle Eq. 14, not the coefficient `B`) + Eq. 5 of that increment (`τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`; `τ + λ μ_t` is not that observed mean) + within-interval `TDPREDEFFECT` carry (`e^{A(t−u)} M x` for `t0 < u < t`; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, not Voelkle Eq. 14) + Eq. 5 of that carry (`τ + λ(μ_t + e^{a(t−u)} m x)`; `τ + λ μ_t` is not that observed mean) + §7.2 level-change `CINT` (`κ = −a m x`; Eq. 3 increment `(1 − e^{a Δt}) m x`) + §7.2 extra-process contribution (`a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; not `κ`, not the increment, not the Dirac; `ε ≥ 0` fails closed) + irregular already-centered residual lag + strong-gated latent means (n=2 residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016); full ESEM/DSEM remaining | ADR 0005; `docs/research/posterior-esem-input-gates.md`; `docs/research/multilevel-event-time-recovery.md`; `docs/research/rubin-total-variance.md`; `docs/research/strong-invariance-latent-means.md` | | Purpose-bound provider payloads | `tepp_api` | implemented-main | provider-payload minimization | expired/not-yet-valid/inverted/cross-tenant/impossible-calendar grant, mapping refusal, audited elevated re-id replay | ADR 0009; `docs/research/provider-payload-minimization.md` | | Adaptive orchestration router | `tepp_api` | accepted-target | active PR | mode selection, document-control denial, ablation, credential-free bind | ADR 0010; `docs/research/adaptive-orchestration-router.md` | | CWL modular connectors | `docs/connectors/*` | implemented-main | — | contract docs + examples | PR #22; live HTTP ports remaining | From 45ba702b6d1f3ae225158c1c785080d71ad59b81 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 06:30:01 +0000 Subject: [PATCH 54/62] =?UTF-8?q?feat(psychometric):=20recover=20Driver=20?= =?UTF-8?q?Eq.=205=20of=20=C2=A77.2=20extra=20process?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; §7.2, pp. 22–23) print extra-process LAMBDA 0: it is not an observed indicator. Original indicators load on the original process after the DRIFT coupling. After a unit identification impulse the scalar observed mean is τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)). τ + λ μ_t is not that observed mean. τ + λ(μ_t + m x) is not that observed mean. The contribution is not E(y_t). The evolved-plus-contribution latent mean is not E(y_t). A zero original-indicator loading is τ. JSS PDF re-opened 2026-08-21T06:12Z. Meredith (1993) and Mislevy (1991) remain unread (Unpaywall 2026-08-21T06:24Z: is_oa false). Still not DSEM, not a Kalman filter, and not ctsem estimation. --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 1 + CLAUDE.md | 2 +- crates/psychometric_core/src/error.rs | 44 +++ crates/psychometric_core/src/event_time.rs | 343 +++++++++++++++++- crates/psychometric_core/src/lib.rs | 19 + ...multilevel_event_time_recovery_contract.rs | 255 ++++++++++++- .../scientific_claim_boundary_contract.rs | 112 +++++- docs/TRACEABILITY.md | 2 +- docs/adr/0005-posterior-esem-dsem.md | 4 +- .../multilevel-event-time-recovery.md | 17 +- docs/validation/temporal-event-foundation.md | 2 +- 12 files changed, 779 insertions(+), 24 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 7ccb7849..3d552e7b 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -61,7 +61,7 @@ boundaries above remain the target modular MSA architecture. | `tepp_simulation` | known-truth temporal/event data generation | | `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics | | `tepp_api` | versioned DTO, schema, and export contracts | -| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; first-occasion `T0TIPREDEFFECT` shift is `t0_b z` and Eq. 3 first-summand carry is `e^{A Δt} t0_b z` (`T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_b z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), first-occasion `T0TDPREDEFFECT` shift is `t0_m x0` and Eq. 3 first-summand carry is `e^{A Δt} t0_m x0` (`T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; §7.2 level-change `CINT` is `κ = −a m x` with `a < 0` so `−κ / a = m x` (`−a m x` is not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2; Eq. 3 of that setting is `(1 − e^{a Δt}) m x`, which is not `m x`, not `κ`, and not `TIPREDEFFECT`; §7.2 extra-process contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`; identification `TDPREDEFFECT` on the extra process is 1; printed extra `DRIFT` is `−0.000001`; not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`; `ε ≥ 0` fails closed), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | +| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; first-occasion `T0TIPREDEFFECT` shift is `t0_b z` and Eq. 3 first-summand carry is `e^{A Δt} t0_b z` (`T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_b z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), first-occasion `T0TDPREDEFFECT` shift is `t0_m x0` and Eq. 3 first-summand carry is `e^{A Δt} t0_m x0` (`T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; §7.2 level-change `CINT` is `κ = −a m x` with `a < 0` so `−κ / a = m x` (`−a m x` is not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2; Eq. 3 of that setting is `(1 − e^{a Δt}) m x`, which is not `m x`, not `κ`, and not `TIPREDEFFECT`; §7.2 extra-process contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`; identification `TDPREDEFFECT` on the extra process is 1; printed extra `DRIFT` is `−0.000001`; not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`; `ε ≥ 0` fails closed; Eq. 5 of that contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; the extra process has `LAMBDA` 0 and is not an observed indicator; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | No crate exposes placeholder production behavior in Task 1. This prevents an empty façade from becoming a de facto public API before its invariants and tests diff --git a/CHANGELOG.md b/CHANGELOG.md index fc9b1d4a..f3ad0b8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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, Eq. 5, p. 5; §7.2, pp. 22–23; Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-21T06:12Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar observed-indicator mean of the extra near-zero-drift latent process contribution. Section 7.2's printed extra process has `LAMBDA` 0 and is not an observed indicator. Original indicators load on the original process after the `DRIFT` coupling `a_{ηξ}`. After a unit identification impulse the latent process at `t` is `μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`. The scalar composition is `E(y_t) = τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))`. Form the evolved-plus-contribution latent mean first, then `τ + λ` of that mean. The evolved observed mean `τ + λ μ_t` is not this composition. The contemporaneous map `τ + λ(μ_t + m x)` is not this composition. The extra-process contribution is not `E(y_t)`. The evolved-plus-contribution latent mean is not `E(y_t)`. A zero original-indicator loading is exactly `τ`. A zero coupling recovers `τ + λ μ_t`. `ε ≥ 0` fails closed. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-21T06:24Z: `is_oa: false`; Springer `content/pdf` is HTML 200). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, §7.2, pp. 22–23; Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T23:10Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar contribution of the extra near-zero-drift latent process. Section 7.2 specifies a lasting level change by that extra process: `T0MEANS`, `CINT`, `T0VAR`, `DIFFUSION`, and `TRAITVAR` of it are fixed to 0; `TDPREDEFFECT` on it is fixed to 1; its `DRIFT` diagonal is very close to 0 (printed example `−0.000001`; precisely 0 causes computational problems); and its effect on the original process is the `DRIFT` coupling `a_{ηξ}`. After a unit identification impulse the scalar contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`). Form `a_{ηξ} x` first. A zero coupling or zero predictor is exactly zero. `ε ≥ 0` fails closed. That contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T23:10Z: `is_oa: false`; Springer `content/pdf` is HTML 200). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, §7.2, pp. 20–21; Eq. 3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T19:50Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar discrete increment of the lasting level-change `CINT`. Section 7.2 sets `CINT` to `TDPREDEFFECT * −DRIFT` (`κ = −a m x`). Equation 3 maps that intercept through `A^{-1}[e^{A Δt} − I] κ`. With `κ = −a m x` the scalar increment is `(e^{a Δt} − 1)/a · (−a m x) = (1 − e^{a Δt}) m x`. Form the level-change `CINT` first, then the discrete intercept map. Underflow of `e^{a Δt}` to `+0` keeps the equilibrium offset `m x`. `(1 − e^{a Δt}) m x` is not the contemporaneous jump `m x`. `(1 − e^{a Δt}) m x` is not `κ`. `(1 − e^{a Δt}) m x` is not `A^{-1}[e^{A Δt} − I] B z`. Stable `a < 0` is required. A zero effect or zero predictor is exactly zero. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T19:45Z: `is_oa: false`; Springer `content/pdf` is HTML 200). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, §7.2, pp. 20–21; Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T19:45Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar lasting level-change `CINT`. Section 7.2 contrasts a sudden Dirac that dissipates back to the process mean with a lasting level change. To generate that lasting change, `CINT` is set to `TDPREDEFFECT * −DRIFT`. The scalar setting is `κ = −a m x`. Form `m x` first, then multiply by `−a`. Stable `a < 0` is required so `−κ / a = m x` is an equilibrium offset. `a ≥ 0` cannot hold a new process mean. `−a m x` is not the contemporaneous jump `m x`. `−a m x` is not a free `CINT`. `−a m x` is not `A^{-1}[e^{A Δt} − I] B z`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not this `CINT` setting. A zero effect or zero predictor is exactly zero. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T19:45Z: `is_oa: false`; Springer `content/pdf` is HTML 200). diff --git a/CLAUDE.md b/CLAUDE.md index 8705df0c..f2ba1b92 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. Putnick and Bornstein (2016, PMC5145197 opened 2026-08-19T22:15Z) require scalar invariance before latent-mean comparison; residual invariance is not a prerequisite. Two-observation series have no residual degrees of freedom (`ordinary_least_squares_fit` returns residual variance `0`) and cap at strong/scalar; they still license means. This is two-group OLS, not MGCFA. Meredith (1993) names remain unread labels. -- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. The contemporaneous time-dependent predictor impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`). Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). The §7.2 level-change form is not that impulse. The observed mean of that contemporaneous impulse is `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-impulse latent mean is not `E(y_t)`. The time-independent predictor increment is `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`). Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. `TIPREDEFFECT` is `B`, not that discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle et al. (2012, Eq. 14). The observed mean of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The within-interval time-dependent impulse carry is `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation). Form `m x` first, then `e^{a(t−u)} m x`. A zero drift is `m x` with no dissipation. Underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept. `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). An impulse at `u = t` is the contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. The observed mean of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The carried latent mean is not `E(y_t)`. The first-occasion time-independent predictor shift is `t0_b z` (Driver et al., 2017, Table 3 `T0TIPREDEFFECT`; Eq. 3 first summand). Form `t0_b z` first, then `e^{a Δt} t0_b z`. Form `μ_t` first, then add that carry. A zero drift is `t0_b z`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. `e^{A Δt} t0_b z` is not `t0_b z`. `T0TIPREDEFFECT` is the coefficient, not the shift. The observed mean of that first-occasion carry is `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The evolved-plus-carry latent mean is not `E(y_t)`. The first-occasion time-dependent predictor shift is `t0_m x0` (Driver et al., 2017, Table 3 `T0TDPREDEFFECT`; Eq. 3 first summand; JSS PDF re-opened 2026-08-20T19:10Z). Form `t0_m x0` first, then `e^{a Δt} t0_m x0`. Form `μ_t` first, then add that carry. A zero drift is `t0_m x0`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`. `e^{A Δt} t0_m x0` is not `t0_m x0`. `T0TDPREDEFFECT` is the coefficient, not the shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The observed mean of that first-occasion TD carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean. The evolved-plus-carry latent mean is not `E(y_t)`. The lasting level-change `CINT` is `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z). Form `m x` first, then multiply by `−a`. Stable `a < 0` is required so `−κ / a = m x` is an equilibrium offset. `a ≥ 0` cannot hold a new process mean. `−a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not this `CINT` setting. Equation 3 maps that intercept as `(1 − e^{a Δt}) m x` (JSS PDF re-opened 2026-08-20T19:50Z). Form the level-change `CINT` first, then the discrete intercept map. Underflow of `e^{a Δt}` to `+0` keeps `m x`. `(1 − e^{a Δt}) m x` is not `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`. The printed §7.2 lasting level change is an extra near-zero-drift latent process (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z). `T0MEANS`, `CINT`, `T0VAR`, `DIFFUSION`, and `TRAITVAR` of that process are fixed to 0; `TDPREDEFFECT` on it is fixed to 1; its `DRIFT` diagonal is very close to 0 (printed example `−0.000001`; precisely 0 causes computational problems); the original process is driven by the `DRIFT` coupling `a_{ηξ}`. After a unit identification impulse the scalar contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`). Form `a_{ηξ} x` first. A zero coupling or zero predictor is exactly zero. `ε ≥ 0` fails closed. That contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. The contemporaneous time-dependent predictor impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`). Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). The §7.2 level-change form is not that impulse. The observed mean of that contemporaneous impulse is `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-impulse latent mean is not `E(y_t)`. The time-independent predictor increment is `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`). Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. `TIPREDEFFECT` is `B`, not that discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle et al. (2012, Eq. 14). The observed mean of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The within-interval time-dependent impulse carry is `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation). Form `m x` first, then `e^{a(t−u)} m x`. A zero drift is `m x` with no dissipation. Underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept. `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). An impulse at `u = t` is the contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. The observed mean of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The carried latent mean is not `E(y_t)`. The first-occasion time-independent predictor shift is `t0_b z` (Driver et al., 2017, Table 3 `T0TIPREDEFFECT`; Eq. 3 first summand). Form `t0_b z` first, then `e^{a Δt} t0_b z`. Form `μ_t` first, then add that carry. A zero drift is `t0_b z`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. `e^{A Δt} t0_b z` is not `t0_b z`. `T0TIPREDEFFECT` is the coefficient, not the shift. The observed mean of that first-occasion carry is `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The evolved-plus-carry latent mean is not `E(y_t)`. The first-occasion time-dependent predictor shift is `t0_m x0` (Driver et al., 2017, Table 3 `T0TDPREDEFFECT`; Eq. 3 first summand; JSS PDF re-opened 2026-08-20T19:10Z). Form `t0_m x0` first, then `e^{a Δt} t0_m x0`. Form `μ_t` first, then add that carry. A zero drift is `t0_m x0`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`. `e^{A Δt} t0_m x0` is not `t0_m x0`. `T0TDPREDEFFECT` is the coefficient, not the shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The observed mean of that first-occasion TD carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean. The evolved-plus-carry latent mean is not `E(y_t)`. The lasting level-change `CINT` is `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z). Form `m x` first, then multiply by `−a`. Stable `a < 0` is required so `−κ / a = m x` is an equilibrium offset. `a ≥ 0` cannot hold a new process mean. `−a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not this `CINT` setting. Equation 3 maps that intercept as `(1 − e^{a Δt}) m x` (JSS PDF re-opened 2026-08-20T19:50Z). Form the level-change `CINT` first, then the discrete intercept map. Underflow of `e^{a Δt}` to `+0` keeps `m x`. `(1 − e^{a Δt}) m x` is not `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`. The printed §7.2 lasting level change is an extra near-zero-drift latent process (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z). `T0MEANS`, `CINT`, `T0VAR`, `DIFFUSION`, and `TRAITVAR` of that process are fixed to 0; `TDPREDEFFECT` on it is fixed to 1; its `DRIFT` diagonal is very close to 0 (printed example `−0.000001`; precisely 0 causes computational problems); the original process is driven by the `DRIFT` coupling `a_{ηξ}`. After a unit identification impulse the scalar contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`). Form `a_{ηξ} x` first. A zero coupling or zero predictor is exactly zero. `ε ≥ 0` fails closed. That contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. The observed mean of that extra-process contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5 of that §7.2 contribution; JSS PDF re-opened 2026-08-21T06:12Z). The extra process has `LAMBDA` 0 and is not an observed indicator. Original indicators load on the original process after the `DRIFT` coupling. The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The contribution is not `E(y_t)`. The evolved-plus-contribution latent mean is not `E(y_t)`. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. diff --git a/crates/psychometric_core/src/error.rs b/crates/psychometric_core/src/error.rs index 32850b39..ac7f1e6f 100644 --- a/crates/psychometric_core/src/error.rs +++ b/crates/psychometric_core/src/error.rs @@ -286,6 +286,22 @@ pub enum PsychometricError { /// Eq. 3 level-change increment. `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` /// is not `(1 − e^{a Δt}) m x`. LevelChangeExtraProcessIsNotIncrement, + /// Driver Eq. 5 of the evolved mean was treated as the Eq. 5 + /// extra-process observed mean. `τ + λ μ_t` is not + /// `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))`. + EvolvedObservedMeanIsNotExtraProcessObservedMean, + /// Driver Eq. 5 of the contemporaneous impulse was treated as + /// the Eq. 5 extra-process observed mean. `τ + λ(μ_t + m x)` is + /// not `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))`. + ImpulseObservedMeanIsNotExtraProcessObservedMean, + /// Driver §7.2 extra-process contribution was treated as `E(y_t)`. + /// The contribution is not `τ + λ` of the evolved-plus-contribution + /// latent mean. The extra process has `LAMBDA` 0 in the printed + /// specification. + ExtraProcessContributionIsNotObservedMean, + /// Driver §7.2 evolved-plus-contribution latent mean was treated + /// as `E(y_t)`. Equation 5 maps `E(y_t) = τ + λ` of that mean. + ExtraProcessLatentMeanIsNotObservedMean, } impl fmt::Display for PsychometricError { @@ -520,6 +536,18 @@ impl fmt::Display for PsychometricError { Self::LevelChangeExtraProcessIsNotIncrement => { "level-change extra-process contribution is not the level-change increment" } + Self::EvolvedObservedMeanIsNotExtraProcessObservedMean => { + "evolved observed mean is not the extra-process observed mean" + } + Self::ImpulseObservedMeanIsNotExtraProcessObservedMean => { + "contemporaneous-impulse observed mean is not the extra-process observed mean" + } + Self::ExtraProcessContributionIsNotObservedMean => { + "extra-process contribution is not the extra-process observed mean" + } + Self::ExtraProcessLatentMeanIsNotObservedMean => { + "evolved-plus-contribution latent mean is not the extra-process observed mean" + } }; formatter.write_str(message) } @@ -885,5 +913,21 @@ mod tests { PsychometricError::LevelChangeExtraProcessIsNotIncrement.to_string(), "level-change extra-process contribution is not the level-change increment" ); + assert_eq!( + PsychometricError::EvolvedObservedMeanIsNotExtraProcessObservedMean.to_string(), + "evolved observed mean is not the extra-process observed mean" + ); + assert_eq!( + PsychometricError::ImpulseObservedMeanIsNotExtraProcessObservedMean.to_string(), + "contemporaneous-impulse observed mean is not the extra-process observed mean" + ); + assert_eq!( + PsychometricError::ExtraProcessContributionIsNotObservedMean.to_string(), + "extra-process contribution is not the extra-process observed mean" + ); + assert_eq!( + PsychometricError::ExtraProcessLatentMeanIsNotObservedMean.to_string(), + "evolved-plus-contribution latent mean is not the extra-process observed mean" + ); } } diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index e34905ec..e622ac62 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -87,7 +87,13 @@ //! contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` //! (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`). That contribution is not //! `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating -//! Dirac `m x`. `ε ≥ 0` fails closed. The JSS article +//! Dirac `m x`. `ε ≥ 0` fails closed. Equation 5 of that extra-process +//! contribution is +//! `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (the extra +//! process has `LAMBDA` 0 and is not an observed indicator; `τ + λ μ_t` +//! is not that observed mean; `τ + λ(μ_t + m x)` is not that observed +//! mean; the contribution is not `E(y_t)`; the evolved-plus-contribution +//! latent mean is not `E(y_t)`). The JSS article //! has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). //! The difference quotient `(x(t+Δt) − x(t)) / Δt` (their //! Eqs. 3–4) is refused. This is not DSEM and not a matrix `expm`. @@ -1697,6 +1703,195 @@ pub fn refuse_level_change_extra_process_as_increment( Err(PsychometricError::LevelChangeExtraProcessIsNotIncrement) } +/// Exact scalar evolved latent mean plus a §7.2 extra-process contribution. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 3, p. 5; §7.2, pp. 22–23; JSS +/// PDF re-opened 2026-08-21T06:12Z from +/// ) +/// write the first two summands as the carried `T0MEANS` and `CINT` +/// increment. Section 7.2 then drives the original process by the +/// extra near-zero-drift latent process through the `DRIFT` coupling +/// `a_{ηξ}`. Form `μ_t` first, then add the extra-process +/// contribution. A zero contribution is exactly `μ_t`. A zero evolved +/// mean is exactly the contribution. The first-occasion map +/// `μ_0 + contribution` is not this composition when the process has +/// already evolved. The contemporaneous Dirac `μ_t + m x` is not this +/// composition. The printed specification puts `TDPREDEFFECT` on the +/// extra process, not on the original process. This is not a Kalman +/// filter and not ctsem estimation. +/// +/// # Errors +/// +/// Propagates [`recover_discrete_latent_mean`] and +/// [`recover_level_change_extra_process_contribution`], and returns +/// [`PsychometricError::InvalidNumericInput`] when the sum overflows. +#[allow(clippy::too_many_arguments)] +pub fn recover_discrete_latent_mean_with_extra_process( + initial_latent_mean: f64, + original_log_rate: f64, + continuous_intercept: f64, + original_from_extra_drift: f64, + time_dependent_predictor: f64, + extra_log_rate: f64, + event_delta: f64, + clock: LagClock, +) -> Result { + let evolved_latent_mean = recover_discrete_latent_mean( + initial_latent_mean, + original_log_rate, + continuous_intercept, + event_delta, + clock, + )?; + let contribution = recover_level_change_extra_process_contribution( + original_from_extra_drift, + time_dependent_predictor, + original_log_rate, + extra_log_rate, + event_delta, + clock, + )?; + if contribution == 0.0 { + return Ok(evolved_latent_mean); + } + if evolved_latent_mean == 0.0 { + return Ok(contribution); + } + require_finite(evolved_latent_mean + contribution) +} + +/// Exact scalar observed mean of a §7.2 extra-process contribution. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Eq. 1–3, pp. 4–5; +/// §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z from +/// ) +/// write `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and +/// `Γ ~ N(τ, Ψ)`. The expected intercept is `τ`. Section 7.2's +/// printed extra process has `LAMBDA` 0: it is not an observed +/// indicator. Original indicators load on the original process after +/// the `DRIFT` coupling. The latent process at `t` after that +/// contribution is `μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`. +/// The scalar composition is +/// `E(y_t) = τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))`. +/// Form the evolved-plus-contribution latent mean first, then +/// `τ + λ` of that mean. Table 2 names `τ` `MANIFESTMEANS`. A zero +/// loading is exactly `τ`. A zero evolved-plus-contribution latent +/// mean is exactly `τ`. A zero intercept is exactly `λ` of that +/// latent mean. The evolved observed mean `τ + λ μ_t` is not this +/// composition when the contribution is nonzero. The contemporaneous +/// map `τ + λ(μ_t + m x)` is not this composition. `MANIFESTMEANS` is +/// not `E(y_t)`. The extra-process contribution is not `E(y_t)`. The +/// evolved-plus-contribution latent mean is not `E(y_t)`. The extra +/// process itself is not an observed indicator. This is not a Kalman +/// filter and not ctsem estimation. +/// +/// # Errors +/// +/// Propagates [`recover_discrete_latent_mean_with_extra_process`] and +/// [`recover_manifest_observed_mean`]. +#[allow(clippy::too_many_arguments)] +pub fn recover_discrete_observed_mean_with_extra_process( + loading: f64, + initial_latent_mean: f64, + original_log_rate: f64, + continuous_intercept: f64, + original_from_extra_drift: f64, + time_dependent_predictor: f64, + extra_log_rate: f64, + manifest_mean: f64, + event_delta: f64, + clock: LagClock, +) -> Result { + let extra_latent_mean = recover_discrete_latent_mean_with_extra_process( + initial_latent_mean, + original_log_rate, + continuous_intercept, + original_from_extra_drift, + time_dependent_predictor, + extra_log_rate, + event_delta, + clock, + )?; + recover_manifest_observed_mean(loading, extra_latent_mean, manifest_mean) +} + +/// Refuse treating the evolved observed mean as the extra-process +/// observed mean. +/// +/// Equation 5 of the Eq. 3 evolved mean is `τ + λ μ_t`. Equation 5 +/// of the §7.2 extra-process contribution is +/// `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))`. Those +/// are not the same map. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::EvolvedObservedMeanIsNotExtraProcessObservedMean`]. +pub fn refuse_evolved_observed_mean_as_extra_process_observed_mean( + evolved_observed_mean: f64, + extra_process_observed_mean: f64, +) -> Result { + let _ = (evolved_observed_mean, extra_process_observed_mean); + Err(PsychometricError::EvolvedObservedMeanIsNotExtraProcessObservedMean) +} + +/// Refuse treating the contemporaneous-impulse observed mean as the +/// extra-process observed mean. +/// +/// Equation 5 of the contemporaneous Dirac is `τ + λ(μ_t + m x)`. +/// Equation 5 of the §7.2 extra-process contribution is +/// `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))`. Those +/// are not the same map. The printed specification puts +/// `TDPREDEFFECT` on the extra process, not on the original process. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::ImpulseObservedMeanIsNotExtraProcessObservedMean`]. +pub fn refuse_impulse_observed_mean_as_extra_process_observed_mean( + impulse_observed_mean: f64, + extra_process_observed_mean: f64, +) -> Result { + let _ = (impulse_observed_mean, extra_process_observed_mean); + Err(PsychometricError::ImpulseObservedMeanIsNotExtraProcessObservedMean) +} + +/// Refuse treating the §7.2 extra-process contribution as `E(y_t)`. +/// +/// The contribution is not `τ + λ` of the evolved-plus-contribution +/// latent mean. The extra process has `LAMBDA` 0 in the printed +/// specification. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::ExtraProcessContributionIsNotObservedMean`]. +pub fn refuse_extra_process_contribution_as_observed_mean( + extra_process_contribution: f64, + extra_process_observed_mean: f64, +) -> Result { + let _ = (extra_process_contribution, extra_process_observed_mean); + Err(PsychometricError::ExtraProcessContributionIsNotObservedMean) +} + +/// Refuse treating the evolved-plus-contribution latent mean as +/// `E(y_t)`. +/// +/// Equation 5 maps `E(y_t) = τ + λ` of that mean. The latent mean is +/// not the observed mean. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::ExtraProcessLatentMeanIsNotObservedMean`]. +pub fn refuse_extra_process_latent_mean_as_observed_mean( + extra_process_latent_mean: f64, + extra_process_observed_mean: f64, +) -> Result { + let _ = (extra_process_latent_mean, extra_process_observed_mean); + Err(PsychometricError::ExtraProcessLatentMeanIsNotObservedMean) +} + /// Exact scalar evolved latent mean plus a contemporaneous impulse. /// /// Driver, Oud, and Voelkle (2017, Eq. 3, p. 5) write the first two @@ -3553,12 +3748,13 @@ mod tests { map_discrete_lag_across_event_intervals, recover_discrete_constant_predictor_effect, recover_discrete_continuous_intercept_effect, recover_discrete_lag_from_log_rate, recover_discrete_lag_one, recover_discrete_lagged_latent_covariance, - recover_discrete_latent_mean, recover_discrete_latent_mean_with_impulse, - recover_discrete_latent_mean_with_impulse_carry, + recover_discrete_latent_mean, recover_discrete_latent_mean_with_extra_process, + recover_discrete_latent_mean_with_impulse, recover_discrete_latent_mean_with_impulse_carry, recover_discrete_latent_mean_with_initial_time_dependent_predictor, recover_discrete_latent_mean_with_initial_time_independent_predictor, recover_discrete_latent_mean_with_time_independent_predictor, recover_discrete_latent_variance, recover_discrete_observed_mean, + recover_discrete_observed_mean_with_extra_process, recover_discrete_observed_mean_with_impulse, recover_discrete_observed_mean_with_impulse_carry, recover_discrete_observed_mean_with_initial_time_dependent_predictor, @@ -3582,15 +3778,19 @@ mod tests { refuse_continuous_intercept_as_discrete_mean_increment, refuse_continuous_intercept_as_initial_latent_mean, refuse_continuous_intercept_as_manifest_means, refuse_difference_quotient_as_local_rate, + refuse_evolved_observed_mean_as_extra_process_observed_mean, refuse_evolved_observed_mean_as_impulse_carry_observed_mean, refuse_evolved_observed_mean_as_impulse_observed_mean, refuse_evolved_observed_mean_as_initial_time_dependent_observed_mean, refuse_evolved_observed_mean_as_initial_time_independent_observed_mean, refuse_evolved_observed_mean_as_time_independent_observed_mean, + refuse_extra_process_contribution_as_observed_mean, + refuse_extra_process_latent_mean_as_observed_mean, refuse_finite_interval_process_noise_as_stationary_variance, refuse_impulse_carry_observed_mean_as_initial_time_dependent_observed_mean, refuse_impulse_carry_observed_mean_as_initial_time_independent_observed_mean, refuse_impulse_carry_observed_mean_as_time_independent_observed_mean, + refuse_impulse_observed_mean_as_extra_process_observed_mean, refuse_impulse_observed_mean_as_impulse_carry_observed_mean, refuse_impulse_observed_mean_as_initial_time_dependent_observed_mean, refuse_impulse_observed_mean_as_initial_time_independent_observed_mean, @@ -6155,6 +6355,143 @@ mod tests { assert!((overflow_fallback - overflow_expected).abs() < 1e-12); } + #[test] + fn extra_process_observed_mean_recovers_driver_equation_five() { + let loading = 2.0_f64; + let coupling = 0.4_f64; + let predictor = 3.0_f64; + let original = -0.5_f64; + let extra = -0.05_f64; + let delta = 2.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let recovered = recover_discrete_observed_mean_with_extra_process( + loading, + initial, + original, + intercept, + coupling, + predictor, + extra, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-extra-process-mean"); + let composed = recover_discrete_latent_mean_with_extra_process( + initial, + original, + intercept, + coupling, + predictor, + extra, + delta, + LagClock::EventTime, + ) + .expect("extra-latent"); + let expected = manifest_mean + loading * composed; + assert!((recovered - expected).abs() < 1e-15); + let evolved_observed = recover_discrete_observed_mean( + loading, + initial, + original, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq3-eq5-mean"); + assert!((evolved_observed - recovered).abs() > 1e-3); + let impulse_observed = recover_discrete_observed_mean_with_impulse( + loading, + initial, + original, + intercept, + coupling, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-impulse-mean"); + assert!((impulse_observed - recovered).abs() > 1e-3); + let contribution = recover_level_change_extra_process_contribution( + coupling, + predictor, + original, + extra, + delta, + LagClock::EventTime, + ) + .expect("extra-process"); + assert_eq!( + refuse_evolved_observed_mean_as_extra_process_observed_mean( + evolved_observed, + recovered + ), + Err(PsychometricError::EvolvedObservedMeanIsNotExtraProcessObservedMean) + ); + assert_eq!( + refuse_impulse_observed_mean_as_extra_process_observed_mean( + impulse_observed, + recovered + ), + Err(PsychometricError::ImpulseObservedMeanIsNotExtraProcessObservedMean) + ); + assert_eq!( + refuse_extra_process_contribution_as_observed_mean(contribution, recovered), + Err(PsychometricError::ExtraProcessContributionIsNotObservedMean) + ); + assert_eq!( + refuse_extra_process_latent_mean_as_observed_mean(composed, recovered), + Err(PsychometricError::ExtraProcessLatentMeanIsNotObservedMean) + ); + } + + #[test] + fn extra_process_observed_mean_zero_loading_is_manifest_mean_and_refuses_clock() { + let loading = 2.0_f64; + let coupling = 0.4_f64; + let predictor = 3.0_f64; + let original = -0.5_f64; + let extra = -0.05_f64; + let delta = 2.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + assert_eq!( + recover_discrete_observed_mean_with_extra_process( + 0.0, + initial, + original, + intercept, + coupling, + predictor, + extra, + manifest_mean, + delta, + LagClock::EventTime + ), + Ok(manifest_mean) + ); + assert_eq!( + recover_discrete_observed_mean_with_extra_process( + loading, + initial, + original, + intercept, + coupling, + predictor, + extra, + manifest_mean, + delta, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + } + #[test] fn discrete_observed_mean_with_impulse_recovers_driver_equation_five() { let loading = 2.0_f64; diff --git a/crates/psychometric_core/src/lib.rs b/crates/psychometric_core/src/lib.rs index 54f758ac..763568fa 100644 --- a/crates/psychometric_core/src/lib.rs +++ b/crates/psychometric_core/src/lib.rs @@ -87,6 +87,13 @@ //! identification `TDPREDEFFECT` on the extra process is 1; `ε < 0`; //! printed extra `DRIFT` is `−0.000001`; not `κ = −a m x`, not //! `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`), +//! recovers the Driver Eq. 5 of that extra-process contribution as +//! `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Eq. 5, +//! p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z; the +//! extra process has `LAMBDA` 0 and is not an observed indicator; +//! `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not +//! that observed mean; the contribution is not `E(y_t)`; the +//! evolved-plus-contribution latent mean is not `E(y_t)`), //! and refuses //! latent-mean comparison below strong invariance. @@ -147,6 +154,8 @@ pub use event_time::recover_discrete_lag_one; pub use event_time::recover_discrete_lagged_latent_covariance; /// Exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`. pub use event_time::recover_discrete_latent_mean; +/// Exact scalar evolved latent mean plus a §7.2 extra-process contribution. +pub use event_time::recover_discrete_latent_mean_with_extra_process; /// Exact scalar evolved latent mean plus a contemporaneous impulse. pub use event_time::recover_discrete_latent_mean_with_impulse; /// Exact scalar evolved latent mean plus a within-interval impulse carry. @@ -161,6 +170,8 @@ pub use event_time::recover_discrete_latent_mean_with_time_independent_predictor pub use event_time::recover_discrete_latent_variance; /// Exact scalar discrete observed mean `τ + λ μ_t` from Eq. 3 then Eq. 5. pub use event_time::recover_discrete_observed_mean; +/// Exact scalar discrete observed mean of a §7.2 extra-process contribution. +pub use event_time::recover_discrete_observed_mean_with_extra_process; /// Exact scalar discrete observed mean of a contemporaneous impulse. pub use event_time::recover_discrete_observed_mean_with_impulse; /// Exact scalar discrete observed mean of a within-interval impulse carry. @@ -227,6 +238,8 @@ pub use event_time::refuse_continuous_intercept_as_initial_latent_mean; pub use event_time::refuse_continuous_intercept_as_manifest_means; /// Refuse the difference quotient as a continuous-time rate. pub use event_time::refuse_difference_quotient_as_local_rate; +/// Refuse treating evolved `τ + λ μ_t` as the extra-process observed mean. +pub use event_time::refuse_evolved_observed_mean_as_extra_process_observed_mean; /// Refuse treating evolved `τ + λ μ_t` as the impulse-carry observed mean. pub use event_time::refuse_evolved_observed_mean_as_impulse_carry_observed_mean; /// Refuse treating evolved `τ + λ μ_t` as the contemporaneous-impulse observed mean. @@ -237,6 +250,10 @@ pub use event_time::refuse_evolved_observed_mean_as_initial_time_dependent_obser pub use event_time::refuse_evolved_observed_mean_as_initial_time_independent_observed_mean; /// Refuse treating evolved `τ + λ μ_t` as the time-independent-predictor observed mean. pub use event_time::refuse_evolved_observed_mean_as_time_independent_observed_mean; +/// Refuse treating the §7.2 extra-process contribution as `E(y_t)`. +pub use event_time::refuse_extra_process_contribution_as_observed_mean; +/// Refuse treating the evolved-plus-contribution latent mean as `E(y_t)`. +pub use event_time::refuse_extra_process_latent_mean_as_observed_mean; /// Refuse treating finite-interval `Q_Δt` as `asymDIFFUSION`. pub use event_time::refuse_finite_interval_process_noise_as_stationary_variance; /// Refuse treating impulse-carry `τ + λ(μ_t + e^{a(t−u)} m x)` as the first-occasion TD-predictor observed mean. @@ -245,6 +262,8 @@ pub use event_time::refuse_impulse_carry_observed_mean_as_initial_time_dependent pub use event_time::refuse_impulse_carry_observed_mean_as_initial_time_independent_observed_mean; /// Refuse treating impulse-carry `τ + λ(μ_t + e^{a(t−u)} m x)` as the time-independent-predictor observed mean. pub use event_time::refuse_impulse_carry_observed_mean_as_time_independent_observed_mean; +/// Refuse treating contemporaneous `τ + λ(μ_t + m x)` as the extra-process observed mean. +pub use event_time::refuse_impulse_observed_mean_as_extra_process_observed_mean; /// Refuse treating contemporaneous `τ + λ(μ_t + m x)` as the impulse-carry observed mean. pub use event_time::refuse_impulse_observed_mean_as_impulse_carry_observed_mean; /// Refuse treating contemporaneous `τ + λ(μ_t + m x)` as the first-occasion TD-predictor observed mean. diff --git a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs index 4e1aa56a..ac2a2be7 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -7,13 +7,13 @@ use psychometric_core::{ ordinary_least_squares_slope, recover_cluster_mean_within_between_slopes, recover_discrete_constant_predictor_effect, recover_discrete_continuous_intercept_effect, recover_discrete_lag_from_log_rate, recover_discrete_lagged_latent_covariance, - recover_discrete_latent_mean, recover_discrete_latent_mean_with_impulse, - recover_discrete_latent_mean_with_impulse_carry, + recover_discrete_latent_mean, recover_discrete_latent_mean_with_extra_process, + recover_discrete_latent_mean_with_impulse, recover_discrete_latent_mean_with_impulse_carry, recover_discrete_latent_mean_with_initial_time_dependent_predictor, recover_discrete_latent_mean_with_initial_time_independent_predictor, recover_discrete_latent_mean_with_time_independent_predictor, recover_discrete_latent_variance, - recover_discrete_observed_mean, recover_discrete_observed_mean_with_impulse, - recover_discrete_observed_mean_with_impulse_carry, + recover_discrete_observed_mean, recover_discrete_observed_mean_with_extra_process, + recover_discrete_observed_mean_with_impulse, recover_discrete_observed_mean_with_impulse_carry, recover_discrete_observed_mean_with_initial_time_dependent_predictor, recover_discrete_observed_mean_with_initial_time_independent_predictor, recover_discrete_observed_mean_with_time_independent_predictor, recover_discrete_process_noise, @@ -34,15 +34,19 @@ use psychometric_core::{ refuse_continuous_intercept_as_discrete_mean_increment, refuse_continuous_intercept_as_initial_latent_mean, refuse_continuous_intercept_as_manifest_means, refuse_difference_quotient_as_local_rate, + refuse_evolved_observed_mean_as_extra_process_observed_mean, refuse_evolved_observed_mean_as_impulse_carry_observed_mean, refuse_evolved_observed_mean_as_impulse_observed_mean, refuse_evolved_observed_mean_as_initial_time_dependent_observed_mean, refuse_evolved_observed_mean_as_initial_time_independent_observed_mean, refuse_evolved_observed_mean_as_time_independent_observed_mean, + refuse_extra_process_contribution_as_observed_mean, + refuse_extra_process_latent_mean_as_observed_mean, refuse_finite_interval_process_noise_as_stationary_variance, refuse_impulse_carry_observed_mean_as_initial_time_dependent_observed_mean, refuse_impulse_carry_observed_mean_as_initial_time_independent_observed_mean, refuse_impulse_carry_observed_mean_as_time_independent_observed_mean, + refuse_impulse_observed_mean_as_extra_process_observed_mean, refuse_impulse_observed_mean_as_impulse_carry_observed_mean, refuse_impulse_observed_mean_as_initial_time_dependent_observed_mean, refuse_impulse_observed_mean_as_initial_time_independent_observed_mean, @@ -3714,3 +3718,246 @@ fn extra_process_contribution_refuses_nonnegative_extra_drift_clock_and_overflow Ok(0.0) ); } + +#[test] +fn extra_process_observed_mean_recovers_driver_equation_five_of_section_seven_point_two() { + let loading = 1.0_f64; + let coupling = 0.569_907_f64; + let predictor = 1.0_f64; + let original = -0.1393_f64; + let extra = -0.000_001_f64; + let delta = 1.0_f64; + let initial = 0.0_f64; + let intercept = 0.0_f64; + let manifest_mean = 0.5_f64; + let observed = recover_discrete_observed_mean_with_extra_process( + loading, + initial, + original, + intercept, + coupling, + predictor, + extra, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-extra-process-mean"); + let composed = recover_discrete_latent_mean_with_extra_process( + initial, + original, + intercept, + coupling, + predictor, + extra, + delta, + LagClock::EventTime, + ) + .expect("extra-latent"); + let contribution = recover_level_change_extra_process_contribution( + coupling, + predictor, + original, + extra, + delta, + LagClock::EventTime, + ) + .expect("extra-process"); + let expected = manifest_mean + loading * composed; + let error = rmse(&[expected], &[observed]); + assert!( + error < 1e-15, + "Driver Eq. 5 of §7.2 extra-process contribution RMSE {error}: got {observed}" + ); + let evolved_observed = recover_discrete_observed_mean( + loading, + initial, + original, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq3-eq5-mean"); + assert!( + rmse(&[expected], &[evolved_observed]) > error, + "τ + λ μ_t is not extra-process E(y_t)" + ); + let impulse_observed = recover_discrete_observed_mean_with_impulse( + loading, + initial, + original, + intercept, + coupling, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-impulse-mean"); + assert!( + rmse(&[expected], &[impulse_observed]) > error, + "τ + λ(μ_t + m x) is not extra-process E(y_t)" + ); + assert!(rmse(&[expected], &[manifest_mean]) > error); + assert!(rmse(&[expected], &[composed]) > error); + assert!(rmse(&[expected], &[contribution]) > error); + assert_eq!( + refuse_evolved_observed_mean_as_extra_process_observed_mean(evolved_observed, observed), + Err(PsychometricError::EvolvedObservedMeanIsNotExtraProcessObservedMean) + ); + assert_eq!( + refuse_impulse_observed_mean_as_extra_process_observed_mean(impulse_observed, observed), + Err(PsychometricError::ImpulseObservedMeanIsNotExtraProcessObservedMean) + ); + assert_eq!( + refuse_extra_process_contribution_as_observed_mean(contribution, observed), + Err(PsychometricError::ExtraProcessContributionIsNotObservedMean) + ); + assert_eq!( + refuse_extra_process_latent_mean_as_observed_mean(composed, observed), + Err(PsychometricError::ExtraProcessLatentMeanIsNotObservedMean) + ); +} + +#[test] +fn extra_process_observed_mean_zero_loading_is_manifest_mean() { + let coupling = 0.4_f64; + let predictor = 3.0_f64; + let original = -0.5_f64; + let extra = -0.05_f64; + let delta = 2.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let zero_loading = recover_discrete_observed_mean_with_extra_process( + 0.0, + initial, + original, + intercept, + coupling, + predictor, + extra, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("zero-loading"); + assert!( + rmse(&[manifest_mean], &[zero_loading]) < 1e-15, + "zero original-indicator loading is τ: got {zero_loading}" + ); + let extra_loading_zero = recover_manifest_observed_mean(0.0, 12.0, manifest_mean) + .expect("extra-process-lambda-zero"); + let original_observed = recover_discrete_observed_mean_with_extra_process( + 2.0, + initial, + original, + intercept, + coupling, + predictor, + extra, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("original-indicator"); + assert!( + rmse(&[extra_loading_zero], &[original_observed]) > 1e-3, + "printed extra-process LAMBDA 0 is τ, not original-indicator E(y_t)" + ); + let zero_coupling = recover_discrete_observed_mean_with_extra_process( + 2.0, + initial, + original, + intercept, + 0.0, + predictor, + extra, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("zero-coupling"); + let evolved_observed = recover_discrete_observed_mean( + 2.0, + initial, + original, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq3-eq5-mean"); + assert!(rmse(&[evolved_observed], &[zero_coupling]) < 1e-15); +} + +#[test] +fn extra_process_observed_mean_refuses_clock_nonpositive_interval_and_nonnegative_drift() { + let coupling = 0.4_f64; + let predictor = 3.0_f64; + let original = -0.5_f64; + let extra = -0.05_f64; + let delta = 2.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + assert_eq!( + recover_discrete_observed_mean_with_extra_process( + 2.0, + initial, + original, + intercept, + coupling, + predictor, + extra, + manifest_mean, + delta, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_discrete_observed_mean_with_extra_process( + 2.0, + initial, + original, + intercept, + coupling, + predictor, + 0.0, + manifest_mean, + delta, + LagClock::EventTime + ), + Err(PsychometricError::LevelChangeExtraProcessRequiresNegativeDrift) + ); + assert_eq!( + recover_discrete_observed_mean_with_extra_process( + 2.0, + initial, + original, + intercept, + coupling, + predictor, + extra, + manifest_mean, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_latent_mean_with_extra_process( + 1e308, + 0.0, + 0.0, + 1e308, + 1.0, + extra, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); +} diff --git a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs index da6179b6..014e062e 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -5,13 +5,13 @@ use psychometric_core::{ ordinary_least_squares_slope, posterior_draw_point_estimate_mean, recover_cluster_mean_within_between_slopes, recover_discrete_constant_predictor_effect, recover_discrete_continuous_intercept_effect, recover_discrete_lagged_latent_covariance, - recover_discrete_latent_mean, recover_discrete_latent_mean_with_impulse, - recover_discrete_latent_mean_with_impulse_carry, + recover_discrete_latent_mean, recover_discrete_latent_mean_with_extra_process, + recover_discrete_latent_mean_with_impulse, recover_discrete_latent_mean_with_impulse_carry, recover_discrete_latent_mean_with_initial_time_dependent_predictor, recover_discrete_latent_mean_with_initial_time_independent_predictor, recover_discrete_latent_mean_with_time_independent_predictor, recover_discrete_latent_variance, - recover_discrete_observed_mean, recover_discrete_observed_mean_with_impulse, - recover_discrete_observed_mean_with_impulse_carry, + recover_discrete_observed_mean, recover_discrete_observed_mean_with_extra_process, + recover_discrete_observed_mean_with_impulse, recover_discrete_observed_mean_with_impulse_carry, recover_discrete_observed_mean_with_initial_time_dependent_predictor, recover_discrete_observed_mean_with_initial_time_independent_predictor, recover_discrete_observed_mean_with_time_independent_predictor, recover_discrete_process_noise, @@ -30,15 +30,19 @@ use psychometric_core::{ refuse_continuous_intercept_as_discrete_mean_increment, refuse_continuous_intercept_as_initial_latent_mean, refuse_continuous_intercept_as_manifest_means, + refuse_evolved_observed_mean_as_extra_process_observed_mean, refuse_evolved_observed_mean_as_impulse_carry_observed_mean, refuse_evolved_observed_mean_as_impulse_observed_mean, refuse_evolved_observed_mean_as_initial_time_dependent_observed_mean, refuse_evolved_observed_mean_as_initial_time_independent_observed_mean, refuse_evolved_observed_mean_as_time_independent_observed_mean, + refuse_extra_process_contribution_as_observed_mean, + refuse_extra_process_latent_mean_as_observed_mean, refuse_finite_interval_process_noise_as_stationary_variance, refuse_impulse_carry_observed_mean_as_initial_time_dependent_observed_mean, refuse_impulse_carry_observed_mean_as_initial_time_independent_observed_mean, refuse_impulse_carry_observed_mean_as_time_independent_observed_mean, + refuse_impulse_observed_mean_as_extra_process_observed_mean, refuse_impulse_observed_mean_as_impulse_carry_observed_mean, refuse_impulse_observed_mean_as_initial_time_dependent_observed_mean, refuse_impulse_observed_mean_as_initial_time_independent_observed_mean, @@ -1813,3 +1817,103 @@ fn extra_process_contribution_is_not_cint_rewrite_increment_or_impulse() { Err(psychometric_core::PsychometricError::LevelChangeExtraProcessIsNotIncrement) ); } + +#[test] +fn extra_process_observed_mean_is_not_evolved_mean_impulse_mean_or_contribution() { + let loading = 2.0_f64; + let coupling = 0.4_f64; + let predictor = 3.0_f64; + let original = -0.5_f64; + let extra = -0.05_f64; + let delta = 2.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let observed = recover_discrete_observed_mean_with_extra_process( + loading, + initial, + original, + intercept, + coupling, + predictor, + extra, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-extra-process-mean"); + let composed = recover_discrete_latent_mean_with_extra_process( + initial, + original, + intercept, + coupling, + predictor, + extra, + delta, + LagClock::EventTime, + ) + .expect("extra-latent"); + let contribution = recover_level_change_extra_process_contribution( + coupling, + predictor, + original, + extra, + delta, + LagClock::EventTime, + ) + .expect("extra-process"); + let evolved_observed = recover_discrete_observed_mean( + loading, + initial, + original, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq3-eq5-mean"); + let impulse_observed = recover_discrete_observed_mean_with_impulse( + loading, + initial, + original, + intercept, + coupling, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-impulse-mean"); + assert!( + (observed - evolved_observed).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 of §7.2): extra-process E(y_t) is not τ + λ μ_t" + ); + assert!( + (observed - impulse_observed).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 of §7.2): extra-process E(y_t) is not τ + λ(μ_t + m x)" + ); + assert!( + (observed - contribution).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 of §7.2): extra-process contribution is not E(y_t)" + ); + assert!( + (observed - composed).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 of §7.2): evolved-plus-contribution latent mean is not E(y_t)" + ); + assert_eq!( + refuse_evolved_observed_mean_as_extra_process_observed_mean(evolved_observed, observed), + Err(psychometric_core::PsychometricError::EvolvedObservedMeanIsNotExtraProcessObservedMean) + ); + assert_eq!( + refuse_impulse_observed_mean_as_extra_process_observed_mean(impulse_observed, observed), + Err(psychometric_core::PsychometricError::ImpulseObservedMeanIsNotExtraProcessObservedMean) + ); + assert_eq!( + refuse_extra_process_contribution_as_observed_mean(contribution, observed), + Err(psychometric_core::PsychometricError::ExtraProcessContributionIsNotObservedMean) + ); + assert_eq!( + refuse_extra_process_latent_mean_as_observed_mean(composed, observed), + Err(psychometric_core::PsychometricError::ExtraProcessLatentMeanIsNotObservedMean) + ); +} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 137c32d5..fe863e8e 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -28,7 +28,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | report template/section/copied/style/modality method effects | ADR 0004/0012; PRD/TRD | simulation truth factors implemented; estimator-side method model remains future | partial | | candidate K statistical/Pareto gates + blinded LLM review | ADR 0012; research | future `model_selection` | accepted-target | | compositional topic correlation / stable clustering | ADR 0005/0012; research | future `network_analysis` | accepted-target | -| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; §7.2 level-change `CINT` is `κ = −a m x` (`a < 0`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`; Eq. 3 of that setting is `(1 − e^{a Δt}) m x`); §7.2 extra-process contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, not the dissipating Dirac; `ε ≥ 0` fails closed), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z); full ESEM/DSEM remaining | partial | +| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; §7.2 level-change `CINT` is `κ = −a m x` (`a < 0`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`; Eq. 3 of that setting is `(1 − e^{a Δt}) m x`); §7.2 extra-process contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, not the dissipating Dirac; `ε ≥ 0` fails closed; Eq. 5 of that contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; extra `LAMBDA` is 0; `τ + λ μ_t` is not that observed mean), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z); full ESEM/DSEM remaining | partial | | CPU bounded multithreading + GPU/VRAM streaming/parity | ADR 0001/0006 | future `compute_backend` | accepted-target | | TDT detection/tracking vs CHRONOS schema/prediction/temporal consistency | ADR 0016; PRD/research | future `event_intelligence` | accepted-target | | evidence-bounded LLM interpretation | ADR 0010/0012; PRD | `tepp_api` router plus future `interpretation_gateway` | partial | diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index ba7c2649..25ed7bf8 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -1,7 +1,7 @@ # ADR 0005 — Posterior-aware ESEM/DSEM and structural interpretation **Decision status:** Accepted -**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), exact scalar contemporaneous `TDPREDEFFECT` impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; the §7.2 level-change form is not that impulse), exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of that Eq. 3 composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar time-independent `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle Eq. 14; `B` is not that discrete increment), exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of that Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of that carried latent mean; `τ + λ μ_t` is not that observed mean), exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; `T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`), exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; `T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand TD composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean), exact scalar §7.2 level-change `CINT` `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; `a < 0` so `−κ / a = m x`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2), exact scalar Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` (not `m x`, not `κ`, and not `TIPREDEFFECT`), exact scalar §7.2 extra near-zero-drift latent process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z; identification `TDPREDEFFECT` on the extra process is 1; extra `DRIFT` printed as `−0.000001`; precisely 0 causes computational problems; `ε = a` is `a_{ηξ} x Δt e^{a Δt}`; not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`; `ε ≥ 0` fails closed), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target +**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), exact scalar contemporaneous `TDPREDEFFECT` impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; the §7.2 level-change form is not that impulse), exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of that Eq. 3 composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar time-independent `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle Eq. 14; `B` is not that discrete increment), exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of that Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of that carried latent mean; `τ + λ μ_t` is not that observed mean), exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; `T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`), exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; `T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand TD composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean), exact scalar §7.2 level-change `CINT` `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; `a < 0` so `−κ / a = m x`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2), exact scalar Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` (not `m x`, not `κ`, and not `TIPREDEFFECT`), exact scalar §7.2 extra near-zero-drift latent process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z; identification `TDPREDEFFECT` on the extra process is 1; extra `DRIFT` printed as `−0.000001`; precisely 0 causes computational problems; `ε = a` is `a_{ηξ} x Δt e^{a Δt}`; not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`; `ε ≥ 0` fails closed), exact scalar observed mean of that extra-process contribution `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z; the extra process has `LAMBDA` 0 and is not an observed indicator; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target **Date:** 2026-08-05 **Supersedes:** None. ADR 0012 governs upstream topic measurement/network coordinates; this ADR governs higher-order psychometric structure and longitudinal interpretation. @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), recovers the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z; `TDPREDEFFECT` is `M`, not `CINT`; `M x` is not `A^{-1}[e^{A Δt} − I] B z`; `M x` is not Voelkle et al., 2012, Eq. 14; the level-change form is not that impulse), recovers the exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition; JSS PDF re-opened 2026-08-20T09:01Z; the evolved map `τ + λ μ_t` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-impulse latent mean is not `E(y_t)`), recovers the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z; `TIPREDEFFECT` is `B`, not `κ`; form `B z` first, then the discrete intercept map; a zero drift is `B z Δt`; `B` is not the discrete increment; `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle Eq. 14), recovers the exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; JSS PDF re-opened 2026-08-20T12:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-increment latent mean is not `E(y_t)`), recovers the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z; form `m x` first, then `e^{a(t−u)} m x`; a zero drift is `m x`; underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept; `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; an impulse at `u = t` is the contemporaneous map; an impulse at `u ≤ t0` is already in `η(t0)`), recovers the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; JSS PDF re-opened 2026-08-20T05:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the carried latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and its Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF opened 2026-08-20T15:14Z; form `t0_b z` first, then `e^{a Δt} t0_b z`; a zero drift is `t0_b z`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; `e^{A Δt} t0_b z` is not `t0_b z`; `T0TIPREDEFFECT` is the coefficient, not the shift), recovers the exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition; JSS PDF re-opened 2026-08-20T15:28Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and its Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; form `t0_m x0` first, then `e^{a Δt} t0_m x0`; a zero drift is `t0_m x0`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `T0TDPREDEFFECT` is the coefficient, not the shift; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), recovers the exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; the first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar §7.2 level-change `CINT` setting `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z; form `m x` first, then multiply by `−a`; `a < 0` so `−κ / a = m x`; `a ≥ 0` cannot hold a new process mean; `−a m x` is not the dissipating Dirac, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`; the extra near-zero-drift latent process also named in §7.2 is a different specification), recovers the exact scalar Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` (JSS PDF re-opened 2026-08-20T19:50Z; form the level-change `CINT` first, then the discrete intercept map; underflow of `e^{a Δt}` to `+0` keeps `m x`; `(1 − e^{a Δt}) m x` is not `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`), recovers the exact scalar §7.2 extra near-zero-drift latent process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z; form `a_{ηξ} x` first; `ε = a` is `a_{ηξ} x Δt e^{a Δt}`; a zero coupling or zero predictor is exactly zero; `ε ≥ 0` cannot hold a lasting extra state; that contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), recovers the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z; `TDPREDEFFECT` is `M`, not `CINT`; `M x` is not `A^{-1}[e^{A Δt} − I] B z`; `M x` is not Voelkle et al., 2012, Eq. 14; the level-change form is not that impulse), recovers the exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition; JSS PDF re-opened 2026-08-20T09:01Z; the evolved map `τ + λ μ_t` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-impulse latent mean is not `E(y_t)`), recovers the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z; `TIPREDEFFECT` is `B`, not `κ`; form `B z` first, then the discrete intercept map; a zero drift is `B z Δt`; `B` is not the discrete increment; `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle Eq. 14), recovers the exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; JSS PDF re-opened 2026-08-20T12:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-increment latent mean is not `E(y_t)`), recovers the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z; form `m x` first, then `e^{a(t−u)} m x`; a zero drift is `m x`; underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept; `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; an impulse at `u = t` is the contemporaneous map; an impulse at `u ≤ t0` is already in `η(t0)`), recovers the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; JSS PDF re-opened 2026-08-20T05:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the carried latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and its Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF opened 2026-08-20T15:14Z; form `t0_b z` first, then `e^{a Δt} t0_b z`; a zero drift is `t0_b z`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; `e^{A Δt} t0_b z` is not `t0_b z`; `T0TIPREDEFFECT` is the coefficient, not the shift), recovers the exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition; JSS PDF re-opened 2026-08-20T15:28Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and its Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; form `t0_m x0` first, then `e^{a Δt} t0_m x0`; a zero drift is `t0_m x0`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `T0TDPREDEFFECT` is the coefficient, not the shift; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), recovers the exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; the first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar §7.2 level-change `CINT` setting `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z; form `m x` first, then multiply by `−a`; `a < 0` so `−κ / a = m x`; `a ≥ 0` cannot hold a new process mean; `−a m x` is not the dissipating Dirac, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`; the extra near-zero-drift latent process also named in §7.2 is a different specification), recovers the exact scalar Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` (JSS PDF re-opened 2026-08-20T19:50Z; form the level-change `CINT` first, then the discrete intercept map; underflow of `e^{a Δt}` to `+0` keeps `m x`; `(1 − e^{a Δt}) m x` is not `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`), recovers the exact scalar §7.2 extra near-zero-drift latent process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z; form `a_{ηξ} x` first; `ε = a` is `a_{ηξ} x Δt e^{a Δt}`; a zero coupling or zero predictor is exactly zero; `ε ≥ 0` cannot hold a lasting extra state; that contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`), recovers the exact scalar observed mean of that extra-process contribution `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z; the extra process has `LAMBDA` 0 and is not an observed indicator; form the evolved-plus-contribution latent mean first, then `τ + λ` of that mean; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. 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 cec730fd..9286bf6c 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -35,15 +35,16 @@ This slice stays inside `psychometric_core`. It does not add a second invariance 29. recover the exact scalar §7.2 level-change `CINT` `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z; form `m x` first, then multiply by `−a`; `a < 0` so `−κ / a = m x`) and refuse treating `−a m x` as the dissipating Dirac `m x`, as a free `CINT`, or as `A^{-1}[e^{A Δt} − I] B z`; `a ≥ 0` cannot hold a new process mean; the extra near-zero-drift latent process also named in §7.2 is a different specification; 30. recover the exact scalar Eq. 3 increment of that level-change `CINT` `(1 − e^{a Δt}) m x` (Driver et al., 2017, §7.2, pp. 20–21; Eq. 3, pp. 4–5; JSS PDF re-opened 2026-08-20T19:50Z; form `κ = −a m x` first, then `A^{-1}[e^{A Δt} − I] κ`; underflow of `e^{a Δt}` to `+0` keeps `m x`) and refuse treating `(1 − e^{a Δt}) m x` as `m x`, as `κ`, or as `A^{-1}[e^{A Δt} − I] B z`; 31. recover the exact scalar §7.2 extra near-zero-drift latent process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z; identification `TDPREDEFFECT` on the extra process is 1; extra `DRIFT` printed as `−0.000001`; precisely 0 causes computational problems; `ε = a` is `a_{ηξ} x Δt e^{a Δt}`) and refuse treating that contribution as `κ = −a m x`, as `(1 − e^{a Δt}) m x`, or as the dissipating Dirac `m x`; `ε ≥ 0` fails closed; -32. refuse pooling discrete lags from unequal event intervals as one coefficient; -33. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); -34. refuse the difference quotient as a continuous-time rate; -35. apply the same event-time map to CWC residuals (still not DSEM); -36. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). +32. recover the exact scalar observed mean of that extra-process contribution `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z; the extra process has `LAMBDA` 0 and is not an observed indicator; form the evolved-plus-contribution latent mean first, then `τ + λ` of that mean) and refuse treating `τ + λ μ_t`, `τ + λ(μ_t + m x)`, the contribution, or the evolved-plus-contribution latent mean as `E(y_t)`; +33. refuse pooling discrete lags from unequal event intervals as one coefficient; +34. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); +35. refuse the difference quotient as a continuous-time rate; +36. apply the same event-time map to CWC residuals (still not DSEM); +37. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). ## Claim boundary -This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. The contemporaneous `TDPREDEFFECT` impulse is not the continuous intercept, not the time-independent discrete effect, and not Voelkle et al. (2012, Eq. 14). The time-independent `TIPREDEFFECT` increment is not the continuous intercept, not the contemporaneous impulse, not Voelkle et al. (2012, Eq. 14), and not the coefficient `B`. The within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). The evolved observed mean `τ + λ μ_t` is not the contemporaneous-impulse observed mean `τ + λ(μ_t + m x)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not the impulse-carry observed mean `τ + λ(μ_t + e^{a(t−u)} m x)` when `u ≠ t`. The evolved observed mean `τ + λ μ_t` is not the impulse-carry observed mean. The carried latent mean is not `E(y_t)`. The evolved-plus-impulse latent mean is not `E(y_t)`. The evolved observed mean `τ + λ μ_t` is not the time-independent-predictor observed mean `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not that time-independent-predictor observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that time-independent-predictor observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TIPREDEFFECT` shift `t0_b z` is not the Eq. 3 process increment `A^{-1}[e^{A Δt} − I] B z`, not `κ`, and not `M x`. The Eq. 3 first-summand carry `e^{A Δt} t0_b z` is not `t0_b z` and is not that process increment. `T0TIPREDEFFECT` is the coefficient, not the first-occasion shift. The evolved observed mean `τ + λ μ_t` is not the first-occasion TI-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_b z)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion observed mean when `u ≠ t0`. The evolved-plus-T0TIPRED latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TDPREDEFFECT` shift `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `κ`. The Eq. 3 first-summand carry `e^{A Δt} t0_m x0` is not `t0_m x0` and is not that within-interval impulse carry. `T0TDPREDEFFECT` is the coefficient, not the first-occasion shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The evolved observed mean `τ + λ μ_t` is not the first-occasion TD-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_m x0)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion TD observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion TD observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion TD observed mean when `u ≠ t0`. The first-occasion TI composition `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that first-occasion TD observed mean. Same numbers as `T0TIPREDEFFECT` yield the same product; Table 3 names a different matrix. The evolved-plus-T0TDPRED latent mean is not `E(y_t)`. The §7.2 level-change `CINT` `κ = −a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. Lasting level change via that `CINT` setting requires `a < 0`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not that `CINT` setting. The Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` is not the dissipating Dirac `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`. Underflow of `e^{a Δt}` to `+0` keeps the equilibrium offset `m x`. The §7.2 extra-process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. Lasting level change via that extra process requires `ε < 0`. Precisely `ε = 0` causes computational problems in the printed specification. +This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. The contemporaneous `TDPREDEFFECT` impulse is not the continuous intercept, not the time-independent discrete effect, and not Voelkle et al. (2012, Eq. 14). The time-independent `TIPREDEFFECT` increment is not the continuous intercept, not the contemporaneous impulse, not Voelkle et al. (2012, Eq. 14), and not the coefficient `B`. The within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). The evolved observed mean `τ + λ μ_t` is not the contemporaneous-impulse observed mean `τ + λ(μ_t + m x)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not the impulse-carry observed mean `τ + λ(μ_t + e^{a(t−u)} m x)` when `u ≠ t`. The evolved observed mean `τ + λ μ_t` is not the impulse-carry observed mean. The carried latent mean is not `E(y_t)`. The evolved-plus-impulse latent mean is not `E(y_t)`. The evolved observed mean `τ + λ μ_t` is not the time-independent-predictor observed mean `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not that time-independent-predictor observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that time-independent-predictor observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TIPREDEFFECT` shift `t0_b z` is not the Eq. 3 process increment `A^{-1}[e^{A Δt} − I] B z`, not `κ`, and not `M x`. The Eq. 3 first-summand carry `e^{A Δt} t0_b z` is not `t0_b z` and is not that process increment. `T0TIPREDEFFECT` is the coefficient, not the first-occasion shift. The evolved observed mean `τ + λ μ_t` is not the first-occasion TI-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_b z)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion observed mean when `u ≠ t0`. The evolved-plus-T0TIPRED latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TDPREDEFFECT` shift `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `κ`. The Eq. 3 first-summand carry `e^{A Δt} t0_m x0` is not `t0_m x0` and is not that within-interval impulse carry. `T0TDPREDEFFECT` is the coefficient, not the first-occasion shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The evolved observed mean `τ + λ μ_t` is not the first-occasion TD-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_m x0)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion TD observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion TD observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion TD observed mean when `u ≠ t0`. The first-occasion TI composition `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that first-occasion TD observed mean. Same numbers as `T0TIPREDEFFECT` yield the same product; Table 3 names a different matrix. The evolved-plus-T0TDPRED latent mean is not `E(y_t)`. The §7.2 level-change `CINT` `κ = −a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. Lasting level change via that `CINT` setting requires `a < 0`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not that `CINT` setting. The Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` is not the dissipating Dirac `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`. Underflow of `e^{a Δt}` to `+0` keeps the equilibrium offset `m x`. The §7.2 extra-process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. Lasting level change via that extra process requires `ε < 0`. Precisely `ε = 0` causes computational problems in the printed specification. The extra-process observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` is not `τ + λ μ_t`, not `τ + λ(μ_t + m x)`, not the contribution, and not the evolved-plus-contribution latent mean. The extra process has `LAMBDA` 0 and is not an observed indicator. ## Authoritative sources @@ -61,7 +62,7 @@ Kish, L. (1965). *Survey sampling*. John Wiley & Sons. Oud, J. H. L., & Jansen, R. A. R. G. (2000). Continuous time state space modeling of panel data by means of SEM. *Psychometrika, 65*(2), 199–215. https://doi.org/10.1007/BF02294374 (cited by Voelkle et al., 2012, Eq. 14 discussion; PDF not opened). -The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:07Z (https://www.zora.uzh.ch/handle/20.500.14742/72792; bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`). Page 16 writes discrete auto-effects as \(A^{*}(\Delta t)=\exp(A\Delta t)\) (Eq. 7). Introducing Intercepts (manuscript p. 20, Eq. 12) adds a continuous-time intercept \(b\) and writes the expected-value solution whose discrete increment is \(A^{-1}(\exp(A\Delta t)-I)b\); the scalar constant-predictor effect is \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). The next paragraph (manuscript p. 21, Eq. 14) writes the discrete effect of a **time-varying** predictor, when the sampling interval equals the constancy interval, as \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product does not depend on the predictor auto-effect. The manuscript calls Eq. 14 a first-order approximation that deteriorates as \(\Delta t\) grows, and defers unmatched sampling/constancy intervals to Oud and Jansen (2000), which is unread. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4; JSS PDF opened 2026-08-20T15:14Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\), the discrete intercept \(b_{\Delta t}=A^{-1}[A_{\Delta t}-I]b\), and the discrete process-noise covariance \(Q_{\Delta t}=\int_{0}^{\Delta t}\operatorname{expm}(A(\Delta t-\tau))LGG^{\top}L^{\top}\operatorname{expm}(A(\Delta t-\tau))^{\top}\,d\tau\). Equation 4 (p. 5) writes that the integral exhibits covariance \(Q_{\Delta t}=\operatorname{irow}(A^{\#^{-1}}[e^{A^{\#}\Delta t}-I]\operatorname{row}(Q))\) with \(A^{\#}=A\otimes I+I\otimes A\). The homogeneous-process consequence (`ξ`, `z` given) is \(Q_{\Delta t}=\operatorname{cov}(\eta_{ti}\mid\eta_{t-1,i})\) and \(\operatorname{cov}(\eta_{ti},\eta_{t-1,i})=A_{\Delta t}\operatorname{cov}(\eta_{t-1,i})\). The law of total variance on that pair is \(\operatorname{Var}(\eta_{ti})=A_{\Delta t}\operatorname{Var}(\eta_{t-1,i})A_{\Delta t}^{\top}+Q_{\Delta t}\). As \(\Delta t\to\infty\) with stable \(a<0\), Eq. 4 becomes \(-q/(2a)\). The JSS summary names that limit `asymDIFFUSION` and takes it as the total within-subject variance (p. 16). Section 4.3 (pp. 9–10) constrains a stationary `T0VAR` to that model-predicted variance and distinguishes it from a predetermined first occasion. The same section (p. 9) adds a stable trait process with `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is that time-invariant between-subject variance. Table 3 (p. 13) names `T0TIPREDEFFECT` the effect of time-independent predictors on latents at `T0` and names `TIPREDEFFECT` `B` separately. The JSS article has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). This slice takes scalar \(L=1\) (every latent subject to system noise); it does not implement the 0/1 selector matrix and is not a Kalman filter. Discrete auto-effects are strictly positive for finite real drift and interval; the inverse \(a=\ln\varphi/\Delta t\) therefore requires \(\varphi>0\). Binary64 `exp` of a large negative argument is `+0` and is refused as a discrete lag; the same underflow is a vanishing lagged covariance and is kept. Equations 3–4 of Voelkle et al. (2012) are the discouraged difference-quotient approximation. Meredith (1993) remains unread (Unpaywall/OpenAlex 2026-08-20T15:14Z: closed; Springer `content/pdf` is HTML 200, not a PDF; archive.org title search empty). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex 2026-08-20T15:14Z: closed; Springer `content/pdf` is HTML 200; ETS landing page is HTML). ERIC ED334221 is Singer and Willett (1991), *From whether to when*, not the 1991 journal article. ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title is not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex 2026-08-18T21:07Z: closed; Radboud landing and bitstream 403). +The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:07Z (https://www.zora.uzh.ch/handle/20.500.14742/72792; bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`). Page 16 writes discrete auto-effects as \(A^{*}(\Delta t)=\exp(A\Delta t)\) (Eq. 7). Introducing Intercepts (manuscript p. 20, Eq. 12) adds a continuous-time intercept \(b\) and writes the expected-value solution whose discrete increment is \(A^{-1}(\exp(A\Delta t)-I)b\); the scalar constant-predictor effect is \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). The next paragraph (manuscript p. 21, Eq. 14) writes the discrete effect of a **time-varying** predictor, when the sampling interval equals the constancy interval, as \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product does not depend on the predictor auto-effect. The manuscript calls Eq. 14 a first-order approximation that deteriorates as \(\Delta t\) grows, and defers unmatched sampling/constancy intervals to Oud and Jansen (2000), which is unread. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4; JSS PDF opened 2026-08-20T15:14Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\), the discrete intercept \(b_{\Delta t}=A^{-1}[A_{\Delta t}-I]b\), and the discrete process-noise covariance \(Q_{\Delta t}=\int_{0}^{\Delta t}\operatorname{expm}(A(\Delta t-\tau))LGG^{\top}L^{\top}\operatorname{expm}(A(\Delta t-\tau))^{\top}\,d\tau\). Equation 4 (p. 5) writes that the integral exhibits covariance \(Q_{\Delta t}=\operatorname{irow}(A^{\#^{-1}}[e^{A^{\#}\Delta t}-I]\operatorname{row}(Q))\) with \(A^{\#}=A\otimes I+I\otimes A\). The homogeneous-process consequence (`ξ`, `z` given) is \(Q_{\Delta t}=\operatorname{cov}(\eta_{ti}\mid\eta_{t-1,i})\) and \(\operatorname{cov}(\eta_{ti},\eta_{t-1,i})=A_{\Delta t}\operatorname{cov}(\eta_{t-1,i})\). The law of total variance on that pair is \(\operatorname{Var}(\eta_{ti})=A_{\Delta t}\operatorname{Var}(\eta_{t-1,i})A_{\Delta t}^{\top}+Q_{\Delta t}\). As \(\Delta t\to\infty\) with stable \(a<0\), Eq. 4 becomes \(-q/(2a)\). The JSS summary names that limit `asymDIFFUSION` and takes it as the total within-subject variance (p. 16). Section 4.3 (pp. 9–10) constrains a stationary `T0VAR` to that model-predicted variance and distinguishes it from a predetermined first occasion. The same section (p. 9) adds a stable trait process with `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is that time-invariant between-subject variance. Table 3 (p. 13) names `T0TIPREDEFFECT` the effect of time-independent predictors on latents at `T0` and names `TIPREDEFFECT` `B` separately. The JSS article has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). This slice takes scalar \(L=1\) (every latent subject to system noise); it does not implement the 0/1 selector matrix and is not a Kalman filter. Discrete auto-effects are strictly positive for finite real drift and interval; the inverse \(a=\ln\varphi/\Delta t\) therefore requires \(\varphi>0\). Binary64 `exp` of a large negative argument is `+0` and is refused as a discrete lag; the same underflow is a vanishing lagged covariance and is kept. Equations 3–4 of Voelkle et al. (2012) are the discouraged difference-quotient approximation. Meredith (1993) remains unread (Unpaywall/OpenAlex 2026-08-21T06:24Z: closed; Springer `content/pdf` is HTML 200, not a PDF; archive.org title search empty). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex 2026-08-21T06:24Z: closed; Springer `content/pdf` is HTML 200; ETS landing page is HTML). ERIC ED334221 is Singer and Willett (1991), *From whether to when*, not the 1991 journal article. ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title is not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex 2026-08-18T21:07Z: closed; Radboud landing and bitstream 403). ## Formula notes @@ -94,6 +95,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - **First-occasion time-dependent-predictor observed-indicator mean.** Driver et al. (2017, Eq. 5, p. 5; Eq. 3 first summand, p. 5; Table 3, p. 13; JSS PDF re-opened 2026-08-20T19:07Z): `y_i(t)=Γ+Λη_i(t)+ζ_i(t)` with `ζ∼N(0,Θ)` and `Γ∼N(τ,Ψ)`. The latent process at `t` after the Table 3 first-occasion TD carry is `μ_t+e^{aΔt}t0_m x0`. The scalar composition is `E(y_t)=τ+λ(μ_t+e^{aΔt}t0_m x0)`. Form the evolved-plus-carry latent mean first, then `τ+λ` of that mean. A zero loading is exactly `τ`. A zero evolved-plus-carry latent mean is exactly `τ`. A zero intercept is exactly `λ(μ_t+carry)`. The evolved observed mean `τ+λμ_t` is not this composition. The process-increment map `τ+λ(μ_t+A^{-1}[e^{AΔt}−I]Bz)` is not this composition. The contemporaneous map `τ+λ(μ_t+mx)` is not this composition. The impulse-carry map `τ+λ(μ_t+e^{a(t−u)}mx)` is not this composition when `u≠t0`. The first-occasion TI map `τ+λ(μ_t+e^{aΔt}t0_b z)` is not this composition. Same numbers as `T0TIPREDEFFECT` yield the same product; Table 3 names a different matrix. `MANIFESTMEANS` is not `E(y_t)`. The evolved-plus-carry latent mean is not `E(y_t)`. `T0TDPREDEFFECT` is the coefficient, not that observed mean. An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **Level-change continuous intercept.** Driver et al. (2017, §7.2, pp. 20–21; Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T19:45Z): a sudden Dirac dissipates back to the process mean. To generate a lasting level change, `CINT` is set to `TDPREDEFFECT * −DRIFT`. The scalar setting is `κ=−a m x`. Form `m x` first, then multiply by `−a`. Stable `a<0` is required so `−κ/a=m x` is an equilibrium offset. `a≥0` cannot hold a new process mean. A zero effect or zero predictor is exactly zero. `−a m x` is not `m x`, not a free `CINT`, and not `A^{-1}[e^{AΔt}−I]Bz`. The extra near-zero-drift latent process also named in §7.2 is a different specification. An overflowing product fails closed. This is not a Kalman filter and not ctsem estimation. - **Level-change extra process.** Driver et al. (2017, §7.2, pp. 22–23; Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T23:10Z): a lasting level change is specified by an extra latent process. `T0MEANS`, `CINT`, `T0VAR`, `DIFFUSION`, and `TRAITVAR` of that process are fixed to 0. `TDPREDEFFECT` on it is fixed to 1. Its `DRIFT` diagonal is very close to 0 (printed example `−0.000001`; precisely 0 causes computational problems). The original process is driven by the `DRIFT` coupling `a_{ηξ}`. After a unit identification impulse the scalar contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`. Form `a_{ηξ} x` first. When `ε = a` the contribution is `a_{ηξ} x Δt e^{a Δt}`. A zero coupling or zero predictor is exactly zero. `ε ≥ 0` fails closed. That contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. An overflowing product, exponential, or quotient fails closed. This is not a Kalman filter, not a matrix `expm`, and not ctsem estimation. +- **Extra-process observed-indicator mean.** Driver et al. (2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z): `y_i(t)=Γ+Λη_i(t)+ζ_i(t)` with `ζ∼N(0,Θ)` and `Γ∼N(τ,Ψ)`. The printed extra process has `LAMBDA` 0 and is not an observed indicator. Original indicators load on the original process after the `DRIFT` coupling. The latent process at `t` after that contribution is `μ_t+a_{ηξ}x(e^{εΔt}−e^{aΔt})/(ε−a)`. The scalar composition is `E(y_t)=τ+λ(μ_t+a_{ηξ}x(e^{εΔt}−e^{aΔt})/(ε−a))`. Form the evolved-plus-contribution latent mean first, then `τ+λ` of that mean. A zero loading is exactly `τ`. A zero evolved-plus-contribution latent mean is exactly `τ`. A zero intercept is exactly `λ` of that latent mean. The evolved observed mean `τ+λμ_t` is not this composition. The contemporaneous map `τ+λ(μ_t+mx)` is not this composition. `MANIFESTMEANS` is not `E(y_t)`. The extra-process contribution is not `E(y_t)`. The evolved-plus-contribution latent mean is not `E(y_t)`. The extra process itself is not an observed indicator. An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **Level-change discrete increment.** Driver et al. (2017, §7.2, pp. 20–21; Eq. 3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T19:50Z): Equation 3 maps `CINT` through `A^{-1}[e^{AΔt}−I]κ`. With `κ=−a m x` the scalar increment is `(e^{aΔt}−1)/a·(−a m x)=(1−e^{aΔt})m x`. Form the level-change `CINT` first, then the discrete intercept map. Underflow of `e^{aΔt}` to `+0` keeps `m x`. A zero effect or zero predictor is exactly zero. `(1−e^{aΔt})m x` is not `m x`, not `κ`, and not `A^{-1}[e^{AΔt}−I]Bz`. An overflowing product or increment fails closed. This is not a Kalman filter and not ctsem estimation. - **CWC-then-lag.** Sample cluster means are removed first. Consecutive within residuals are then fitted by least squares to \(r_{t+\Delta t}\approx\exp(a\Delta t)\,r_{t}\) on event time. Same-sign pair-wise logs initialize the scalar Newton step. Sign-flipping \(T=2\) CWC pairs have no real logarithm and fail closed. Curran and Bauer (2011, pp. 607–608) show that this person-mean subtraction on a raw autoregressive series does **not** isolate the lagged within-person effect; the helper therefore does not claim to recover the raw-process drift. - **Already-centered irregular residual.** The caller supplies lagged within residuals. The mean of \(a=\ln(r_{t+\Delta t}/r_t)/\Delta t\) is the exact scalar map. Intervals may be irregular. The helper does not center again. This is not DSEM. @@ -130,6 +132,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - Driver et al. (2017, §7.2) recovers a known level-change `CINT` \(\kappa=-a\,mx\) at machine-scale RMSE, and that RMSE is smaller than treating the dissipating Dirac \(mx\), a free `CINT`, or `TIPREDEFFECT` as that setting; \(-\kappa/a\) recovers \(mx\) when \(a<0\); \(a\ge 0\) with a nonzero impulse fails closed; a zero effect or zero predictor is exactly zero; an overflowing product fails closed; - Driver et al. (2017, §7.2 / Eq. 3) recovers a known level-change increment \((1-e^{a\Delta t})mx\) at machine-scale RMSE, and that RMSE is smaller than treating the dissipating Dirac \(mx\), the intercept \(\kappa\), or `TIPREDEFFECT` as that increment; underflow of \(e^{a\Delta t}\) to `+0` keeps \(mx\); \(a\ge 0\) with a nonzero impulse fails closed; a non-event clock, a non-positive interval, and an overflowing product fail closed; - Driver et al. (2017, §7.2, pp. 22–23) recovers a known extra-process contribution \(a_{\eta\xi}x(e^{\varepsilon\Delta t}-e^{a\Delta t})/(\varepsilon-a)\) at machine-scale RMSE, and that RMSE is smaller than treating \(\kappa=-amx\), \((1-e^{a\Delta t})mx\), or the dissipating Dirac \(mx\) as that contribution; \(\varepsilon=a\) is \(a_{\eta\xi}x\Delta t\,e^{a\Delta t}\); \(\varepsilon\ge 0\) with a nonzero contribution fails closed; a zero coupling or zero predictor is exactly zero; a non-event clock, a non-positive interval, and an overflowing product fail closed; +- Driver et al. (2017, Eq. 5 of the §7.2 extra-process contribution; JSS PDF re-opened 2026-08-21T06:12Z) recovers a known \(E(y_t)=\tau+\lambda(\mu_t+a_{\eta\xi}x(e^{\varepsilon\Delta t}-e^{a\Delta t})/(\varepsilon-a))\) at machine-scale RMSE, and that RMSE is smaller than treating \(\tau+\lambda\mu_t\), \(\tau+\lambda(\mu_t+mx)\), the contribution, `MANIFESTMEANS`, or the evolved-plus-contribution latent mean as \(E(y_t)\); the extra process has `LAMBDA` 0 and is not an observed indicator; a zero original-indicator loading is \(\tau\); a zero coupling recovers \(\tau+\lambda\mu_t\); \(\varepsilon\ge 0\) with a nonzero contribution fails closed; a non-event clock, a non-positive interval, and an overflowing product or sum fail closed; - pooling discrete lags from unequal intervals fails closed; - CWC-then-lag on a two-cluster decaying series has smaller computed RMSE than a level-pooled series when the latter is identified; - already-centered irregular residuals recover a known drift at machine-scale RMSE, and that RMSE is smaller than CWC of the corresponding raw autoregressive series (Curran & Bauer, 2011, pp. 607–608); diff --git a/docs/validation/temporal-event-foundation.md b/docs/validation/temporal-event-foundation.md index 3cba5afa..1d344c3a 100644 --- a/docs/validation/temporal-event-foundation.md +++ b/docs/validation/temporal-event-foundation.md @@ -23,7 +23,7 @@ This report tracks exact-head scientific and engineering evidence required befor | Truth corpora / manifests | `tepp_simulation` | implemented-main | — | deterministic generator tests | Task 10 / PR #18 | | Recovery metrics | `validation_core` | implemented-main | — | RMSE/bias/coverage/MC gates | Task 11 / PR #19 | | Versioned API/export contracts | `tepp_api` | implemented-main | naruon HTTP interchange | unknown-field/version/limit + naruon HTTPS interchange tests | Task 12 / PR #21; live HTTP service remaining | -| Psychometric structural input gates | `psychometric_core` | partial | stacked psychometric PR | construct-class refusal + ALR/ILR boundary + true-loading RMSE + posterior-draw point-estimate mean + Rubin `T` + CWC within/between + CWC contextual effect + event-time log-rate + constant- and time-varying-predictor discrete effects + exact scalar discrete process noise + lagged latent covariance and unconditional latent variance + stationary within-subject variance + trait-plus-state variance + observed-indicator variance + discrete latent mean (`T0MEANS`/`CINT`) + evolved observed mean (`τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`) + contemporaneous `TDPREDEFFECT` impulse (`m x`; not `CINT`, not `TIPREDEFFECT`, not Voelkle Eq. 14) + Eq. 5 of that contemporaneous impulse (`τ + λ(μ_t + m x)`; `τ + λ μ_t` is not that observed mean) + time-independent `TIPREDEFFECT` increment (`A^{-1}[e^{A Δt} − I] B z`; not `CINT`, not `M x`, not Voelkle Eq. 14, not the coefficient `B`) + Eq. 5 of that increment (`τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`; `τ + λ μ_t` is not that observed mean) + within-interval `TDPREDEFFECT` carry (`e^{A(t−u)} M x` for `t0 < u < t`; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, not Voelkle Eq. 14) + Eq. 5 of that carry (`τ + λ(μ_t + e^{a(t−u)} m x)`; `τ + λ μ_t` is not that observed mean) + §7.2 level-change `CINT` (`κ = −a m x`; Eq. 3 increment `(1 − e^{a Δt}) m x`) + §7.2 extra-process contribution (`a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; not `κ`, not the increment, not the Dirac; `ε ≥ 0` fails closed) + irregular already-centered residual lag + strong-gated latent means (n=2 residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016); full ESEM/DSEM remaining | ADR 0005; `docs/research/posterior-esem-input-gates.md`; `docs/research/multilevel-event-time-recovery.md`; `docs/research/rubin-total-variance.md`; `docs/research/strong-invariance-latent-means.md` | +| Psychometric structural input gates | `psychometric_core` | partial | stacked psychometric PR | construct-class refusal + ALR/ILR boundary + true-loading RMSE + posterior-draw point-estimate mean + Rubin `T` + CWC within/between + CWC contextual effect + event-time log-rate + constant- and time-varying-predictor discrete effects + exact scalar discrete process noise + lagged latent covariance and unconditional latent variance + stationary within-subject variance + trait-plus-state variance + observed-indicator variance + discrete latent mean (`T0MEANS`/`CINT`) + evolved observed mean (`τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`) + contemporaneous `TDPREDEFFECT` impulse (`m x`; not `CINT`, not `TIPREDEFFECT`, not Voelkle Eq. 14) + Eq. 5 of that contemporaneous impulse (`τ + λ(μ_t + m x)`; `τ + λ μ_t` is not that observed mean) + time-independent `TIPREDEFFECT` increment (`A^{-1}[e^{A Δt} − I] B z`; not `CINT`, not `M x`, not Voelkle Eq. 14, not the coefficient `B`) + Eq. 5 of that increment (`τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`; `τ + λ μ_t` is not that observed mean) + within-interval `TDPREDEFFECT` carry (`e^{A(t−u)} M x` for `t0 < u < t`; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, not Voelkle Eq. 14) + Eq. 5 of that carry (`τ + λ(μ_t + e^{a(t−u)} m x)`; `τ + λ μ_t` is not that observed mean) + §7.2 level-change `CINT` (`κ = −a m x`; Eq. 3 increment `(1 − e^{a Δt}) m x`) + §7.2 extra-process contribution (`a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; not `κ`, not the increment, not the Dirac; `ε ≥ 0` fails closed) + Eq. 5 of that extra-process contribution (`τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; extra `LAMBDA` is 0; `τ + λ μ_t` is not that observed mean) + irregular already-centered residual lag + strong-gated latent means (n=2 residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016); full ESEM/DSEM remaining | ADR 0005; `docs/research/posterior-esem-input-gates.md`; `docs/research/multilevel-event-time-recovery.md`; `docs/research/rubin-total-variance.md`; `docs/research/strong-invariance-latent-means.md` | | Purpose-bound provider payloads | `tepp_api` | implemented-main | provider-payload minimization | expired/not-yet-valid/inverted/cross-tenant/impossible-calendar grant, mapping refusal, audited elevated re-id replay | ADR 0009; `docs/research/provider-payload-minimization.md` | | Adaptive orchestration router | `tepp_api` | accepted-target | active PR | mode selection, document-control denial, ablation, credential-free bind | ADR 0010; `docs/research/adaptive-orchestration-router.md` | | CWL modular connectors | `docs/connectors/*` | implemented-main | — | contract docs + examples | PR #22; live HTTP ports remaining | From 56b507a06411d0fe978bbb5c8a9ab8b9f81b583c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 06:38:01 +0000 Subject: [PATCH 55/62] =?UTF-8?q?feat(psychometric):=20recover=20Driver=20?= =?UTF-8?q?=C2=A77.2=20extra=20process=20after=20t0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Driver, Oud, and Voelkle (2017, §7.2, pp. 22–23) name T0TDPREDEFFECT when the extra process begins at t = 0 and TDPREDEFFECT when it begins after t = 0. T0TDPREDEFFECT uses Δt = t − t0 for both the original-process evolution and the extra drive. TDPREDEFFECT after t0 uses t − u with t0 < u < t for the extra drive while μ_t still uses Δt. Equation 5 of that after-t0 contribution is τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)). The first-occasion extra-process observed mean is not that map when u ≠ t0. e^{a(t−u)} m x is a Dirac on the original process, not this DRIFT drive. An impulse at u = t0 or u = t is not interior. JSS PDF re-opened 2026-08-21T06:32Z. Still not DSEM, not a Kalman filter, and not ctsem estimation. --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 1 + CLAUDE.md | 2 +- crates/psychometric_core/src/error.rs | 59 +++ crates/psychometric_core/src/event_time.rs | 484 +++++++++++++++++- crates/psychometric_core/src/lib.rs | 32 ++ ...multilevel_event_time_recovery_contract.rs | 206 +++++++- .../scientific_claim_boundary_contract.rs | 154 +++++- docs/TRACEABILITY.md | 2 +- docs/adr/0005-posterior-esem-dsem.md | 4 +- .../multilevel-event-time-recovery.md | 15 +- docs/validation/temporal-event-foundation.md | 2 +- 12 files changed, 933 insertions(+), 30 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 3d552e7b..2c84e936 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -61,7 +61,7 @@ boundaries above remain the target modular MSA architecture. | `tepp_simulation` | known-truth temporal/event data generation | | `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics | | `tepp_api` | versioned DTO, schema, and export contracts | -| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; first-occasion `T0TIPREDEFFECT` shift is `t0_b z` and Eq. 3 first-summand carry is `e^{A Δt} t0_b z` (`T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_b z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), first-occasion `T0TDPREDEFFECT` shift is `t0_m x0` and Eq. 3 first-summand carry is `e^{A Δt} t0_m x0` (`T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; §7.2 level-change `CINT` is `κ = −a m x` with `a < 0` so `−κ / a = m x` (`−a m x` is not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2; Eq. 3 of that setting is `(1 − e^{a Δt}) m x`, which is not `m x`, not `κ`, and not `TIPREDEFFECT`; §7.2 extra-process contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`; identification `TDPREDEFFECT` on the extra process is 1; printed extra `DRIFT` is `−0.000001`; not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`; `ε ≥ 0` fails closed; Eq. 5 of that contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; the extra process has `LAMBDA` 0 and is not an observed indicator; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | +| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; first-occasion `T0TIPREDEFFECT` shift is `t0_b z` and Eq. 3 first-summand carry is `e^{A Δt} t0_b z` (`T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_b z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), first-occasion `T0TDPREDEFFECT` shift is `t0_m x0` and Eq. 3 first-summand carry is `e^{A Δt} t0_m x0` (`T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; §7.2 level-change `CINT` is `κ = −a m x` with `a < 0` so `−κ / a = m x` (`−a m x` is not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2; Eq. 3 of that setting is `(1 − e^{a Δt}) m x`, which is not `m x`, not `κ`, and not `TIPREDEFFECT`; §7.2 extra-process contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`; identification `TDPREDEFFECT` on the extra process is 1; printed extra `DRIFT` is `−0.000001`; not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`; `ε ≥ 0` fails closed; Eq. 5 of that contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; the extra process has `LAMBDA` 0 and is not an observed indicator; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`; after-t0 extra-process `TDPREDEFFECT` is `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` while `μ_t` uses `Δt`; Eq. 5 of that after-t0 contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)`; the first-occasion extra-process observed mean is not that observed mean when `u ≠ t0`; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | No crate exposes placeholder production behavior in Task 1. This prevents an empty façade from becoming a de facto public API before its invariants and tests diff --git a/CHANGELOG.md b/CHANGELOG.md index f3ad0b8b..05f9508e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:32Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar observed-indicator mean of an extra-process `TDPREDEFFECT` after `t0`. Section 7.2 names `T0TDPREDEFFECT` when the extra process begins at `t = 0` and `TDPREDEFFECT` when it begins after `t = 0`. The printed extra process has `LAMBDA` 0. Original indicators load on the original process after the `DRIFT` coupling over `t − u` with `t0 < u < t` while `μ_t` still uses `Δt = t − t0`. The scalar composition is `E(y_t) = τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))`. The first-occasion extra-process observed mean uses `Δt` for both the evolution and the extra drive and is not this composition when `u ≠ t0`. The evolved observed mean `τ + λ μ_t` is not this composition. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is a Dirac on the original process and is not this `DRIFT` drive. An impulse at `u = t0` or `u = t` is not interior. A zero original-indicator loading is exactly `τ`. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-21T06:24Z: `is_oa: false`; Springer `content/pdf` is HTML 200). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; §7.2, pp. 22–23; Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-21T06:12Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar observed-indicator mean of the extra near-zero-drift latent process contribution. Section 7.2's printed extra process has `LAMBDA` 0 and is not an observed indicator. Original indicators load on the original process after the `DRIFT` coupling `a_{ηξ}`. After a unit identification impulse the latent process at `t` is `μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`. The scalar composition is `E(y_t) = τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))`. Form the evolved-plus-contribution latent mean first, then `τ + λ` of that mean. The evolved observed mean `τ + λ μ_t` is not this composition. The contemporaneous map `τ + λ(μ_t + m x)` is not this composition. The extra-process contribution is not `E(y_t)`. The evolved-plus-contribution latent mean is not `E(y_t)`. A zero original-indicator loading is exactly `τ`. A zero coupling recovers `τ + λ μ_t`. `ε ≥ 0` fails closed. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-21T06:24Z: `is_oa: false`; Springer `content/pdf` is HTML 200). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, §7.2, pp. 22–23; Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T23:10Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar contribution of the extra near-zero-drift latent process. Section 7.2 specifies a lasting level change by that extra process: `T0MEANS`, `CINT`, `T0VAR`, `DIFFUSION`, and `TRAITVAR` of it are fixed to 0; `TDPREDEFFECT` on it is fixed to 1; its `DRIFT` diagonal is very close to 0 (printed example `−0.000001`; precisely 0 causes computational problems); and its effect on the original process is the `DRIFT` coupling `a_{ηξ}`. After a unit identification impulse the scalar contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`). Form `a_{ηξ} x` first. A zero coupling or zero predictor is exactly zero. `ε ≥ 0` fails closed. That contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T23:10Z: `is_oa: false`; Springer `content/pdf` is HTML 200). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, §7.2, pp. 20–21; Eq. 3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T19:50Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar discrete increment of the lasting level-change `CINT`. Section 7.2 sets `CINT` to `TDPREDEFFECT * −DRIFT` (`κ = −a m x`). Equation 3 maps that intercept through `A^{-1}[e^{A Δt} − I] κ`. With `κ = −a m x` the scalar increment is `(e^{a Δt} − 1)/a · (−a m x) = (1 − e^{a Δt}) m x`. Form the level-change `CINT` first, then the discrete intercept map. Underflow of `e^{a Δt}` to `+0` keeps the equilibrium offset `m x`. `(1 − e^{a Δt}) m x` is not the contemporaneous jump `m x`. `(1 − e^{a Δt}) m x` is not `κ`. `(1 − e^{a Δt}) m x` is not `A^{-1}[e^{A Δt} − I] B z`. Stable `a < 0` is required. A zero effect or zero predictor is exactly zero. Still not a Kalman filter, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T19:45Z: `is_oa: false`; Springer `content/pdf` is HTML 200). diff --git a/CLAUDE.md b/CLAUDE.md index f2ba1b92..862d22bb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. Putnick and Bornstein (2016, PMC5145197 opened 2026-08-19T22:15Z) require scalar invariance before latent-mean comparison; residual invariance is not a prerequisite. Two-observation series have no residual degrees of freedom (`ordinary_least_squares_fit` returns residual variance `0`) and cap at strong/scalar; they still license means. This is two-group OLS, not MGCFA. Meredith (1993) names remain unread labels. -- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. The contemporaneous time-dependent predictor impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`). Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). The §7.2 level-change form is not that impulse. The observed mean of that contemporaneous impulse is `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-impulse latent mean is not `E(y_t)`. The time-independent predictor increment is `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`). Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. `TIPREDEFFECT` is `B`, not that discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle et al. (2012, Eq. 14). The observed mean of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The within-interval time-dependent impulse carry is `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation). Form `m x` first, then `e^{a(t−u)} m x`. A zero drift is `m x` with no dissipation. Underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept. `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). An impulse at `u = t` is the contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. The observed mean of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The carried latent mean is not `E(y_t)`. The first-occasion time-independent predictor shift is `t0_b z` (Driver et al., 2017, Table 3 `T0TIPREDEFFECT`; Eq. 3 first summand). Form `t0_b z` first, then `e^{a Δt} t0_b z`. Form `μ_t` first, then add that carry. A zero drift is `t0_b z`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. `e^{A Δt} t0_b z` is not `t0_b z`. `T0TIPREDEFFECT` is the coefficient, not the shift. The observed mean of that first-occasion carry is `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The evolved-plus-carry latent mean is not `E(y_t)`. The first-occasion time-dependent predictor shift is `t0_m x0` (Driver et al., 2017, Table 3 `T0TDPREDEFFECT`; Eq. 3 first summand; JSS PDF re-opened 2026-08-20T19:10Z). Form `t0_m x0` first, then `e^{a Δt} t0_m x0`. Form `μ_t` first, then add that carry. A zero drift is `t0_m x0`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`. `e^{A Δt} t0_m x0` is not `t0_m x0`. `T0TDPREDEFFECT` is the coefficient, not the shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The observed mean of that first-occasion TD carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean. The evolved-plus-carry latent mean is not `E(y_t)`. The lasting level-change `CINT` is `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z). Form `m x` first, then multiply by `−a`. Stable `a < 0` is required so `−κ / a = m x` is an equilibrium offset. `a ≥ 0` cannot hold a new process mean. `−a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not this `CINT` setting. Equation 3 maps that intercept as `(1 − e^{a Δt}) m x` (JSS PDF re-opened 2026-08-20T19:50Z). Form the level-change `CINT` first, then the discrete intercept map. Underflow of `e^{a Δt}` to `+0` keeps `m x`. `(1 − e^{a Δt}) m x` is not `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`. The printed §7.2 lasting level change is an extra near-zero-drift latent process (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z). `T0MEANS`, `CINT`, `T0VAR`, `DIFFUSION`, and `TRAITVAR` of that process are fixed to 0; `TDPREDEFFECT` on it is fixed to 1; its `DRIFT` diagonal is very close to 0 (printed example `−0.000001`; precisely 0 causes computational problems); the original process is driven by the `DRIFT` coupling `a_{ηξ}`. After a unit identification impulse the scalar contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`). Form `a_{ηξ} x` first. A zero coupling or zero predictor is exactly zero. `ε ≥ 0` fails closed. That contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. The observed mean of that extra-process contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5 of that §7.2 contribution; JSS PDF re-opened 2026-08-21T06:12Z). The extra process has `LAMBDA` 0 and is not an observed indicator. Original indicators load on the original process after the `DRIFT` coupling. The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The contribution is not `E(y_t)`. The evolved-plus-contribution latent mean is not `E(y_t)`. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. The contemporaneous time-dependent predictor impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`). Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). The §7.2 level-change form is not that impulse. The observed mean of that contemporaneous impulse is `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-impulse latent mean is not `E(y_t)`. The time-independent predictor increment is `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`). Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. `TIPREDEFFECT` is `B`, not that discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle et al. (2012, Eq. 14). The observed mean of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The within-interval time-dependent impulse carry is `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation). Form `m x` first, then `e^{a(t−u)} m x`. A zero drift is `m x` with no dissipation. Underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept. `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). An impulse at `u = t` is the contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. The observed mean of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The carried latent mean is not `E(y_t)`. The first-occasion time-independent predictor shift is `t0_b z` (Driver et al., 2017, Table 3 `T0TIPREDEFFECT`; Eq. 3 first summand). Form `t0_b z` first, then `e^{a Δt} t0_b z`. Form `μ_t` first, then add that carry. A zero drift is `t0_b z`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. `e^{A Δt} t0_b z` is not `t0_b z`. `T0TIPREDEFFECT` is the coefficient, not the shift. The observed mean of that first-occasion carry is `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The evolved-plus-carry latent mean is not `E(y_t)`. The first-occasion time-dependent predictor shift is `t0_m x0` (Driver et al., 2017, Table 3 `T0TDPREDEFFECT`; Eq. 3 first summand; JSS PDF re-opened 2026-08-20T19:10Z). Form `t0_m x0` first, then `e^{a Δt} t0_m x0`. Form `μ_t` first, then add that carry. A zero drift is `t0_m x0`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`. `e^{A Δt} t0_m x0` is not `t0_m x0`. `T0TDPREDEFFECT` is the coefficient, not the shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The observed mean of that first-occasion TD carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean. The evolved-plus-carry latent mean is not `E(y_t)`. The lasting level-change `CINT` is `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z). Form `m x` first, then multiply by `−a`. Stable `a < 0` is required so `−κ / a = m x` is an equilibrium offset. `a ≥ 0` cannot hold a new process mean. `−a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not this `CINT` setting. Equation 3 maps that intercept as `(1 − e^{a Δt}) m x` (JSS PDF re-opened 2026-08-20T19:50Z). Form the level-change `CINT` first, then the discrete intercept map. Underflow of `e^{a Δt}` to `+0` keeps `m x`. `(1 − e^{a Δt}) m x` is not `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`. The printed §7.2 lasting level change is an extra near-zero-drift latent process (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z). `T0MEANS`, `CINT`, `T0VAR`, `DIFFUSION`, and `TRAITVAR` of that process are fixed to 0; `TDPREDEFFECT` on it is fixed to 1; its `DRIFT` diagonal is very close to 0 (printed example `−0.000001`; precisely 0 causes computational problems); the original process is driven by the `DRIFT` coupling `a_{ηξ}`. After a unit identification impulse the scalar contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`). Form `a_{ηξ} x` first. A zero coupling or zero predictor is exactly zero. `ε ≥ 0` fails closed. That contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. The observed mean of that extra-process contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5 of that §7.2 contribution; JSS PDF re-opened 2026-08-21T06:12Z). The extra process has `LAMBDA` 0 and is not an observed indicator. Original indicators load on the original process after the `DRIFT` coupling. The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The contribution is not `E(y_t)`. The evolved-plus-contribution latent mean is not `E(y_t)`. `T0TDPREDEFFECT` on the extra process begins at `t = 0` and uses `Δt = t − t0` for both the original-process evolution and the extra drive. `TDPREDEFFECT` after `t0` uses `t − u` with `t0 < u < t` for the extra drive while `μ_t` still uses `Δt`. The observed mean of that after-t0 extra-process contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, Eq. 5 of that §7.2 after-t0 contribution; JSS PDF re-opened 2026-08-21T06:32Z). The first-occasion extra-process observed mean is not that observed mean when `u ≠ t0`. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is a Dirac on the original process and is not that `DRIFT` drive. An impulse at `u = t0` or `u = t` is not interior. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. diff --git a/crates/psychometric_core/src/error.rs b/crates/psychometric_core/src/error.rs index ac7f1e6f..a8a0e013 100644 --- a/crates/psychometric_core/src/error.rs +++ b/crates/psychometric_core/src/error.rs @@ -302,6 +302,28 @@ pub enum PsychometricError { /// Driver §7.2 evolved-plus-contribution latent mean was treated /// as `E(y_t)`. Equation 5 maps `E(y_t) = τ + λ` of that mean. ExtraProcessLatentMeanIsNotObservedMean, + /// Driver Eq. 5 of the first-occasion extra process was treated + /// as the Eq. 5 after-t0 extra-process observed mean. `T0TDPREDEFFECT` + /// on the extra process uses `Δt = t − t0`. `TDPREDEFFECT` after + /// `t0` uses `t − u` with `t0 < u < t`. + ExtraProcessObservedMeanIsNotAfterExtraProcessObservedMean, + /// Driver Eq. 5 of the evolved mean was treated as the Eq. 5 + /// after-t0 extra-process observed mean. `τ + λ μ_t` is not + /// `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))`. + EvolvedObservedMeanIsNotAfterExtraProcessObservedMean, + /// Driver Eq. 5 of the impulse carry was treated as the Eq. 5 + /// after-t0 extra-process observed mean. `e^{a(t−u)} m x` is a + /// Dirac on the original process. Extra-process `TDPREDEFFECT` + /// after `t0` drives the original process through `DRIFT`. + ImpulseCarryObservedMeanIsNotAfterExtraProcessObservedMean, + /// Driver §7.2 after-t0 extra-process contribution was treated + /// as `E(y_t)`. The contribution is not `τ + λ` of the + /// evolved-plus-after-contribution latent mean. + AfterExtraProcessContributionIsNotObservedMean, + /// Driver §7.2 evolved-plus-after-contribution latent mean was + /// treated as `E(y_t)`. Equation 5 maps `E(y_t) = τ + λ` of + /// that mean. + AfterExtraProcessLatentMeanIsNotObservedMean, } impl fmt::Display for PsychometricError { @@ -548,6 +570,21 @@ impl fmt::Display for PsychometricError { Self::ExtraProcessLatentMeanIsNotObservedMean => { "evolved-plus-contribution latent mean is not the extra-process observed mean" } + Self::ExtraProcessObservedMeanIsNotAfterExtraProcessObservedMean => { + "first-occasion extra-process observed mean is not the after-t0 extra-process observed mean" + } + Self::EvolvedObservedMeanIsNotAfterExtraProcessObservedMean => { + "evolved observed mean is not the after-t0 extra-process observed mean" + } + Self::ImpulseCarryObservedMeanIsNotAfterExtraProcessObservedMean => { + "impulse-carry observed mean is not the after-t0 extra-process observed mean" + } + Self::AfterExtraProcessContributionIsNotObservedMean => { + "after-t0 extra-process contribution is not the after-t0 extra-process observed mean" + } + Self::AfterExtraProcessLatentMeanIsNotObservedMean => { + "evolved-plus-after-contribution latent mean is not the after-t0 extra-process observed mean" + } }; formatter.write_str(message) } @@ -929,5 +966,27 @@ mod tests { PsychometricError::ExtraProcessLatentMeanIsNotObservedMean.to_string(), "evolved-plus-contribution latent mean is not the extra-process observed mean" ); + assert_eq!( + PsychometricError::ExtraProcessObservedMeanIsNotAfterExtraProcessObservedMean + .to_string(), + "first-occasion extra-process observed mean is not the after-t0 extra-process observed mean" + ); + assert_eq!( + PsychometricError::EvolvedObservedMeanIsNotAfterExtraProcessObservedMean.to_string(), + "evolved observed mean is not the after-t0 extra-process observed mean" + ); + assert_eq!( + PsychometricError::ImpulseCarryObservedMeanIsNotAfterExtraProcessObservedMean + .to_string(), + "impulse-carry observed mean is not the after-t0 extra-process observed mean" + ); + assert_eq!( + PsychometricError::AfterExtraProcessContributionIsNotObservedMean.to_string(), + "after-t0 extra-process contribution is not the after-t0 extra-process observed mean" + ); + assert_eq!( + PsychometricError::AfterExtraProcessLatentMeanIsNotObservedMean.to_string(), + "evolved-plus-after-contribution latent mean is not the after-t0 extra-process observed mean" + ); } } diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index e622ac62..5d28c36f 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -93,7 +93,15 @@ //! process has `LAMBDA` 0 and is not an observed indicator; `τ + λ μ_t` //! is not that observed mean; `τ + λ(μ_t + m x)` is not that observed //! mean; the contribution is not `E(y_t)`; the evolved-plus-contribution -//! latent mean is not `E(y_t)`). The JSS article +//! latent mean is not `E(y_t)`). `T0TDPREDEFFECT` on the extra process +//! begins at `t = 0` and uses `Δt = t − t0` for both the original-process +//! evolution and the extra drive. `TDPREDEFFECT` after `t0` uses +//! `t − u` with `t0 < u < t` for the extra drive while `μ_t` still +//! uses `Δt`. Equation 5 of that after-t0 contribution is +//! `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (the +//! first-occasion extra-process observed mean is not that observed +//! mean when `u ≠ t0`; `e^{a(t−u)} m x` is a Dirac on the original +//! process, not this `DRIFT` drive). The JSS article //! has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). //! The difference quotient `(x(t+Δt) − x(t)) / Δt` (their //! Eqs. 3–4) is refused. This is not DSEM and not a matrix `expm`. @@ -1892,6 +1900,276 @@ pub fn refuse_extra_process_latent_mean_as_observed_mean( Err(PsychometricError::ExtraProcessLatentMeanIsNotObservedMean) } +/// Exact scalar §7.2 extra-process contribution of a `TDPREDEFFECT` +/// impulse strictly after `t0`. +/// +/// Driver, Oud, and Voelkle (2017, §7.2, pp. 22–23; JSS PDF +/// re-opened 2026-08-21T06:32Z from +/// ) +/// name `T0TDPREDEFFECT` when the extra process begins at `t = 0` +/// and `TDPREDEFFECT` when it begins after `t = 0`. The printed +/// extra `TDPREDEFFECT` is 1. The original process is driven through +/// the `DRIFT` coupling, not through a Dirac on the original +/// process. After an identification impulse at `u` with +/// `t0 < u < t` the scalar contribution is +/// `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)`. Form the +/// interior interval `t − u` first, then the extra-process +/// contribution on that interval. An impulse at `u = t0` is the +/// first-occasion extra-process map. An impulse at `u = t` has not +/// yet driven the original process. This is not a Kalman filter, +/// not a matrix `expm`, and not ctsem estimation. +/// +/// # Errors +/// +/// Returns [`PsychometricError::NonPositiveInterval`] when +/// `t − u` is not strictly interior to `(0, t − t0)`, and +/// otherwise propagates +/// [`recover_level_change_extra_process_contribution`]. +pub fn recover_level_change_extra_process_contribution_after( + original_from_extra_drift: f64, + time_dependent_predictor: f64, + original_log_rate: f64, + extra_log_rate: f64, + event_delta: f64, + elapsed_after_impulse: f64, + clock: LagClock, +) -> Result { + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + if !event_delta.is_finite() || event_delta <= 0.0 { + return Err(PsychometricError::NonPositiveInterval); + } + if !elapsed_after_impulse.is_finite() || elapsed_after_impulse <= 0.0 { + return Err(PsychometricError::NonPositiveInterval); + } + if elapsed_after_impulse >= event_delta { + return Err(PsychometricError::NonPositiveInterval); + } + recover_level_change_extra_process_contribution( + original_from_extra_drift, + time_dependent_predictor, + original_log_rate, + extra_log_rate, + elapsed_after_impulse, + clock, + ) +} + +/// Exact scalar evolved latent mean plus a §7.2 extra-process +/// contribution after `t0`. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 3, p. 5; §7.2, pp. 22–23; +/// JSS PDF re-opened 2026-08-21T06:32Z) evolve `T0MEANS` and `CINT` +/// over `Δt = t − t0`. `TDPREDEFFECT` on the extra process after +/// `t0` drives the original process only over `t − u` with +/// `t0 < u < t`. Form `μ_t` first, then add the after-t0 +/// extra-process contribution. A zero contribution is exactly +/// `μ_t`. The first-occasion extra-process map uses `Δt` for both +/// the evolution and the extra drive and is not this composition +/// when `u ≠ t0`. The impulse-carry `μ_t + e^{a(t−u)} m x` is a +/// Dirac on the original process and is not this composition. +/// +/// # Errors +/// +/// Propagates [`recover_discrete_latent_mean`] and +/// [`recover_level_change_extra_process_contribution_after`], and +/// returns [`PsychometricError::InvalidNumericInput`] when the sum +/// overflows. +#[allow(clippy::too_many_arguments)] +pub fn recover_discrete_latent_mean_with_extra_process_after( + initial_latent_mean: f64, + original_log_rate: f64, + continuous_intercept: f64, + original_from_extra_drift: f64, + time_dependent_predictor: f64, + extra_log_rate: f64, + event_delta: f64, + elapsed_after_impulse: f64, + clock: LagClock, +) -> Result { + let evolved_latent_mean = recover_discrete_latent_mean( + initial_latent_mean, + original_log_rate, + continuous_intercept, + event_delta, + clock, + )?; + let contribution = recover_level_change_extra_process_contribution_after( + original_from_extra_drift, + time_dependent_predictor, + original_log_rate, + extra_log_rate, + event_delta, + elapsed_after_impulse, + clock, + )?; + if contribution == 0.0 { + return Ok(evolved_latent_mean); + } + if evolved_latent_mean == 0.0 { + return Ok(contribution); + } + require_finite(evolved_latent_mean + contribution) +} + +/// Exact scalar observed mean of a §7.2 extra-process contribution +/// after `t0`. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; §7.2, pp. 22–23; +/// JSS PDF re-opened 2026-08-21T06:32Z) write +/// `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and +/// `Γ ~ N(τ, Ψ)`. The printed extra process has `LAMBDA` 0. +/// Original indicators load on the original process after the +/// `DRIFT` coupling over `t − u` with `t0 < u < t`. The scalar +/// composition is +/// `E(y_t) = τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))`. +/// Form the evolved-plus-after-contribution latent mean first, then +/// `τ + λ` of that mean. The first-occasion extra-process observed +/// mean uses `Δt` for both the evolution and the extra drive and is +/// not this composition when `u ≠ t0`. The evolved observed mean +/// `τ + λ μ_t` is not this composition. The impulse-carry map +/// `τ + λ(μ_t + e^{a(t−u)} m x)` is not this composition. The +/// extra process itself is not an observed indicator. This is not a +/// Kalman filter and not ctsem estimation. +/// +/// # Errors +/// +/// Propagates [`recover_discrete_latent_mean_with_extra_process_after`] +/// and [`recover_manifest_observed_mean`]. +#[allow(clippy::too_many_arguments)] +pub fn recover_discrete_observed_mean_with_extra_process_after( + loading: f64, + initial_latent_mean: f64, + original_log_rate: f64, + continuous_intercept: f64, + original_from_extra_drift: f64, + time_dependent_predictor: f64, + extra_log_rate: f64, + manifest_mean: f64, + event_delta: f64, + elapsed_after_impulse: f64, + clock: LagClock, +) -> Result { + let extra_latent_mean = recover_discrete_latent_mean_with_extra_process_after( + initial_latent_mean, + original_log_rate, + continuous_intercept, + original_from_extra_drift, + time_dependent_predictor, + extra_log_rate, + event_delta, + elapsed_after_impulse, + clock, + )?; + recover_manifest_observed_mean(loading, extra_latent_mean, manifest_mean) +} + +/// Refuse treating the first-occasion extra-process observed mean +/// as the after-t0 extra-process observed mean. +/// +/// `T0TDPREDEFFECT` on the extra process uses `Δt = t − t0`. +/// `TDPREDEFFECT` after `t0` uses `t − u` with `t0 < u < t`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::ExtraProcessObservedMeanIsNotAfterExtraProcessObservedMean`]. +pub fn refuse_extra_process_observed_mean_as_after_extra_process_observed_mean( + extra_process_observed_mean: f64, + after_extra_process_observed_mean: f64, +) -> Result { + let _ = ( + extra_process_observed_mean, + after_extra_process_observed_mean, + ); + Err(PsychometricError::ExtraProcessObservedMeanIsNotAfterExtraProcessObservedMean) +} + +/// Refuse treating the evolved observed mean as the after-t0 +/// extra-process observed mean. +/// +/// Equation 5 of the Eq. 3 evolved mean is `τ + λ μ_t`. Equation 5 +/// of the after-t0 extra-process contribution is +/// `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::EvolvedObservedMeanIsNotAfterExtraProcessObservedMean`]. +pub fn refuse_evolved_observed_mean_as_after_extra_process_observed_mean( + evolved_observed_mean: f64, + after_extra_process_observed_mean: f64, +) -> Result { + let _ = (evolved_observed_mean, after_extra_process_observed_mean); + Err(PsychometricError::EvolvedObservedMeanIsNotAfterExtraProcessObservedMean) +} + +/// Refuse treating the impulse-carry observed mean as the after-t0 +/// extra-process observed mean. +/// +/// `e^{a(t−u)} m x` is a Dirac on the original process. Extra-process +/// `TDPREDEFFECT` after `t0` drives the original process through +/// `DRIFT`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::ImpulseCarryObservedMeanIsNotAfterExtraProcessObservedMean`]. +pub fn refuse_impulse_carry_observed_mean_as_after_extra_process_observed_mean( + impulse_carry_observed_mean: f64, + after_extra_process_observed_mean: f64, +) -> Result { + let _ = ( + impulse_carry_observed_mean, + after_extra_process_observed_mean, + ); + Err(PsychometricError::ImpulseCarryObservedMeanIsNotAfterExtraProcessObservedMean) +} + +/// Refuse treating the after-t0 extra-process contribution as +/// `E(y_t)`. +/// +/// The contribution is not `τ + λ` of the +/// evolved-plus-after-contribution latent mean. The extra process +/// has `LAMBDA` 0 in the printed specification. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::AfterExtraProcessContributionIsNotObservedMean`]. +pub fn refuse_after_extra_process_contribution_as_observed_mean( + after_extra_process_contribution: f64, + after_extra_process_observed_mean: f64, +) -> Result { + let _ = ( + after_extra_process_contribution, + after_extra_process_observed_mean, + ); + Err(PsychometricError::AfterExtraProcessContributionIsNotObservedMean) +} + +/// Refuse treating the evolved-plus-after-contribution latent mean +/// as `E(y_t)`. +/// +/// Equation 5 maps `E(y_t) = τ + λ` of that mean. The latent mean +/// is not the observed mean. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::AfterExtraProcessLatentMeanIsNotObservedMean`]. +pub fn refuse_after_extra_process_latent_mean_as_observed_mean( + after_extra_process_latent_mean: f64, + after_extra_process_observed_mean: f64, +) -> Result { + let _ = ( + after_extra_process_latent_mean, + after_extra_process_observed_mean, + ); + Err(PsychometricError::AfterExtraProcessLatentMeanIsNotObservedMean) +} + /// Exact scalar evolved latent mean plus a contemporaneous impulse. /// /// Driver, Oud, and Voelkle (2017, Eq. 3, p. 5) write the first two @@ -3749,12 +4027,14 @@ mod tests { recover_discrete_continuous_intercept_effect, recover_discrete_lag_from_log_rate, recover_discrete_lag_one, recover_discrete_lagged_latent_covariance, recover_discrete_latent_mean, recover_discrete_latent_mean_with_extra_process, + recover_discrete_latent_mean_with_extra_process_after, recover_discrete_latent_mean_with_impulse, recover_discrete_latent_mean_with_impulse_carry, recover_discrete_latent_mean_with_initial_time_dependent_predictor, recover_discrete_latent_mean_with_initial_time_independent_predictor, recover_discrete_latent_mean_with_time_independent_predictor, recover_discrete_latent_variance, recover_discrete_observed_mean, recover_discrete_observed_mean_with_extra_process, + recover_discrete_observed_mean_with_extra_process_after, recover_discrete_observed_mean_with_impulse, recover_discrete_observed_mean_with_impulse_carry, recover_discrete_observed_mean_with_initial_time_dependent_predictor, @@ -3769,15 +4049,18 @@ mod tests { recover_initial_time_independent_predictor_effect, recover_irregular_centered_residual_log_rate, recover_level_change_continuous_intercept, recover_level_change_discrete_increment, recover_level_change_extra_process_contribution, - recover_local_log_rate, recover_manifest_lagged_observed_covariance, - recover_manifest_observed_mean, recover_manifest_observed_variance, - recover_manifest_trait_plus_state_observed_variance, recover_stationary_latent_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_level_change_extra_process_contribution_after, recover_local_log_rate, + recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, + recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, + recover_stationary_latent_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_continuous_intercept_as_discrete_mean_increment, refuse_continuous_intercept_as_initial_latent_mean, refuse_continuous_intercept_as_manifest_means, refuse_difference_quotient_as_local_rate, + refuse_evolved_observed_mean_as_after_extra_process_observed_mean, refuse_evolved_observed_mean_as_extra_process_observed_mean, refuse_evolved_observed_mean_as_impulse_carry_observed_mean, refuse_evolved_observed_mean_as_impulse_observed_mean, @@ -3786,7 +4069,9 @@ mod tests { refuse_evolved_observed_mean_as_time_independent_observed_mean, refuse_extra_process_contribution_as_observed_mean, refuse_extra_process_latent_mean_as_observed_mean, + refuse_extra_process_observed_mean_as_after_extra_process_observed_mean, refuse_finite_interval_process_noise_as_stationary_variance, + refuse_impulse_carry_observed_mean_as_after_extra_process_observed_mean, refuse_impulse_carry_observed_mean_as_initial_time_dependent_observed_mean, refuse_impulse_carry_observed_mean_as_initial_time_independent_observed_mean, refuse_impulse_carry_observed_mean_as_time_independent_observed_mean, @@ -6492,6 +6777,191 @@ mod tests { ); } + #[test] + #[allow(clippy::too_many_lines)] + fn after_extra_process_observed_mean_recovers_driver_equation_five() { + let loading = 2.0_f64; + let coupling = 0.4_f64; + let predictor = 3.0_f64; + let original = -0.5_f64; + let extra = -0.05_f64; + let delta = 2.0_f64; + let elapsed = 1.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let recovered = recover_discrete_observed_mean_with_extra_process_after( + loading, + initial, + original, + intercept, + coupling, + predictor, + extra, + manifest_mean, + delta, + elapsed, + LagClock::EventTime, + ) + .expect("eq5-after-extra-process-mean"); + let composed = recover_discrete_latent_mean_with_extra_process_after( + initial, + original, + intercept, + coupling, + predictor, + extra, + delta, + elapsed, + LagClock::EventTime, + ) + .expect("after-extra-latent"); + let expected = manifest_mean + loading * composed; + assert!((recovered - expected).abs() < 1e-15); + let first_occasion = recover_discrete_observed_mean_with_extra_process( + loading, + initial, + original, + intercept, + coupling, + predictor, + extra, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-t0-extra-process-mean"); + assert!((first_occasion - recovered).abs() > 1e-3); + let evolved_observed = recover_discrete_observed_mean( + loading, + initial, + original, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq3-eq5-mean"); + assert!((evolved_observed - recovered).abs() > 1e-3); + let carry_observed = recover_discrete_observed_mean_with_impulse_carry( + loading, + initial, + original, + intercept, + coupling, + predictor, + manifest_mean, + delta, + elapsed, + LagClock::EventTime, + ) + .expect("eq5-impulse-carry-mean"); + assert!((carry_observed - recovered).abs() > 1e-3); + let contribution = recover_level_change_extra_process_contribution_after( + coupling, + predictor, + original, + extra, + delta, + elapsed, + LagClock::EventTime, + ) + .expect("after-extra-process"); + assert_eq!( + refuse_extra_process_observed_mean_as_after_extra_process_observed_mean( + first_occasion, + recovered + ), + Err(PsychometricError::ExtraProcessObservedMeanIsNotAfterExtraProcessObservedMean) + ); + assert_eq!( + refuse_evolved_observed_mean_as_after_extra_process_observed_mean( + evolved_observed, + recovered + ), + Err(PsychometricError::EvolvedObservedMeanIsNotAfterExtraProcessObservedMean) + ); + assert_eq!( + refuse_impulse_carry_observed_mean_as_after_extra_process_observed_mean( + carry_observed, + recovered + ), + Err(PsychometricError::ImpulseCarryObservedMeanIsNotAfterExtraProcessObservedMean) + ); + assert_eq!( + refuse_after_extra_process_contribution_as_observed_mean(contribution, recovered), + Err(PsychometricError::AfterExtraProcessContributionIsNotObservedMean) + ); + assert_eq!( + refuse_after_extra_process_latent_mean_as_observed_mean(composed, recovered), + Err(PsychometricError::AfterExtraProcessLatentMeanIsNotObservedMean) + ); + } + + #[test] + fn after_extra_process_contribution_refuses_non_interior_interval() { + let coupling = 0.4_f64; + let predictor = 3.0_f64; + let original = -0.5_f64; + let extra = -0.05_f64; + assert_eq!( + recover_level_change_extra_process_contribution_after( + coupling, + predictor, + original, + extra, + 2.0, + 2.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_level_change_extra_process_contribution_after( + coupling, + predictor, + original, + extra, + 2.0, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_observed_mean_with_extra_process_after( + 2.0, + 1.0, + original, + 0.3, + coupling, + predictor, + extra, + 0.5, + 2.0, + 1.0, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_discrete_observed_mean_with_extra_process_after( + 0.0, + 1.0, + original, + 0.3, + coupling, + predictor, + extra, + 0.5, + 2.0, + 1.0, + LagClock::EventTime + ), + Ok(0.5) + ); + } + #[test] fn discrete_observed_mean_with_impulse_recovers_driver_equation_five() { let loading = 2.0_f64; diff --git a/crates/psychometric_core/src/lib.rs b/crates/psychometric_core/src/lib.rs index 763568fa..58094293 100644 --- a/crates/psychometric_core/src/lib.rs +++ b/crates/psychometric_core/src/lib.rs @@ -94,6 +94,22 @@ //! `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not //! that observed mean; the contribution is not `E(y_t)`; the //! evolved-plus-contribution latent mean is not `E(y_t)`), +//! recovers the Driver §7.2 after-t0 extra-process `TDPREDEFFECT` +//! contribution as `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` +//! for `t0 < u < t` (`T0TDPREDEFFECT` uses `Δt = t − t0` for both +//! the evolution and the extra drive; an impulse at `u = t0` is not +//! this map; an impulse at `u = t` has not yet driven the original +//! process; `e^{a(t−u)} m x` is a Dirac on the original process, not +//! this `DRIFT` drive), +//! recovers the Driver Eq. 5 of that after-t0 extra-process +//! contribution as +//! `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` +//! (JSS PDF re-opened 2026-08-21T06:32Z; the first-occasion +//! extra-process observed mean is not that observed mean when +//! `u ≠ t0`; `τ + λ μ_t` is not that observed mean; the impulse-carry +//! map is not that observed mean; the after-t0 contribution is not +//! `E(y_t)`; the evolved-plus-after-contribution latent mean is not +//! `E(y_t)`), //! and refuses //! latent-mean comparison below strong invariance. @@ -156,6 +172,8 @@ pub use event_time::recover_discrete_lagged_latent_covariance; pub use event_time::recover_discrete_latent_mean; /// Exact scalar evolved latent mean plus a §7.2 extra-process contribution. pub use event_time::recover_discrete_latent_mean_with_extra_process; +/// Exact scalar evolved latent mean plus a §7.2 extra-process contribution after t0. +pub use event_time::recover_discrete_latent_mean_with_extra_process_after; /// Exact scalar evolved latent mean plus a contemporaneous impulse. pub use event_time::recover_discrete_latent_mean_with_impulse; /// Exact scalar evolved latent mean plus a within-interval impulse carry. @@ -172,6 +190,8 @@ pub use event_time::recover_discrete_latent_variance; pub use event_time::recover_discrete_observed_mean; /// Exact scalar discrete observed mean of a §7.2 extra-process contribution. pub use event_time::recover_discrete_observed_mean_with_extra_process; +/// Exact scalar discrete observed mean of a §7.2 extra-process contribution after t0. +pub use event_time::recover_discrete_observed_mean_with_extra_process_after; /// Exact scalar discrete observed mean of a contemporaneous impulse. pub use event_time::recover_discrete_observed_mean_with_impulse; /// Exact scalar discrete observed mean of a within-interval impulse carry. @@ -208,6 +228,8 @@ pub use event_time::recover_level_change_continuous_intercept; pub use event_time::recover_level_change_discrete_increment; /// Exact scalar §7.2 extra-process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`. pub use event_time::recover_level_change_extra_process_contribution; +/// Exact scalar §7.2 extra-process contribution after t0 on `t − u`. +pub use event_time::recover_level_change_extra_process_contribution_after; /// Exact scalar inverse `a = ln(φ) / Δt`. pub use event_time::recover_local_log_rate; /// Exact scalar lagged observed-indicator covariance `λ² cov(η) + ψ`. @@ -230,6 +252,10 @@ pub use event_time::recover_trait_plus_state_lagged_covariance; pub use event_time::recover_trait_plus_state_latent_variance; /// CWC-then-event-time local log-rate (not DSEM; not raw-process AR drift). pub use event_time::recover_within_residual_event_time_log_rate; +/// Refuse treating the after-t0 extra-process contribution as `E(y_t)`. +pub use event_time::refuse_after_extra_process_contribution_as_observed_mean; +/// Refuse treating the evolved-plus-after-contribution latent mean as `E(y_t)`. +pub use event_time::refuse_after_extra_process_latent_mean_as_observed_mean; /// Refuse treating Driver Table 2 `CINT` as the discrete mean increment. pub use event_time::refuse_continuous_intercept_as_discrete_mean_increment; /// Refuse treating Driver Table 2 `CINT` as `T0MEANS`. @@ -238,6 +264,8 @@ pub use event_time::refuse_continuous_intercept_as_initial_latent_mean; pub use event_time::refuse_continuous_intercept_as_manifest_means; /// Refuse the difference quotient as a continuous-time rate. pub use event_time::refuse_difference_quotient_as_local_rate; +/// Refuse treating evolved `τ + λ μ_t` as the after-t0 extra-process observed mean. +pub use event_time::refuse_evolved_observed_mean_as_after_extra_process_observed_mean; /// Refuse treating evolved `τ + λ μ_t` as the extra-process observed mean. pub use event_time::refuse_evolved_observed_mean_as_extra_process_observed_mean; /// Refuse treating evolved `τ + λ μ_t` as the impulse-carry observed mean. @@ -254,8 +282,12 @@ pub use event_time::refuse_evolved_observed_mean_as_time_independent_observed_me pub use event_time::refuse_extra_process_contribution_as_observed_mean; /// Refuse treating the evolved-plus-contribution latent mean as `E(y_t)`. pub use event_time::refuse_extra_process_latent_mean_as_observed_mean; +/// Refuse treating the first-occasion extra-process observed mean as the after-t0 extra-process observed mean. +pub use event_time::refuse_extra_process_observed_mean_as_after_extra_process_observed_mean; /// Refuse treating finite-interval `Q_Δt` as `asymDIFFUSION`. pub use event_time::refuse_finite_interval_process_noise_as_stationary_variance; +/// Refuse treating impulse-carry `τ + λ(μ_t + e^{a(t−u)} m x)` as the after-t0 extra-process observed mean. +pub use event_time::refuse_impulse_carry_observed_mean_as_after_extra_process_observed_mean; /// Refuse treating impulse-carry `τ + λ(μ_t + e^{a(t−u)} m x)` as the first-occasion TD-predictor observed mean. pub use event_time::refuse_impulse_carry_observed_mean_as_initial_time_dependent_observed_mean; /// Refuse treating impulse-carry `τ + λ(μ_t + e^{a(t−u)} m x)` as the first-occasion TI-predictor observed mean. diff --git a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs index ac2a2be7..17705429 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -8,11 +8,13 @@ use psychometric_core::{ recover_discrete_constant_predictor_effect, recover_discrete_continuous_intercept_effect, recover_discrete_lag_from_log_rate, recover_discrete_lagged_latent_covariance, recover_discrete_latent_mean, recover_discrete_latent_mean_with_extra_process, + recover_discrete_latent_mean_with_extra_process_after, recover_discrete_latent_mean_with_impulse, recover_discrete_latent_mean_with_impulse_carry, recover_discrete_latent_mean_with_initial_time_dependent_predictor, recover_discrete_latent_mean_with_initial_time_independent_predictor, recover_discrete_latent_mean_with_time_independent_predictor, recover_discrete_latent_variance, recover_discrete_observed_mean, recover_discrete_observed_mean_with_extra_process, + recover_discrete_observed_mean_with_extra_process_after, recover_discrete_observed_mean_with_impulse, recover_discrete_observed_mean_with_impulse_carry, recover_discrete_observed_mean_with_initial_time_dependent_predictor, recover_discrete_observed_mean_with_initial_time_independent_predictor, @@ -25,15 +27,19 @@ use psychometric_core::{ recover_initial_time_independent_predictor_effect, recover_irregular_centered_residual_log_rate, recover_kish_weighted_slope, recover_level_change_continuous_intercept, recover_level_change_discrete_increment, - recover_level_change_extra_process_contribution, recover_manifest_lagged_observed_covariance, - recover_manifest_observed_mean, recover_manifest_observed_variance, - recover_manifest_trait_plus_state_observed_variance, recover_stationary_latent_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_level_change_extra_process_contribution, + recover_level_change_extra_process_contribution_after, + recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, + recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, + recover_stationary_latent_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_continuous_intercept_as_discrete_mean_increment, refuse_continuous_intercept_as_initial_latent_mean, refuse_continuous_intercept_as_manifest_means, refuse_difference_quotient_as_local_rate, + refuse_evolved_observed_mean_as_after_extra_process_observed_mean, refuse_evolved_observed_mean_as_extra_process_observed_mean, refuse_evolved_observed_mean_as_impulse_carry_observed_mean, refuse_evolved_observed_mean_as_impulse_observed_mean, @@ -42,7 +48,9 @@ use psychometric_core::{ refuse_evolved_observed_mean_as_time_independent_observed_mean, refuse_extra_process_contribution_as_observed_mean, refuse_extra_process_latent_mean_as_observed_mean, + refuse_extra_process_observed_mean_as_after_extra_process_observed_mean, refuse_finite_interval_process_noise_as_stationary_variance, + refuse_impulse_carry_observed_mean_as_after_extra_process_observed_mean, refuse_impulse_carry_observed_mean_as_initial_time_dependent_observed_mean, refuse_impulse_carry_observed_mean_as_initial_time_independent_observed_mean, refuse_impulse_carry_observed_mean_as_time_independent_observed_mean, @@ -3961,3 +3969,189 @@ fn extra_process_observed_mean_refuses_clock_nonpositive_interval_and_nonnegativ Err(PsychometricError::InvalidNumericInput) ); } + +#[test] +#[allow(clippy::too_many_lines)] +fn after_extra_process_observed_mean_recovers_driver_equation_five_after_t0() { + let loading = 1.0_f64; + let coupling = 1.0_f64; + let predictor = 1.0_f64; + let original = -0.4_f64; + let extra = -0.000_001_f64; + let delta = 2.0_f64; + let elapsed = 1.0_f64; + let initial = 0.0_f64; + let intercept = 0.0_f64; + let manifest_mean = 0.5_f64; + let observed = recover_discrete_observed_mean_with_extra_process_after( + loading, + initial, + original, + intercept, + coupling, + predictor, + extra, + manifest_mean, + delta, + elapsed, + LagClock::EventTime, + ) + .expect("eq5-after-extra-process-mean"); + let composed = recover_discrete_latent_mean_with_extra_process_after( + initial, + original, + intercept, + coupling, + predictor, + extra, + delta, + elapsed, + LagClock::EventTime, + ) + .expect("after-extra-latent"); + let contribution = recover_level_change_extra_process_contribution_after( + coupling, + predictor, + original, + extra, + delta, + elapsed, + LagClock::EventTime, + ) + .expect("after-extra-process"); + let expected = manifest_mean + loading * composed; + let error = rmse(&[expected], &[observed]); + assert!( + error < 1e-15, + "Driver Eq. 5 of §7.2 after-t0 extra-process RMSE {error}: got {observed}" + ); + let first_occasion = recover_discrete_observed_mean_with_extra_process( + loading, + initial, + original, + intercept, + coupling, + predictor, + extra, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-t0-extra-process-mean"); + assert!( + rmse(&[expected], &[first_occasion]) > error, + "T0TDPREDEFFECT extra-process E(y_t) is not after-t0 E(y_t)" + ); + let evolved_observed = recover_discrete_observed_mean( + loading, + initial, + original, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq3-eq5-mean"); + assert!(rmse(&[expected], &[evolved_observed]) > error); + let carry_observed = recover_discrete_observed_mean_with_impulse_carry( + loading, + initial, + original, + intercept, + coupling, + predictor, + manifest_mean, + delta, + elapsed, + LagClock::EventTime, + ) + .expect("eq5-impulse-carry-mean"); + assert!( + rmse(&[expected], &[carry_observed]) > error, + "e^{{a(t-u)}} m x is not extra-process DRIFT drive" + ); + assert!(rmse(&[expected], &[manifest_mean]) > error); + assert!(rmse(&[expected], &[composed]) > error); + assert!(rmse(&[expected], &[contribution]) > error); + assert_eq!( + refuse_extra_process_observed_mean_as_after_extra_process_observed_mean( + first_occasion, + observed + ), + Err(PsychometricError::ExtraProcessObservedMeanIsNotAfterExtraProcessObservedMean) + ); + assert_eq!( + refuse_evolved_observed_mean_as_after_extra_process_observed_mean( + evolved_observed, + observed + ), + Err(PsychometricError::EvolvedObservedMeanIsNotAfterExtraProcessObservedMean) + ); + assert_eq!( + refuse_impulse_carry_observed_mean_as_after_extra_process_observed_mean( + carry_observed, + observed + ), + Err(PsychometricError::ImpulseCarryObservedMeanIsNotAfterExtraProcessObservedMean) + ); + assert_eq!( + refuse_after_extra_process_contribution_as_observed_mean(contribution, observed), + Err(PsychometricError::AfterExtraProcessContributionIsNotObservedMean) + ); + assert_eq!( + refuse_after_extra_process_latent_mean_as_observed_mean(composed, observed), + Err(PsychometricError::AfterExtraProcessLatentMeanIsNotObservedMean) + ); +} + +#[test] +fn after_extra_process_observed_mean_refuses_non_interior_interval_and_clock() { + let coupling = 1.0_f64; + let predictor = 1.0_f64; + let original = -0.4_f64; + let extra = -0.000_001_f64; + assert_eq!( + recover_level_change_extra_process_contribution_after( + coupling, + predictor, + original, + extra, + 2.0, + 2.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_observed_mean_with_extra_process_after( + 1.0, + 0.0, + original, + 0.0, + coupling, + predictor, + extra, + 0.5, + 2.0, + 1.0, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_discrete_observed_mean_with_extra_process_after( + 0.0, + 0.0, + original, + 0.0, + coupling, + predictor, + extra, + 0.5, + 2.0, + 1.0, + LagClock::EventTime + ), + Ok(0.5) + ); +} diff --git a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs index 014e062e..b9baeee9 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -6,11 +6,13 @@ use psychometric_core::{ recover_cluster_mean_within_between_slopes, recover_discrete_constant_predictor_effect, recover_discrete_continuous_intercept_effect, recover_discrete_lagged_latent_covariance, recover_discrete_latent_mean, recover_discrete_latent_mean_with_extra_process, + recover_discrete_latent_mean_with_extra_process_after, recover_discrete_latent_mean_with_impulse, recover_discrete_latent_mean_with_impulse_carry, recover_discrete_latent_mean_with_initial_time_dependent_predictor, recover_discrete_latent_mean_with_initial_time_independent_predictor, recover_discrete_latent_mean_with_time_independent_predictor, recover_discrete_latent_variance, recover_discrete_observed_mean, recover_discrete_observed_mean_with_extra_process, + recover_discrete_observed_mean_with_extra_process_after, recover_discrete_observed_mean_with_impulse, recover_discrete_observed_mean_with_impulse_carry, recover_discrete_observed_mean_with_initial_time_dependent_predictor, recover_discrete_observed_mean_with_initial_time_independent_predictor, @@ -22,14 +24,18 @@ use psychometric_core::{ recover_initial_time_independent_predictor_effect, recover_irregular_centered_residual_log_rate, recover_level_change_continuous_intercept, recover_level_change_discrete_increment, recover_level_change_extra_process_contribution, - recover_loading_point_estimate_mean, recover_manifest_lagged_observed_covariance, - recover_manifest_observed_mean, recover_manifest_observed_variance, - recover_manifest_trait_plus_state_observed_variance, recover_stationary_latent_variance, - recover_time_dependent_predictor_impulse, recover_time_dependent_predictor_impulse_carry, - recover_trait_plus_state_latent_variance, recover_within_residual_event_time_log_rate, + recover_level_change_extra_process_contribution_after, recover_loading_point_estimate_mean, + recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, + recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, + recover_stationary_latent_variance, recover_time_dependent_predictor_impulse, + recover_time_dependent_predictor_impulse_carry, 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_continuous_intercept_as_discrete_mean_increment, refuse_continuous_intercept_as_initial_latent_mean, refuse_continuous_intercept_as_manifest_means, + refuse_evolved_observed_mean_as_after_extra_process_observed_mean, refuse_evolved_observed_mean_as_extra_process_observed_mean, refuse_evolved_observed_mean_as_impulse_carry_observed_mean, refuse_evolved_observed_mean_as_impulse_observed_mean, @@ -38,7 +44,9 @@ use psychometric_core::{ refuse_evolved_observed_mean_as_time_independent_observed_mean, refuse_extra_process_contribution_as_observed_mean, refuse_extra_process_latent_mean_as_observed_mean, + refuse_extra_process_observed_mean_as_after_extra_process_observed_mean, refuse_finite_interval_process_noise_as_stationary_variance, + refuse_impulse_carry_observed_mean_as_after_extra_process_observed_mean, refuse_impulse_carry_observed_mean_as_initial_time_dependent_observed_mean, refuse_impulse_carry_observed_mean_as_initial_time_independent_observed_mean, refuse_impulse_carry_observed_mean_as_time_independent_observed_mean, @@ -1917,3 +1925,139 @@ fn extra_process_observed_mean_is_not_evolved_mean_impulse_mean_or_contribution( Err(psychometric_core::PsychometricError::ExtraProcessLatentMeanIsNotObservedMean) ); } + +#[test] +#[allow(clippy::too_many_lines)] +fn after_extra_process_observed_mean_is_not_t0_extra_evolved_or_impulse_carry() { + let loading = 2.0_f64; + let coupling = 0.4_f64; + let predictor = 3.0_f64; + let original = -0.5_f64; + let extra = -0.05_f64; + let delta = 2.0_f64; + let elapsed = 1.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let observed = recover_discrete_observed_mean_with_extra_process_after( + loading, + initial, + original, + intercept, + coupling, + predictor, + extra, + manifest_mean, + delta, + elapsed, + LagClock::EventTime, + ) + .expect("eq5-after-extra-process-mean"); + let composed = recover_discrete_latent_mean_with_extra_process_after( + initial, + original, + intercept, + coupling, + predictor, + extra, + delta, + elapsed, + LagClock::EventTime, + ) + .expect("after-extra-latent"); + let contribution = recover_level_change_extra_process_contribution_after( + coupling, + predictor, + original, + extra, + delta, + elapsed, + LagClock::EventTime, + ) + .expect("after-extra-process"); + let first_occasion = recover_discrete_observed_mean_with_extra_process( + loading, + initial, + original, + intercept, + coupling, + predictor, + extra, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-t0-extra-process-mean"); + let evolved_observed = recover_discrete_observed_mean( + loading, + initial, + original, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq3-eq5-mean"); + let carry_observed = recover_discrete_observed_mean_with_impulse_carry( + loading, + initial, + original, + intercept, + coupling, + predictor, + manifest_mean, + delta, + elapsed, + LagClock::EventTime, + ) + .expect("eq5-impulse-carry-mean"); + assert!( + (observed - first_occasion).abs() > 1e-3, + "Driver et al. (2017, §7.2): T0TDPREDEFFECT extra E(y_t) is not after-t0 E(y_t)" + ); + assert!( + (observed - evolved_observed).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 of §7.2 after t0): after-t0 E(y_t) is not τ + λ μ_t" + ); + assert!( + (observed - carry_observed).abs() > 1e-3, + "Driver et al. (2017, §7.2): e^{{a(t-u)}} m x is not extra-process DRIFT drive" + ); + assert!((observed - contribution).abs() > 1e-3); + assert!((observed - composed).abs() > 1e-3); + assert_eq!( + refuse_extra_process_observed_mean_as_after_extra_process_observed_mean( + first_occasion, + observed + ), + Err( + psychometric_core::PsychometricError::ExtraProcessObservedMeanIsNotAfterExtraProcessObservedMean + ) + ); + assert_eq!( + refuse_evolved_observed_mean_as_after_extra_process_observed_mean( + evolved_observed, + observed + ), + Err( + psychometric_core::PsychometricError::EvolvedObservedMeanIsNotAfterExtraProcessObservedMean + ) + ); + assert_eq!( + refuse_impulse_carry_observed_mean_as_after_extra_process_observed_mean( + carry_observed, + observed + ), + Err( + psychometric_core::PsychometricError::ImpulseCarryObservedMeanIsNotAfterExtraProcessObservedMean + ) + ); + assert_eq!( + refuse_after_extra_process_contribution_as_observed_mean(contribution, observed), + Err(psychometric_core::PsychometricError::AfterExtraProcessContributionIsNotObservedMean) + ); + assert_eq!( + refuse_after_extra_process_latent_mean_as_observed_mean(composed, observed), + Err(psychometric_core::PsychometricError::AfterExtraProcessLatentMeanIsNotObservedMean) + ); +} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index fe863e8e..4576af58 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -28,7 +28,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | report template/section/copied/style/modality method effects | ADR 0004/0012; PRD/TRD | simulation truth factors implemented; estimator-side method model remains future | partial | | candidate K statistical/Pareto gates + blinded LLM review | ADR 0012; research | future `model_selection` | accepted-target | | compositional topic correlation / stable clustering | ADR 0005/0012; research | future `network_analysis` | accepted-target | -| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; §7.2 level-change `CINT` is `κ = −a m x` (`a < 0`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`; Eq. 3 of that setting is `(1 − e^{a Δt}) m x`); §7.2 extra-process contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, not the dissipating Dirac; `ε ≥ 0` fails closed; Eq. 5 of that contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; extra `LAMBDA` is 0; `τ + λ μ_t` is not that observed mean), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z); full ESEM/DSEM remaining | partial | +| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; §7.2 level-change `CINT` is `κ = −a m x` (`a < 0`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`; Eq. 3 of that setting is `(1 − e^{a Δt}) m x`); §7.2 extra-process contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, not the dissipating Dirac; `ε ≥ 0` fails closed; Eq. 5 of that contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; extra `LAMBDA` is 0; `τ + λ μ_t` is not that observed mean; after-t0 extra-process `TDPREDEFFECT` uses `t − u` with `t0 < u < t` while `μ_t` uses `Δt`; that after-t0 observed mean is not the first-occasion extra-process observed mean), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z); full ESEM/DSEM remaining | partial | | CPU bounded multithreading + GPU/VRAM streaming/parity | ADR 0001/0006 | future `compute_backend` | accepted-target | | TDT detection/tracking vs CHRONOS schema/prediction/temporal consistency | ADR 0016; PRD/research | future `event_intelligence` | accepted-target | | evidence-bounded LLM interpretation | ADR 0010/0012; PRD | `tepp_api` router plus future `interpretation_gateway` | partial | diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index 25ed7bf8..771c847b 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -1,7 +1,7 @@ # ADR 0005 — Posterior-aware ESEM/DSEM and structural interpretation **Decision status:** Accepted -**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), exact scalar contemporaneous `TDPREDEFFECT` impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; the §7.2 level-change form is not that impulse), exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of that Eq. 3 composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar time-independent `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle Eq. 14; `B` is not that discrete increment), exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of that Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of that carried latent mean; `τ + λ μ_t` is not that observed mean), exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; `T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`), exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; `T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand TD composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean), exact scalar §7.2 level-change `CINT` `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; `a < 0` so `−κ / a = m x`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2), exact scalar Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` (not `m x`, not `κ`, and not `TIPREDEFFECT`), exact scalar §7.2 extra near-zero-drift latent process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z; identification `TDPREDEFFECT` on the extra process is 1; extra `DRIFT` printed as `−0.000001`; precisely 0 causes computational problems; `ε = a` is `a_{ηξ} x Δt e^{a Δt}`; not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`; `ε ≥ 0` fails closed), exact scalar observed mean of that extra-process contribution `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z; the extra process has `LAMBDA` 0 and is not an observed indicator; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target +**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), exact scalar contemporaneous `TDPREDEFFECT` impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; the §7.2 level-change form is not that impulse), exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of that Eq. 3 composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar time-independent `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle Eq. 14; `B` is not that discrete increment), exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of that Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of that carried latent mean; `τ + λ μ_t` is not that observed mean), exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; `T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`), exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; `T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand TD composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean), exact scalar §7.2 level-change `CINT` `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; `a < 0` so `−κ / a = m x`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2), exact scalar Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` (not `m x`, not `κ`, and not `TIPREDEFFECT`), exact scalar §7.2 extra near-zero-drift latent process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z; identification `TDPREDEFFECT` on the extra process is 1; extra `DRIFT` printed as `−0.000001`; precisely 0 causes computational problems; `ε = a` is `a_{ηξ} x Δt e^{a Δt}`; not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`; `ε ≥ 0` fails closed), exact scalar observed mean of that extra-process contribution `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z; the extra process has `LAMBDA` 0 and is not an observed indicator; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`), exact scalar after-t0 extra-process contribution `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` and Eq. 5 observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:32Z; `T0TDPREDEFFECT` uses `Δt` for both the evolution and the extra drive; `TDPREDEFFECT` after `t0` uses `t − u`; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target **Date:** 2026-08-05 **Supersedes:** None. ADR 0012 governs upstream topic measurement/network coordinates; this ADR governs higher-order psychometric structure and longitudinal interpretation. @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), recovers the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z; `TDPREDEFFECT` is `M`, not `CINT`; `M x` is not `A^{-1}[e^{A Δt} − I] B z`; `M x` is not Voelkle et al., 2012, Eq. 14; the level-change form is not that impulse), recovers the exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition; JSS PDF re-opened 2026-08-20T09:01Z; the evolved map `τ + λ μ_t` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-impulse latent mean is not `E(y_t)`), recovers the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z; `TIPREDEFFECT` is `B`, not `κ`; form `B z` first, then the discrete intercept map; a zero drift is `B z Δt`; `B` is not the discrete increment; `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle Eq. 14), recovers the exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; JSS PDF re-opened 2026-08-20T12:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-increment latent mean is not `E(y_t)`), recovers the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z; form `m x` first, then `e^{a(t−u)} m x`; a zero drift is `m x`; underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept; `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; an impulse at `u = t` is the contemporaneous map; an impulse at `u ≤ t0` is already in `η(t0)`), recovers the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; JSS PDF re-opened 2026-08-20T05:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the carried latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and its Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF opened 2026-08-20T15:14Z; form `t0_b z` first, then `e^{a Δt} t0_b z`; a zero drift is `t0_b z`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; `e^{A Δt} t0_b z` is not `t0_b z`; `T0TIPREDEFFECT` is the coefficient, not the shift), recovers the exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition; JSS PDF re-opened 2026-08-20T15:28Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and its Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; form `t0_m x0` first, then `e^{a Δt} t0_m x0`; a zero drift is `t0_m x0`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `T0TDPREDEFFECT` is the coefficient, not the shift; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), recovers the exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; the first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar §7.2 level-change `CINT` setting `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z; form `m x` first, then multiply by `−a`; `a < 0` so `−κ / a = m x`; `a ≥ 0` cannot hold a new process mean; `−a m x` is not the dissipating Dirac, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`; the extra near-zero-drift latent process also named in §7.2 is a different specification), recovers the exact scalar Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` (JSS PDF re-opened 2026-08-20T19:50Z; form the level-change `CINT` first, then the discrete intercept map; underflow of `e^{a Δt}` to `+0` keeps `m x`; `(1 − e^{a Δt}) m x` is not `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`), recovers the exact scalar §7.2 extra near-zero-drift latent process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z; form `a_{ηξ} x` first; `ε = a` is `a_{ηξ} x Δt e^{a Δt}`; a zero coupling or zero predictor is exactly zero; `ε ≥ 0` cannot hold a lasting extra state; that contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`), recovers the exact scalar observed mean of that extra-process contribution `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z; the extra process has `LAMBDA` 0 and is not an observed indicator; form the evolved-plus-contribution latent mean first, then `τ + λ` of that mean; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), recovers the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z; `TDPREDEFFECT` is `M`, not `CINT`; `M x` is not `A^{-1}[e^{A Δt} − I] B z`; `M x` is not Voelkle et al., 2012, Eq. 14; the level-change form is not that impulse), recovers the exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition; JSS PDF re-opened 2026-08-20T09:01Z; the evolved map `τ + λ μ_t` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-impulse latent mean is not `E(y_t)`), recovers the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z; `TIPREDEFFECT` is `B`, not `κ`; form `B z` first, then the discrete intercept map; a zero drift is `B z Δt`; `B` is not the discrete increment; `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle Eq. 14), recovers the exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; JSS PDF re-opened 2026-08-20T12:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-increment latent mean is not `E(y_t)`), recovers the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z; form `m x` first, then `e^{a(t−u)} m x`; a zero drift is `m x`; underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept; `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; an impulse at `u = t` is the contemporaneous map; an impulse at `u ≤ t0` is already in `η(t0)`), recovers the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; JSS PDF re-opened 2026-08-20T05:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the carried latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and its Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF opened 2026-08-20T15:14Z; form `t0_b z` first, then `e^{a Δt} t0_b z`; a zero drift is `t0_b z`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; `e^{A Δt} t0_b z` is not `t0_b z`; `T0TIPREDEFFECT` is the coefficient, not the shift), recovers the exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition; JSS PDF re-opened 2026-08-20T15:28Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and its Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; form `t0_m x0` first, then `e^{a Δt} t0_m x0`; a zero drift is `t0_m x0`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `T0TDPREDEFFECT` is the coefficient, not the shift; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), recovers the exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; the first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar §7.2 level-change `CINT` setting `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z; form `m x` first, then multiply by `−a`; `a < 0` so `−κ / a = m x`; `a ≥ 0` cannot hold a new process mean; `−a m x` is not the dissipating Dirac, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`; the extra near-zero-drift latent process also named in §7.2 is a different specification), recovers the exact scalar Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` (JSS PDF re-opened 2026-08-20T19:50Z; form the level-change `CINT` first, then the discrete intercept map; underflow of `e^{a Δt}` to `+0` keeps `m x`; `(1 − e^{a Δt}) m x` is not `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`), recovers the exact scalar §7.2 extra near-zero-drift latent process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z; form `a_{ηξ} x` first; `ε = a` is `a_{ηξ} x Δt e^{a Δt}`; a zero coupling or zero predictor is exactly zero; `ε ≥ 0` cannot hold a lasting extra state; that contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`), recovers the exact scalar observed mean of that extra-process contribution `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z; the extra process has `LAMBDA` 0 and is not an observed indicator; form the evolved-plus-contribution latent mean first, then `τ + λ` of that mean; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`), recovers the exact scalar after-t0 extra-process contribution `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` and its Eq. 5 observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:32Z; `T0TDPREDEFFECT` uses `Δt` for both the evolution and the extra drive; `TDPREDEFFECT` after `t0` uses `t − u` while `μ_t` still uses `Δt`; an impulse at `u = t0` or `u = t` is not interior; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. 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 9286bf6c..65420c8a 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -36,15 +36,16 @@ This slice stays inside `psychometric_core`. It does not add a second invariance 30. recover the exact scalar Eq. 3 increment of that level-change `CINT` `(1 − e^{a Δt}) m x` (Driver et al., 2017, §7.2, pp. 20–21; Eq. 3, pp. 4–5; JSS PDF re-opened 2026-08-20T19:50Z; form `κ = −a m x` first, then `A^{-1}[e^{A Δt} − I] κ`; underflow of `e^{a Δt}` to `+0` keeps `m x`) and refuse treating `(1 − e^{a Δt}) m x` as `m x`, as `κ`, or as `A^{-1}[e^{A Δt} − I] B z`; 31. recover the exact scalar §7.2 extra near-zero-drift latent process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z; identification `TDPREDEFFECT` on the extra process is 1; extra `DRIFT` printed as `−0.000001`; precisely 0 causes computational problems; `ε = a` is `a_{ηξ} x Δt e^{a Δt}`) and refuse treating that contribution as `κ = −a m x`, as `(1 − e^{a Δt}) m x`, or as the dissipating Dirac `m x`; `ε ≥ 0` fails closed; 32. recover the exact scalar observed mean of that extra-process contribution `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z; the extra process has `LAMBDA` 0 and is not an observed indicator; form the evolved-plus-contribution latent mean first, then `τ + λ` of that mean) and refuse treating `τ + λ μ_t`, `τ + λ(μ_t + m x)`, the contribution, or the evolved-plus-contribution latent mean as `E(y_t)`; -33. refuse pooling discrete lags from unequal event intervals as one coefficient; -34. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); -35. refuse the difference quotient as a continuous-time rate; -36. apply the same event-time map to CWC residuals (still not DSEM); -37. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). +33. recover the exact scalar after-t0 extra-process contribution `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` and its Eq. 5 observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:32Z; `T0TDPREDEFFECT` uses `Δt = t − t0` for both the evolution and the extra drive; `TDPREDEFFECT` after `t0` uses `t − u` for the extra drive while `μ_t` still uses `Δt`; an impulse at `u = t0` or `u = t` is not interior; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive) and refuse treating the first-occasion extra-process observed mean, `τ + λ μ_t`, the impulse-carry observed mean, the after-t0 contribution, or the evolved-plus-after-contribution latent mean as that `E(y_t)`; +34. refuse pooling discrete lags from unequal event intervals as one coefficient; +35. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); +36. refuse the difference quotient as a continuous-time rate; +37. apply the same event-time map to CWC residuals (still not DSEM); +38. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). ## Claim boundary -This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. The contemporaneous `TDPREDEFFECT` impulse is not the continuous intercept, not the time-independent discrete effect, and not Voelkle et al. (2012, Eq. 14). The time-independent `TIPREDEFFECT` increment is not the continuous intercept, not the contemporaneous impulse, not Voelkle et al. (2012, Eq. 14), and not the coefficient `B`. The within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). The evolved observed mean `τ + λ μ_t` is not the contemporaneous-impulse observed mean `τ + λ(μ_t + m x)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not the impulse-carry observed mean `τ + λ(μ_t + e^{a(t−u)} m x)` when `u ≠ t`. The evolved observed mean `τ + λ μ_t` is not the impulse-carry observed mean. The carried latent mean is not `E(y_t)`. The evolved-plus-impulse latent mean is not `E(y_t)`. The evolved observed mean `τ + λ μ_t` is not the time-independent-predictor observed mean `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not that time-independent-predictor observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that time-independent-predictor observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TIPREDEFFECT` shift `t0_b z` is not the Eq. 3 process increment `A^{-1}[e^{A Δt} − I] B z`, not `κ`, and not `M x`. The Eq. 3 first-summand carry `e^{A Δt} t0_b z` is not `t0_b z` and is not that process increment. `T0TIPREDEFFECT` is the coefficient, not the first-occasion shift. The evolved observed mean `τ + λ μ_t` is not the first-occasion TI-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_b z)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion observed mean when `u ≠ t0`. The evolved-plus-T0TIPRED latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TDPREDEFFECT` shift `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `κ`. The Eq. 3 first-summand carry `e^{A Δt} t0_m x0` is not `t0_m x0` and is not that within-interval impulse carry. `T0TDPREDEFFECT` is the coefficient, not the first-occasion shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The evolved observed mean `τ + λ μ_t` is not the first-occasion TD-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_m x0)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion TD observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion TD observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion TD observed mean when `u ≠ t0`. The first-occasion TI composition `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that first-occasion TD observed mean. Same numbers as `T0TIPREDEFFECT` yield the same product; Table 3 names a different matrix. The evolved-plus-T0TDPRED latent mean is not `E(y_t)`. The §7.2 level-change `CINT` `κ = −a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. Lasting level change via that `CINT` setting requires `a < 0`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not that `CINT` setting. The Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` is not the dissipating Dirac `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`. Underflow of `e^{a Δt}` to `+0` keeps the equilibrium offset `m x`. The §7.2 extra-process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. Lasting level change via that extra process requires `ε < 0`. Precisely `ε = 0` causes computational problems in the printed specification. The extra-process observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` is not `τ + λ μ_t`, not `τ + λ(μ_t + m x)`, not the contribution, and not the evolved-plus-contribution latent mean. The extra process has `LAMBDA` 0 and is not an observed indicator. +This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. The contemporaneous `TDPREDEFFECT` impulse is not the continuous intercept, not the time-independent discrete effect, and not Voelkle et al. (2012, Eq. 14). The time-independent `TIPREDEFFECT` increment is not the continuous intercept, not the contemporaneous impulse, not Voelkle et al. (2012, Eq. 14), and not the coefficient `B`. The within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). The evolved observed mean `τ + λ μ_t` is not the contemporaneous-impulse observed mean `τ + λ(μ_t + m x)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not the impulse-carry observed mean `τ + λ(μ_t + e^{a(t−u)} m x)` when `u ≠ t`. The evolved observed mean `τ + λ μ_t` is not the impulse-carry observed mean. The carried latent mean is not `E(y_t)`. The evolved-plus-impulse latent mean is not `E(y_t)`. The evolved observed mean `τ + λ μ_t` is not the time-independent-predictor observed mean `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not that time-independent-predictor observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that time-independent-predictor observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TIPREDEFFECT` shift `t0_b z` is not the Eq. 3 process increment `A^{-1}[e^{A Δt} − I] B z`, not `κ`, and not `M x`. The Eq. 3 first-summand carry `e^{A Δt} t0_b z` is not `t0_b z` and is not that process increment. `T0TIPREDEFFECT` is the coefficient, not the first-occasion shift. The evolved observed mean `τ + λ μ_t` is not the first-occasion TI-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_b z)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion observed mean when `u ≠ t0`. The evolved-plus-T0TIPRED latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TDPREDEFFECT` shift `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `κ`. The Eq. 3 first-summand carry `e^{A Δt} t0_m x0` is not `t0_m x0` and is not that within-interval impulse carry. `T0TDPREDEFFECT` is the coefficient, not the first-occasion shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The evolved observed mean `τ + λ μ_t` is not the first-occasion TD-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_m x0)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion TD observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion TD observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion TD observed mean when `u ≠ t0`. The first-occasion TI composition `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that first-occasion TD observed mean. Same numbers as `T0TIPREDEFFECT` yield the same product; Table 3 names a different matrix. The evolved-plus-T0TDPRED latent mean is not `E(y_t)`. The §7.2 level-change `CINT` `κ = −a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. Lasting level change via that `CINT` setting requires `a < 0`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not that `CINT` setting. The Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` is not the dissipating Dirac `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`. Underflow of `e^{a Δt}` to `+0` keeps the equilibrium offset `m x`. The §7.2 extra-process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. Lasting level change via that extra process requires `ε < 0`. Precisely `ε = 0` causes computational problems in the printed specification. The extra-process observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` is not `τ + λ μ_t`, not `τ + λ(μ_t + m x)`, not the contribution, and not the evolved-plus-contribution latent mean. The extra process has `LAMBDA` 0 and is not an observed indicator. `T0TDPREDEFFECT` on the extra process uses `Δt = t − t0` for both the original-process evolution and the extra drive. `TDPREDEFFECT` after `t0` uses `t − u` with `t0 < u < t` for the extra drive while `μ_t` still uses `Δt`. The after-t0 extra-process observed mean is not the first-occasion extra-process observed mean when `u ≠ t0`. The impulse-carry `e^{a(t−u)} m x` is a Dirac on the original process and is not that `DRIFT` drive. ## Authoritative sources @@ -96,6 +97,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - **Level-change continuous intercept.** Driver et al. (2017, §7.2, pp. 20–21; Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T19:45Z): a sudden Dirac dissipates back to the process mean. To generate a lasting level change, `CINT` is set to `TDPREDEFFECT * −DRIFT`. The scalar setting is `κ=−a m x`. Form `m x` first, then multiply by `−a`. Stable `a<0` is required so `−κ/a=m x` is an equilibrium offset. `a≥0` cannot hold a new process mean. A zero effect or zero predictor is exactly zero. `−a m x` is not `m x`, not a free `CINT`, and not `A^{-1}[e^{AΔt}−I]Bz`. The extra near-zero-drift latent process also named in §7.2 is a different specification. An overflowing product fails closed. This is not a Kalman filter and not ctsem estimation. - **Level-change extra process.** Driver et al. (2017, §7.2, pp. 22–23; Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T23:10Z): a lasting level change is specified by an extra latent process. `T0MEANS`, `CINT`, `T0VAR`, `DIFFUSION`, and `TRAITVAR` of that process are fixed to 0. `TDPREDEFFECT` on it is fixed to 1. Its `DRIFT` diagonal is very close to 0 (printed example `−0.000001`; precisely 0 causes computational problems). The original process is driven by the `DRIFT` coupling `a_{ηξ}`. After a unit identification impulse the scalar contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`. Form `a_{ηξ} x` first. When `ε = a` the contribution is `a_{ηξ} x Δt e^{a Δt}`. A zero coupling or zero predictor is exactly zero. `ε ≥ 0` fails closed. That contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. An overflowing product, exponential, or quotient fails closed. This is not a Kalman filter, not a matrix `expm`, and not ctsem estimation. - **Extra-process observed-indicator mean.** Driver et al. (2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z): `y_i(t)=Γ+Λη_i(t)+ζ_i(t)` with `ζ∼N(0,Θ)` and `Γ∼N(τ,Ψ)`. The printed extra process has `LAMBDA` 0 and is not an observed indicator. Original indicators load on the original process after the `DRIFT` coupling. The latent process at `t` after that contribution is `μ_t+a_{ηξ}x(e^{εΔt}−e^{aΔt})/(ε−a)`. The scalar composition is `E(y_t)=τ+λ(μ_t+a_{ηξ}x(e^{εΔt}−e^{aΔt})/(ε−a))`. Form the evolved-plus-contribution latent mean first, then `τ+λ` of that mean. A zero loading is exactly `τ`. A zero evolved-plus-contribution latent mean is exactly `τ`. A zero intercept is exactly `λ` of that latent mean. The evolved observed mean `τ+λμ_t` is not this composition. The contemporaneous map `τ+λ(μ_t+mx)` is not this composition. `MANIFESTMEANS` is not `E(y_t)`. The extra-process contribution is not `E(y_t)`. The evolved-plus-contribution latent mean is not `E(y_t)`. The extra process itself is not an observed indicator. An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. +- **After-t0 extra-process observed-indicator mean.** Driver et al. (2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:32Z): `T0TDPREDEFFECT` begins the extra process at `t = 0`. `TDPREDEFFECT` begins it after `t = 0`. The interior case `t0 < u < t` drives the original process over `t − u` while `μ_t` still uses `Δt = t − t0`. The scalar composition is `E(y_t)=τ+λ(μ_t+a_{ηξ}x(e^{ε(t−u)}−e^{a(t−u)})/(ε−a))`. Form the evolved-plus-after-contribution latent mean first, then `τ+λ` of that mean. An impulse at `u = t0` is the first-occasion extra-process map. An impulse at `u = t` has not yet driven the original process. `e^{a(t−u)}mx` is a Dirac on the original process, not this `DRIFT` drive. A zero loading is exactly `τ`. The first-occasion extra-process observed mean is not this composition when `u ≠ t0`. The evolved observed mean `τ+λμ_t` is not this composition. The extra process itself is not an observed indicator. An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **Level-change discrete increment.** Driver et al. (2017, §7.2, pp. 20–21; Eq. 3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T19:50Z): Equation 3 maps `CINT` through `A^{-1}[e^{AΔt}−I]κ`. With `κ=−a m x` the scalar increment is `(e^{aΔt}−1)/a·(−a m x)=(1−e^{aΔt})m x`. Form the level-change `CINT` first, then the discrete intercept map. Underflow of `e^{aΔt}` to `+0` keeps `m x`. A zero effect or zero predictor is exactly zero. `(1−e^{aΔt})m x` is not `m x`, not `κ`, and not `A^{-1}[e^{AΔt}−I]Bz`. An overflowing product or increment fails closed. This is not a Kalman filter and not ctsem estimation. - **CWC-then-lag.** Sample cluster means are removed first. Consecutive within residuals are then fitted by least squares to \(r_{t+\Delta t}\approx\exp(a\Delta t)\,r_{t}\) on event time. Same-sign pair-wise logs initialize the scalar Newton step. Sign-flipping \(T=2\) CWC pairs have no real logarithm and fail closed. Curran and Bauer (2011, pp. 607–608) show that this person-mean subtraction on a raw autoregressive series does **not** isolate the lagged within-person effect; the helper therefore does not claim to recover the raw-process drift. - **Already-centered irregular residual.** The caller supplies lagged within residuals. The mean of \(a=\ln(r_{t+\Delta t}/r_t)/\Delta t\) is the exact scalar map. Intervals may be irregular. The helper does not center again. This is not DSEM. @@ -133,6 +135,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - Driver et al. (2017, §7.2 / Eq. 3) recovers a known level-change increment \((1-e^{a\Delta t})mx\) at machine-scale RMSE, and that RMSE is smaller than treating the dissipating Dirac \(mx\), the intercept \(\kappa\), or `TIPREDEFFECT` as that increment; underflow of \(e^{a\Delta t}\) to `+0` keeps \(mx\); \(a\ge 0\) with a nonzero impulse fails closed; a non-event clock, a non-positive interval, and an overflowing product fail closed; - Driver et al. (2017, §7.2, pp. 22–23) recovers a known extra-process contribution \(a_{\eta\xi}x(e^{\varepsilon\Delta t}-e^{a\Delta t})/(\varepsilon-a)\) at machine-scale RMSE, and that RMSE is smaller than treating \(\kappa=-amx\), \((1-e^{a\Delta t})mx\), or the dissipating Dirac \(mx\) as that contribution; \(\varepsilon=a\) is \(a_{\eta\xi}x\Delta t\,e^{a\Delta t}\); \(\varepsilon\ge 0\) with a nonzero contribution fails closed; a zero coupling or zero predictor is exactly zero; a non-event clock, a non-positive interval, and an overflowing product fail closed; - Driver et al. (2017, Eq. 5 of the §7.2 extra-process contribution; JSS PDF re-opened 2026-08-21T06:12Z) recovers a known \(E(y_t)=\tau+\lambda(\mu_t+a_{\eta\xi}x(e^{\varepsilon\Delta t}-e^{a\Delta t})/(\varepsilon-a))\) at machine-scale RMSE, and that RMSE is smaller than treating \(\tau+\lambda\mu_t\), \(\tau+\lambda(\mu_t+mx)\), the contribution, `MANIFESTMEANS`, or the evolved-plus-contribution latent mean as \(E(y_t)\); the extra process has `LAMBDA` 0 and is not an observed indicator; a zero original-indicator loading is \(\tau\); a zero coupling recovers \(\tau+\lambda\mu_t\); \(\varepsilon\ge 0\) with a nonzero contribution fails closed; a non-event clock, a non-positive interval, and an overflowing product or sum fail closed; +- Driver et al. (2017, Eq. 5 of the §7.2 after-t0 extra-process contribution; JSS PDF re-opened 2026-08-21T06:32Z) recovers a known \(E(y_t)=\tau+\lambda(\mu_t+a_{\eta\xi}x(e^{\varepsilon(t-u)}-e^{a(t-u)})/(\varepsilon-a))\) at machine-scale RMSE for \(t_0 Date: Fri, 21 Aug 2026 13:22:44 +0000 Subject: [PATCH 56/62] =?UTF-8?q?feat(psychometric):=20recover=20Driver=20?= =?UTF-8?q?=C2=A77.2=20asymTIPREDEFFECT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Driver, Oud, and Voelkle (2017, §7.2, pp. 20–21) name asymTIPREDEFFECT the expected total change in process means given an increase of 1 on a time-independent predictor. For stable a < 0 that total change is -A^{-1} B. The scalar map is -B z / a. Form B z first, then divide by -a. A zero coefficient or zero predictor is exactly zero. a >= 0 cannot hold a finite process-mean change. -B z / a is not the coefficient B, not A^{-1}[e^{A Delta t} - I] B z, not CINT, and not M x. JSS PDF opened 2026-08-21T13:08Z. Still not DSEM, not a Kalman filter, and not ctsem estimation. --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 1 + CLAUDE.md | 2 +- crates/psychometric_core/src/error.rs | 56 ++++ crates/psychometric_core/src/event_time.rs | 290 +++++++++++++++++- crates/psychometric_core/src/lib.rs | 15 + ...multilevel_event_time_recovery_contract.rs | 135 +++++++- .../scientific_claim_boundary_contract.rs | 62 ++++ docs/TRACEABILITY.md | 2 +- docs/adr/0005-posterior-esem-dsem.md | 4 +- .../multilevel-event-time-recovery.md | 17 +- docs/validation/temporal-event-foundation.md | 2 +- 12 files changed, 566 insertions(+), 22 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 2c84e936..c888e2cf 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -61,7 +61,7 @@ boundaries above remain the target modular MSA architecture. | `tepp_simulation` | known-truth temporal/event data generation | | `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics | | `tepp_api` | versioned DTO, schema, and export contracts | -| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; first-occasion `T0TIPREDEFFECT` shift is `t0_b z` and Eq. 3 first-summand carry is `e^{A Δt} t0_b z` (`T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_b z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), first-occasion `T0TDPREDEFFECT` shift is `t0_m x0` and Eq. 3 first-summand carry is `e^{A Δt} t0_m x0` (`T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; §7.2 level-change `CINT` is `κ = −a m x` with `a < 0` so `−κ / a = m x` (`−a m x` is not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2; Eq. 3 of that setting is `(1 − e^{a Δt}) m x`, which is not `m x`, not `κ`, and not `TIPREDEFFECT`; §7.2 extra-process contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`; identification `TDPREDEFFECT` on the extra process is 1; printed extra `DRIFT` is `−0.000001`; not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`; `ε ≥ 0` fails closed; Eq. 5 of that contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; the extra process has `LAMBDA` 0 and is not an observed indicator; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`; after-t0 extra-process `TDPREDEFFECT` is `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` while `μ_t` uses `Δt`; Eq. 5 of that after-t0 contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)`; the first-occasion extra-process observed mean is not that observed mean when `u ≠ t0`; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | +| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; first-occasion `T0TIPREDEFFECT` shift is `t0_b z` and Eq. 3 first-summand carry is `e^{A Δt} t0_b z` (`T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_b z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), first-occasion `T0TDPREDEFFECT` shift is `t0_m x0` and Eq. 3 first-summand carry is `e^{A Δt} t0_m x0` (`T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; §7.2 level-change `CINT` is `κ = −a m x` with `a < 0` so `−κ / a = m x` (`−a m x` is not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2; Eq. 3 of that setting is `(1 − e^{a Δt}) m x`, which is not `m x`, not `κ`, and not `TIPREDEFFECT`; §7.2 extra-process contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`; identification `TDPREDEFFECT` on the extra process is 1; printed extra `DRIFT` is `−0.000001`; not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`; `ε ≥ 0` fails closed; Eq. 5 of that contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; the extra process has `LAMBDA` 0 and is not an observed indicator; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`; after-t0 extra-process `TDPREDEFFECT` is `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` while `μ_t` uses `Δt`; Eq. 5 of that after-t0 contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)`; the first-occasion extra-process observed mean is not that observed mean when `u ≠ t0`; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive; §7.2 `asymTIPREDEFFECT` is `-B z / a` for `a < 0` (`-B z / a` is not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`)), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | No crate exposes placeholder production behavior in Task 1. This prevents an empty façade from becoming a de facto public API before its invariants and tests diff --git a/CHANGELOG.md b/CHANGELOG.md index 05f9508e..a8656bf2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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, §7.2, pp. 20–21; Eq. 3, p. 5; Table 2, p. 12; JSS PDF opened 2026-08-21T13:08Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar `asymTIPREDEFFECT`. Table 2 names `B` `TIPREDEFFECT`. Equation 3 maps a finite event interval as `A^{-1}[e^{A Δt} − I] B z`. Section 7.2 names `asymTIPREDEFFECT` the expected total change in process means given an increase of 1 on a time-independent predictor. For stable `a < 0` that total change is `-A^{-1} B`. The scalar map is `-B z / a`. Form `B z` first, then divide by `-a`. A zero coefficient or zero predictor is exactly zero. `a ≥ 0` cannot hold a finite process-mean change and fails closed. `-B z / a` is not the coefficient `B`, not the finite-interval increment `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. Printed LeisureTime `TIPREDEFFECT` `−0.225` / `asymTIPREDEFFECT` `−1.673` and Happiness `0.549` / `0.219` reconstruct under this map. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-21T06:24Z: `is_oa: false`; Springer `content/pdf` is HTML 200). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:32Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar observed-indicator mean of an extra-process `TDPREDEFFECT` after `t0`. Section 7.2 names `T0TDPREDEFFECT` when the extra process begins at `t = 0` and `TDPREDEFFECT` when it begins after `t = 0`. The printed extra process has `LAMBDA` 0. Original indicators load on the original process after the `DRIFT` coupling over `t − u` with `t0 < u < t` while `μ_t` still uses `Δt = t − t0`. The scalar composition is `E(y_t) = τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))`. The first-occasion extra-process observed mean uses `Δt` for both the evolution and the extra drive and is not this composition when `u ≠ t0`. The evolved observed mean `τ + λ μ_t` is not this composition. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is a Dirac on the original process and is not this `DRIFT` drive. An impulse at `u = t0` or `u = t` is not interior. A zero original-indicator loading is exactly `τ`. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-21T06:24Z: `is_oa: false`; Springer `content/pdf` is HTML 200). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; §7.2, pp. 22–23; Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-21T06:12Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar observed-indicator mean of the extra near-zero-drift latent process contribution. Section 7.2's printed extra process has `LAMBDA` 0 and is not an observed indicator. Original indicators load on the original process after the `DRIFT` coupling `a_{ηξ}`. After a unit identification impulse the latent process at `t` is `μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`. The scalar composition is `E(y_t) = τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))`. Form the evolved-plus-contribution latent mean first, then `τ + λ` of that mean. The evolved observed mean `τ + λ μ_t` is not this composition. The contemporaneous map `τ + λ(μ_t + m x)` is not this composition. The extra-process contribution is not `E(y_t)`. The evolved-plus-contribution latent mean is not `E(y_t)`. A zero original-indicator loading is exactly `τ`. A zero coupling recovers `τ + λ μ_t`. `ε ≥ 0` fails closed. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-21T06:24Z: `is_oa: false`; Springer `content/pdf` is HTML 200). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, §7.2, pp. 22–23; Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T23:10Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar contribution of the extra near-zero-drift latent process. Section 7.2 specifies a lasting level change by that extra process: `T0MEANS`, `CINT`, `T0VAR`, `DIFFUSION`, and `TRAITVAR` of it are fixed to 0; `TDPREDEFFECT` on it is fixed to 1; its `DRIFT` diagonal is very close to 0 (printed example `−0.000001`; precisely 0 causes computational problems); and its effect on the original process is the `DRIFT` coupling `a_{ηξ}`. After a unit identification impulse the scalar contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`). Form `a_{ηξ} x` first. A zero coupling or zero predictor is exactly zero. `ε ≥ 0` fails closed. That contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-20T23:10Z: `is_oa: false`; Springer `content/pdf` is HTML 200). diff --git a/CLAUDE.md b/CLAUDE.md index 862d22bb..046d7e2e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. Putnick and Bornstein (2016, PMC5145197 opened 2026-08-19T22:15Z) require scalar invariance before latent-mean comparison; residual invariance is not a prerequisite. Two-observation series have no residual degrees of freedom (`ordinary_least_squares_fit` returns residual variance `0`) and cap at strong/scalar; they still license means. This is two-group OLS, not MGCFA. Meredith (1993) names remain unread labels. -- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. The contemporaneous time-dependent predictor impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`). Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). The §7.2 level-change form is not that impulse. The observed mean of that contemporaneous impulse is `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-impulse latent mean is not `E(y_t)`. The time-independent predictor increment is `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`). Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. `TIPREDEFFECT` is `B`, not that discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle et al. (2012, Eq. 14). The observed mean of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The within-interval time-dependent impulse carry is `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation). Form `m x` first, then `e^{a(t−u)} m x`. A zero drift is `m x` with no dissipation. Underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept. `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). An impulse at `u = t` is the contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. The observed mean of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The carried latent mean is not `E(y_t)`. The first-occasion time-independent predictor shift is `t0_b z` (Driver et al., 2017, Table 3 `T0TIPREDEFFECT`; Eq. 3 first summand). Form `t0_b z` first, then `e^{a Δt} t0_b z`. Form `μ_t` first, then add that carry. A zero drift is `t0_b z`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. `e^{A Δt} t0_b z` is not `t0_b z`. `T0TIPREDEFFECT` is the coefficient, not the shift. The observed mean of that first-occasion carry is `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The evolved-plus-carry latent mean is not `E(y_t)`. The first-occasion time-dependent predictor shift is `t0_m x0` (Driver et al., 2017, Table 3 `T0TDPREDEFFECT`; Eq. 3 first summand; JSS PDF re-opened 2026-08-20T19:10Z). Form `t0_m x0` first, then `e^{a Δt} t0_m x0`. Form `μ_t` first, then add that carry. A zero drift is `t0_m x0`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`. `e^{A Δt} t0_m x0` is not `t0_m x0`. `T0TDPREDEFFECT` is the coefficient, not the shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The observed mean of that first-occasion TD carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean. The evolved-plus-carry latent mean is not `E(y_t)`. The lasting level-change `CINT` is `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z). Form `m x` first, then multiply by `−a`. Stable `a < 0` is required so `−κ / a = m x` is an equilibrium offset. `a ≥ 0` cannot hold a new process mean. `−a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not this `CINT` setting. Equation 3 maps that intercept as `(1 − e^{a Δt}) m x` (JSS PDF re-opened 2026-08-20T19:50Z). Form the level-change `CINT` first, then the discrete intercept map. Underflow of `e^{a Δt}` to `+0` keeps `m x`. `(1 − e^{a Δt}) m x` is not `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`. The printed §7.2 lasting level change is an extra near-zero-drift latent process (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z). `T0MEANS`, `CINT`, `T0VAR`, `DIFFUSION`, and `TRAITVAR` of that process are fixed to 0; `TDPREDEFFECT` on it is fixed to 1; its `DRIFT` diagonal is very close to 0 (printed example `−0.000001`; precisely 0 causes computational problems); the original process is driven by the `DRIFT` coupling `a_{ηξ}`. After a unit identification impulse the scalar contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`). Form `a_{ηξ} x` first. A zero coupling or zero predictor is exactly zero. `ε ≥ 0` fails closed. That contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. The observed mean of that extra-process contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5 of that §7.2 contribution; JSS PDF re-opened 2026-08-21T06:12Z). The extra process has `LAMBDA` 0 and is not an observed indicator. Original indicators load on the original process after the `DRIFT` coupling. The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The contribution is not `E(y_t)`. The evolved-plus-contribution latent mean is not `E(y_t)`. `T0TDPREDEFFECT` on the extra process begins at `t = 0` and uses `Δt = t − t0` for both the original-process evolution and the extra drive. `TDPREDEFFECT` after `t0` uses `t − u` with `t0 < u < t` for the extra drive while `μ_t` still uses `Δt`. The observed mean of that after-t0 extra-process contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, Eq. 5 of that §7.2 after-t0 contribution; JSS PDF re-opened 2026-08-21T06:32Z). The first-occasion extra-process observed mean is not that observed mean when `u ≠ t0`. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is a Dirac on the original process and is not that `DRIFT` drive. An impulse at `u = t0` or `u = t` is not interior. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. The contemporaneous time-dependent predictor impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`). Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). The §7.2 level-change form is not that impulse. The observed mean of that contemporaneous impulse is `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-impulse latent mean is not `E(y_t)`. The time-independent predictor increment is `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`). Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. `TIPREDEFFECT` is `B`, not that discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle et al. (2012, Eq. 14). The observed mean of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The within-interval time-dependent impulse carry is `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation). Form `m x` first, then `e^{a(t−u)} m x`. A zero drift is `m x` with no dissipation. Underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept. `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). An impulse at `u = t` is the contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. The observed mean of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The carried latent mean is not `E(y_t)`. The first-occasion time-independent predictor shift is `t0_b z` (Driver et al., 2017, Table 3 `T0TIPREDEFFECT`; Eq. 3 first summand). Form `t0_b z` first, then `e^{a Δt} t0_b z`. Form `μ_t` first, then add that carry. A zero drift is `t0_b z`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. `e^{A Δt} t0_b z` is not `t0_b z`. `T0TIPREDEFFECT` is the coefficient, not the shift. The observed mean of that first-occasion carry is `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The evolved-plus-carry latent mean is not `E(y_t)`. The first-occasion time-dependent predictor shift is `t0_m x0` (Driver et al., 2017, Table 3 `T0TDPREDEFFECT`; Eq. 3 first summand; JSS PDF re-opened 2026-08-20T19:10Z). Form `t0_m x0` first, then `e^{a Δt} t0_m x0`. Form `μ_t` first, then add that carry. A zero drift is `t0_m x0`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`. `e^{A Δt} t0_m x0` is not `t0_m x0`. `T0TDPREDEFFECT` is the coefficient, not the shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The observed mean of that first-occasion TD carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean. The evolved-plus-carry latent mean is not `E(y_t)`. The lasting level-change `CINT` is `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z). Form `m x` first, then multiply by `−a`. Stable `a < 0` is required so `−κ / a = m x` is an equilibrium offset. `a ≥ 0` cannot hold a new process mean. `−a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not this `CINT` setting. Equation 3 maps that intercept as `(1 − e^{a Δt}) m x` (JSS PDF re-opened 2026-08-20T19:50Z). Form the level-change `CINT` first, then the discrete intercept map. Underflow of `e^{a Δt}` to `+0` keeps `m x`. `(1 − e^{a Δt}) m x` is not `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`. The printed §7.2 lasting level change is an extra near-zero-drift latent process (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z). `T0MEANS`, `CINT`, `T0VAR`, `DIFFUSION`, and `TRAITVAR` of that process are fixed to 0; `TDPREDEFFECT` on it is fixed to 1; its `DRIFT` diagonal is very close to 0 (printed example `−0.000001`; precisely 0 causes computational problems); the original process is driven by the `DRIFT` coupling `a_{ηξ}`. After a unit identification impulse the scalar contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`). Form `a_{ηξ} x` first. A zero coupling or zero predictor is exactly zero. `ε ≥ 0` fails closed. That contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. The observed mean of that extra-process contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5 of that §7.2 contribution; JSS PDF re-opened 2026-08-21T06:12Z). The extra process has `LAMBDA` 0 and is not an observed indicator. Original indicators load on the original process after the `DRIFT` coupling. The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The contribution is not `E(y_t)`. The evolved-plus-contribution latent mean is not `E(y_t)`. `T0TDPREDEFFECT` on the extra process begins at `t = 0` and uses `Δt = t − t0` for both the original-process evolution and the extra drive. `TDPREDEFFECT` after `t0` uses `t − u` with `t0 < u < t` for the extra drive while `μ_t` still uses `Δt`. The observed mean of that after-t0 extra-process contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, Eq. 5 of that §7.2 after-t0 contribution; JSS PDF re-opened 2026-08-21T06:32Z). The first-occasion extra-process observed mean is not that observed mean when `u ≠ t0`. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is a Dirac on the original process and is not that `DRIFT` drive. An impulse at `u = t0` or `u = t` is not interior. The asymptotic time-independent predictor effect is `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z). Form `B z` first, then divide by `-a`. Stable `a < 0` is required. `a ≥ 0` cannot hold a finite process-mean change. `-B z / a` is not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. diff --git a/crates/psychometric_core/src/error.rs b/crates/psychometric_core/src/error.rs index a8a0e013..5632ff42 100644 --- a/crates/psychometric_core/src/error.rs +++ b/crates/psychometric_core/src/error.rs @@ -324,6 +324,23 @@ pub enum PsychometricError { /// treated as `E(y_t)`. Equation 5 maps `E(y_t) = τ + λ` of /// that mean. AfterExtraProcessLatentMeanIsNotObservedMean, + /// Driver §7.2 `asymTIPREDEFFECT` was requested for a non-stable + /// drift. The expected total change in process means is `-B z / a` + /// and requires `a < 0`. + AsymptoticTimeIndependentEffectRequiresStableDrift, + /// Driver §7.2 `asymTIPREDEFFECT` was treated as `TIPREDEFFECT`. + /// `-B z / a` is not the coefficient `B`. + AsymptoticTimeIndependentEffectIsNotCoefficient, + /// Driver §7.2 `asymTIPREDEFFECT` was treated as the finite-interval + /// discrete increment. `-B z / a` is not + /// `A^{-1}[e^{A Δt} − I] B z`. + AsymptoticTimeIndependentEffectIsNotDiscreteEffect, + /// Driver §7.2 `asymTIPREDEFFECT` was treated as `CINT`. + /// `-B z / a` is not `κ`. + AsymptoticTimeIndependentEffectIsNotContinuousIntercept, + /// Driver §7.2 `asymTIPREDEFFECT` was treated as the contemporaneous + /// Dirac. `-B z / a` is not `M x`. + AsymptoticTimeIndependentEffectIsNotTimeDependentImpulse, } impl fmt::Display for PsychometricError { @@ -585,6 +602,21 @@ impl fmt::Display for PsychometricError { Self::AfterExtraProcessLatentMeanIsNotObservedMean => { "evolved-plus-after-contribution latent mean is not the after-t0 extra-process observed mean" } + Self::AsymptoticTimeIndependentEffectRequiresStableDrift => { + "asymptotic time-independent predictor effect requires a stable negative drift" + } + Self::AsymptoticTimeIndependentEffectIsNotCoefficient => { + "asymptotic time-independent predictor effect is not the TIPREDEFFECT coefficient" + } + Self::AsymptoticTimeIndependentEffectIsNotDiscreteEffect => { + "asymptotic time-independent predictor effect is not the finite-interval discrete increment" + } + Self::AsymptoticTimeIndependentEffectIsNotContinuousIntercept => { + "asymptotic time-independent predictor effect is not the continuous intercept" + } + Self::AsymptoticTimeIndependentEffectIsNotTimeDependentImpulse => { + "asymptotic time-independent predictor effect is not the contemporaneous impulse" + } }; formatter.write_str(message) } @@ -989,4 +1021,28 @@ mod tests { "evolved-plus-after-contribution latent mean is not the after-t0 extra-process observed mean" ); } + + #[test] + fn asymptotic_time_independent_boundary_messages_are_stable() { + assert_eq!( + PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift.to_string(), + "asymptotic time-independent predictor effect requires a stable negative drift" + ); + assert_eq!( + PsychometricError::AsymptoticTimeIndependentEffectIsNotCoefficient.to_string(), + "asymptotic time-independent predictor effect is not the TIPREDEFFECT coefficient" + ); + assert_eq!( + PsychometricError::AsymptoticTimeIndependentEffectIsNotDiscreteEffect.to_string(), + "asymptotic time-independent predictor effect is not the finite-interval discrete increment" + ); + assert_eq!( + PsychometricError::AsymptoticTimeIndependentEffectIsNotContinuousIntercept.to_string(), + "asymptotic time-independent predictor effect is not the continuous intercept" + ); + assert_eq!( + PsychometricError::AsymptoticTimeIndependentEffectIsNotTimeDependentImpulse.to_string(), + "asymptotic time-independent predictor effect is not the contemporaneous impulse" + ); + } } diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index 5d28c36f..e9a42b32 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -51,7 +51,12 @@ //! the process mean. Equation 3's second summand also //! maps the time-independent predictor as `A^{-1}[e^{A Δt} − I] B z` //! (Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle -//! Eq. 14). Table 3 (p. 13) names a different matrix +//! Eq. 14). Section 7.2 (pp. 20–21; JSS PDF opened 2026-08-21T13:08Z) +//! names `asymTIPREDEFFECT` the expected total change in process +//! means given a unit increase on a time-independent predictor. The +//! scalar map is `-B z / a` for stable `a < 0`. That total change is +//! not the coefficient `B`, not the finite-interval increment +//! `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. Table 3 (p. 13) names a different matrix //! `T0TIPREDEFFECT` for time-independent predictors on latents at //! `T0`. The scalar first-occasion shift is `t0_b z`. Equation 3's //! first summand carries that shift as `e^{A Δt} t0_b z`. That carry @@ -2528,6 +2533,125 @@ pub fn refuse_time_independent_coefficient_as_discrete_effect( Err(PsychometricError::TimeIndependentCoefficientIsNotDiscreteEffect) } +/// Exact scalar §7.2 `asymTIPREDEFFECT`. +/// +/// Driver, Oud, and Voelkle (2017, §7.2, pp. 20–21; Eq. 3, p. 5; +/// Table 2, p. 12; JSS PDF opened 2026-08-21T13:08Z from +/// ) +/// name `TIPREDEFFECT` the continuous-time coefficient `B`. Equation 3 +/// maps a finite event interval as `A^{-1}[e^{A Δt} − I] B z`. Section +/// 7.2 then names `asymTIPREDEFFECT` the expected total change in +/// process means given an increase of 1 on the time-independent +/// predictor. For stable `a < 0` that total change is `-A^{-1} B`. +/// The scalar map is `-B z / a`. Form `B z` first, then divide by +/// `-a`. A zero coefficient or zero predictor is exactly zero. +/// `a ≥ 0` cannot hold a finite process-mean change and fails closed. +/// `-B z / a` is not the coefficient `B`, not the finite-interval +/// increment `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. +/// This is not a Kalman filter, not a matrix `expm`, and not ctsem +/// estimation. +/// +/// # Errors +/// +/// Returns [`PsychometricError::EventTimeRequired`] for any non-event +/// clock, [`PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift`] +/// when the drift is not strictly negative and the effect is nonzero, +/// and [`PsychometricError::InvalidNumericInput`] when an input is +/// non-finite or `B z` or the quotient overflows. +pub fn recover_asymptotic_time_independent_predictor_effect( + time_independent_effect: f64, + time_independent_predictor: f64, + log_rate: f64, + clock: LagClock, +) -> Result { + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + if !time_independent_effect.is_finite() + || !time_independent_predictor.is_finite() + || !log_rate.is_finite() + { + return Err(PsychometricError::InvalidNumericInput); + } + if time_independent_effect == 0.0 || time_independent_predictor == 0.0 { + return Ok(0.0); + } + if log_rate >= 0.0 { + return Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift); + } + let continuous = require_finite(time_independent_effect * time_independent_predictor)?; + require_finite(continuous / -log_rate) +} + +/// Refuse treating §7.2 `asymTIPREDEFFECT` as `TIPREDEFFECT`. +/// +/// `-B z / a` is the expected total change in process means. Table 2 +/// names `B` `TIPREDEFFECT`. The coefficient is not that total change. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::AsymptoticTimeIndependentEffectIsNotCoefficient`]. +pub fn refuse_asymptotic_time_independent_effect_as_coefficient( + asymptotic_effect: f64, + time_independent_coefficient: f64, +) -> Result { + let _ = (asymptotic_effect, time_independent_coefficient); + Err(PsychometricError::AsymptoticTimeIndependentEffectIsNotCoefficient) +} + +/// Refuse treating §7.2 `asymTIPREDEFFECT` as the finite-interval +/// discrete increment. +/// +/// `-B z / a` is the `Δt → ∞` limit of `A^{-1}[e^{A Δt} − I] B z` +/// under stable `a < 0`. A finite event interval is not that limit. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::AsymptoticTimeIndependentEffectIsNotDiscreteEffect`]. +pub fn refuse_asymptotic_time_independent_effect_as_discrete_effect( + asymptotic_effect: f64, + time_independent_increment: f64, +) -> Result { + let _ = (asymptotic_effect, time_independent_increment); + Err(PsychometricError::AsymptoticTimeIndependentEffectIsNotDiscreteEffect) +} + +/// Refuse treating §7.2 `asymTIPREDEFFECT` as `CINT`. +/// +/// `-B z / a` is the expected total change from a time-independent +/// predictor. Table 2 names `κ` `CINT`. Those are not the same map. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::AsymptoticTimeIndependentEffectIsNotContinuousIntercept`]. +pub fn refuse_asymptotic_time_independent_effect_as_continuous_intercept( + asymptotic_effect: f64, + continuous_intercept: f64, +) -> Result { + let _ = (asymptotic_effect, continuous_intercept); + Err(PsychometricError::AsymptoticTimeIndependentEffectIsNotContinuousIntercept) +} + +/// Refuse treating §7.2 `asymTIPREDEFFECT` as `M x`. +/// +/// The fourth-summand impulse is contemporaneous. The asymptotic +/// time-independent effect is a new process mean, not a Dirac. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::AsymptoticTimeIndependentEffectIsNotTimeDependentImpulse`]. +pub fn refuse_asymptotic_time_independent_effect_as_time_dependent_impulse( + asymptotic_effect: f64, + time_dependent_impulse: f64, +) -> Result { + let _ = (asymptotic_effect, time_dependent_impulse); + Err(PsychometricError::AsymptoticTimeIndependentEffectIsNotTimeDependentImpulse) +} + /// Exact scalar observed mean of a time-independent predictor. /// /// Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Eq. 3, p. 5; Table 2, @@ -4023,10 +4147,12 @@ pub(crate) fn fit_scalar_log_rate(pairs: &[(f64, f64, f64)]) -> Result 1e-3); + assert!((recovered - discrete).abs() > 1e-3); + assert!((recovered - impulse).abs() > 1e-3); + assert_eq!( + refuse_asymptotic_time_independent_effect_as_coefficient(recovered, effect), + Err(PsychometricError::AsymptoticTimeIndependentEffectIsNotCoefficient) + ); + assert_eq!( + refuse_asymptotic_time_independent_effect_as_discrete_effect(recovered, discrete), + Err(PsychometricError::AsymptoticTimeIndependentEffectIsNotDiscreteEffect) + ); + assert_eq!( + refuse_asymptotic_time_independent_effect_as_continuous_intercept(recovered, 0.3), + Err(PsychometricError::AsymptoticTimeIndependentEffectIsNotContinuousIntercept) + ); + assert_eq!( + refuse_asymptotic_time_independent_effect_as_time_dependent_impulse(recovered, impulse), + Err(PsychometricError::AsymptoticTimeIndependentEffectIsNotTimeDependentImpulse) + ); + } + + #[test] + fn asymptotic_time_independent_effect_invalid_inputs_fail_closed() { + let effect = -0.225_f64; + let predictor = 1.0_f64; + let log_rate = -0.134_488_942_f64; + assert_eq!( + recover_asymptotic_time_independent_predictor_effect( + effect, + predictor, + log_rate, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_asymptotic_time_independent_predictor_effect( + effect, + predictor, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) + ); + assert_eq!( + recover_asymptotic_time_independent_predictor_effect( + effect, + predictor, + 0.5, + LagClock::EventTime + ), + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) + ); + assert_eq!( + recover_asymptotic_time_independent_predictor_effect( + f64::NAN, + predictor, + log_rate, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_asymptotic_time_independent_predictor_effect( + 1e308, + 2.0, + log_rate, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_asymptotic_time_independent_predictor_effect( + 1e308, + 1.0, + -1e-308, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + } + #[test] fn discrete_observed_mean_with_impulse_recovers_driver_equation_five() { let loading = 2.0_f64; diff --git a/crates/psychometric_core/src/lib.rs b/crates/psychometric_core/src/lib.rs index 58094293..cfdee41c 100644 --- a/crates/psychometric_core/src/lib.rs +++ b/crates/psychometric_core/src/lib.rs @@ -110,6 +110,11 @@ //! map is not that observed mean; the after-t0 contribution is not //! `E(y_t)`; the evolved-plus-after-contribution latent mean is not //! `E(y_t)`), +//! recovers the Driver §7.2 `asymTIPREDEFFECT` as `-B z / a` +//! (pp. 20–21; JSS PDF opened 2026-08-21T13:08Z; expected total +//! change in process means given a time-independent predictor; +//! `a < 0`; not the coefficient `B`, not +//! `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`), //! and refuses //! latent-mean comparison below strong invariance. @@ -158,6 +163,8 @@ pub use event_time::LagClock; 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 §7.2 `asymTIPREDEFFECT` `-B z / a`. +pub use event_time::recover_asymptotic_time_independent_predictor_effect; /// Exact scalar discrete effect of a constant event-time predictor. pub use event_time::recover_discrete_constant_predictor_effect; /// Exact scalar discrete intercept increment `A^{-1}[e^{A Δt} − I] κ`. @@ -256,6 +263,14 @@ pub use event_time::recover_within_residual_event_time_log_rate; pub use event_time::refuse_after_extra_process_contribution_as_observed_mean; /// Refuse treating the evolved-plus-after-contribution latent mean as `E(y_t)`. pub use event_time::refuse_after_extra_process_latent_mean_as_observed_mean; +/// Refuse treating §7.2 `asymTIPREDEFFECT` as `TIPREDEFFECT` `B`. +pub use event_time::refuse_asymptotic_time_independent_effect_as_coefficient; +/// Refuse treating §7.2 `asymTIPREDEFFECT` as `CINT`. +pub use event_time::refuse_asymptotic_time_independent_effect_as_continuous_intercept; +/// Refuse treating §7.2 `asymTIPREDEFFECT` as the finite-interval discrete increment. +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 Driver Table 2 `CINT` as the discrete mean increment. pub use event_time::refuse_continuous_intercept_as_discrete_mean_increment; /// Refuse treating Driver Table 2 `CINT` as `T0MEANS`. 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 17705429..5a9fad2d 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -4,10 +4,11 @@ use psychometric_core::{ ClusteredEventScore, ClusteredScore, EventOccasion, IndicatorKind, LagClock, LaggedWithinResidual, PsychometricError, map_discrete_lag_across_event_intervals, - ordinary_least_squares_slope, recover_cluster_mean_within_between_slopes, - recover_discrete_constant_predictor_effect, recover_discrete_continuous_intercept_effect, - recover_discrete_lag_from_log_rate, recover_discrete_lagged_latent_covariance, - recover_discrete_latent_mean, recover_discrete_latent_mean_with_extra_process, + ordinary_least_squares_slope, recover_asymptotic_time_independent_predictor_effect, + recover_cluster_mean_within_between_slopes, recover_discrete_constant_predictor_effect, + recover_discrete_continuous_intercept_effect, recover_discrete_lag_from_log_rate, + recover_discrete_lagged_latent_covariance, recover_discrete_latent_mean, + recover_discrete_latent_mean_with_extra_process, recover_discrete_latent_mean_with_extra_process_after, recover_discrete_latent_mean_with_impulse, recover_discrete_latent_mean_with_impulse_carry, recover_discrete_latent_mean_with_initial_time_dependent_predictor, @@ -36,6 +37,10 @@ use psychometric_core::{ 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_time_independent_effect_as_coefficient, + 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_continuous_intercept_as_discrete_mean_increment, refuse_continuous_intercept_as_initial_latent_mean, refuse_continuous_intercept_as_manifest_means, refuse_difference_quotient_as_local_rate, @@ -4155,3 +4160,125 @@ fn after_extra_process_observed_mean_refuses_non_interior_interval_and_clock() { Ok(0.5) ); } + +#[test] +fn asymptotic_time_independent_effect_recovers_driver_section_seven_point_two() { + // Driver et al. (2017, §7.2, p. 21) print LeisureTime + // TIPREDEFFECT = −0.225 and asymTIPREDEFFECT = −1.673 for a unit + // increase. Reconstruct a = −B / asym. + let effect = -0.225_f64; + let predictor = 1.0_f64; + let printed_asym = -1.673_f64; + let log_rate = -effect / printed_asym; + let recovered = recover_asymptotic_time_independent_predictor_effect( + effect, + predictor, + log_rate, + LagClock::EventTime, + ) + .expect("asymTIPREDEFFECT"); + let expected = -(effect * predictor) / log_rate; + let error = rmse(&[expected], &[recovered]); + assert!( + error < 1e-15, + "Driver §7.2 asymTIPREDEFFECT RMSE {error}: got {recovered}" + ); + assert!( + rmse(&[printed_asym], &[recovered]) < 1e-12, + "printed LeisureTime asymTIPREDEFFECT" + ); + let discrete = recover_discrete_time_independent_predictor_effect( + effect, + predictor, + log_rate, + 1.0, + LagClock::EventTime, + ) + .expect("discreteTIPREDEFFECT"); + let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("impulse"); + assert!( + rmse(&[recovered], &[effect]) > error, + "TIPREDEFFECT B is not asymTIPREDEFFECT" + ); + assert!( + rmse(&[recovered], &[discrete]) > error, + "A^{{-1}}[e^{{A Δt}} − I] B z is not -B z / a" + ); + assert!(rmse(&[recovered], &[impulse]) > error); + let happiness = recover_asymptotic_time_independent_predictor_effect( + 0.549, + 1.0, + -0.549 / 0.219, + LagClock::EventTime, + ) + .expect("happiness-asym"); + assert!(rmse(&[0.219], &[happiness]) < 1e-12); + assert_eq!( + refuse_asymptotic_time_independent_effect_as_coefficient(recovered, effect), + Err(PsychometricError::AsymptoticTimeIndependentEffectIsNotCoefficient) + ); + assert_eq!( + refuse_asymptotic_time_independent_effect_as_discrete_effect(recovered, discrete), + Err(PsychometricError::AsymptoticTimeIndependentEffectIsNotDiscreteEffect) + ); + assert_eq!( + refuse_asymptotic_time_independent_effect_as_continuous_intercept(recovered, 0.3), + Err(PsychometricError::AsymptoticTimeIndependentEffectIsNotContinuousIntercept) + ); + assert_eq!( + refuse_asymptotic_time_independent_effect_as_time_dependent_impulse(recovered, impulse), + Err(PsychometricError::AsymptoticTimeIndependentEffectIsNotTimeDependentImpulse) + ); +} + +#[test] +fn asymptotic_time_independent_effect_refuses_unstable_drift_and_non_event_clocks() { + let effect = -0.225_f64; + let predictor = 1.0_f64; + let log_rate = -0.134_488_942_f64; + assert_eq!( + recover_asymptotic_time_independent_predictor_effect( + effect, + predictor, + log_rate, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_asymptotic_time_independent_predictor_effect( + effect, + predictor, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) + ); + assert_eq!( + recover_asymptotic_time_independent_predictor_effect( + effect, + predictor, + 0.5, + LagClock::EventTime + ), + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) + ); + assert_eq!( + recover_asymptotic_time_independent_predictor_effect( + 1e308, + 2.0, + log_rate, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_asymptotic_time_independent_predictor_effect( + 0.0, + predictor, + 0.0, + LagClock::EventTime + ), + Ok(0.0) + ); +} diff --git a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs index b9baeee9..47d0a4ac 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -3,6 +3,7 @@ use psychometric_core::{ ClusteredEventScore, ClusteredScore, IndicatorKind, LagClock, LaggedWithinResidual, ordinary_least_squares_slope, posterior_draw_point_estimate_mean, + recover_asymptotic_time_independent_predictor_effect, recover_cluster_mean_within_between_slopes, recover_discrete_constant_predictor_effect, recover_discrete_continuous_intercept_effect, recover_discrete_lagged_latent_covariance, recover_discrete_latent_mean, recover_discrete_latent_mean_with_extra_process, @@ -32,6 +33,10 @@ use psychometric_core::{ 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_time_independent_effect_as_coefficient, + 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_continuous_intercept_as_discrete_mean_increment, refuse_continuous_intercept_as_initial_latent_mean, refuse_continuous_intercept_as_manifest_means, @@ -2061,3 +2066,60 @@ fn after_extra_process_observed_mean_is_not_t0_extra_evolved_or_impulse_carry() Err(psychometric_core::PsychometricError::AfterExtraProcessLatentMeanIsNotObservedMean) ); } + +#[test] +fn asymptotic_time_independent_effect_is_not_coefficient_discrete_cint_or_impulse() { + let effect = -0.225_f64; + let predictor = 2.0_f64; + let log_rate = -0.134_488_942_f64; + let recovered = recover_asymptotic_time_independent_predictor_effect( + effect, + predictor, + log_rate, + LagClock::EventTime, + ) + .expect("asymTIPREDEFFECT"); + let discrete = recover_discrete_time_independent_predictor_effect( + effect, + predictor, + log_rate, + 1.0, + LagClock::EventTime, + ) + .expect("discreteTIPREDEFFECT"); + let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("impulse"); + assert!( + (recovered - effect).abs() > 1e-3, + "Driver et al. (2017, §7.2, pp. 20–21): asymTIPREDEFFECT is not TIPREDEFFECT B" + ); + assert!( + (recovered - discrete).abs() > 1e-3, + "Driver et al. (2017, §7.2): -B z / a is not A^{{-1}}[e^{{A Δt}} − I] B z" + ); + assert!( + (recovered - impulse).abs() > 1e-3, + "Driver et al. (2017, §7.2): -B z / a is not M x" + ); + assert_eq!( + refuse_asymptotic_time_independent_effect_as_coefficient(recovered, effect), + Err(psychometric_core::PsychometricError::AsymptoticTimeIndependentEffectIsNotCoefficient) + ); + assert_eq!( + refuse_asymptotic_time_independent_effect_as_discrete_effect(recovered, discrete), + Err( + psychometric_core::PsychometricError::AsymptoticTimeIndependentEffectIsNotDiscreteEffect + ) + ); + assert_eq!( + refuse_asymptotic_time_independent_effect_as_continuous_intercept(recovered, 0.3), + Err( + psychometric_core::PsychometricError::AsymptoticTimeIndependentEffectIsNotContinuousIntercept + ) + ); + assert_eq!( + refuse_asymptotic_time_independent_effect_as_time_dependent_impulse(recovered, impulse), + Err( + psychometric_core::PsychometricError::AsymptoticTimeIndependentEffectIsNotTimeDependentImpulse + ) + ); +} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 4576af58..03eadeb0 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -28,7 +28,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | report template/section/copied/style/modality method effects | ADR 0004/0012; PRD/TRD | simulation truth factors implemented; estimator-side method model remains future | partial | | candidate K statistical/Pareto gates + blinded LLM review | ADR 0012; research | future `model_selection` | accepted-target | | compositional topic correlation / stable clustering | ADR 0005/0012; research | future `network_analysis` | accepted-target | -| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; §7.2 level-change `CINT` is `κ = −a m x` (`a < 0`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`; Eq. 3 of that setting is `(1 − e^{a Δt}) m x`); §7.2 extra-process contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, not the dissipating Dirac; `ε ≥ 0` fails closed; Eq. 5 of that contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; extra `LAMBDA` is 0; `τ + λ μ_t` is not that observed mean; after-t0 extra-process `TDPREDEFFECT` uses `t − u` with `t0 < u < t` while `μ_t` uses `Δt`; that after-t0 observed mean is not the first-occasion extra-process observed mean), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z); full ESEM/DSEM remaining | partial | +| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; §7.2 level-change `CINT` is `κ = −a m x` (`a < 0`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`; Eq. 3 of that setting is `(1 − e^{a Δt}) m x`); §7.2 extra-process contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, not the dissipating Dirac; `ε ≥ 0` fails closed; Eq. 5 of that contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; extra `LAMBDA` is 0; `τ + λ μ_t` is not that observed mean; after-t0 extra-process `TDPREDEFFECT` uses `t − u` with `t0 < u < t` while `μ_t` uses `Δt`; that after-t0 observed mean is not the first-occasion extra-process observed mean; §7.2 `asymTIPREDEFFECT` is `-B z / a` for `a < 0` and is not `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z); full ESEM/DSEM remaining | partial | | CPU bounded multithreading + GPU/VRAM streaming/parity | ADR 0001/0006 | future `compute_backend` | accepted-target | | TDT detection/tracking vs CHRONOS schema/prediction/temporal consistency | ADR 0016; PRD/research | future `event_intelligence` | accepted-target | | evidence-bounded LLM interpretation | ADR 0010/0012; PRD | `tepp_api` router plus future `interpretation_gateway` | partial | diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index 771c847b..ef21a198 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -1,7 +1,7 @@ # ADR 0005 — Posterior-aware ESEM/DSEM and structural interpretation **Decision status:** Accepted -**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), exact scalar contemporaneous `TDPREDEFFECT` impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; the §7.2 level-change form is not that impulse), exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of that Eq. 3 composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar time-independent `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle Eq. 14; `B` is not that discrete increment), exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of that Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of that carried latent mean; `τ + λ μ_t` is not that observed mean), exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; `T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`), exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; `T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand TD composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean), exact scalar §7.2 level-change `CINT` `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; `a < 0` so `−κ / a = m x`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2), exact scalar Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` (not `m x`, not `κ`, and not `TIPREDEFFECT`), exact scalar §7.2 extra near-zero-drift latent process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z; identification `TDPREDEFFECT` on the extra process is 1; extra `DRIFT` printed as `−0.000001`; precisely 0 causes computational problems; `ε = a` is `a_{ηξ} x Δt e^{a Δt}`; not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`; `ε ≥ 0` fails closed), exact scalar observed mean of that extra-process contribution `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z; the extra process has `LAMBDA` 0 and is not an observed indicator; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`), exact scalar after-t0 extra-process contribution `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` and Eq. 5 observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:32Z; `T0TDPREDEFFECT` uses `Δt` for both the evolution and the extra drive; `TDPREDEFFECT` after `t0` uses `t − u`; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target +**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), exact scalar contemporaneous `TDPREDEFFECT` impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; the §7.2 level-change form is not that impulse), exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of that Eq. 3 composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar time-independent `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle Eq. 14; `B` is not that discrete increment), exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of that Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of that carried latent mean; `τ + λ μ_t` is not that observed mean), exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; `T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`), exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; `T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand TD composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean), exact scalar §7.2 level-change `CINT` `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; `a < 0` so `−κ / a = m x`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2), exact scalar Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` (not `m x`, not `κ`, and not `TIPREDEFFECT`), exact scalar §7.2 extra near-zero-drift latent process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z; identification `TDPREDEFFECT` on the extra process is 1; extra `DRIFT` printed as `−0.000001`; precisely 0 causes computational problems; `ε = a` is `a_{ηξ} x Δt e^{a Δt}`; not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`; `ε ≥ 0` fails closed), exact scalar observed mean of that extra-process contribution `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z; the extra process has `LAMBDA` 0 and is not an observed indicator; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`), exact scalar after-t0 extra-process contribution `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` and Eq. 5 observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:32Z; `T0TDPREDEFFECT` uses `Δt` for both the evolution and the extra drive; `TDPREDEFFECT` after `t0` uses `t − u`; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive), exact scalar §7.2 `asymTIPREDEFFECT` `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z; expected total change in process means given a time-independent predictor; `a < 0`; not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; `a ≥ 0` fails closed), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target **Date:** 2026-08-05 **Supersedes:** None. ADR 0012 governs upstream topic measurement/network coordinates; this ADR governs higher-order psychometric structure and longitudinal interpretation. @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), recovers the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z; `TDPREDEFFECT` is `M`, not `CINT`; `M x` is not `A^{-1}[e^{A Δt} − I] B z`; `M x` is not Voelkle et al., 2012, Eq. 14; the level-change form is not that impulse), recovers the exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition; JSS PDF re-opened 2026-08-20T09:01Z; the evolved map `τ + λ μ_t` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-impulse latent mean is not `E(y_t)`), recovers the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z; `TIPREDEFFECT` is `B`, not `κ`; form `B z` first, then the discrete intercept map; a zero drift is `B z Δt`; `B` is not the discrete increment; `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle Eq. 14), recovers the exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; JSS PDF re-opened 2026-08-20T12:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-increment latent mean is not `E(y_t)`), recovers the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z; form `m x` first, then `e^{a(t−u)} m x`; a zero drift is `m x`; underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept; `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; an impulse at `u = t` is the contemporaneous map; an impulse at `u ≤ t0` is already in `η(t0)`), recovers the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; JSS PDF re-opened 2026-08-20T05:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the carried latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and its Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF opened 2026-08-20T15:14Z; form `t0_b z` first, then `e^{a Δt} t0_b z`; a zero drift is `t0_b z`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; `e^{A Δt} t0_b z` is not `t0_b z`; `T0TIPREDEFFECT` is the coefficient, not the shift), recovers the exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition; JSS PDF re-opened 2026-08-20T15:28Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and its Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; form `t0_m x0` first, then `e^{a Δt} t0_m x0`; a zero drift is `t0_m x0`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `T0TDPREDEFFECT` is the coefficient, not the shift; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), recovers the exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; the first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar §7.2 level-change `CINT` setting `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z; form `m x` first, then multiply by `−a`; `a < 0` so `−κ / a = m x`; `a ≥ 0` cannot hold a new process mean; `−a m x` is not the dissipating Dirac, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`; the extra near-zero-drift latent process also named in §7.2 is a different specification), recovers the exact scalar Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` (JSS PDF re-opened 2026-08-20T19:50Z; form the level-change `CINT` first, then the discrete intercept map; underflow of `e^{a Δt}` to `+0` keeps `m x`; `(1 − e^{a Δt}) m x` is not `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`), recovers the exact scalar §7.2 extra near-zero-drift latent process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z; form `a_{ηξ} x` first; `ε = a` is `a_{ηξ} x Δt e^{a Δt}`; a zero coupling or zero predictor is exactly zero; `ε ≥ 0` cannot hold a lasting extra state; that contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`), recovers the exact scalar observed mean of that extra-process contribution `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z; the extra process has `LAMBDA` 0 and is not an observed indicator; form the evolved-plus-contribution latent mean first, then `τ + λ` of that mean; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`), recovers the exact scalar after-t0 extra-process contribution `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` and its Eq. 5 observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:32Z; `T0TDPREDEFFECT` uses `Δt` for both the evolution and the extra drive; `TDPREDEFFECT` after `t0` uses `t − u` while `μ_t` still uses `Δt`; an impulse at `u = t0` or `u = t` is not interior; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), recovers the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z; `TDPREDEFFECT` is `M`, not `CINT`; `M x` is not `A^{-1}[e^{A Δt} − I] B z`; `M x` is not Voelkle et al., 2012, Eq. 14; the level-change form is not that impulse), recovers the exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition; JSS PDF re-opened 2026-08-20T09:01Z; the evolved map `τ + λ μ_t` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-impulse latent mean is not `E(y_t)`), recovers the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z; `TIPREDEFFECT` is `B`, not `κ`; form `B z` first, then the discrete intercept map; a zero drift is `B z Δt`; `B` is not the discrete increment; `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle Eq. 14), recovers the exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; JSS PDF re-opened 2026-08-20T12:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-increment latent mean is not `E(y_t)`), recovers the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z; form `m x` first, then `e^{a(t−u)} m x`; a zero drift is `m x`; underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept; `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; an impulse at `u = t` is the contemporaneous map; an impulse at `u ≤ t0` is already in `η(t0)`), recovers the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; JSS PDF re-opened 2026-08-20T05:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the carried latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and its Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF opened 2026-08-20T15:14Z; form `t0_b z` first, then `e^{a Δt} t0_b z`; a zero drift is `t0_b z`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; `e^{A Δt} t0_b z` is not `t0_b z`; `T0TIPREDEFFECT` is the coefficient, not the shift), recovers the exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition; JSS PDF re-opened 2026-08-20T15:28Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and its Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; form `t0_m x0` first, then `e^{a Δt} t0_m x0`; a zero drift is `t0_m x0`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `T0TDPREDEFFECT` is the coefficient, not the shift; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), recovers the exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; the first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar §7.2 level-change `CINT` setting `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z; form `m x` first, then multiply by `−a`; `a < 0` so `−κ / a = m x`; `a ≥ 0` cannot hold a new process mean; `−a m x` is not the dissipating Dirac, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`; the extra near-zero-drift latent process also named in §7.2 is a different specification), recovers the exact scalar Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` (JSS PDF re-opened 2026-08-20T19:50Z; form the level-change `CINT` first, then the discrete intercept map; underflow of `e^{a Δt}` to `+0` keeps `m x`; `(1 − e^{a Δt}) m x` is not `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`), recovers the exact scalar §7.2 extra near-zero-drift latent process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z; form `a_{ηξ} x` first; `ε = a` is `a_{ηξ} x Δt e^{a Δt}`; a zero coupling or zero predictor is exactly zero; `ε ≥ 0` cannot hold a lasting extra state; that contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`), recovers the exact scalar observed mean of that extra-process contribution `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z; the extra process has `LAMBDA` 0 and is not an observed indicator; form the evolved-plus-contribution latent mean first, then `τ + λ` of that mean; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`), recovers the exact scalar after-t0 extra-process contribution `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` and its Eq. 5 observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:32Z; `T0TDPREDEFFECT` uses `Δt` for both the evolution and the extra drive; `TDPREDEFFECT` after `t0` uses `t − u` while `μ_t` still uses `Δt`; an impulse at `u = t0` or `u = t` is not interior; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive), recovers the exact scalar §7.2 `asymTIPREDEFFECT` `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z; form `B z` first, then divide by `-a`; `a < 0`; a zero coefficient or zero predictor is exactly zero; `a ≥ 0` cannot hold a finite process-mean change; `-B z / a` is not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. 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 65420c8a..b9fa90e6 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -37,15 +37,16 @@ This slice stays inside `psychometric_core`. It does not add a second invariance 31. recover the exact scalar §7.2 extra near-zero-drift latent process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z; identification `TDPREDEFFECT` on the extra process is 1; extra `DRIFT` printed as `−0.000001`; precisely 0 causes computational problems; `ε = a` is `a_{ηξ} x Δt e^{a Δt}`) and refuse treating that contribution as `κ = −a m x`, as `(1 − e^{a Δt}) m x`, or as the dissipating Dirac `m x`; `ε ≥ 0` fails closed; 32. recover the exact scalar observed mean of that extra-process contribution `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z; the extra process has `LAMBDA` 0 and is not an observed indicator; form the evolved-plus-contribution latent mean first, then `τ + λ` of that mean) and refuse treating `τ + λ μ_t`, `τ + λ(μ_t + m x)`, the contribution, or the evolved-plus-contribution latent mean as `E(y_t)`; 33. recover the exact scalar after-t0 extra-process contribution `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` and its Eq. 5 observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:32Z; `T0TDPREDEFFECT` uses `Δt = t − t0` for both the evolution and the extra drive; `TDPREDEFFECT` after `t0` uses `t − u` for the extra drive while `μ_t` still uses `Δt`; an impulse at `u = t0` or `u = t` is not interior; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive) and refuse treating the first-occasion extra-process observed mean, `τ + λ μ_t`, the impulse-carry observed mean, the after-t0 contribution, or the evolved-plus-after-contribution latent mean as that `E(y_t)`; -34. refuse pooling discrete lags from unequal event intervals as one coefficient; -35. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); -36. refuse the difference quotient as a continuous-time rate; -37. apply the same event-time map to CWC residuals (still not DSEM); -38. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). +34. recover the exact scalar §7.2 `asymTIPREDEFFECT` `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; Eq. 3, p. 5; Table 2, p. 12; JSS PDF opened 2026-08-21T13:08Z; expected total change in process means given a time-independent predictor; form `B z` first, then divide by `-a`; `a < 0`; a zero coefficient or zero predictor is exactly zero) and refuse treating `-B z / a` as the coefficient `B`, as `A^{-1}[e^{A Δt} − I] B z`, as `CINT`, or as `M x`; `a ≥ 0` cannot hold a finite process-mean change; +35. refuse pooling discrete lags from unequal event intervals as one coefficient; +36. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); +37. refuse the difference quotient as a continuous-time rate; +38. apply the same event-time map to CWC residuals (still not DSEM); +39. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). ## Claim boundary -This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. The contemporaneous `TDPREDEFFECT` impulse is not the continuous intercept, not the time-independent discrete effect, and not Voelkle et al. (2012, Eq. 14). The time-independent `TIPREDEFFECT` increment is not the continuous intercept, not the contemporaneous impulse, not Voelkle et al. (2012, Eq. 14), and not the coefficient `B`. The within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). The evolved observed mean `τ + λ μ_t` is not the contemporaneous-impulse observed mean `τ + λ(μ_t + m x)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not the impulse-carry observed mean `τ + λ(μ_t + e^{a(t−u)} m x)` when `u ≠ t`. The evolved observed mean `τ + λ μ_t` is not the impulse-carry observed mean. The carried latent mean is not `E(y_t)`. The evolved-plus-impulse latent mean is not `E(y_t)`. The evolved observed mean `τ + λ μ_t` is not the time-independent-predictor observed mean `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not that time-independent-predictor observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that time-independent-predictor observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TIPREDEFFECT` shift `t0_b z` is not the Eq. 3 process increment `A^{-1}[e^{A Δt} − I] B z`, not `κ`, and not `M x`. The Eq. 3 first-summand carry `e^{A Δt} t0_b z` is not `t0_b z` and is not that process increment. `T0TIPREDEFFECT` is the coefficient, not the first-occasion shift. The evolved observed mean `τ + λ μ_t` is not the first-occasion TI-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_b z)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion observed mean when `u ≠ t0`. The evolved-plus-T0TIPRED latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TDPREDEFFECT` shift `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `κ`. The Eq. 3 first-summand carry `e^{A Δt} t0_m x0` is not `t0_m x0` and is not that within-interval impulse carry. `T0TDPREDEFFECT` is the coefficient, not the first-occasion shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The evolved observed mean `τ + λ μ_t` is not the first-occasion TD-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_m x0)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion TD observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion TD observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion TD observed mean when `u ≠ t0`. The first-occasion TI composition `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that first-occasion TD observed mean. Same numbers as `T0TIPREDEFFECT` yield the same product; Table 3 names a different matrix. The evolved-plus-T0TDPRED latent mean is not `E(y_t)`. The §7.2 level-change `CINT` `κ = −a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. Lasting level change via that `CINT` setting requires `a < 0`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not that `CINT` setting. The Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` is not the dissipating Dirac `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`. Underflow of `e^{a Δt}` to `+0` keeps the equilibrium offset `m x`. The §7.2 extra-process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. Lasting level change via that extra process requires `ε < 0`. Precisely `ε = 0` causes computational problems in the printed specification. The extra-process observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` is not `τ + λ μ_t`, not `τ + λ(μ_t + m x)`, not the contribution, and not the evolved-plus-contribution latent mean. The extra process has `LAMBDA` 0 and is not an observed indicator. `T0TDPREDEFFECT` on the extra process uses `Δt = t − t0` for both the original-process evolution and the extra drive. `TDPREDEFFECT` after `t0` uses `t − u` with `t0 < u < t` for the extra drive while `μ_t` still uses `Δt`. The after-t0 extra-process observed mean is not the first-occasion extra-process observed mean when `u ≠ t0`. The impulse-carry `e^{a(t−u)} m x` is a Dirac on the original process and is not that `DRIFT` drive. +This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. The contemporaneous `TDPREDEFFECT` impulse is not the continuous intercept, not the time-independent discrete effect, and not Voelkle et al. (2012, Eq. 14). The time-independent `TIPREDEFFECT` increment is not the continuous intercept, not the contemporaneous impulse, not Voelkle et al. (2012, Eq. 14), and not the coefficient `B`. The within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). The evolved observed mean `τ + λ μ_t` is not the contemporaneous-impulse observed mean `τ + λ(μ_t + m x)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not the impulse-carry observed mean `τ + λ(μ_t + e^{a(t−u)} m x)` when `u ≠ t`. The evolved observed mean `τ + λ μ_t` is not the impulse-carry observed mean. The carried latent mean is not `E(y_t)`. The evolved-plus-impulse latent mean is not `E(y_t)`. The evolved observed mean `τ + λ μ_t` is not the time-independent-predictor observed mean `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not that time-independent-predictor observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that time-independent-predictor observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TIPREDEFFECT` shift `t0_b z` is not the Eq. 3 process increment `A^{-1}[e^{A Δt} − I] B z`, not `κ`, and not `M x`. The Eq. 3 first-summand carry `e^{A Δt} t0_b z` is not `t0_b z` and is not that process increment. `T0TIPREDEFFECT` is the coefficient, not the first-occasion shift. The evolved observed mean `τ + λ μ_t` is not the first-occasion TI-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_b z)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion observed mean when `u ≠ t0`. The evolved-plus-T0TIPRED latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TDPREDEFFECT` shift `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `κ`. The Eq. 3 first-summand carry `e^{A Δt} t0_m x0` is not `t0_m x0` and is not that within-interval impulse carry. `T0TDPREDEFFECT` is the coefficient, not the first-occasion shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The evolved observed mean `τ + λ μ_t` is not the first-occasion TD-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_m x0)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion TD observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion TD observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion TD observed mean when `u ≠ t0`. The first-occasion TI composition `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that first-occasion TD observed mean. Same numbers as `T0TIPREDEFFECT` yield the same product; Table 3 names a different matrix. The evolved-plus-T0TDPRED latent mean is not `E(y_t)`. The §7.2 level-change `CINT` `κ = −a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. Lasting level change via that `CINT` setting requires `a < 0`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not that `CINT` setting. The Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` is not the dissipating Dirac `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`. Underflow of `e^{a Δt}` to `+0` keeps the equilibrium offset `m x`. The §7.2 extra-process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. Lasting level change via that extra process requires `ε < 0`. Precisely `ε = 0` causes computational problems in the printed specification. The extra-process observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` is not `τ + λ μ_t`, not `τ + λ(μ_t + m x)`, not the contribution, and not the evolved-plus-contribution latent mean. The extra process has `LAMBDA` 0 and is not an observed indicator. `T0TDPREDEFFECT` on the extra process uses `Δt = t − t0` for both the original-process evolution and the extra drive. `TDPREDEFFECT` after `t0` uses `t − u` with `t0 < u < t` for the extra drive while `μ_t` still uses `Δt`. The after-t0 extra-process observed mean is not the first-occasion extra-process observed mean when `u ≠ t0`. The impulse-carry `e^{a(t−u)} m x` is a Dirac on the original process and is not that `DRIFT` drive. The §7.2 `asymTIPREDEFFECT` `-B z / a` is the expected total change in process means given a time-independent predictor. It is not the coefficient `B`, not the finite-interval increment `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. Lasting asymptotic change via that map requires `a < 0`. ## Authoritative sources @@ -63,7 +64,7 @@ Kish, L. (1965). *Survey sampling*. John Wiley & Sons. Oud, J. H. L., & Jansen, R. A. R. G. (2000). Continuous time state space modeling of panel data by means of SEM. *Psychometrika, 65*(2), 199–215. https://doi.org/10.1007/BF02294374 (cited by Voelkle et al., 2012, Eq. 14 discussion; PDF not opened). -The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:07Z (https://www.zora.uzh.ch/handle/20.500.14742/72792; bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`). Page 16 writes discrete auto-effects as \(A^{*}(\Delta t)=\exp(A\Delta t)\) (Eq. 7). Introducing Intercepts (manuscript p. 20, Eq. 12) adds a continuous-time intercept \(b\) and writes the expected-value solution whose discrete increment is \(A^{-1}(\exp(A\Delta t)-I)b\); the scalar constant-predictor effect is \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). The next paragraph (manuscript p. 21, Eq. 14) writes the discrete effect of a **time-varying** predictor, when the sampling interval equals the constancy interval, as \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product does not depend on the predictor auto-effect. The manuscript calls Eq. 14 a first-order approximation that deteriorates as \(\Delta t\) grows, and defers unmatched sampling/constancy intervals to Oud and Jansen (2000), which is unread. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4; JSS PDF opened 2026-08-20T15:14Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\), the discrete intercept \(b_{\Delta t}=A^{-1}[A_{\Delta t}-I]b\), and the discrete process-noise covariance \(Q_{\Delta t}=\int_{0}^{\Delta t}\operatorname{expm}(A(\Delta t-\tau))LGG^{\top}L^{\top}\operatorname{expm}(A(\Delta t-\tau))^{\top}\,d\tau\). Equation 4 (p. 5) writes that the integral exhibits covariance \(Q_{\Delta t}=\operatorname{irow}(A^{\#^{-1}}[e^{A^{\#}\Delta t}-I]\operatorname{row}(Q))\) with \(A^{\#}=A\otimes I+I\otimes A\). The homogeneous-process consequence (`ξ`, `z` given) is \(Q_{\Delta t}=\operatorname{cov}(\eta_{ti}\mid\eta_{t-1,i})\) and \(\operatorname{cov}(\eta_{ti},\eta_{t-1,i})=A_{\Delta t}\operatorname{cov}(\eta_{t-1,i})\). The law of total variance on that pair is \(\operatorname{Var}(\eta_{ti})=A_{\Delta t}\operatorname{Var}(\eta_{t-1,i})A_{\Delta t}^{\top}+Q_{\Delta t}\). As \(\Delta t\to\infty\) with stable \(a<0\), Eq. 4 becomes \(-q/(2a)\). The JSS summary names that limit `asymDIFFUSION` and takes it as the total within-subject variance (p. 16). Section 4.3 (pp. 9–10) constrains a stationary `T0VAR` to that model-predicted variance and distinguishes it from a predetermined first occasion. The same section (p. 9) adds a stable trait process with `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is that time-invariant between-subject variance. Table 3 (p. 13) names `T0TIPREDEFFECT` the effect of time-independent predictors on latents at `T0` and names `TIPREDEFFECT` `B` separately. The JSS article has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). This slice takes scalar \(L=1\) (every latent subject to system noise); it does not implement the 0/1 selector matrix and is not a Kalman filter. Discrete auto-effects are strictly positive for finite real drift and interval; the inverse \(a=\ln\varphi/\Delta t\) therefore requires \(\varphi>0\). Binary64 `exp` of a large negative argument is `+0` and is refused as a discrete lag; the same underflow is a vanishing lagged covariance and is kept. Equations 3–4 of Voelkle et al. (2012) are the discouraged difference-quotient approximation. Meredith (1993) remains unread (Unpaywall/OpenAlex 2026-08-21T06:24Z: closed; Springer `content/pdf` is HTML 200, not a PDF; archive.org title search empty). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex 2026-08-21T06:24Z: closed; Springer `content/pdf` is HTML 200; ETS landing page is HTML). ERIC ED334221 is Singer and Willett (1991), *From whether to when*, not the 1991 journal article. ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title is not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex 2026-08-18T21:07Z: closed; Radboud landing and bitstream 403). +The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:07Z (https://www.zora.uzh.ch/handle/20.500.14742/72792; bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`). Page 16 writes discrete auto-effects as \(A^{*}(\Delta t)=\exp(A\Delta t)\) (Eq. 7). Introducing Intercepts (manuscript p. 20, Eq. 12) adds a continuous-time intercept \(b\) and writes the expected-value solution whose discrete increment is \(A^{-1}(\exp(A\Delta t)-I)b\); the scalar constant-predictor effect is \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). The next paragraph (manuscript p. 21, Eq. 14) writes the discrete effect of a **time-varying** predictor, when the sampling interval equals the constancy interval, as \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product does not depend on the predictor auto-effect. The manuscript calls Eq. 14 a first-order approximation that deteriorates as \(\Delta t\) grows, and defers unmatched sampling/constancy intervals to Oud and Jansen (2000), which is unread. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4; JSS PDF opened 2026-08-21T13:08Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\), the discrete intercept \(b_{\Delta t}=A^{-1}[A_{\Delta t}-I]b\), and the discrete process-noise covariance \(Q_{\Delta t}=\int_{0}^{\Delta t}\operatorname{expm}(A(\Delta t-\tau))LGG^{\top}L^{\top}\operatorname{expm}(A(\Delta t-\tau))^{\top}\,d\tau\). Equation 4 (p. 5) writes that the integral exhibits covariance \(Q_{\Delta t}=\operatorname{irow}(A^{\#^{-1}}[e^{A^{\#}\Delta t}-I]\operatorname{row}(Q))\) with \(A^{\#}=A\otimes I+I\otimes A\). The homogeneous-process consequence (`ξ`, `z` given) is \(Q_{\Delta t}=\operatorname{cov}(\eta_{ti}\mid\eta_{t-1,i})\) and \(\operatorname{cov}(\eta_{ti},\eta_{t-1,i})=A_{\Delta t}\operatorname{cov}(\eta_{t-1,i})\). The law of total variance on that pair is \(\operatorname{Var}(\eta_{ti})=A_{\Delta t}\operatorname{Var}(\eta_{t-1,i})A_{\Delta t}^{\top}+Q_{\Delta t}\). As \(\Delta t\to\infty\) with stable \(a<0\), Eq. 4 becomes \(-q/(2a)\). The JSS summary names that limit `asymDIFFUSION` and takes it as the total within-subject variance (p. 16). Section 4.3 (pp. 9–10) constrains a stationary `T0VAR` to that model-predicted variance and distinguishes it from a predetermined first occasion. The same section (p. 9) adds a stable trait process with `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is that time-invariant between-subject variance. Table 3 (p. 13) names `T0TIPREDEFFECT` the effect of time-independent predictors on latents at `T0` and names `TIPREDEFFECT` `B` separately. The JSS article has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). This slice takes scalar \(L=1\) (every latent subject to system noise); it does not implement the 0/1 selector matrix and is not a Kalman filter. Discrete auto-effects are strictly positive for finite real drift and interval; the inverse \(a=\ln\varphi/\Delta t\) therefore requires \(\varphi>0\). Binary64 `exp` of a large negative argument is `+0` and is refused as a discrete lag; the same underflow is a vanishing lagged covariance and is kept. Equations 3–4 of Voelkle et al. (2012) are the discouraged difference-quotient approximation. Meredith (1993) remains unread (Unpaywall/OpenAlex 2026-08-21T06:24Z: closed; Springer `content/pdf` is HTML 200, not a PDF; archive.org title search empty). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex 2026-08-21T06:24Z: closed; Springer `content/pdf` is HTML 200; ETS landing page is HTML). ERIC ED334221 is Singer and Willett (1991), *From whether to when*, not the 1991 journal article. ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title is not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex 2026-08-18T21:07Z: closed; Radboud landing and bitstream 403). ## Formula notes @@ -98,6 +99,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - **Level-change extra process.** Driver et al. (2017, §7.2, pp. 22–23; Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T23:10Z): a lasting level change is specified by an extra latent process. `T0MEANS`, `CINT`, `T0VAR`, `DIFFUSION`, and `TRAITVAR` of that process are fixed to 0. `TDPREDEFFECT` on it is fixed to 1. Its `DRIFT` diagonal is very close to 0 (printed example `−0.000001`; precisely 0 causes computational problems). The original process is driven by the `DRIFT` coupling `a_{ηξ}`. After a unit identification impulse the scalar contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`. Form `a_{ηξ} x` first. When `ε = a` the contribution is `a_{ηξ} x Δt e^{a Δt}`. A zero coupling or zero predictor is exactly zero. `ε ≥ 0` fails closed. That contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. An overflowing product, exponential, or quotient fails closed. This is not a Kalman filter, not a matrix `expm`, and not ctsem estimation. - **Extra-process observed-indicator mean.** Driver et al. (2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z): `y_i(t)=Γ+Λη_i(t)+ζ_i(t)` with `ζ∼N(0,Θ)` and `Γ∼N(τ,Ψ)`. The printed extra process has `LAMBDA` 0 and is not an observed indicator. Original indicators load on the original process after the `DRIFT` coupling. The latent process at `t` after that contribution is `μ_t+a_{ηξ}x(e^{εΔt}−e^{aΔt})/(ε−a)`. The scalar composition is `E(y_t)=τ+λ(μ_t+a_{ηξ}x(e^{εΔt}−e^{aΔt})/(ε−a))`. Form the evolved-plus-contribution latent mean first, then `τ+λ` of that mean. A zero loading is exactly `τ`. A zero evolved-plus-contribution latent mean is exactly `τ`. A zero intercept is exactly `λ` of that latent mean. The evolved observed mean `τ+λμ_t` is not this composition. The contemporaneous map `τ+λ(μ_t+mx)` is not this composition. `MANIFESTMEANS` is not `E(y_t)`. The extra-process contribution is not `E(y_t)`. The evolved-plus-contribution latent mean is not `E(y_t)`. The extra process itself is not an observed indicator. An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **After-t0 extra-process observed-indicator mean.** Driver et al. (2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:32Z): `T0TDPREDEFFECT` begins the extra process at `t = 0`. `TDPREDEFFECT` begins it after `t = 0`. The interior case `t0 < u < t` drives the original process over `t − u` while `μ_t` still uses `Δt = t − t0`. The scalar composition is `E(y_t)=τ+λ(μ_t+a_{ηξ}x(e^{ε(t−u)}−e^{a(t−u)})/(ε−a))`. Form the evolved-plus-after-contribution latent mean first, then `τ+λ` of that mean. An impulse at `u = t0` is the first-occasion extra-process map. An impulse at `u = t` has not yet driven the original process. `e^{a(t−u)}mx` is a Dirac on the original process, not this `DRIFT` drive. A zero loading is exactly `τ`. The first-occasion extra-process observed mean is not this composition when `u ≠ t0`. The evolved observed mean `τ+λμ_t` is not this composition. The extra process itself is not an observed indicator. An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. +- **Asymptotic time-independent predictor effect.** Driver et al. (2017, §7.2, pp. 20–21; Eq. 3, p. 5; Table 2, p. 12; JSS PDF opened 2026-08-21T13:08Z): Table 2 names `B` `TIPREDEFFECT`. Equation 3 maps a finite event interval as `A^{-1}[e^{AΔt}−I]Bz`. Section 7.2 names `asymTIPREDEFFECT` the expected total change in process means given an increase of 1 on a time-independent predictor. For stable `a<0` that `Δt→∞` limit is `-A^{-1}B`. The scalar map is `-Bz/a`. Form `Bz` first, then divide by `-a`. A zero coefficient or zero predictor is exactly zero. `a≥0` cannot hold a finite process-mean change. `-Bz/a` is not the coefficient `B`, not `A^{-1}[e^{AΔt}−I]Bz`, not `CINT`, and not `Mx`. An overflowing product or quotient fails closed. This is not a Kalman filter and not ctsem estimation. - **Level-change discrete increment.** Driver et al. (2017, §7.2, pp. 20–21; Eq. 3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T19:50Z): Equation 3 maps `CINT` through `A^{-1}[e^{AΔt}−I]κ`. With `κ=−a m x` the scalar increment is `(e^{aΔt}−1)/a·(−a m x)=(1−e^{aΔt})m x`. Form the level-change `CINT` first, then the discrete intercept map. Underflow of `e^{aΔt}` to `+0` keeps `m x`. A zero effect or zero predictor is exactly zero. `(1−e^{aΔt})m x` is not `m x`, not `κ`, and not `A^{-1}[e^{AΔt}−I]Bz`. An overflowing product or increment fails closed. This is not a Kalman filter and not ctsem estimation. - **CWC-then-lag.** Sample cluster means are removed first. Consecutive within residuals are then fitted by least squares to \(r_{t+\Delta t}\approx\exp(a\Delta t)\,r_{t}\) on event time. Same-sign pair-wise logs initialize the scalar Newton step. Sign-flipping \(T=2\) CWC pairs have no real logarithm and fail closed. Curran and Bauer (2011, pp. 607–608) show that this person-mean subtraction on a raw autoregressive series does **not** isolate the lagged within-person effect; the helper therefore does not claim to recover the raw-process drift. - **Already-centered irregular residual.** The caller supplies lagged within residuals. The mean of \(a=\ln(r_{t+\Delta t}/r_t)/\Delta t\) is the exact scalar map. Intervals may be irregular. The helper does not center again. This is not DSEM. @@ -136,6 +138,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - Driver et al. (2017, §7.2, pp. 22–23) recovers a known extra-process contribution \(a_{\eta\xi}x(e^{\varepsilon\Delta t}-e^{a\Delta t})/(\varepsilon-a)\) at machine-scale RMSE, and that RMSE is smaller than treating \(\kappa=-amx\), \((1-e^{a\Delta t})mx\), or the dissipating Dirac \(mx\) as that contribution; \(\varepsilon=a\) is \(a_{\eta\xi}x\Delta t\,e^{a\Delta t}\); \(\varepsilon\ge 0\) with a nonzero contribution fails closed; a zero coupling or zero predictor is exactly zero; a non-event clock, a non-positive interval, and an overflowing product fail closed; - Driver et al. (2017, Eq. 5 of the §7.2 extra-process contribution; JSS PDF re-opened 2026-08-21T06:12Z) recovers a known \(E(y_t)=\tau+\lambda(\mu_t+a_{\eta\xi}x(e^{\varepsilon\Delta t}-e^{a\Delta t})/(\varepsilon-a))\) at machine-scale RMSE, and that RMSE is smaller than treating \(\tau+\lambda\mu_t\), \(\tau+\lambda(\mu_t+mx)\), the contribution, `MANIFESTMEANS`, or the evolved-plus-contribution latent mean as \(E(y_t)\); the extra process has `LAMBDA` 0 and is not an observed indicator; a zero original-indicator loading is \(\tau\); a zero coupling recovers \(\tau+\lambda\mu_t\); \(\varepsilon\ge 0\) with a nonzero contribution fails closed; a non-event clock, a non-positive interval, and an overflowing product or sum fail closed; - Driver et al. (2017, Eq. 5 of the §7.2 after-t0 extra-process contribution; JSS PDF re-opened 2026-08-21T06:32Z) recovers a known \(E(y_t)=\tau+\lambda(\mu_t+a_{\eta\xi}x(e^{\varepsilon(t-u)}-e^{a(t-u)})/(\varepsilon-a))\) at machine-scale RMSE for \(t_0 Date: Fri, 21 Aug 2026 13:27:59 +0000 Subject: [PATCH 57/62] =?UTF-8?q?feat(psychometric):=20recover=20Driver=20?= =?UTF-8?q?=C2=A77.2=20addedTIPREDVAR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Driver, Oud, and Voelkle (2017, §7.2, pp. 20–21) name addedTIPREDVAR the stable between-subject variance accounted for by time-independent predictors. For stable a < 0 and predictor variance v >= 0 the scalar map is (B / a)^2 v. Form the unit asymptotic effect -B / a first, then square, then multiply by v. A zero coefficient or zero predictor variance is exactly zero. v < 0 fails closed. (B / a)^2 v is not TRAITVAR, not asymDIFFUSION, and not -B z / a. The printed 2-latent addedTIPREDVAR 2.838 is not this scalar map. JSS PDF opened 2026-08-21T13:08Z. Still not DSEM, not a Kalman filter, and not ctsem estimation. --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 1 + CLAUDE.md | 2 +- crates/psychometric_core/src/error.rs | 33 +++ crates/psychometric_core/src/event_time.rs | 262 +++++++++++++++++- crates/psychometric_core/src/lib.rs | 12 + ...multilevel_event_time_recovery_contract.rs | 92 ++++++ .../scientific_claim_boundary_contract.rs | 60 ++++ docs/TRACEABILITY.md | 2 +- docs/adr/0005-posterior-esem-dsem.md | 4 +- .../multilevel-event-time-recovery.md | 15 +- docs/validation/temporal-event-foundation.md | 2 +- 12 files changed, 474 insertions(+), 13 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index c888e2cf..1a500e9f 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -61,7 +61,7 @@ boundaries above remain the target modular MSA architecture. | `tepp_simulation` | known-truth temporal/event data generation | | `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics | | `tepp_api` | versioned DTO, schema, and export contracts | -| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; first-occasion `T0TIPREDEFFECT` shift is `t0_b z` and Eq. 3 first-summand carry is `e^{A Δt} t0_b z` (`T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_b z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), first-occasion `T0TDPREDEFFECT` shift is `t0_m x0` and Eq. 3 first-summand carry is `e^{A Δt} t0_m x0` (`T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; §7.2 level-change `CINT` is `κ = −a m x` with `a < 0` so `−κ / a = m x` (`−a m x` is not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2; Eq. 3 of that setting is `(1 − e^{a Δt}) m x`, which is not `m x`, not `κ`, and not `TIPREDEFFECT`; §7.2 extra-process contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`; identification `TDPREDEFFECT` on the extra process is 1; printed extra `DRIFT` is `−0.000001`; not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`; `ε ≥ 0` fails closed; Eq. 5 of that contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; the extra process has `LAMBDA` 0 and is not an observed indicator; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`; after-t0 extra-process `TDPREDEFFECT` is `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` while `μ_t` uses `Δt`; Eq. 5 of that after-t0 contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)`; the first-occasion extra-process observed mean is not that observed mean when `u ≠ t0`; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive; §7.2 `asymTIPREDEFFECT` is `-B z / a` for `a < 0` (`-B z / a` is not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`)), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | +| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; first-occasion `T0TIPREDEFFECT` shift is `t0_b z` and Eq. 3 first-summand carry is `e^{A Δt} t0_b z` (`T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_b z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), first-occasion `T0TDPREDEFFECT` shift is `t0_m x0` and Eq. 3 first-summand carry is `e^{A Δt} t0_m x0` (`T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; §7.2 level-change `CINT` is `κ = −a m x` with `a < 0` so `−κ / a = m x` (`−a m x` is not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2; Eq. 3 of that setting is `(1 − e^{a Δt}) m x`, which is not `m x`, not `κ`, and not `TIPREDEFFECT`; §7.2 extra-process contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`; identification `TDPREDEFFECT` on the extra process is 1; printed extra `DRIFT` is `−0.000001`; not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`; `ε ≥ 0` fails closed; Eq. 5 of that contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; the extra process has `LAMBDA` 0 and is not an observed indicator; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`; after-t0 extra-process `TDPREDEFFECT` is `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` while `μ_t` uses `Δt`; Eq. 5 of that after-t0 contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)`; the first-occasion extra-process observed mean is not that observed mean when `u ≠ t0`; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive; §7.2 `asymTIPREDEFFECT` is `-B z / a` for `a < 0` (`-B z / a` is not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; §7.2 `addedTIPREDVAR` is `(B / a)² v`, not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`)), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | No crate exposes placeholder production behavior in Task 1. This prevents an empty façade from becoming a de facto public API before its invariants and tests diff --git a/CHANGELOG.md b/CHANGELOG.md index a8656bf2..c0a30704 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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, §7.2, pp. 20–21; Eq. 3, p. 5; Table 2, p. 12; JSS PDF opened 2026-08-21T13:08Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar `addedTIPREDVAR`. Section 7.2 names that matrix the stable between-subject variance accounted for by time-independent predictors. For stable `a < 0` and predictor variance `v ≥ 0` the scalar map is `(B / a)² v`. Form the unit asymptotic effect `-B / a` first, then square, then multiply by `v`. A zero coefficient or zero predictor variance is exactly zero. `v < 0` fails closed. `a ≥ 0` cannot hold a finite process-mean change and fails closed. `(B / a)² v` is not `TRAITVAR`, not `asymDIFFUSION`, and not the expected total change `-B z / a`. The printed 2-latent `addedTIPREDVAR` 2.838 is not this scalar map. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-21T06:24Z: `is_oa: false`; Springer `content/pdf` is HTML 200). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, §7.2, pp. 20–21; Eq. 3, p. 5; Table 2, p. 12; JSS PDF opened 2026-08-21T13:08Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar `asymTIPREDEFFECT`. Table 2 names `B` `TIPREDEFFECT`. Equation 3 maps a finite event interval as `A^{-1}[e^{A Δt} − I] B z`. Section 7.2 names `asymTIPREDEFFECT` the expected total change in process means given an increase of 1 on a time-independent predictor. For stable `a < 0` that total change is `-A^{-1} B`. The scalar map is `-B z / a`. Form `B z` first, then divide by `-a`. A zero coefficient or zero predictor is exactly zero. `a ≥ 0` cannot hold a finite process-mean change and fails closed. `-B z / a` is not the coefficient `B`, not the finite-interval increment `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. Printed LeisureTime `TIPREDEFFECT` `−0.225` / `asymTIPREDEFFECT` `−1.673` and Happiness `0.549` / `0.219` reconstruct under this map. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-21T06:24Z: `is_oa: false`; Springer `content/pdf` is HTML 200). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:32Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar observed-indicator mean of an extra-process `TDPREDEFFECT` after `t0`. Section 7.2 names `T0TDPREDEFFECT` when the extra process begins at `t = 0` and `TDPREDEFFECT` when it begins after `t = 0`. The printed extra process has `LAMBDA` 0. Original indicators load on the original process after the `DRIFT` coupling over `t − u` with `t0 < u < t` while `μ_t` still uses `Δt = t − t0`. The scalar composition is `E(y_t) = τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))`. The first-occasion extra-process observed mean uses `Δt` for both the evolution and the extra drive and is not this composition when `u ≠ t0`. The evolved observed mean `τ + λ μ_t` is not this composition. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is a Dirac on the original process and is not this `DRIFT` drive. An impulse at `u = t0` or `u = t` is not interior. A zero original-indicator loading is exactly `τ`. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-21T06:24Z: `is_oa: false`; Springer `content/pdf` is HTML 200). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; §7.2, pp. 22–23; Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-21T06:12Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar observed-indicator mean of the extra near-zero-drift latent process contribution. Section 7.2's printed extra process has `LAMBDA` 0 and is not an observed indicator. Original indicators load on the original process after the `DRIFT` coupling `a_{ηξ}`. After a unit identification impulse the latent process at `t` is `μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`. The scalar composition is `E(y_t) = τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))`. Form the evolved-plus-contribution latent mean first, then `τ + λ` of that mean. The evolved observed mean `τ + λ μ_t` is not this composition. The contemporaneous map `τ + λ(μ_t + m x)` is not this composition. The extra-process contribution is not `E(y_t)`. The evolved-plus-contribution latent mean is not `E(y_t)`. A zero original-indicator loading is exactly `τ`. A zero coupling recovers `τ + λ μ_t`. `ε ≥ 0` fails closed. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-21T06:24Z: `is_oa: false`; Springer `content/pdf` is HTML 200). diff --git a/CLAUDE.md b/CLAUDE.md index 046d7e2e..f0e27d96 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. Putnick and Bornstein (2016, PMC5145197 opened 2026-08-19T22:15Z) require scalar invariance before latent-mean comparison; residual invariance is not a prerequisite. Two-observation series have no residual degrees of freedom (`ordinary_least_squares_fit` returns residual variance `0`) and cap at strong/scalar; they still license means. This is two-group OLS, not MGCFA. Meredith (1993) names remain unread labels. -- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. The contemporaneous time-dependent predictor impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`). Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). The §7.2 level-change form is not that impulse. The observed mean of that contemporaneous impulse is `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-impulse latent mean is not `E(y_t)`. The time-independent predictor increment is `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`). Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. `TIPREDEFFECT` is `B`, not that discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle et al. (2012, Eq. 14). The observed mean of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The within-interval time-dependent impulse carry is `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation). Form `m x` first, then `e^{a(t−u)} m x`. A zero drift is `m x` with no dissipation. Underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept. `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). An impulse at `u = t` is the contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. The observed mean of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The carried latent mean is not `E(y_t)`. The first-occasion time-independent predictor shift is `t0_b z` (Driver et al., 2017, Table 3 `T0TIPREDEFFECT`; Eq. 3 first summand). Form `t0_b z` first, then `e^{a Δt} t0_b z`. Form `μ_t` first, then add that carry. A zero drift is `t0_b z`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. `e^{A Δt} t0_b z` is not `t0_b z`. `T0TIPREDEFFECT` is the coefficient, not the shift. The observed mean of that first-occasion carry is `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The evolved-plus-carry latent mean is not `E(y_t)`. The first-occasion time-dependent predictor shift is `t0_m x0` (Driver et al., 2017, Table 3 `T0TDPREDEFFECT`; Eq. 3 first summand; JSS PDF re-opened 2026-08-20T19:10Z). Form `t0_m x0` first, then `e^{a Δt} t0_m x0`. Form `μ_t` first, then add that carry. A zero drift is `t0_m x0`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`. `e^{A Δt} t0_m x0` is not `t0_m x0`. `T0TDPREDEFFECT` is the coefficient, not the shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The observed mean of that first-occasion TD carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean. The evolved-plus-carry latent mean is not `E(y_t)`. The lasting level-change `CINT` is `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z). Form `m x` first, then multiply by `−a`. Stable `a < 0` is required so `−κ / a = m x` is an equilibrium offset. `a ≥ 0` cannot hold a new process mean. `−a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not this `CINT` setting. Equation 3 maps that intercept as `(1 − e^{a Δt}) m x` (JSS PDF re-opened 2026-08-20T19:50Z). Form the level-change `CINT` first, then the discrete intercept map. Underflow of `e^{a Δt}` to `+0` keeps `m x`. `(1 − e^{a Δt}) m x` is not `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`. The printed §7.2 lasting level change is an extra near-zero-drift latent process (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z). `T0MEANS`, `CINT`, `T0VAR`, `DIFFUSION`, and `TRAITVAR` of that process are fixed to 0; `TDPREDEFFECT` on it is fixed to 1; its `DRIFT` diagonal is very close to 0 (printed example `−0.000001`; precisely 0 causes computational problems); the original process is driven by the `DRIFT` coupling `a_{ηξ}`. After a unit identification impulse the scalar contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`). Form `a_{ηξ} x` first. A zero coupling or zero predictor is exactly zero. `ε ≥ 0` fails closed. That contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. The observed mean of that extra-process contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5 of that §7.2 contribution; JSS PDF re-opened 2026-08-21T06:12Z). The extra process has `LAMBDA` 0 and is not an observed indicator. Original indicators load on the original process after the `DRIFT` coupling. The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The contribution is not `E(y_t)`. The evolved-plus-contribution latent mean is not `E(y_t)`. `T0TDPREDEFFECT` on the extra process begins at `t = 0` and uses `Δt = t − t0` for both the original-process evolution and the extra drive. `TDPREDEFFECT` after `t0` uses `t − u` with `t0 < u < t` for the extra drive while `μ_t` still uses `Δt`. The observed mean of that after-t0 extra-process contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, Eq. 5 of that §7.2 after-t0 contribution; JSS PDF re-opened 2026-08-21T06:32Z). The first-occasion extra-process observed mean is not that observed mean when `u ≠ t0`. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is a Dirac on the original process and is not that `DRIFT` drive. An impulse at `u = t0` or `u = t` is not interior. The asymptotic time-independent predictor effect is `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z). Form `B z` first, then divide by `-a`. Stable `a < 0` is required. `a ≥ 0` cannot hold a finite process-mean change. `-B z / a` is not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. The contemporaneous time-dependent predictor impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`). Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). The §7.2 level-change form is not that impulse. The observed mean of that contemporaneous impulse is `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-impulse latent mean is not `E(y_t)`. The time-independent predictor increment is `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`). Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. `TIPREDEFFECT` is `B`, not that discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle et al. (2012, Eq. 14). The observed mean of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The within-interval time-dependent impulse carry is `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation). Form `m x` first, then `e^{a(t−u)} m x`. A zero drift is `m x` with no dissipation. Underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept. `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). An impulse at `u = t` is the contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. The observed mean of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The carried latent mean is not `E(y_t)`. The first-occasion time-independent predictor shift is `t0_b z` (Driver et al., 2017, Table 3 `T0TIPREDEFFECT`; Eq. 3 first summand). Form `t0_b z` first, then `e^{a Δt} t0_b z`. Form `μ_t` first, then add that carry. A zero drift is `t0_b z`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. `e^{A Δt} t0_b z` is not `t0_b z`. `T0TIPREDEFFECT` is the coefficient, not the shift. The observed mean of that first-occasion carry is `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The evolved-plus-carry latent mean is not `E(y_t)`. The first-occasion time-dependent predictor shift is `t0_m x0` (Driver et al., 2017, Table 3 `T0TDPREDEFFECT`; Eq. 3 first summand; JSS PDF re-opened 2026-08-20T19:10Z). Form `t0_m x0` first, then `e^{a Δt} t0_m x0`. Form `μ_t` first, then add that carry. A zero drift is `t0_m x0`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`. `e^{A Δt} t0_m x0` is not `t0_m x0`. `T0TDPREDEFFECT` is the coefficient, not the shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The observed mean of that first-occasion TD carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean. The evolved-plus-carry latent mean is not `E(y_t)`. The lasting level-change `CINT` is `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z). Form `m x` first, then multiply by `−a`. Stable `a < 0` is required so `−κ / a = m x` is an equilibrium offset. `a ≥ 0` cannot hold a new process mean. `−a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not this `CINT` setting. Equation 3 maps that intercept as `(1 − e^{a Δt}) m x` (JSS PDF re-opened 2026-08-20T19:50Z). Form the level-change `CINT` first, then the discrete intercept map. Underflow of `e^{a Δt}` to `+0` keeps `m x`. `(1 − e^{a Δt}) m x` is not `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`. The printed §7.2 lasting level change is an extra near-zero-drift latent process (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z). `T0MEANS`, `CINT`, `T0VAR`, `DIFFUSION`, and `TRAITVAR` of that process are fixed to 0; `TDPREDEFFECT` on it is fixed to 1; its `DRIFT` diagonal is very close to 0 (printed example `−0.000001`; precisely 0 causes computational problems); the original process is driven by the `DRIFT` coupling `a_{ηξ}`. After a unit identification impulse the scalar contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`). Form `a_{ηξ} x` first. A zero coupling or zero predictor is exactly zero. `ε ≥ 0` fails closed. That contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. The observed mean of that extra-process contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5 of that §7.2 contribution; JSS PDF re-opened 2026-08-21T06:12Z). The extra process has `LAMBDA` 0 and is not an observed indicator. Original indicators load on the original process after the `DRIFT` coupling. The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The contribution is not `E(y_t)`. The evolved-plus-contribution latent mean is not `E(y_t)`. `T0TDPREDEFFECT` on the extra process begins at `t = 0` and uses `Δt = t − t0` for both the original-process evolution and the extra drive. `TDPREDEFFECT` after `t0` uses `t − u` with `t0 < u < t` for the extra drive while `μ_t` still uses `Δt`. The observed mean of that after-t0 extra-process contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, Eq. 5 of that §7.2 after-t0 contribution; JSS PDF re-opened 2026-08-21T06:32Z). The first-occasion extra-process observed mean is not that observed mean when `u ≠ t0`. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is a Dirac on the original process and is not that `DRIFT` drive. An impulse at `u = t0` or `u = t` is not interior. The asymptotic time-independent predictor effect is `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z). Form `B z` first, then divide by `-a`. Stable `a < 0` is required. `a ≥ 0` cannot hold a finite process-mean change. `-B z / a` is not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. The asymptotic time-independent predictor variance is `(B / a)² v` (Driver et al., 2017, §7.2, pp. 20–21 `addedTIPREDVAR`). Form the unit asymptotic effect first, then square, then multiply by `v`. `(B / a)² v` is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. diff --git a/crates/psychometric_core/src/error.rs b/crates/psychometric_core/src/error.rs index 5632ff42..9d94d0d8 100644 --- a/crates/psychometric_core/src/error.rs +++ b/crates/psychometric_core/src/error.rs @@ -341,6 +341,17 @@ pub enum PsychometricError { /// Driver §7.2 `asymTIPREDEFFECT` was treated as the contemporaneous /// Dirac. `-B z / a` is not `M x`. AsymptoticTimeIndependentEffectIsNotTimeDependentImpulse, + /// Driver §7.2 `addedTIPREDVAR` was treated as `TRAITVAR`. + /// `(B / a)² v` is between-subject variance accounted for by a + /// time-independent predictor, not a zero-drift trait process. + AsymptoticTimeIndependentVarianceIsNotTraitVariance, + /// Driver §7.2 `addedTIPREDVAR` was treated as `asymDIFFUSION`. + /// `(B / a)² v` is not the stationary within-subject variance. + AsymptoticTimeIndependentVarianceIsNotStationaryWithinSubject, + /// Driver §7.2 `addedTIPREDVAR` was treated as `asymTIPREDEFFECT`. + /// `(B / a)² v` is a variance, not the expected total change in + /// process means. + AsymptoticTimeIndependentVarianceIsNotAsymptoticEffect, } impl fmt::Display for PsychometricError { @@ -617,6 +628,15 @@ impl fmt::Display for PsychometricError { Self::AsymptoticTimeIndependentEffectIsNotTimeDependentImpulse => { "asymptotic time-independent predictor effect is not the contemporaneous impulse" } + Self::AsymptoticTimeIndependentVarianceIsNotTraitVariance => { + "asymptotic time-independent predictor variance is not trait variance" + } + Self::AsymptoticTimeIndependentVarianceIsNotStationaryWithinSubject => { + "asymptotic time-independent predictor variance is not the stationary within-subject variance" + } + Self::AsymptoticTimeIndependentVarianceIsNotAsymptoticEffect => { + "asymptotic time-independent predictor variance is not the expected total change in process means" + } }; formatter.write_str(message) } @@ -1044,5 +1064,18 @@ mod tests { PsychometricError::AsymptoticTimeIndependentEffectIsNotTimeDependentImpulse.to_string(), "asymptotic time-independent predictor effect is not the contemporaneous impulse" ); + assert_eq!( + PsychometricError::AsymptoticTimeIndependentVarianceIsNotTraitVariance.to_string(), + "asymptotic time-independent predictor variance is not trait variance" + ); + assert_eq!( + PsychometricError::AsymptoticTimeIndependentVarianceIsNotStationaryWithinSubject + .to_string(), + "asymptotic time-independent predictor variance is not the stationary within-subject variance" + ); + assert_eq!( + PsychometricError::AsymptoticTimeIndependentVarianceIsNotAsymptoticEffect.to_string(), + "asymptotic time-independent predictor variance is not the expected total change in process means" + ); } } diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index e9a42b32..eb93d109 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -56,7 +56,12 @@ //! means given a unit increase on a time-independent predictor. The //! scalar map is `-B z / a` for stable `a < 0`. That total change is //! not the coefficient `B`, not the finite-interval increment -//! `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. Table 3 (p. 13) names a different matrix +//! `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. Section 7.2 +//! then names `addedTIPREDVAR` the stable between-subject variance +//! accounted for by those predictors. The scalar map is `(B / a)² v` +//! for predictor variance `v ≥ 0`. That variance is not `TRAITVAR`, +//! not `asymDIFFUSION`, and not the expected total change `-B z / a`. +//! Table 3 (p. 13) names a different matrix //! `T0TIPREDEFFECT` for time-independent predictors on latents at //! `T0`. The scalar first-occasion shift is `t0_b z`. Equation 3's //! first summand carries that shift as `e^{A Δt} t0_b z`. That carry @@ -2652,6 +2657,114 @@ pub fn refuse_asymptotic_time_independent_effect_as_time_dependent_impulse( Err(PsychometricError::AsymptoticTimeIndependentEffectIsNotTimeDependentImpulse) } +/// Exact scalar §7.2 `addedTIPREDVAR`. +/// +/// Driver, Oud, and Voelkle (2017, §7.2, pp. 20–21; Eq. 3, p. 5; +/// Table 2, p. 12; JSS PDF opened 2026-08-21T13:08Z from +/// ) +/// name `asymTIPREDEFFECT` the expected total change in process means +/// given a unit increase on a time-independent predictor. The scalar +/// map is `-B / a` for stable `a < 0`. Section 7.2 then names +/// `addedTIPREDVAR` the stable between-subject variance accounted for +/// by those predictors. For predictor variance `v ≥ 0` that variance +/// is `(-B / a)² v`. Form the unit asymptotic effect first, then +/// square, then multiply by `v`. A zero coefficient or zero predictor +/// variance is exactly zero. `v < 0` fails closed. `a ≥ 0` cannot hold +/// a finite process-mean change and fails closed. `(B / a)² v` is not +/// `TRAITVAR`, not `asymDIFFUSION`, and not the expected total change +/// `-B z / a`. This is not a Kalman filter, not a matrix `expm`, and +/// not ctsem estimation. +/// +/// # Errors +/// +/// Returns [`PsychometricError::EventTimeRequired`] for any non-event +/// clock, [`PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift`] +/// when the drift is not strictly negative and the variance is nonzero, +/// and [`PsychometricError::InvalidNumericInput`] when an input is +/// non-finite, the predictor variance is negative, or the product +/// overflows. +pub fn recover_asymptotic_time_independent_predictor_variance( + time_independent_effect: f64, + predictor_variance: f64, + log_rate: f64, + clock: LagClock, +) -> Result { + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + if !time_independent_effect.is_finite() + || !predictor_variance.is_finite() + || !log_rate.is_finite() + || predictor_variance < 0.0 + { + return Err(PsychometricError::InvalidNumericInput); + } + if time_independent_effect == 0.0 || predictor_variance == 0.0 { + return Ok(0.0); + } + let unit_effect = recover_asymptotic_time_independent_predictor_effect( + time_independent_effect, + 1.0, + log_rate, + clock, + )?; + let squared = require_finite(unit_effect * unit_effect)?; + require_finite(squared * predictor_variance) +} + +/// Refuse treating §7.2 `addedTIPREDVAR` as `TRAITVAR`. +/// +/// `(B / a)² v` is between-subject variance accounted for by a +/// time-independent predictor. Section 4.3 `TRAITVAR` is a zero-drift +/// latent process. Those are not the same map. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::AsymptoticTimeIndependentVarianceIsNotTraitVariance`]. +pub fn refuse_asymptotic_time_independent_variance_as_trait_variance( + added_predictor_variance: f64, + trait_variance: f64, +) -> Result { + let _ = (added_predictor_variance, trait_variance); + Err(PsychometricError::AsymptoticTimeIndependentVarianceIsNotTraitVariance) +} + +/// Refuse treating §7.2 `addedTIPREDVAR` as `asymDIFFUSION`. +/// +/// `(B / a)² v` is between-subject variance from a time-independent +/// predictor. `asymDIFFUSION` is the stationary within-subject +/// variance `-q / (2 a)`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::AsymptoticTimeIndependentVarianceIsNotStationaryWithinSubject`]. +pub fn refuse_asymptotic_time_independent_variance_as_stationary_within_subject( + added_predictor_variance: f64, + stationary_variance: f64, +) -> Result { + let _ = (added_predictor_variance, stationary_variance); + Err(PsychometricError::AsymptoticTimeIndependentVarianceIsNotStationaryWithinSubject) +} + +/// Refuse treating §7.2 `addedTIPREDVAR` as `asymTIPREDEFFECT`. +/// +/// `(B / a)² v` is a variance. `-B z / a` is the expected total +/// change in process means. Those are not the same map. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::AsymptoticTimeIndependentVarianceIsNotAsymptoticEffect`]. +pub fn refuse_asymptotic_time_independent_variance_as_asymptotic_effect( + added_predictor_variance: f64, + asymptotic_effect: f64, +) -> Result { + let _ = (added_predictor_variance, asymptotic_effect); + Err(PsychometricError::AsymptoticTimeIndependentVarianceIsNotAsymptoticEffect) +} + /// Exact scalar observed mean of a time-independent predictor. /// /// Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Eq. 3, p. 5; Table 2, @@ -4149,6 +4262,7 @@ mod tests { ClusteredEventScore, EventOccasion, LagClock, LaggedWithinResidual, fit_scalar_log_rate, map_discrete_lag_across_event_intervals, recover_asymptotic_time_independent_predictor_effect, + recover_asymptotic_time_independent_predictor_variance, recover_discrete_constant_predictor_effect, recover_discrete_continuous_intercept_effect, recover_discrete_lag_from_log_rate, recover_discrete_lag_one, recover_discrete_lagged_latent_covariance, recover_discrete_latent_mean, @@ -4187,6 +4301,9 @@ mod tests { 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_variance_as_asymptotic_effect, + refuse_asymptotic_time_independent_variance_as_stationary_within_subject, + refuse_asymptotic_time_independent_variance_as_trait_variance, refuse_continuous_intercept_as_discrete_mean_increment, refuse_continuous_intercept_as_initial_latent_mean, refuse_continuous_intercept_as_manifest_means, refuse_difference_quotient_as_local_rate, @@ -7242,6 +7359,149 @@ mod tests { ); } + #[test] + fn asymptotic_time_independent_variance_recovers_driver_section_seven_point_two() { + // Driver et al. (2017, §7.2, p. 21) print LeisureTime + // asymTIPREDEFFECT = −1.673. addedTIPREDVAR is the variance of + // that mean shift. Reconstruct a from B and the printed total + // change; the printed 2.838 is the 2-latent TRAITVAR model, not + // this scalar map. + let effect = -0.225_f64; + let printed_asym = -1.673_f64; + let log_rate = -effect / printed_asym; + let predictor_variance = 1.0_f64; + let recovered = recover_asymptotic_time_independent_predictor_variance( + effect, + predictor_variance, + log_rate, + LagClock::EventTime, + ) + .expect("addedTIPREDVAR"); + let expected = printed_asym * printed_asym * predictor_variance; + assert!((recovered - expected).abs() < 1e-12); + let doubled = recover_asymptotic_time_independent_predictor_variance( + effect, + 2.0, + log_rate, + LagClock::EventTime, + ) + .expect("doubled-v"); + assert!((doubled - 2.0 * expected).abs() < 1e-12); + assert_eq!( + recover_asymptotic_time_independent_predictor_variance( + 0.0, + predictor_variance, + 0.0, + LagClock::EventTime + ), + Ok(0.0) + ); + assert_eq!( + recover_asymptotic_time_independent_predictor_variance( + effect, + 0.0, + 0.0, + LagClock::EventTime + ), + Ok(0.0) + ); + } + + #[test] + fn asymptotic_time_independent_variance_is_not_trait_stationary_or_mean_effect() { + let effect = -0.225_f64; + let log_rate = -0.134_488_942_f64; + let predictor_variance = 2.0_f64; + let recovered = recover_asymptotic_time_independent_predictor_variance( + effect, + predictor_variance, + log_rate, + LagClock::EventTime, + ) + .expect("addedTIPREDVAR"); + let mean_effect = recover_asymptotic_time_independent_predictor_effect( + effect, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("asymTIPREDEFFECT"); + let stationary = recover_stationary_latent_variance(0.4, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + let trait_plus = recover_trait_plus_state_latent_variance(0.8, 0.3).expect("trait"); + assert!((recovered - mean_effect).abs() > 1e-3); + assert!((recovered - stationary).abs() > 1e-3); + assert!((recovered - trait_plus).abs() > 1e-3); + assert_eq!( + refuse_asymptotic_time_independent_variance_as_trait_variance(recovered, trait_plus), + Err(PsychometricError::AsymptoticTimeIndependentVarianceIsNotTraitVariance) + ); + assert_eq!( + refuse_asymptotic_time_independent_variance_as_stationary_within_subject( + recovered, stationary + ), + Err(PsychometricError::AsymptoticTimeIndependentVarianceIsNotStationaryWithinSubject) + ); + assert_eq!( + refuse_asymptotic_time_independent_variance_as_asymptotic_effect( + recovered, + mean_effect + ), + Err(PsychometricError::AsymptoticTimeIndependentVarianceIsNotAsymptoticEffect) + ); + } + + #[test] + fn asymptotic_time_independent_variance_invalid_inputs_fail_closed() { + let effect = -0.225_f64; + let log_rate = -0.134_488_942_f64; + assert_eq!( + recover_asymptotic_time_independent_predictor_variance( + effect, + 1.0, + log_rate, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_asymptotic_time_independent_predictor_variance( + effect, + 1.0, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) + ); + assert_eq!( + recover_asymptotic_time_independent_predictor_variance( + effect, + -1.0, + log_rate, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_asymptotic_time_independent_predictor_variance( + 1e308, + 1.0, + -1e-308, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_asymptotic_time_independent_predictor_variance( + 1e200, + 1.0, + -1e-200, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + } + #[test] fn discrete_observed_mean_with_impulse_recovers_driver_equation_five() { let loading = 2.0_f64; diff --git a/crates/psychometric_core/src/lib.rs b/crates/psychometric_core/src/lib.rs index cfdee41c..9fd03336 100644 --- a/crates/psychometric_core/src/lib.rs +++ b/crates/psychometric_core/src/lib.rs @@ -115,6 +115,10 @@ //! change in process means given a time-independent predictor; //! `a < 0`; not the coefficient `B`, not //! `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`), +//! recovers the Driver §7.2 `addedTIPREDVAR` as `(B / a)² v` +//! (pp. 20–21; stable between-subject variance accounted for by a +//! time-independent predictor with variance `v`; not `TRAITVAR`, +//! not `asymDIFFUSION`, and not `-B z / a`), //! and refuses //! latent-mean comparison below strong invariance. @@ -165,6 +169,8 @@ pub use event_time::LaggedWithinResidual; pub use event_time::map_discrete_lag_across_event_intervals; /// Exact scalar §7.2 `asymTIPREDEFFECT` `-B z / a`. pub use event_time::recover_asymptotic_time_independent_predictor_effect; +/// Exact scalar §7.2 `addedTIPREDVAR` `(B / a)² v`. +pub use event_time::recover_asymptotic_time_independent_predictor_variance; /// Exact scalar discrete effect of a constant event-time predictor. pub use event_time::recover_discrete_constant_predictor_effect; /// Exact scalar discrete intercept increment `A^{-1}[e^{A Δt} − I] κ`. @@ -271,6 +277,12 @@ 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 `asymTIPREDEFFECT`. +pub use event_time::refuse_asymptotic_time_independent_variance_as_asymptotic_effect; +/// Refuse treating §7.2 `addedTIPREDVAR` as `asymDIFFUSION`. +pub use event_time::refuse_asymptotic_time_independent_variance_as_stationary_within_subject; +/// Refuse treating §7.2 `addedTIPREDVAR` as `TRAITVAR`. +pub use event_time::refuse_asymptotic_time_independent_variance_as_trait_variance; /// Refuse treating Driver Table 2 `CINT` as the discrete mean increment. pub use event_time::refuse_continuous_intercept_as_discrete_mean_increment; /// Refuse treating Driver Table 2 `CINT` as `T0MEANS`. 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 5a9fad2d..ee04df16 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -5,6 +5,7 @@ use psychometric_core::{ ClusteredEventScore, ClusteredScore, EventOccasion, IndicatorKind, LagClock, LaggedWithinResidual, PsychometricError, map_discrete_lag_across_event_intervals, ordinary_least_squares_slope, 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, recover_discrete_lagged_latent_covariance, recover_discrete_latent_mean, @@ -41,6 +42,9 @@ 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_variance_as_asymptotic_effect, + refuse_asymptotic_time_independent_variance_as_stationary_within_subject, + refuse_asymptotic_time_independent_variance_as_trait_variance, refuse_continuous_intercept_as_discrete_mean_increment, refuse_continuous_intercept_as_initial_latent_mean, refuse_continuous_intercept_as_manifest_means, refuse_difference_quotient_as_local_rate, @@ -4282,3 +4286,91 @@ fn asymptotic_time_independent_effect_refuses_unstable_drift_and_non_event_clock Ok(0.0) ); } + +#[test] +fn asymptotic_time_independent_variance_recovers_driver_section_seven_point_two() { + let effect = -0.225_f64; + let printed_asym = -1.673_f64; + let log_rate = -effect / printed_asym; + let predictor_variance = 1.0_f64; + let recovered = recover_asymptotic_time_independent_predictor_variance( + effect, + predictor_variance, + log_rate, + LagClock::EventTime, + ) + .expect("addedTIPREDVAR"); + let expected = printed_asym * printed_asym * predictor_variance; + let error = rmse(&[expected], &[recovered]); + assert!( + error < 1e-12, + "Driver §7.2 addedTIPREDVAR RMSE {error}: got {recovered}" + ); + let mean_effect = recover_asymptotic_time_independent_predictor_effect( + effect, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("asymTIPREDEFFECT"); + let stationary = recover_stationary_latent_variance(0.4, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + let trait_plus = recover_trait_plus_state_latent_variance(0.8, 0.3).expect("trait"); + assert!( + rmse(&[recovered], &[mean_effect]) > error, + "asymTIPREDEFFECT is not addedTIPREDVAR" + ); + assert!(rmse(&[recovered], &[stationary]) > error); + assert!(rmse(&[recovered], &[trait_plus]) > error); + assert_eq!( + refuse_asymptotic_time_independent_variance_as_trait_variance(recovered, trait_plus), + Err(PsychometricError::AsymptoticTimeIndependentVarianceIsNotTraitVariance) + ); + assert_eq!( + refuse_asymptotic_time_independent_variance_as_stationary_within_subject( + recovered, stationary + ), + Err(PsychometricError::AsymptoticTimeIndependentVarianceIsNotStationaryWithinSubject) + ); + assert_eq!( + refuse_asymptotic_time_independent_variance_as_asymptotic_effect(recovered, mean_effect), + Err(PsychometricError::AsymptoticTimeIndependentVarianceIsNotAsymptoticEffect) + ); +} + +#[test] +fn asymptotic_time_independent_variance_refuses_unstable_drift_and_non_event_clocks() { + let effect = -0.225_f64; + let log_rate = -0.134_488_942_f64; + assert_eq!( + recover_asymptotic_time_independent_predictor_variance( + effect, + 1.0, + log_rate, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_asymptotic_time_independent_predictor_variance( + effect, + 1.0, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) + ); + assert_eq!( + recover_asymptotic_time_independent_predictor_variance( + effect, + -1.0, + log_rate, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_asymptotic_time_independent_predictor_variance(0.0, 1.0, 0.0, LagClock::EventTime), + Ok(0.0) + ); +} diff --git a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs index 47d0a4ac..71473c83 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -4,6 +4,7 @@ use psychometric_core::{ ClusteredEventScore, ClusteredScore, IndicatorKind, LagClock, LaggedWithinResidual, ordinary_least_squares_slope, posterior_draw_point_estimate_mean, 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_lagged_latent_covariance, recover_discrete_latent_mean, recover_discrete_latent_mean_with_extra_process, @@ -37,6 +38,9 @@ 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_variance_as_asymptotic_effect, + refuse_asymptotic_time_independent_variance_as_stationary_within_subject, + refuse_asymptotic_time_independent_variance_as_trait_variance, refuse_continuous_intercept_as_discrete_mean_increment, refuse_continuous_intercept_as_initial_latent_mean, refuse_continuous_intercept_as_manifest_means, @@ -2123,3 +2127,59 @@ fn asymptotic_time_independent_effect_is_not_coefficient_discrete_cint_or_impuls ) ); } + +#[test] +fn asymptotic_time_independent_variance_is_not_trait_stationary_or_mean_effect() { + let effect = -0.225_f64; + let log_rate = -0.134_488_942_f64; + let recovered = recover_asymptotic_time_independent_predictor_variance( + effect, + 2.0, + log_rate, + LagClock::EventTime, + ) + .expect("addedTIPREDVAR"); + let mean_effect = recover_asymptotic_time_independent_predictor_effect( + effect, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("asymTIPREDEFFECT"); + let stationary = recover_stationary_latent_variance(0.4, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + let trait_plus = recover_trait_plus_state_latent_variance(0.8, 0.3).expect("trait"); + assert!( + (recovered - mean_effect).abs() > 1e-3, + "Driver et al. (2017, §7.2, pp. 20–21): addedTIPREDVAR is not asymTIPREDEFFECT" + ); + assert!( + (recovered - stationary).abs() > 1e-3, + "Driver et al. (2017, §7.2): addedTIPREDVAR is not asymDIFFUSION" + ); + assert!( + (recovered - trait_plus).abs() > 1e-3, + "Driver et al. (2017, §7.2): addedTIPREDVAR is not TRAITVAR" + ); + assert_eq!( + refuse_asymptotic_time_independent_variance_as_trait_variance(recovered, trait_plus), + Err( + psychometric_core::PsychometricError::AsymptoticTimeIndependentVarianceIsNotTraitVariance + ) + ); + assert_eq!( + refuse_asymptotic_time_independent_variance_as_stationary_within_subject( + recovered, + stationary + ), + Err( + psychometric_core::PsychometricError::AsymptoticTimeIndependentVarianceIsNotStationaryWithinSubject + ) + ); + assert_eq!( + refuse_asymptotic_time_independent_variance_as_asymptotic_effect(recovered, mean_effect), + Err( + psychometric_core::PsychometricError::AsymptoticTimeIndependentVarianceIsNotAsymptoticEffect + ) + ); +} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 03eadeb0..87a482b6 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -28,7 +28,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | report template/section/copied/style/modality method effects | ADR 0004/0012; PRD/TRD | simulation truth factors implemented; estimator-side method model remains future | partial | | candidate K statistical/Pareto gates + blinded LLM review | ADR 0012; research | future `model_selection` | accepted-target | | compositional topic correlation / stable clustering | ADR 0005/0012; research | future `network_analysis` | accepted-target | -| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; §7.2 level-change `CINT` is `κ = −a m x` (`a < 0`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`; Eq. 3 of that setting is `(1 − e^{a Δt}) m x`); §7.2 extra-process contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, not the dissipating Dirac; `ε ≥ 0` fails closed; Eq. 5 of that contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; extra `LAMBDA` is 0; `τ + λ μ_t` is not that observed mean; after-t0 extra-process `TDPREDEFFECT` uses `t − u` with `t0 < u < t` while `μ_t` uses `Δt`; that after-t0 observed mean is not the first-occasion extra-process observed mean; §7.2 `asymTIPREDEFFECT` is `-B z / a` for `a < 0` and is not `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z); full ESEM/DSEM remaining | partial | +| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; §7.2 level-change `CINT` is `κ = −a m x` (`a < 0`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`; Eq. 3 of that setting is `(1 − e^{a Δt}) m x`); §7.2 extra-process contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, not the dissipating Dirac; `ε ≥ 0` fails closed; Eq. 5 of that contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; extra `LAMBDA` is 0; `τ + λ μ_t` is not that observed mean; after-t0 extra-process `TDPREDEFFECT` uses `t − u` with `t0 < u < t` while `μ_t` uses `Δt`; that after-t0 observed mean is not the first-occasion extra-process observed mean; §7.2 `asymTIPREDEFFECT` is `-B z / a` for `a < 0` and is not `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; §7.2 `addedTIPREDVAR` is `(B / a)² v` and is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z); full ESEM/DSEM remaining | partial | | CPU bounded multithreading + GPU/VRAM streaming/parity | ADR 0001/0006 | future `compute_backend` | accepted-target | | TDT detection/tracking vs CHRONOS schema/prediction/temporal consistency | ADR 0016; PRD/research | future `event_intelligence` | accepted-target | | evidence-bounded LLM interpretation | ADR 0010/0012; PRD | `tepp_api` router plus future `interpretation_gateway` | partial | diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index ef21a198..da0fd67d 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -1,7 +1,7 @@ # ADR 0005 — Posterior-aware ESEM/DSEM and structural interpretation **Decision status:** Accepted -**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), exact scalar contemporaneous `TDPREDEFFECT` impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; the §7.2 level-change form is not that impulse), exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of that Eq. 3 composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar time-independent `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle Eq. 14; `B` is not that discrete increment), exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of that Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of that carried latent mean; `τ + λ μ_t` is not that observed mean), exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; `T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`), exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; `T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand TD composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean), exact scalar §7.2 level-change `CINT` `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; `a < 0` so `−κ / a = m x`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2), exact scalar Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` (not `m x`, not `κ`, and not `TIPREDEFFECT`), exact scalar §7.2 extra near-zero-drift latent process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z; identification `TDPREDEFFECT` on the extra process is 1; extra `DRIFT` printed as `−0.000001`; precisely 0 causes computational problems; `ε = a` is `a_{ηξ} x Δt e^{a Δt}`; not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`; `ε ≥ 0` fails closed), exact scalar observed mean of that extra-process contribution `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z; the extra process has `LAMBDA` 0 and is not an observed indicator; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`), exact scalar after-t0 extra-process contribution `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` and Eq. 5 observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:32Z; `T0TDPREDEFFECT` uses `Δt` for both the evolution and the extra drive; `TDPREDEFFECT` after `t0` uses `t − u`; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive), exact scalar §7.2 `asymTIPREDEFFECT` `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z; expected total change in process means given a time-independent predictor; `a < 0`; not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; `a ≥ 0` fails closed), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target +**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), exact scalar contemporaneous `TDPREDEFFECT` impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; the §7.2 level-change form is not that impulse), exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of that Eq. 3 composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar time-independent `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle Eq. 14; `B` is not that discrete increment), exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of that Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of that carried latent mean; `τ + λ μ_t` is not that observed mean), exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; `T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`), exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; `T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand TD composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean), exact scalar §7.2 level-change `CINT` `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; `a < 0` so `−κ / a = m x`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2), exact scalar Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` (not `m x`, not `κ`, and not `TIPREDEFFECT`), exact scalar §7.2 extra near-zero-drift latent process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z; identification `TDPREDEFFECT` on the extra process is 1; extra `DRIFT` printed as `−0.000001`; precisely 0 causes computational problems; `ε = a` is `a_{ηξ} x Δt e^{a Δt}`; not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`; `ε ≥ 0` fails closed), exact scalar observed mean of that extra-process contribution `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z; the extra process has `LAMBDA` 0 and is not an observed indicator; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`), exact scalar after-t0 extra-process contribution `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` and Eq. 5 observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:32Z; `T0TDPREDEFFECT` uses `Δt` for both the evolution and the extra drive; `TDPREDEFFECT` after `t0` uses `t − u`; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive), exact scalar §7.2 `asymTIPREDEFFECT` `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z; expected total change in process means given a time-independent predictor; `a < 0`; not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; `a ≥ 0` fails closed), exact scalar §7.2 `addedTIPREDVAR` `(B / a)² v` (Driver et al., 2017, §7.2, pp. 20–21; stable between-subject variance accounted for by a time-independent predictor; not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target **Date:** 2026-08-05 **Supersedes:** None. ADR 0012 governs upstream topic measurement/network coordinates; this ADR governs higher-order psychometric structure and longitudinal interpretation. @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), recovers the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z; `TDPREDEFFECT` is `M`, not `CINT`; `M x` is not `A^{-1}[e^{A Δt} − I] B z`; `M x` is not Voelkle et al., 2012, Eq. 14; the level-change form is not that impulse), recovers the exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition; JSS PDF re-opened 2026-08-20T09:01Z; the evolved map `τ + λ μ_t` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-impulse latent mean is not `E(y_t)`), recovers the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z; `TIPREDEFFECT` is `B`, not `κ`; form `B z` first, then the discrete intercept map; a zero drift is `B z Δt`; `B` is not the discrete increment; `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle Eq. 14), recovers the exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; JSS PDF re-opened 2026-08-20T12:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-increment latent mean is not `E(y_t)`), recovers the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z; form `m x` first, then `e^{a(t−u)} m x`; a zero drift is `m x`; underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept; `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; an impulse at `u = t` is the contemporaneous map; an impulse at `u ≤ t0` is already in `η(t0)`), recovers the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; JSS PDF re-opened 2026-08-20T05:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the carried latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and its Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF opened 2026-08-20T15:14Z; form `t0_b z` first, then `e^{a Δt} t0_b z`; a zero drift is `t0_b z`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; `e^{A Δt} t0_b z` is not `t0_b z`; `T0TIPREDEFFECT` is the coefficient, not the shift), recovers the exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition; JSS PDF re-opened 2026-08-20T15:28Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and its Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; form `t0_m x0` first, then `e^{a Δt} t0_m x0`; a zero drift is `t0_m x0`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `T0TDPREDEFFECT` is the coefficient, not the shift; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), recovers the exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; the first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar §7.2 level-change `CINT` setting `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z; form `m x` first, then multiply by `−a`; `a < 0` so `−κ / a = m x`; `a ≥ 0` cannot hold a new process mean; `−a m x` is not the dissipating Dirac, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`; the extra near-zero-drift latent process also named in §7.2 is a different specification), recovers the exact scalar Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` (JSS PDF re-opened 2026-08-20T19:50Z; form the level-change `CINT` first, then the discrete intercept map; underflow of `e^{a Δt}` to `+0` keeps `m x`; `(1 − e^{a Δt}) m x` is not `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`), recovers the exact scalar §7.2 extra near-zero-drift latent process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z; form `a_{ηξ} x` first; `ε = a` is `a_{ηξ} x Δt e^{a Δt}`; a zero coupling or zero predictor is exactly zero; `ε ≥ 0` cannot hold a lasting extra state; that contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`), recovers the exact scalar observed mean of that extra-process contribution `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z; the extra process has `LAMBDA` 0 and is not an observed indicator; form the evolved-plus-contribution latent mean first, then `τ + λ` of that mean; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`), recovers the exact scalar after-t0 extra-process contribution `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` and its Eq. 5 observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:32Z; `T0TDPREDEFFECT` uses `Δt` for both the evolution and the extra drive; `TDPREDEFFECT` after `t0` uses `t − u` while `μ_t` still uses `Δt`; an impulse at `u = t0` or `u = t` is not interior; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive), recovers the exact scalar §7.2 `asymTIPREDEFFECT` `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z; form `B z` first, then divide by `-a`; `a < 0`; a zero coefficient or zero predictor is exactly zero; `a ≥ 0` cannot hold a finite process-mean change; `-B z / a` is not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), recovers the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z; `TDPREDEFFECT` is `M`, not `CINT`; `M x` is not `A^{-1}[e^{A Δt} − I] B z`; `M x` is not Voelkle et al., 2012, Eq. 14; the level-change form is not that impulse), recovers the exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition; JSS PDF re-opened 2026-08-20T09:01Z; the evolved map `τ + λ μ_t` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-impulse latent mean is not `E(y_t)`), recovers the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z; `TIPREDEFFECT` is `B`, not `κ`; form `B z` first, then the discrete intercept map; a zero drift is `B z Δt`; `B` is not the discrete increment; `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle Eq. 14), recovers the exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; JSS PDF re-opened 2026-08-20T12:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-increment latent mean is not `E(y_t)`), recovers the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z; form `m x` first, then `e^{a(t−u)} m x`; a zero drift is `m x`; underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept; `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; an impulse at `u = t` is the contemporaneous map; an impulse at `u ≤ t0` is already in `η(t0)`), recovers the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; JSS PDF re-opened 2026-08-20T05:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the carried latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and its Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF opened 2026-08-20T15:14Z; form `t0_b z` first, then `e^{a Δt} t0_b z`; a zero drift is `t0_b z`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; `e^{A Δt} t0_b z` is not `t0_b z`; `T0TIPREDEFFECT` is the coefficient, not the shift), recovers the exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition; JSS PDF re-opened 2026-08-20T15:28Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and its Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; form `t0_m x0` first, then `e^{a Δt} t0_m x0`; a zero drift is `t0_m x0`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `T0TDPREDEFFECT` is the coefficient, not the shift; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), recovers the exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; the first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar §7.2 level-change `CINT` setting `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z; form `m x` first, then multiply by `−a`; `a < 0` so `−κ / a = m x`; `a ≥ 0` cannot hold a new process mean; `−a m x` is not the dissipating Dirac, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`; the extra near-zero-drift latent process also named in §7.2 is a different specification), recovers the exact scalar Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` (JSS PDF re-opened 2026-08-20T19:50Z; form the level-change `CINT` first, then the discrete intercept map; underflow of `e^{a Δt}` to `+0` keeps `m x`; `(1 − e^{a Δt}) m x` is not `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`), recovers the exact scalar §7.2 extra near-zero-drift latent process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z; form `a_{ηξ} x` first; `ε = a` is `a_{ηξ} x Δt e^{a Δt}`; a zero coupling or zero predictor is exactly zero; `ε ≥ 0` cannot hold a lasting extra state; that contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`), recovers the exact scalar observed mean of that extra-process contribution `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z; the extra process has `LAMBDA` 0 and is not an observed indicator; form the evolved-plus-contribution latent mean first, then `τ + λ` of that mean; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`), recovers the exact scalar after-t0 extra-process contribution `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` and its Eq. 5 observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:32Z; `T0TDPREDEFFECT` uses `Δt` for both the evolution and the extra drive; `TDPREDEFFECT` after `t0` uses `t − u` while `μ_t` still uses `Δt`; an impulse at `u = t0` or `u = t` is not interior; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive), recovers the exact scalar §7.2 `asymTIPREDEFFECT` `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z; form `B z` first, then divide by `-a`; `a < 0`; a zero coefficient or zero predictor is exactly zero; `a ≥ 0` cannot hold a finite process-mean change; `-B z / a` is not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`), recovers the exact scalar §7.2 `addedTIPREDVAR` `(B / a)² v` (Driver et al., 2017, §7.2, pp. 20–21; form the unit asymptotic effect first, then square, then multiply by `v`; `v ≥ 0`; a zero coefficient or zero predictor variance is exactly zero; `(B / a)² v` is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. 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 b9fa90e6..4c4de8a8 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -38,15 +38,16 @@ This slice stays inside `psychometric_core`. It does not add a second invariance 32. recover the exact scalar observed mean of that extra-process contribution `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z; the extra process has `LAMBDA` 0 and is not an observed indicator; form the evolved-plus-contribution latent mean first, then `τ + λ` of that mean) and refuse treating `τ + λ μ_t`, `τ + λ(μ_t + m x)`, the contribution, or the evolved-plus-contribution latent mean as `E(y_t)`; 33. recover the exact scalar after-t0 extra-process contribution `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` and its Eq. 5 observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:32Z; `T0TDPREDEFFECT` uses `Δt = t − t0` for both the evolution and the extra drive; `TDPREDEFFECT` after `t0` uses `t − u` for the extra drive while `μ_t` still uses `Δt`; an impulse at `u = t0` or `u = t` is not interior; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive) and refuse treating the first-occasion extra-process observed mean, `τ + λ μ_t`, the impulse-carry observed mean, the after-t0 contribution, or the evolved-plus-after-contribution latent mean as that `E(y_t)`; 34. recover the exact scalar §7.2 `asymTIPREDEFFECT` `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; Eq. 3, p. 5; Table 2, p. 12; JSS PDF opened 2026-08-21T13:08Z; expected total change in process means given a time-independent predictor; form `B z` first, then divide by `-a`; `a < 0`; a zero coefficient or zero predictor is exactly zero) and refuse treating `-B z / a` as the coefficient `B`, as `A^{-1}[e^{A Δt} − I] B z`, as `CINT`, or as `M x`; `a ≥ 0` cannot hold a finite process-mean change; -35. refuse pooling discrete lags from unequal event intervals as one coefficient; -36. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); -37. refuse the difference quotient as a continuous-time rate; -38. apply the same event-time map to CWC residuals (still not DSEM); -39. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). +35. recover the exact scalar §7.2 `addedTIPREDVAR` `(B / a)² v` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z; stable between-subject variance accounted for by a time-independent predictor; form the unit asymptotic effect first, then square, then multiply by `v`; `v ≥ 0`; a zero coefficient or zero predictor variance is exactly zero) and refuse treating `(B / a)² v` as `TRAITVAR`, as `asymDIFFUSION`, or as `-B z / a`; +36. refuse pooling discrete lags from unequal event intervals as one coefficient; +37. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); +38. refuse the difference quotient as a continuous-time rate; +39. apply the same event-time map to CWC residuals (still not DSEM); +40. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). ## Claim boundary -This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. The contemporaneous `TDPREDEFFECT` impulse is not the continuous intercept, not the time-independent discrete effect, and not Voelkle et al. (2012, Eq. 14). The time-independent `TIPREDEFFECT` increment is not the continuous intercept, not the contemporaneous impulse, not Voelkle et al. (2012, Eq. 14), and not the coefficient `B`. The within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). The evolved observed mean `τ + λ μ_t` is not the contemporaneous-impulse observed mean `τ + λ(μ_t + m x)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not the impulse-carry observed mean `τ + λ(μ_t + e^{a(t−u)} m x)` when `u ≠ t`. The evolved observed mean `τ + λ μ_t` is not the impulse-carry observed mean. The carried latent mean is not `E(y_t)`. The evolved-plus-impulse latent mean is not `E(y_t)`. The evolved observed mean `τ + λ μ_t` is not the time-independent-predictor observed mean `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not that time-independent-predictor observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that time-independent-predictor observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TIPREDEFFECT` shift `t0_b z` is not the Eq. 3 process increment `A^{-1}[e^{A Δt} − I] B z`, not `κ`, and not `M x`. The Eq. 3 first-summand carry `e^{A Δt} t0_b z` is not `t0_b z` and is not that process increment. `T0TIPREDEFFECT` is the coefficient, not the first-occasion shift. The evolved observed mean `τ + λ μ_t` is not the first-occasion TI-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_b z)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion observed mean when `u ≠ t0`. The evolved-plus-T0TIPRED latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TDPREDEFFECT` shift `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `κ`. The Eq. 3 first-summand carry `e^{A Δt} t0_m x0` is not `t0_m x0` and is not that within-interval impulse carry. `T0TDPREDEFFECT` is the coefficient, not the first-occasion shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The evolved observed mean `τ + λ μ_t` is not the first-occasion TD-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_m x0)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion TD observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion TD observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion TD observed mean when `u ≠ t0`. The first-occasion TI composition `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that first-occasion TD observed mean. Same numbers as `T0TIPREDEFFECT` yield the same product; Table 3 names a different matrix. The evolved-plus-T0TDPRED latent mean is not `E(y_t)`. The §7.2 level-change `CINT` `κ = −a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. Lasting level change via that `CINT` setting requires `a < 0`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not that `CINT` setting. The Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` is not the dissipating Dirac `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`. Underflow of `e^{a Δt}` to `+0` keeps the equilibrium offset `m x`. The §7.2 extra-process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. Lasting level change via that extra process requires `ε < 0`. Precisely `ε = 0` causes computational problems in the printed specification. The extra-process observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` is not `τ + λ μ_t`, not `τ + λ(μ_t + m x)`, not the contribution, and not the evolved-plus-contribution latent mean. The extra process has `LAMBDA` 0 and is not an observed indicator. `T0TDPREDEFFECT` on the extra process uses `Δt = t − t0` for both the original-process evolution and the extra drive. `TDPREDEFFECT` after `t0` uses `t − u` with `t0 < u < t` for the extra drive while `μ_t` still uses `Δt`. The after-t0 extra-process observed mean is not the first-occasion extra-process observed mean when `u ≠ t0`. The impulse-carry `e^{a(t−u)} m x` is a Dirac on the original process and is not that `DRIFT` drive. The §7.2 `asymTIPREDEFFECT` `-B z / a` is the expected total change in process means given a time-independent predictor. It is not the coefficient `B`, not the finite-interval increment `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. Lasting asymptotic change via that map requires `a < 0`. +This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. The contemporaneous `TDPREDEFFECT` impulse is not the continuous intercept, not the time-independent discrete effect, and not Voelkle et al. (2012, Eq. 14). The time-independent `TIPREDEFFECT` increment is not the continuous intercept, not the contemporaneous impulse, not Voelkle et al. (2012, Eq. 14), and not the coefficient `B`. The within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). The evolved observed mean `τ + λ μ_t` is not the contemporaneous-impulse observed mean `τ + λ(μ_t + m x)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not the impulse-carry observed mean `τ + λ(μ_t + e^{a(t−u)} m x)` when `u ≠ t`. The evolved observed mean `τ + λ μ_t` is not the impulse-carry observed mean. The carried latent mean is not `E(y_t)`. The evolved-plus-impulse latent mean is not `E(y_t)`. The evolved observed mean `τ + λ μ_t` is not the time-independent-predictor observed mean `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not that time-independent-predictor observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that time-independent-predictor observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TIPREDEFFECT` shift `t0_b z` is not the Eq. 3 process increment `A^{-1}[e^{A Δt} − I] B z`, not `κ`, and not `M x`. The Eq. 3 first-summand carry `e^{A Δt} t0_b z` is not `t0_b z` and is not that process increment. `T0TIPREDEFFECT` is the coefficient, not the first-occasion shift. The evolved observed mean `τ + λ μ_t` is not the first-occasion TI-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_b z)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion observed mean when `u ≠ t0`. The evolved-plus-T0TIPRED latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TDPREDEFFECT` shift `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `κ`. The Eq. 3 first-summand carry `e^{A Δt} t0_m x0` is not `t0_m x0` and is not that within-interval impulse carry. `T0TDPREDEFFECT` is the coefficient, not the first-occasion shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The evolved observed mean `τ + λ μ_t` is not the first-occasion TD-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_m x0)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion TD observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion TD observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion TD observed mean when `u ≠ t0`. The first-occasion TI composition `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that first-occasion TD observed mean. Same numbers as `T0TIPREDEFFECT` yield the same product; Table 3 names a different matrix. The evolved-plus-T0TDPRED latent mean is not `E(y_t)`. The §7.2 level-change `CINT` `κ = −a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. Lasting level change via that `CINT` setting requires `a < 0`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not that `CINT` setting. The Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` is not the dissipating Dirac `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`. Underflow of `e^{a Δt}` to `+0` keeps the equilibrium offset `m x`. The §7.2 extra-process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. Lasting level change via that extra process requires `ε < 0`. Precisely `ε = 0` causes computational problems in the printed specification. The extra-process observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` is not `τ + λ μ_t`, not `τ + λ(μ_t + m x)`, not the contribution, and not the evolved-plus-contribution latent mean. The extra process has `LAMBDA` 0 and is not an observed indicator. `T0TDPREDEFFECT` on the extra process uses `Δt = t − t0` for both the original-process evolution and the extra drive. `TDPREDEFFECT` after `t0` uses `t − u` with `t0 < u < t` for the extra drive while `μ_t` still uses `Δt`. The after-t0 extra-process observed mean is not the first-occasion extra-process observed mean when `u ≠ t0`. The impulse-carry `e^{a(t−u)} m x` is a Dirac on the original process and is not that `DRIFT` drive. The §7.2 `asymTIPREDEFFECT` `-B z / a` is the expected total change in process means given a time-independent predictor. It is not the coefficient `B`, not the finite-interval increment `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. Lasting asymptotic change via that map requires `a < 0`. The §7.2 `addedTIPREDVAR` `(B / a)² v` is the stable between-subject variance accounted for by that predictor. It is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`. ## Authoritative sources @@ -100,6 +101,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - **Extra-process observed-indicator mean.** Driver et al. (2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z): `y_i(t)=Γ+Λη_i(t)+ζ_i(t)` with `ζ∼N(0,Θ)` and `Γ∼N(τ,Ψ)`. The printed extra process has `LAMBDA` 0 and is not an observed indicator. Original indicators load on the original process after the `DRIFT` coupling. The latent process at `t` after that contribution is `μ_t+a_{ηξ}x(e^{εΔt}−e^{aΔt})/(ε−a)`. The scalar composition is `E(y_t)=τ+λ(μ_t+a_{ηξ}x(e^{εΔt}−e^{aΔt})/(ε−a))`. Form the evolved-plus-contribution latent mean first, then `τ+λ` of that mean. A zero loading is exactly `τ`. A zero evolved-plus-contribution latent mean is exactly `τ`. A zero intercept is exactly `λ` of that latent mean. The evolved observed mean `τ+λμ_t` is not this composition. The contemporaneous map `τ+λ(μ_t+mx)` is not this composition. `MANIFESTMEANS` is not `E(y_t)`. The extra-process contribution is not `E(y_t)`. The evolved-plus-contribution latent mean is not `E(y_t)`. The extra process itself is not an observed indicator. An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **After-t0 extra-process observed-indicator mean.** Driver et al. (2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:32Z): `T0TDPREDEFFECT` begins the extra process at `t = 0`. `TDPREDEFFECT` begins it after `t = 0`. The interior case `t0 < u < t` drives the original process over `t − u` while `μ_t` still uses `Δt = t − t0`. The scalar composition is `E(y_t)=τ+λ(μ_t+a_{ηξ}x(e^{ε(t−u)}−e^{a(t−u)})/(ε−a))`. Form the evolved-plus-after-contribution latent mean first, then `τ+λ` of that mean. An impulse at `u = t0` is the first-occasion extra-process map. An impulse at `u = t` has not yet driven the original process. `e^{a(t−u)}mx` is a Dirac on the original process, not this `DRIFT` drive. A zero loading is exactly `τ`. The first-occasion extra-process observed mean is not this composition when `u ≠ t0`. The evolved observed mean `τ+λμ_t` is not this composition. The extra process itself is not an observed indicator. An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **Asymptotic time-independent predictor effect.** Driver et al. (2017, §7.2, pp. 20–21; Eq. 3, p. 5; Table 2, p. 12; JSS PDF opened 2026-08-21T13:08Z): Table 2 names `B` `TIPREDEFFECT`. Equation 3 maps a finite event interval as `A^{-1}[e^{AΔt}−I]Bz`. Section 7.2 names `asymTIPREDEFFECT` the expected total change in process means given an increase of 1 on a time-independent predictor. For stable `a<0` that `Δt→∞` limit is `-A^{-1}B`. The scalar map is `-Bz/a`. Form `Bz` first, then divide by `-a`. A zero coefficient or zero predictor is exactly zero. `a≥0` cannot hold a finite process-mean change. `-Bz/a` is not the coefficient `B`, not `A^{-1}[e^{AΔt}−I]Bz`, not `CINT`, and not `Mx`. An overflowing product or quotient fails closed. This is not a Kalman filter and not ctsem estimation. +- **Asymptotic time-independent predictor variance.** Driver et al. (2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z): Section 7.2 names `addedTIPREDVAR` the stable between-subject variance accounted for by time-independent predictors. For predictor variance `v≥0` the scalar map is `(B/a)²v`. Form the unit asymptotic effect `-B/a` first, then square, then multiply by `v`. A zero coefficient or zero predictor variance is exactly zero. `v<0` fails closed. `(B/a)²v` is not `TRAITVAR`, not `asymDIFFUSION`, and not `-Bz/a`. The printed 2-latent `addedTIPREDVAR` 2.838 is not this scalar map. An overflowing product fails closed. This is not a Kalman filter and not ctsem estimation. - **Level-change discrete increment.** Driver et al. (2017, §7.2, pp. 20–21; Eq. 3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T19:50Z): Equation 3 maps `CINT` through `A^{-1}[e^{AΔt}−I]κ`. With `κ=−a m x` the scalar increment is `(e^{aΔt}−1)/a·(−a m x)=(1−e^{aΔt})m x`. Form the level-change `CINT` first, then the discrete intercept map. Underflow of `e^{aΔt}` to `+0` keeps `m x`. A zero effect or zero predictor is exactly zero. `(1−e^{aΔt})m x` is not `m x`, not `κ`, and not `A^{-1}[e^{AΔt}−I]Bz`. An overflowing product or increment fails closed. This is not a Kalman filter and not ctsem estimation. - **CWC-then-lag.** Sample cluster means are removed first. Consecutive within residuals are then fitted by least squares to \(r_{t+\Delta t}\approx\exp(a\Delta t)\,r_{t}\) on event time. Same-sign pair-wise logs initialize the scalar Newton step. Sign-flipping \(T=2\) CWC pairs have no real logarithm and fail closed. Curran and Bauer (2011, pp. 607–608) show that this person-mean subtraction on a raw autoregressive series does **not** isolate the lagged within-person effect; the helper therefore does not claim to recover the raw-process drift. - **Already-centered irregular residual.** The caller supplies lagged within residuals. The mean of \(a=\ln(r_{t+\Delta t}/r_t)/\Delta t\) is the exact scalar map. Intervals may be irregular. The helper does not center again. This is not DSEM. @@ -139,6 +141,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - Driver et al. (2017, Eq. 5 of the §7.2 extra-process contribution; JSS PDF re-opened 2026-08-21T06:12Z) recovers a known \(E(y_t)=\tau+\lambda(\mu_t+a_{\eta\xi}x(e^{\varepsilon\Delta t}-e^{a\Delta t})/(\varepsilon-a))\) at machine-scale RMSE, and that RMSE is smaller than treating \(\tau+\lambda\mu_t\), \(\tau+\lambda(\mu_t+mx)\), the contribution, `MANIFESTMEANS`, or the evolved-plus-contribution latent mean as \(E(y_t)\); the extra process has `LAMBDA` 0 and is not an observed indicator; a zero original-indicator loading is \(\tau\); a zero coupling recovers \(\tau+\lambda\mu_t\); \(\varepsilon\ge 0\) with a nonzero contribution fails closed; a non-event clock, a non-positive interval, and an overflowing product or sum fail closed; - Driver et al. (2017, Eq. 5 of the §7.2 after-t0 extra-process contribution; JSS PDF re-opened 2026-08-21T06:32Z) recovers a known \(E(y_t)=\tau+\lambda(\mu_t+a_{\eta\xi}x(e^{\varepsilon(t-u)}-e^{a(t-u)})/(\varepsilon-a))\) at machine-scale RMSE for \(t_0 Date: Fri, 21 Aug 2026 16:25:46 +0000 Subject: [PATCH 58/62] feat(psychometric): recover Driver Table 2 asymCINT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Driver, Oud, and Voelkle (2017, Table 2, p. 12; Eq. 3) name asymCINT the asymptotic (Δt = ∞) expected change in processes for a 1 unit change in intercept. For stable a < 0 the scalar map is -κ / a. Form κ first, then divide by -a. A zero intercept is exactly zero. a >= 0 fails closed. -κ / a is not κ, not A^{-1}[e^{A Δt} − I] κ, not T0MEANS, and not -B z / a. Page 16 T0MEANS stationarity includes TI predictors; that composition is not this intercept-only map. JSS PDF opened 2026-08-21T16:13Z. Still not DSEM, not a Kalman filter, and not ctsem estimation. --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 1 + CLAUDE.md | 2 +- crates/psychometric_core/src/error.rs | 56 ++++ crates/psychometric_core/src/event_time.rs | 246 +++++++++++++++++- crates/psychometric_core/src/lib.rs | 16 ++ ...multilevel_event_time_recovery_contract.rs | 85 +++++- .../scientific_claim_boundary_contract.rs | 67 ++++- docs/TRACEABILITY.md | 2 +- docs/adr/0005-posterior-esem-dsem.md | 4 +- docs/adr/README.md | 2 +- .../multilevel-event-time-recovery.md | 17 +- docs/validation/temporal-event-foundation.md | 2 +- 13 files changed, 485 insertions(+), 17 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 1a500e9f..6271d3ab 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -61,7 +61,7 @@ boundaries above remain the target modular MSA architecture. | `tepp_simulation` | known-truth temporal/event data generation | | `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics | | `tepp_api` | versioned DTO, schema, and export contracts | -| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; first-occasion `T0TIPREDEFFECT` shift is `t0_b z` and Eq. 3 first-summand carry is `e^{A Δt} t0_b z` (`T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_b z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), first-occasion `T0TDPREDEFFECT` shift is `t0_m x0` and Eq. 3 first-summand carry is `e^{A Δt} t0_m x0` (`T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; §7.2 level-change `CINT` is `κ = −a m x` with `a < 0` so `−κ / a = m x` (`−a m x` is not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2; Eq. 3 of that setting is `(1 − e^{a Δt}) m x`, which is not `m x`, not `κ`, and not `TIPREDEFFECT`; §7.2 extra-process contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`; identification `TDPREDEFFECT` on the extra process is 1; printed extra `DRIFT` is `−0.000001`; not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`; `ε ≥ 0` fails closed; Eq. 5 of that contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; the extra process has `LAMBDA` 0 and is not an observed indicator; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`; after-t0 extra-process `TDPREDEFFECT` is `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` while `μ_t` uses `Δt`; Eq. 5 of that after-t0 contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)`; the first-occasion extra-process observed mean is not that observed mean when `u ≠ t0`; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive; §7.2 `asymTIPREDEFFECT` is `-B z / a` for `a < 0` (`-B z / a` is not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; §7.2 `addedTIPREDVAR` is `(B / a)² v`, not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`)), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | +| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; first-occasion `T0TIPREDEFFECT` shift is `t0_b z` and Eq. 3 first-summand carry is `e^{A Δt} t0_b z` (`T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_b z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), first-occasion `T0TDPREDEFFECT` shift is `t0_m x0` and Eq. 3 first-summand carry is `e^{A Δt} t0_m x0` (`T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; §7.2 level-change `CINT` is `κ = −a m x` with `a < 0` so `−κ / a = m x` (`−a m x` is not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2; Eq. 3 of that setting is `(1 − e^{a Δt}) m x`, which is not `m x`, not `κ`, and not `TIPREDEFFECT`; §7.2 extra-process contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`; identification `TDPREDEFFECT` on the extra process is 1; printed extra `DRIFT` is `−0.000001`; not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`; `ε ≥ 0` fails closed; Eq. 5 of that contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; the extra process has `LAMBDA` 0 and is not an observed indicator; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`; after-t0 extra-process `TDPREDEFFECT` is `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` while `μ_t` uses `Δt`; Eq. 5 of that after-t0 contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)`; the first-occasion extra-process observed mean is not that observed mean when `u ≠ t0`; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive; §7.2 `asymTIPREDEFFECT` is `-B z / a` for `a < 0` (`-B z / a` is not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; §7.2 `addedTIPREDVAR` is `(B / a)² v`, not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`; Table 2 `asymCINT` is `-κ / a` for `a < 0` and is not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`)), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | No crate exposes placeholder production behavior in Task 1. This prevents an empty façade from becoming a de facto public API before its invariants and tests diff --git a/CHANGELOG.md b/CHANGELOG.md index c0a30704..4193cae7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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, Table 2, p. 12; Eq. 3, p. 5; §4.3 / p. 16; JSS PDF opened 2026-08-21T16:13Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar `asymCINT`. Table 2 names `κ` `CINT` and names `asymCINT` the asymptotic (`Δt = ∞`) expected change in processes for a 1 unit change in intercept. Equation 3 maps a finite event interval as `A^{-1}[e^{A Δt} − I] κ`. For stable `a < 0` that `Δt → ∞` limit is `-A^{-1} κ`. The scalar map is `-κ / a`. A unit intercept is `-1 / a`. Form `κ` first, then divide by `-a`. A zero intercept is exactly zero. `a ≥ 0` cannot hold a finite process-mean change and fails closed. `-κ / a` is not `κ`, not the finite-interval increment `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `asymTIPREDEFFECT` `-B z / a`. Page 16 notes that a `T0MEANS` stationarity constraint includes time-independent predictors; that composition is not this intercept-only map. The printed 2-latent `CINT` values are not this scalar map. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-21T16:21Z: `is_oa: false`; Springer `content/pdf` is HTML 200). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, §7.2, pp. 20–21; Eq. 3, p. 5; Table 2, p. 12; JSS PDF opened 2026-08-21T13:08Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar `addedTIPREDVAR`. Section 7.2 names that matrix the stable between-subject variance accounted for by time-independent predictors. For stable `a < 0` and predictor variance `v ≥ 0` the scalar map is `(B / a)² v`. Form the unit asymptotic effect `-B / a` first, then square, then multiply by `v`. A zero coefficient or zero predictor variance is exactly zero. `v < 0` fails closed. `a ≥ 0` cannot hold a finite process-mean change and fails closed. `(B / a)² v` is not `TRAITVAR`, not `asymDIFFUSION`, and not the expected total change `-B z / a`. The printed 2-latent `addedTIPREDVAR` 2.838 is not this scalar map. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-21T06:24Z: `is_oa: false`; Springer `content/pdf` is HTML 200). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, §7.2, pp. 20–21; Eq. 3, p. 5; Table 2, p. 12; JSS PDF opened 2026-08-21T13:08Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar `asymTIPREDEFFECT`. Table 2 names `B` `TIPREDEFFECT`. Equation 3 maps a finite event interval as `A^{-1}[e^{A Δt} − I] B z`. Section 7.2 names `asymTIPREDEFFECT` the expected total change in process means given an increase of 1 on a time-independent predictor. For stable `a < 0` that total change is `-A^{-1} B`. The scalar map is `-B z / a`. Form `B z` first, then divide by `-a`. A zero coefficient or zero predictor is exactly zero. `a ≥ 0` cannot hold a finite process-mean change and fails closed. `-B z / a` is not the coefficient `B`, not the finite-interval increment `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. Printed LeisureTime `TIPREDEFFECT` `−0.225` / `asymTIPREDEFFECT` `−1.673` and Happiness `0.549` / `0.219` reconstruct under this map. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-21T06:24Z: `is_oa: false`; Springer `content/pdf` is HTML 200). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:32Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar observed-indicator mean of an extra-process `TDPREDEFFECT` after `t0`. Section 7.2 names `T0TDPREDEFFECT` when the extra process begins at `t = 0` and `TDPREDEFFECT` when it begins after `t = 0`. The printed extra process has `LAMBDA` 0. Original indicators load on the original process after the `DRIFT` coupling over `t − u` with `t0 < u < t` while `μ_t` still uses `Δt = t − t0`. The scalar composition is `E(y_t) = τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))`. The first-occasion extra-process observed mean uses `Δt` for both the evolution and the extra drive and is not this composition when `u ≠ t0`. The evolved observed mean `τ + λ μ_t` is not this composition. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is a Dirac on the original process and is not this `DRIFT` drive. An impulse at `u = t0` or `u = t` is not interior. A zero original-indicator loading is exactly `τ`. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-21T06:24Z: `is_oa: false`; Springer `content/pdf` is HTML 200). diff --git a/CLAUDE.md b/CLAUDE.md index f0e27d96..1a41632e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. Putnick and Bornstein (2016, PMC5145197 opened 2026-08-19T22:15Z) require scalar invariance before latent-mean comparison; residual invariance is not a prerequisite. Two-observation series have no residual degrees of freedom (`ordinary_least_squares_fit` returns residual variance `0`) and cap at strong/scalar; they still license means. This is two-group OLS, not MGCFA. Meredith (1993) names remain unread labels. -- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. The contemporaneous time-dependent predictor impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`). Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). The §7.2 level-change form is not that impulse. The observed mean of that contemporaneous impulse is `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-impulse latent mean is not `E(y_t)`. The time-independent predictor increment is `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`). Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. `TIPREDEFFECT` is `B`, not that discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle et al. (2012, Eq. 14). The observed mean of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The within-interval time-dependent impulse carry is `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation). Form `m x` first, then `e^{a(t−u)} m x`. A zero drift is `m x` with no dissipation. Underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept. `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). An impulse at `u = t` is the contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. The observed mean of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The carried latent mean is not `E(y_t)`. The first-occasion time-independent predictor shift is `t0_b z` (Driver et al., 2017, Table 3 `T0TIPREDEFFECT`; Eq. 3 first summand). Form `t0_b z` first, then `e^{a Δt} t0_b z`. Form `μ_t` first, then add that carry. A zero drift is `t0_b z`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. `e^{A Δt} t0_b z` is not `t0_b z`. `T0TIPREDEFFECT` is the coefficient, not the shift. The observed mean of that first-occasion carry is `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The evolved-plus-carry latent mean is not `E(y_t)`. The first-occasion time-dependent predictor shift is `t0_m x0` (Driver et al., 2017, Table 3 `T0TDPREDEFFECT`; Eq. 3 first summand; JSS PDF re-opened 2026-08-20T19:10Z). Form `t0_m x0` first, then `e^{a Δt} t0_m x0`. Form `μ_t` first, then add that carry. A zero drift is `t0_m x0`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`. `e^{A Δt} t0_m x0` is not `t0_m x0`. `T0TDPREDEFFECT` is the coefficient, not the shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The observed mean of that first-occasion TD carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean. The evolved-plus-carry latent mean is not `E(y_t)`. The lasting level-change `CINT` is `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z). Form `m x` first, then multiply by `−a`. Stable `a < 0` is required so `−κ / a = m x` is an equilibrium offset. `a ≥ 0` cannot hold a new process mean. `−a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not this `CINT` setting. Equation 3 maps that intercept as `(1 − e^{a Δt}) m x` (JSS PDF re-opened 2026-08-20T19:50Z). Form the level-change `CINT` first, then the discrete intercept map. Underflow of `e^{a Δt}` to `+0` keeps `m x`. `(1 − e^{a Δt}) m x` is not `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`. The printed §7.2 lasting level change is an extra near-zero-drift latent process (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z). `T0MEANS`, `CINT`, `T0VAR`, `DIFFUSION`, and `TRAITVAR` of that process are fixed to 0; `TDPREDEFFECT` on it is fixed to 1; its `DRIFT` diagonal is very close to 0 (printed example `−0.000001`; precisely 0 causes computational problems); the original process is driven by the `DRIFT` coupling `a_{ηξ}`. After a unit identification impulse the scalar contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`). Form `a_{ηξ} x` first. A zero coupling or zero predictor is exactly zero. `ε ≥ 0` fails closed. That contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. The observed mean of that extra-process contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5 of that §7.2 contribution; JSS PDF re-opened 2026-08-21T06:12Z). The extra process has `LAMBDA` 0 and is not an observed indicator. Original indicators load on the original process after the `DRIFT` coupling. The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The contribution is not `E(y_t)`. The evolved-plus-contribution latent mean is not `E(y_t)`. `T0TDPREDEFFECT` on the extra process begins at `t = 0` and uses `Δt = t − t0` for both the original-process evolution and the extra drive. `TDPREDEFFECT` after `t0` uses `t − u` with `t0 < u < t` for the extra drive while `μ_t` still uses `Δt`. The observed mean of that after-t0 extra-process contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, Eq. 5 of that §7.2 after-t0 contribution; JSS PDF re-opened 2026-08-21T06:32Z). The first-occasion extra-process observed mean is not that observed mean when `u ≠ t0`. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is a Dirac on the original process and is not that `DRIFT` drive. An impulse at `u = t0` or `u = t` is not interior. The asymptotic time-independent predictor effect is `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z). Form `B z` first, then divide by `-a`. Stable `a < 0` is required. `a ≥ 0` cannot hold a finite process-mean change. `-B z / a` is not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. The asymptotic time-independent predictor variance is `(B / a)² v` (Driver et al., 2017, §7.2, pp. 20–21 `addedTIPREDVAR`). Form the unit asymptotic effect first, then square, then multiply by `v`. `(B / a)² v` is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. The contemporaneous time-dependent predictor impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`). Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). The §7.2 level-change form is not that impulse. The observed mean of that contemporaneous impulse is `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-impulse latent mean is not `E(y_t)`. The time-independent predictor increment is `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`). Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. `TIPREDEFFECT` is `B`, not that discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle et al. (2012, Eq. 14). The observed mean of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The within-interval time-dependent impulse carry is `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation). Form `m x` first, then `e^{a(t−u)} m x`. A zero drift is `m x` with no dissipation. Underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept. `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). An impulse at `u = t` is the contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. The observed mean of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The carried latent mean is not `E(y_t)`. The first-occasion time-independent predictor shift is `t0_b z` (Driver et al., 2017, Table 3 `T0TIPREDEFFECT`; Eq. 3 first summand). Form `t0_b z` first, then `e^{a Δt} t0_b z`. Form `μ_t` first, then add that carry. A zero drift is `t0_b z`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. `e^{A Δt} t0_b z` is not `t0_b z`. `T0TIPREDEFFECT` is the coefficient, not the shift. The observed mean of that first-occasion carry is `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The evolved-plus-carry latent mean is not `E(y_t)`. The first-occasion time-dependent predictor shift is `t0_m x0` (Driver et al., 2017, Table 3 `T0TDPREDEFFECT`; Eq. 3 first summand; JSS PDF re-opened 2026-08-20T19:10Z). Form `t0_m x0` first, then `e^{a Δt} t0_m x0`. Form `μ_t` first, then add that carry. A zero drift is `t0_m x0`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`. `e^{A Δt} t0_m x0` is not `t0_m x0`. `T0TDPREDEFFECT` is the coefficient, not the shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The observed mean of that first-occasion TD carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean. The evolved-plus-carry latent mean is not `E(y_t)`. The lasting level-change `CINT` is `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z). Form `m x` first, then multiply by `−a`. Stable `a < 0` is required so `−κ / a = m x` is an equilibrium offset. `a ≥ 0` cannot hold a new process mean. `−a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not this `CINT` setting. Equation 3 maps that intercept as `(1 − e^{a Δt}) m x` (JSS PDF re-opened 2026-08-20T19:50Z). Form the level-change `CINT` first, then the discrete intercept map. Underflow of `e^{a Δt}` to `+0` keeps `m x`. `(1 − e^{a Δt}) m x` is not `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`. The printed §7.2 lasting level change is an extra near-zero-drift latent process (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z). `T0MEANS`, `CINT`, `T0VAR`, `DIFFUSION`, and `TRAITVAR` of that process are fixed to 0; `TDPREDEFFECT` on it is fixed to 1; its `DRIFT` diagonal is very close to 0 (printed example `−0.000001`; precisely 0 causes computational problems); the original process is driven by the `DRIFT` coupling `a_{ηξ}`. After a unit identification impulse the scalar contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`). Form `a_{ηξ} x` first. A zero coupling or zero predictor is exactly zero. `ε ≥ 0` fails closed. That contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. The observed mean of that extra-process contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5 of that §7.2 contribution; JSS PDF re-opened 2026-08-21T06:12Z). The extra process has `LAMBDA` 0 and is not an observed indicator. Original indicators load on the original process after the `DRIFT` coupling. The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The contribution is not `E(y_t)`. The evolved-plus-contribution latent mean is not `E(y_t)`. `T0TDPREDEFFECT` on the extra process begins at `t = 0` and uses `Δt = t − t0` for both the original-process evolution and the extra drive. `TDPREDEFFECT` after `t0` uses `t − u` with `t0 < u < t` for the extra drive while `μ_t` still uses `Δt`. The observed mean of that after-t0 extra-process contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, Eq. 5 of that §7.2 after-t0 contribution; JSS PDF re-opened 2026-08-21T06:32Z). The first-occasion extra-process observed mean is not that observed mean when `u ≠ t0`. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is a Dirac on the original process and is not that `DRIFT` drive. An impulse at `u = t0` or `u = t` is not interior. The asymptotic time-independent predictor effect is `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z). Form `B z` first, then divide by `-a`. Stable `a < 0` is required. `a ≥ 0` cannot hold a finite process-mean change. `-B z / a` is not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. The asymptotic time-independent predictor variance is `(B / a)² v` (Driver et al., 2017, §7.2, pp. 20–21 `addedTIPREDVAR`). Form the unit asymptotic effect first, then square, then multiply by `v`. `(B / a)² v` is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`. The asymptotic continuous intercept is `-κ / a` (Driver et al., 2017, Table 2, p. 12 `asymCINT`; Eq. 3 as `Δt → ∞`; JSS PDF opened 2026-08-21T16:13Z). Form `κ` first, then divide by `-a`. Stable `a < 0` is required. `-κ / a` is not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. diff --git a/crates/psychometric_core/src/error.rs b/crates/psychometric_core/src/error.rs index 9d94d0d8..6499b8e2 100644 --- a/crates/psychometric_core/src/error.rs +++ b/crates/psychometric_core/src/error.rs @@ -352,6 +352,26 @@ pub enum PsychometricError { /// `(B / a)² v` is a variance, not the expected total change in /// process means. AsymptoticTimeIndependentVarianceIsNotAsymptoticEffect, + /// Driver Table 2 `asymCINT` was requested for a non-stable drift. + /// The expected change in process means for a change in intercept + /// is `-κ / a` and requires `a < 0`. + AsymptoticContinuousInterceptRequiresStableDrift, + /// Driver Table 2 `asymCINT` was treated as `CINT`. + /// `-κ / a` is not `κ`. + AsymptoticContinuousInterceptIsNotContinuousIntercept, + /// Driver Table 2 `asymCINT` was treated as the finite-interval + /// discrete intercept increment. `-κ / a` is not + /// `A^{-1}[e^{A Δt} − I] κ`. + AsymptoticContinuousInterceptIsNotDiscreteIncrement, + /// Driver Table 2 `asymCINT` was treated as `T0MEANS`. + /// `-κ / a` is not the first-occasion latent mean. + AsymptoticContinuousInterceptIsNotInitialLatentMean, + /// Driver Table 2 `asymCINT` was treated as `asymTIPREDEFFECT`. + /// `-κ / a` is the intercept contribution. `-B z / a` is the + /// time-independent predictor contribution. Page 16 of the JSS + /// article notes that a `T0MEANS` stationarity constraint includes + /// time-independent predictors; that composition is not this map. + AsymptoticContinuousInterceptIsNotAsymptoticTimeIndependentEffect, } impl fmt::Display for PsychometricError { @@ -637,6 +657,21 @@ impl fmt::Display for PsychometricError { Self::AsymptoticTimeIndependentVarianceIsNotAsymptoticEffect => { "asymptotic time-independent predictor variance is not the expected total change in process means" } + Self::AsymptoticContinuousInterceptRequiresStableDrift => { + "asymptotic continuous intercept requires a stable negative drift" + } + Self::AsymptoticContinuousInterceptIsNotContinuousIntercept => { + "asymptotic continuous intercept is not the continuous intercept" + } + Self::AsymptoticContinuousInterceptIsNotDiscreteIncrement => { + "asymptotic continuous intercept is not the finite-interval discrete increment" + } + Self::AsymptoticContinuousInterceptIsNotInitialLatentMean => { + "asymptotic continuous intercept is not the first-occasion latent mean" + } + Self::AsymptoticContinuousInterceptIsNotAsymptoticTimeIndependentEffect => { + "asymptotic continuous intercept is not the asymptotic time-independent predictor effect" + } }; formatter.write_str(message) } @@ -1077,5 +1112,26 @@ mod tests { PsychometricError::AsymptoticTimeIndependentVarianceIsNotAsymptoticEffect.to_string(), "asymptotic time-independent predictor variance is not the expected total change in process means" ); + assert_eq!( + PsychometricError::AsymptoticContinuousInterceptRequiresStableDrift.to_string(), + "asymptotic continuous intercept requires a stable negative drift" + ); + assert_eq!( + PsychometricError::AsymptoticContinuousInterceptIsNotContinuousIntercept.to_string(), + "asymptotic continuous intercept is not the continuous intercept" + ); + assert_eq!( + PsychometricError::AsymptoticContinuousInterceptIsNotDiscreteIncrement.to_string(), + "asymptotic continuous intercept is not the finite-interval discrete increment" + ); + assert_eq!( + PsychometricError::AsymptoticContinuousInterceptIsNotInitialLatentMean.to_string(), + "asymptotic continuous intercept is not the first-occasion latent mean" + ); + assert_eq!( + PsychometricError::AsymptoticContinuousInterceptIsNotAsymptoticTimeIndependentEffect + .to_string(), + "asymptotic continuous intercept is not the asymptotic time-independent predictor effect" + ); } } diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index eb93d109..c26f0dde 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -61,6 +61,15 @@ //! accounted for by those predictors. The scalar map is `(B / a)² v` //! for predictor variance `v ≥ 0`. That variance is not `TRAITVAR`, //! not `asymDIFFUSION`, and not the expected total change `-B z / a`. +//! Table 2 (p. 12) names `asymCINT` the asymptotic (`Δt = ∞`) +//! expected change in processes for a 1 unit change in intercept +//! (`CINT`). Equation 3 maps a finite event interval as +//! `A^{-1}[e^{A Δt} − I] κ`. For stable `a < 0` that `Δt → ∞` limit +//! is `-κ / a`. A unit intercept is `-1 / a`. That intercept +//! contribution is not `κ`, not the finite-interval increment, not +//! `T0MEANS`, and not `asymTIPREDEFFECT` `-B z / a`. Page 16 notes +//! that a `T0MEANS` stationarity constraint includes time-independent +//! predictors; that composition is not this intercept-only map. //! Table 3 (p. 13) names a different matrix //! `T0TIPREDEFFECT` for time-independent predictors on latents at //! `T0`. The scalar first-occasion shift is `t0_b z`. Equation 3's @@ -2765,6 +2774,124 @@ pub fn refuse_asymptotic_time_independent_variance_as_asymptotic_effect( Err(PsychometricError::AsymptoticTimeIndependentVarianceIsNotAsymptoticEffect) } +/// Exact scalar Table 2 `asymCINT`. +/// +/// Driver, Oud, and Voelkle (2017, Table 2, p. 12; Eq. 3, p. 5; +/// §4.3 / p. 16; JSS PDF opened 2026-08-21T16:13Z from +/// ) +/// name `asymCINT` the asymptotic (`Δt = ∞`) expected change in +/// processes for a 1 unit change in intercept (`CINT`). Table 2 names +/// `κ` `CINT`. Equation 3 maps a finite event interval as +/// `A^{-1}[e^{A Δt} − I] κ`. For stable `a < 0` that `Δt → ∞` limit +/// is `-A^{-1} κ`. The scalar map is `-κ / a`. A unit intercept is +/// `-1 / a`. Form `κ` first, then divide by `-a`. A zero intercept is +/// exactly zero. `a ≥ 0` cannot hold a finite process-mean change and +/// fails closed. `-κ / a` is not `κ`, not the finite-interval +/// increment `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not +/// `asymTIPREDEFFECT` `-B z / a`. Page 16 notes that a `T0MEANS` +/// stationarity constraint includes time-independent predictors; that +/// composition is not this intercept-only map. The printed 2-latent +/// `CINT` values are not this scalar map. This is not a Kalman filter, +/// not a matrix `expm`, and not ctsem estimation. +/// +/// # Errors +/// +/// Returns [`PsychometricError::EventTimeRequired`] for any non-event +/// clock, [`PsychometricError::AsymptoticContinuousInterceptRequiresStableDrift`] +/// when the drift is not strictly negative and the intercept is +/// nonzero, and [`PsychometricError::InvalidNumericInput`] when an +/// input is non-finite or the quotient overflows. +pub fn recover_asymptotic_continuous_intercept( + continuous_intercept: f64, + log_rate: f64, + clock: LagClock, +) -> Result { + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + if !continuous_intercept.is_finite() || !log_rate.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + if continuous_intercept == 0.0 { + return Ok(0.0); + } + if log_rate >= 0.0 { + return Err(PsychometricError::AsymptoticContinuousInterceptRequiresStableDrift); + } + require_finite(continuous_intercept / -log_rate) +} + +/// Refuse treating Table 2 `asymCINT` as `CINT`. +/// +/// `-κ / a` is the expected change in process means. Table 2 names +/// `κ` `CINT`. The intercept is not that total change. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::AsymptoticContinuousInterceptIsNotContinuousIntercept`]. +pub fn refuse_asymptotic_continuous_intercept_as_continuous_intercept( + asymptotic_intercept: f64, + continuous_intercept: f64, +) -> Result { + let _ = (asymptotic_intercept, continuous_intercept); + Err(PsychometricError::AsymptoticContinuousInterceptIsNotContinuousIntercept) +} + +/// Refuse treating Table 2 `asymCINT` as the finite-interval discrete +/// intercept increment. +/// +/// `-κ / a` is the `Δt → ∞` limit of `A^{-1}[e^{A Δt} − I] κ` under +/// stable `a < 0`. A finite event interval is not that limit. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::AsymptoticContinuousInterceptIsNotDiscreteIncrement`]. +pub fn refuse_asymptotic_continuous_intercept_as_discrete_increment( + asymptotic_intercept: f64, + discrete_increment: f64, +) -> Result { + let _ = (asymptotic_intercept, discrete_increment); + Err(PsychometricError::AsymptoticContinuousInterceptIsNotDiscreteIncrement) +} + +/// Refuse treating Table 2 `asymCINT` as `T0MEANS`. +/// +/// `-κ / a` is the intercept contribution to the stationary process +/// mean. Table 2 names `μ_0` `T0MEANS`. Those are not the same map. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::AsymptoticContinuousInterceptIsNotInitialLatentMean`]. +pub fn refuse_asymptotic_continuous_intercept_as_initial_latent_mean( + asymptotic_intercept: f64, + initial_latent_mean: f64, +) -> Result { + let _ = (asymptotic_intercept, initial_latent_mean); + Err(PsychometricError::AsymptoticContinuousInterceptIsNotInitialLatentMean) +} + +/// Refuse treating Table 2 `asymCINT` as `asymTIPREDEFFECT`. +/// +/// `-κ / a` is the intercept contribution. `-B z / a` is the +/// time-independent predictor contribution. Page 16 notes that a +/// `T0MEANS` stationarity constraint includes time-independent +/// predictors; that composition is not this intercept-only map. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::AsymptoticContinuousInterceptIsNotAsymptoticTimeIndependentEffect`]. +pub fn refuse_asymptotic_continuous_intercept_as_asymptotic_time_independent_effect( + asymptotic_intercept: f64, + asymptotic_time_independent_effect: f64, +) -> Result { + let _ = (asymptotic_intercept, asymptotic_time_independent_effect); + Err(PsychometricError::AsymptoticContinuousInterceptIsNotAsymptoticTimeIndependentEffect) +} + /// Exact scalar observed mean of a time-independent predictor. /// /// Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Eq. 3, p. 5; Table 2, @@ -4260,7 +4387,7 @@ pub(crate) fn fit_scalar_log_rate(pairs: &[(f64, f64, f64)]) -> Result 1e-3); + assert!((recovered - discrete).abs() > 1e-3); + assert!((recovered - 2.823).abs() > 1e-3); + assert!((recovered - tipred).abs() > 1e-3); + assert_eq!( + refuse_asymptotic_continuous_intercept_as_continuous_intercept(recovered, intercept), + Err(PsychometricError::AsymptoticContinuousInterceptIsNotContinuousIntercept) + ); + assert_eq!( + refuse_asymptotic_continuous_intercept_as_discrete_increment(recovered, discrete), + Err(PsychometricError::AsymptoticContinuousInterceptIsNotDiscreteIncrement) + ); + assert_eq!( + refuse_asymptotic_continuous_intercept_as_initial_latent_mean(recovered, 2.823), + Err(PsychometricError::AsymptoticContinuousInterceptIsNotInitialLatentMean) + ); + assert_eq!( + refuse_asymptotic_continuous_intercept_as_asymptotic_time_independent_effect( + recovered, tipred + ), + Err( + PsychometricError::AsymptoticContinuousInterceptIsNotAsymptoticTimeIndependentEffect + ) + ); + } + + #[test] + fn asymptotic_continuous_intercept_invalid_inputs_fail_closed() { + let intercept = 0.3_f64; + let log_rate = -0.134_488_942_f64; + assert_eq!( + recover_asymptotic_continuous_intercept(intercept, log_rate, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_asymptotic_continuous_intercept(intercept, 0.0, LagClock::EventTime), + Err(PsychometricError::AsymptoticContinuousInterceptRequiresStableDrift) + ); + assert_eq!( + recover_asymptotic_continuous_intercept(intercept, 0.5, LagClock::EventTime), + Err(PsychometricError::AsymptoticContinuousInterceptRequiresStableDrift) + ); + assert_eq!( + recover_asymptotic_continuous_intercept(f64::NAN, log_rate, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_asymptotic_continuous_intercept(1e308, -1e-308, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + } + #[test] fn discrete_observed_mean_with_impulse_recovers_driver_equation_five() { let loading = 2.0_f64; diff --git a/crates/psychometric_core/src/lib.rs b/crates/psychometric_core/src/lib.rs index 9fd03336..8a951098 100644 --- a/crates/psychometric_core/src/lib.rs +++ b/crates/psychometric_core/src/lib.rs @@ -119,6 +119,12 @@ //! (pp. 20–21; stable between-subject variance accounted for by a //! time-independent predictor with variance `v`; not `TRAITVAR`, //! not `asymDIFFUSION`, and not `-B z / a`), +//! recovers the Driver Table 2 `asymCINT` as `-κ / a` +//! (p. 12; Eq. 3 as `Δt → ∞`; JSS PDF opened 2026-08-21T16:13Z; +//! expected change in process means for a unit intercept; `a < 0`; +//! not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not +//! `-B z / a`; p. 16 `T0MEANS` stationarity includes TI predictors; +//! that composition is not this intercept-only map), //! and refuses //! latent-mean comparison below strong invariance. @@ -167,6 +173,8 @@ pub use event_time::LagClock; 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`. +pub use event_time::recover_asymptotic_continuous_intercept; /// Exact scalar §7.2 `asymTIPREDEFFECT` `-B z / a`. pub use event_time::recover_asymptotic_time_independent_predictor_effect; /// Exact scalar §7.2 `addedTIPREDVAR` `(B / a)² v`. @@ -269,6 +277,14 @@ pub use event_time::recover_within_residual_event_time_log_rate; pub use event_time::refuse_after_extra_process_contribution_as_observed_mean; /// Refuse treating the evolved-plus-after-contribution latent mean as `E(y_t)`. pub use event_time::refuse_after_extra_process_latent_mean_as_observed_mean; +/// Refuse treating Table 2 `asymCINT` as `asymTIPREDEFFECT`. +pub use event_time::refuse_asymptotic_continuous_intercept_as_asymptotic_time_independent_effect; +/// Refuse treating Table 2 `asymCINT` as `CINT`. +pub use event_time::refuse_asymptotic_continuous_intercept_as_continuous_intercept; +/// Refuse treating Table 2 `asymCINT` as the finite-interval discrete increment. +pub use event_time::refuse_asymptotic_continuous_intercept_as_discrete_increment; +/// Refuse treating Table 2 `asymCINT` as `T0MEANS`. +pub use event_time::refuse_asymptotic_continuous_intercept_as_initial_latent_mean; /// Refuse treating §7.2 `asymTIPREDEFFECT` as `TIPREDEFFECT` `B`. pub use event_time::refuse_asymptotic_time_independent_effect_as_coefficient; /// Refuse treating §7.2 `asymTIPREDEFFECT` as `CINT`. 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 ee04df16..a92cbe7e 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -4,7 +4,8 @@ use psychometric_core::{ ClusteredEventScore, ClusteredScore, EventOccasion, IndicatorKind, LagClock, LaggedWithinResidual, PsychometricError, map_discrete_lag_across_event_intervals, - ordinary_least_squares_slope, recover_asymptotic_time_independent_predictor_effect, + 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, @@ -38,6 +39,10 @@ use psychometric_core::{ 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, + refuse_asymptotic_continuous_intercept_as_continuous_intercept, + refuse_asymptotic_continuous_intercept_as_discrete_increment, + refuse_asymptotic_continuous_intercept_as_initial_latent_mean, refuse_asymptotic_time_independent_effect_as_coefficient, refuse_asymptotic_time_independent_effect_as_continuous_intercept, refuse_asymptotic_time_independent_effect_as_discrete_effect, @@ -4374,3 +4379,81 @@ fn asymptotic_time_independent_variance_refuses_unstable_drift_and_non_event_clo Ok(0.0) ); } + +#[test] +fn asymptotic_continuous_intercept_recovers_driver_table_two() { + let printed_effect = -0.225_f64; + let printed_asym = -1.673_f64; + let log_rate = -printed_effect / printed_asym; + let intercept = 0.3_f64; + let recovered = + recover_asymptotic_continuous_intercept(intercept, log_rate, LagClock::EventTime) + .expect("asymCINT"); + let expected = intercept / -log_rate; + let error = rmse(&[expected], &[recovered]); + assert!( + error < 1e-12, + "Driver Table 2 asymCINT RMSE {error}: got {recovered}" + ); + let discrete = + recover_discrete_continuous_intercept_effect(intercept, log_rate, 1.0, LagClock::EventTime) + .expect("dtCINT"); + let tipred = recover_asymptotic_time_independent_predictor_effect( + printed_effect, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("asymTIPREDEFFECT"); + assert!( + rmse(&[recovered], &[intercept]) > error, + "CINT is not asymCINT" + ); + assert!(rmse(&[recovered], &[discrete]) > error); + assert!(rmse(&[recovered], &[2.823]) > error); + assert!(rmse(&[recovered], &[tipred]) > error); + assert_eq!( + refuse_asymptotic_continuous_intercept_as_continuous_intercept(recovered, intercept), + Err(PsychometricError::AsymptoticContinuousInterceptIsNotContinuousIntercept) + ); + assert_eq!( + refuse_asymptotic_continuous_intercept_as_discrete_increment(recovered, discrete), + Err(PsychometricError::AsymptoticContinuousInterceptIsNotDiscreteIncrement) + ); + assert_eq!( + refuse_asymptotic_continuous_intercept_as_initial_latent_mean(recovered, 2.823), + Err(PsychometricError::AsymptoticContinuousInterceptIsNotInitialLatentMean) + ); + assert_eq!( + refuse_asymptotic_continuous_intercept_as_asymptotic_time_independent_effect( + recovered, tipred + ), + Err(PsychometricError::AsymptoticContinuousInterceptIsNotAsymptoticTimeIndependentEffect) + ); +} + +#[test] +fn asymptotic_continuous_intercept_refuses_unstable_drift_and_non_event_clocks() { + let intercept = 0.3_f64; + let log_rate = -0.134_488_942_f64; + assert_eq!( + recover_asymptotic_continuous_intercept(intercept, log_rate, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_asymptotic_continuous_intercept(intercept, 0.0, LagClock::EventTime), + Err(PsychometricError::AsymptoticContinuousInterceptRequiresStableDrift) + ); + assert_eq!( + recover_asymptotic_continuous_intercept(intercept, 0.5, LagClock::EventTime), + Err(PsychometricError::AsymptoticContinuousInterceptRequiresStableDrift) + ); + assert_eq!( + recover_asymptotic_continuous_intercept(f64::NAN, log_rate, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_asymptotic_continuous_intercept(0.0, 0.0, LagClock::EventTime), + Ok(0.0) + ); +} diff --git a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs index 71473c83..89fbe4b6 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -3,7 +3,7 @@ use psychometric_core::{ ClusteredEventScore, ClusteredScore, IndicatorKind, LagClock, LaggedWithinResidual, ordinary_least_squares_slope, posterior_draw_point_estimate_mean, - recover_asymptotic_time_independent_predictor_effect, + 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_lagged_latent_covariance, @@ -34,6 +34,10 @@ use psychometric_core::{ 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, + refuse_asymptotic_continuous_intercept_as_continuous_intercept, + refuse_asymptotic_continuous_intercept_as_discrete_increment, + refuse_asymptotic_continuous_intercept_as_initial_latent_mean, refuse_asymptotic_time_independent_effect_as_coefficient, refuse_asymptotic_time_independent_effect_as_continuous_intercept, refuse_asymptotic_time_independent_effect_as_discrete_effect, @@ -2183,3 +2187,64 @@ fn asymptotic_time_independent_variance_is_not_trait_stationary_or_mean_effect() ) ); } + +#[test] +fn asymptotic_continuous_intercept_is_not_cint_increment_t0_or_tipred() { + let intercept = 0.3_f64; + let log_rate = -0.134_488_942_f64; + let recovered = + recover_asymptotic_continuous_intercept(intercept, log_rate, LagClock::EventTime) + .expect("asymCINT"); + let discrete = + recover_discrete_continuous_intercept_effect(intercept, log_rate, 1.0, LagClock::EventTime) + .expect("dtCINT"); + let tipred = recover_asymptotic_time_independent_predictor_effect( + -0.225, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("asymTIPREDEFFECT"); + assert!( + (recovered - intercept).abs() > 1e-3, + "Driver et al. (2017, Table 2, p. 12): asymCINT is not CINT" + ); + assert!( + (recovered - discrete).abs() > 1e-3, + "Driver et al. (2017, Table 2): -κ / a is not A^{{-1}}[e^{{A Δt}} − I] κ" + ); + assert!( + (recovered - 2.823).abs() > 1e-3, + "Driver et al. (2017, Table 2): -κ / a is not T0MEANS" + ); + assert!( + (recovered - tipred).abs() > 1e-3, + "Driver et al. (2017, Table 2): -κ / a is not -B z / a" + ); + assert_eq!( + refuse_asymptotic_continuous_intercept_as_continuous_intercept(recovered, intercept), + Err( + psychometric_core::PsychometricError::AsymptoticContinuousInterceptIsNotContinuousIntercept + ) + ); + assert_eq!( + refuse_asymptotic_continuous_intercept_as_discrete_increment(recovered, discrete), + Err( + psychometric_core::PsychometricError::AsymptoticContinuousInterceptIsNotDiscreteIncrement + ) + ); + assert_eq!( + refuse_asymptotic_continuous_intercept_as_initial_latent_mean(recovered, 2.823), + Err( + psychometric_core::PsychometricError::AsymptoticContinuousInterceptIsNotInitialLatentMean + ) + ); + assert_eq!( + refuse_asymptotic_continuous_intercept_as_asymptotic_time_independent_effect( + recovered, tipred + ), + Err( + psychometric_core::PsychometricError::AsymptoticContinuousInterceptIsNotAsymptoticTimeIndependentEffect + ) + ); +} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 87a482b6..56a2245d 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -28,7 +28,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | report template/section/copied/style/modality method effects | ADR 0004/0012; PRD/TRD | simulation truth factors implemented; estimator-side method model remains future | partial | | candidate K statistical/Pareto gates + blinded LLM review | ADR 0012; research | future `model_selection` | accepted-target | | compositional topic correlation / stable clustering | ADR 0005/0012; research | future `network_analysis` | accepted-target | -| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; §7.2 level-change `CINT` is `κ = −a m x` (`a < 0`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`; Eq. 3 of that setting is `(1 − e^{a Δt}) m x`); §7.2 extra-process contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, not the dissipating Dirac; `ε ≥ 0` fails closed; Eq. 5 of that contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; extra `LAMBDA` is 0; `τ + λ μ_t` is not that observed mean; after-t0 extra-process `TDPREDEFFECT` uses `t − u` with `t0 < u < t` while `μ_t` uses `Δt`; that after-t0 observed mean is not the first-occasion extra-process observed mean; §7.2 `asymTIPREDEFFECT` is `-B z / a` for `a < 0` and is not `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; §7.2 `addedTIPREDVAR` is `(B / a)² v` and is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z); full ESEM/DSEM remaining | partial | +| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; §7.2 level-change `CINT` is `κ = −a m x` (`a < 0`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`; Eq. 3 of that setting is `(1 − e^{a Δt}) m x`); §7.2 extra-process contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, not the dissipating Dirac; `ε ≥ 0` fails closed; Eq. 5 of that contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; extra `LAMBDA` is 0; `τ + λ μ_t` is not that observed mean; after-t0 extra-process `TDPREDEFFECT` uses `t − u` with `t0 < u < t` while `μ_t` uses `Δt`; that after-t0 observed mean is not the first-occasion extra-process observed mean; §7.2 `asymTIPREDEFFECT` is `-B z / a` for `a < 0` and is not `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; §7.2 `addedTIPREDVAR` is `(B / a)² v` and is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`; Table 2 `asymCINT` is `-κ / a` for `a < 0` and is not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z); full ESEM/DSEM remaining | partial | | CPU bounded multithreading + GPU/VRAM streaming/parity | ADR 0001/0006 | future `compute_backend` | accepted-target | | TDT detection/tracking vs CHRONOS schema/prediction/temporal consistency | ADR 0016; PRD/research | future `event_intelligence` | accepted-target | | evidence-bounded LLM interpretation | ADR 0010/0012; PRD | `tepp_api` router plus future `interpretation_gateway` | partial | diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index da0fd67d..1452fe96 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -1,7 +1,7 @@ # ADR 0005 — Posterior-aware ESEM/DSEM and structural interpretation **Decision status:** Accepted -**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), exact scalar contemporaneous `TDPREDEFFECT` impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; the §7.2 level-change form is not that impulse), exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of that Eq. 3 composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar time-independent `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle Eq. 14; `B` is not that discrete increment), exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of that Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of that carried latent mean; `τ + λ μ_t` is not that observed mean), exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; `T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`), exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; `T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand TD composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean), exact scalar §7.2 level-change `CINT` `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; `a < 0` so `−κ / a = m x`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2), exact scalar Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` (not `m x`, not `κ`, and not `TIPREDEFFECT`), exact scalar §7.2 extra near-zero-drift latent process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z; identification `TDPREDEFFECT` on the extra process is 1; extra `DRIFT` printed as `−0.000001`; precisely 0 causes computational problems; `ε = a` is `a_{ηξ} x Δt e^{a Δt}`; not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`; `ε ≥ 0` fails closed), exact scalar observed mean of that extra-process contribution `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z; the extra process has `LAMBDA` 0 and is not an observed indicator; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`), exact scalar after-t0 extra-process contribution `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` and Eq. 5 observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:32Z; `T0TDPREDEFFECT` uses `Δt` for both the evolution and the extra drive; `TDPREDEFFECT` after `t0` uses `t − u`; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive), exact scalar §7.2 `asymTIPREDEFFECT` `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z; expected total change in process means given a time-independent predictor; `a < 0`; not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; `a ≥ 0` fails closed), exact scalar §7.2 `addedTIPREDVAR` `(B / a)² v` (Driver et al., 2017, §7.2, pp. 20–21; stable between-subject variance accounted for by a time-independent predictor; not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target +**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), exact scalar contemporaneous `TDPREDEFFECT` impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; the §7.2 level-change form is not that impulse), exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of that Eq. 3 composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar time-independent `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle Eq. 14; `B` is not that discrete increment), exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of that Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of that carried latent mean; `τ + λ μ_t` is not that observed mean), exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; `T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`), exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; `T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand TD composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean), exact scalar §7.2 level-change `CINT` `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; `a < 0` so `−κ / a = m x`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2), exact scalar Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` (not `m x`, not `κ`, and not `TIPREDEFFECT`), exact scalar §7.2 extra near-zero-drift latent process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z; identification `TDPREDEFFECT` on the extra process is 1; extra `DRIFT` printed as `−0.000001`; precisely 0 causes computational problems; `ε = a` is `a_{ηξ} x Δt e^{a Δt}`; not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`; `ε ≥ 0` fails closed), exact scalar observed mean of that extra-process contribution `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z; the extra process has `LAMBDA` 0 and is not an observed indicator; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`), exact scalar after-t0 extra-process contribution `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` and Eq. 5 observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:32Z; `T0TDPREDEFFECT` uses `Δt` for both the evolution and the extra drive; `TDPREDEFFECT` after `t0` uses `t − u`; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive), exact scalar §7.2 `asymTIPREDEFFECT` `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z; expected total change in process means given a time-independent predictor; `a < 0`; not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; `a ≥ 0` fails closed), exact scalar §7.2 `addedTIPREDVAR` `(B / a)² v` (Driver et al., 2017, §7.2, pp. 20–21; stable between-subject variance accounted for by a time-independent predictor; not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`), exact scalar Table 2 `asymCINT` `-κ / a` (Driver et al., 2017, Table 2, p. 12; Eq. 3 as `Δt → ∞`; `a < 0`; not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`; `a ≥ 0` fails closed), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target **Date:** 2026-08-05 **Supersedes:** None. ADR 0012 governs upstream topic measurement/network coordinates; this ADR governs higher-order psychometric structure and longitudinal interpretation. @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), recovers the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z; `TDPREDEFFECT` is `M`, not `CINT`; `M x` is not `A^{-1}[e^{A Δt} − I] B z`; `M x` is not Voelkle et al., 2012, Eq. 14; the level-change form is not that impulse), recovers the exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition; JSS PDF re-opened 2026-08-20T09:01Z; the evolved map `τ + λ μ_t` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-impulse latent mean is not `E(y_t)`), recovers the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z; `TIPREDEFFECT` is `B`, not `κ`; form `B z` first, then the discrete intercept map; a zero drift is `B z Δt`; `B` is not the discrete increment; `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle Eq. 14), recovers the exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; JSS PDF re-opened 2026-08-20T12:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-increment latent mean is not `E(y_t)`), recovers the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z; form `m x` first, then `e^{a(t−u)} m x`; a zero drift is `m x`; underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept; `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; an impulse at `u = t` is the contemporaneous map; an impulse at `u ≤ t0` is already in `η(t0)`), recovers the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; JSS PDF re-opened 2026-08-20T05:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the carried latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and its Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF opened 2026-08-20T15:14Z; form `t0_b z` first, then `e^{a Δt} t0_b z`; a zero drift is `t0_b z`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; `e^{A Δt} t0_b z` is not `t0_b z`; `T0TIPREDEFFECT` is the coefficient, not the shift), recovers the exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition; JSS PDF re-opened 2026-08-20T15:28Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and its Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; form `t0_m x0` first, then `e^{a Δt} t0_m x0`; a zero drift is `t0_m x0`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `T0TDPREDEFFECT` is the coefficient, not the shift; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), recovers the exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; the first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar §7.2 level-change `CINT` setting `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z; form `m x` first, then multiply by `−a`; `a < 0` so `−κ / a = m x`; `a ≥ 0` cannot hold a new process mean; `−a m x` is not the dissipating Dirac, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`; the extra near-zero-drift latent process also named in §7.2 is a different specification), recovers the exact scalar Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` (JSS PDF re-opened 2026-08-20T19:50Z; form the level-change `CINT` first, then the discrete intercept map; underflow of `e^{a Δt}` to `+0` keeps `m x`; `(1 − e^{a Δt}) m x` is not `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`), recovers the exact scalar §7.2 extra near-zero-drift latent process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z; form `a_{ηξ} x` first; `ε = a` is `a_{ηξ} x Δt e^{a Δt}`; a zero coupling or zero predictor is exactly zero; `ε ≥ 0` cannot hold a lasting extra state; that contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`), recovers the exact scalar observed mean of that extra-process contribution `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z; the extra process has `LAMBDA` 0 and is not an observed indicator; form the evolved-plus-contribution latent mean first, then `τ + λ` of that mean; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`), recovers the exact scalar after-t0 extra-process contribution `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` and its Eq. 5 observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:32Z; `T0TDPREDEFFECT` uses `Δt` for both the evolution and the extra drive; `TDPREDEFFECT` after `t0` uses `t − u` while `μ_t` still uses `Δt`; an impulse at `u = t0` or `u = t` is not interior; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive), recovers the exact scalar §7.2 `asymTIPREDEFFECT` `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z; form `B z` first, then divide by `-a`; `a < 0`; a zero coefficient or zero predictor is exactly zero; `a ≥ 0` cannot hold a finite process-mean change; `-B z / a` is not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`), recovers the exact scalar §7.2 `addedTIPREDVAR` `(B / a)² v` (Driver et al., 2017, §7.2, pp. 20–21; form the unit asymptotic effect first, then square, then multiply by `v`; `v ≥ 0`; a zero coefficient or zero predictor variance is exactly zero; `(B / a)² v` is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), recovers the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z; `TDPREDEFFECT` is `M`, not `CINT`; `M x` is not `A^{-1}[e^{A Δt} − I] B z`; `M x` is not Voelkle et al., 2012, Eq. 14; the level-change form is not that impulse), recovers the exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition; JSS PDF re-opened 2026-08-20T09:01Z; the evolved map `τ + λ μ_t` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-impulse latent mean is not `E(y_t)`), recovers the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z; `TIPREDEFFECT` is `B`, not `κ`; form `B z` first, then the discrete intercept map; a zero drift is `B z Δt`; `B` is not the discrete increment; `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle Eq. 14), recovers the exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; JSS PDF re-opened 2026-08-20T12:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-increment latent mean is not `E(y_t)`), recovers the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z; form `m x` first, then `e^{a(t−u)} m x`; a zero drift is `m x`; underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept; `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; an impulse at `u = t` is the contemporaneous map; an impulse at `u ≤ t0` is already in `η(t0)`), recovers the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; JSS PDF re-opened 2026-08-20T05:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the carried latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and its Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF opened 2026-08-20T15:14Z; form `t0_b z` first, then `e^{a Δt} t0_b z`; a zero drift is `t0_b z`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; `e^{A Δt} t0_b z` is not `t0_b z`; `T0TIPREDEFFECT` is the coefficient, not the shift), recovers the exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition; JSS PDF re-opened 2026-08-20T15:28Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and its Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; form `t0_m x0` first, then `e^{a Δt} t0_m x0`; a zero drift is `t0_m x0`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `T0TDPREDEFFECT` is the coefficient, not the shift; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), recovers the exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; the first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar §7.2 level-change `CINT` setting `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z; form `m x` first, then multiply by `−a`; `a < 0` so `−κ / a = m x`; `a ≥ 0` cannot hold a new process mean; `−a m x` is not the dissipating Dirac, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`; the extra near-zero-drift latent process also named in §7.2 is a different specification), recovers the exact scalar Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` (JSS PDF re-opened 2026-08-20T19:50Z; form the level-change `CINT` first, then the discrete intercept map; underflow of `e^{a Δt}` to `+0` keeps `m x`; `(1 − e^{a Δt}) m x` is not `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`), recovers the exact scalar §7.2 extra near-zero-drift latent process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z; form `a_{ηξ} x` first; `ε = a` is `a_{ηξ} x Δt e^{a Δt}`; a zero coupling or zero predictor is exactly zero; `ε ≥ 0` cannot hold a lasting extra state; that contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`), recovers the exact scalar observed mean of that extra-process contribution `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z; the extra process has `LAMBDA` 0 and is not an observed indicator; form the evolved-plus-contribution latent mean first, then `τ + λ` of that mean; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`), recovers the exact scalar after-t0 extra-process contribution `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` and its Eq. 5 observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:32Z; `T0TDPREDEFFECT` uses `Δt` for both the evolution and the extra drive; `TDPREDEFFECT` after `t0` uses `t − u` while `μ_t` still uses `Δt`; an impulse at `u = t0` or `u = t` is not interior; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive), recovers the exact scalar §7.2 `asymTIPREDEFFECT` `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z; form `B z` first, then divide by `-a`; `a < 0`; a zero coefficient or zero predictor is exactly zero; `a ≥ 0` cannot hold a finite process-mean change; `-B z / a` is not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`), recovers the exact scalar §7.2 `addedTIPREDVAR` `(B / a)² v` (Driver et al., 2017, §7.2, pp. 20–21; form the unit asymptotic effect first, then square, then multiply by `v`; `v ≥ 0`; a zero coefficient or zero predictor variance is exactly zero; `(B / a)² v` is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`), recovers the exact scalar Table 2 `asymCINT` `-κ / a` (Driver et al., 2017, Table 2, p. 12; Eq. 3 as `Δt → ∞`; JSS PDF opened 2026-08-21T16:13Z; form `κ` first, then divide by `-a`; `a < 0`; a zero intercept is exactly zero; `-κ / a` is not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. 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/adr/README.md b/docs/adr/README.md index a4cd88a6..8fd7c2b6 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -10,7 +10,7 @@ Read [`ADR_POLICY.md`](ADR_POLICY.md) first. **Decision status and implementatio | [0002](0002-six-clock-temporal-semantics.md) | Six-clock temporal semantics and fail-closed historical leakage prevention | Accepted | active-PR | Unmerged PR #8 is the canonical Task 3 replacement implementing typed clocks/intervals against the current protected-main lineage; conflicted PR #5 is superseded lineage. Later graph/split enforcement remains target work. | | [0003](0003-relational-event-multiple-membership.md) | Relational event ontology and time-varying cross-classified multiple membership | Accepted | partial | Weighted time-varying membership network/roles are active-PR (PR #12); full multilevel estimators, graph ontology, and persistence remain accepted-target. ADR 0016 owns event-intelligence tasks. | | [0004](0004-shared-multilingual-latent-space.md) | One shared multilingual latent space with explicit invariance status | Accepted | accepted-target | ADR 0012 owns the full topic-estimator/backend/global-topic contract. | -| [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | partial | Input gates, posterior-draw point estimates, Rubin `T` on OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects, exact scalar discrete process noise, lagged latent covariance and unconditional latent variance, stationary within-subject variance (`asymDIFFUSION`), trait-plus-state variance (`TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; Eq. 5 of the contemporaneous impulse is `τ + λ(μ_t + m x)`; `τ + λ μ_t` is not that observed mean; Eq. 5 of the time-independent predictor is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; Eq. 5 of the within-interval impulse carry is `τ + λ(μ_t + e^{a(t−u)} m x)`; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`), irregular already-centered residual lag, and strong-gated latent means are on the stacked psychometric PR; full ESEM/DSEM estimator remains accepted-target. | +| [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | partial | Input gates, posterior-draw point estimates, Rubin `T` on OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects, exact scalar discrete process noise, lagged latent covariance and unconditional latent variance, stationary within-subject variance (`asymDIFFUSION`), trait-plus-state variance (`TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; Eq. 5 of the contemporaneous impulse is `τ + λ(μ_t + m x)`; `τ + λ μ_t` is not that observed mean; Eq. 5 of the time-independent predictor is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; Eq. 5 of the within-interval impulse carry is `τ + λ(μ_t + e^{a(t−u)} m x)`; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; Table 2 `asymCINT` is `-κ / a` for `a < 0` and is not `κ`, not the finite-interval increment, not `T0MEANS`, and not `-B z / a`; §7.2 `addedTIPREDVAR` is `(B / a)² v` and is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`), irregular already-centered residual lag, and strong-gated latent means are on the stacked psychometric PR; full ESEM/DSEM estimator remains accepted-target. | | [0006](0006-vram-gpu-nvidia-orchestration.md) | VRAM-adaptive GPU compute and model-credential boundary | Accepted | accepted-target | LLM orchestration policy superseded by ADR 0010; autonomous development authority governed by ADR 0015. | | [0007](0007-rust-workspace-quality-gates.md) | Explicit Rust workspace, pinned toolchains, and exact quality gates | Accepted | implemented-main | ADR 0014 governs scientific/product claim promotion beyond repository-quality tooling. | | [0008](0008-immutable-evidence-identities-digests-and-spans.md) | Immutable evidence identities, `SHA-256` digests, exact spans, and strict wire reconstruction | Accepted | implemented-main | ADR 0013 governs future persistence/reproducibility/split authority. | diff --git a/docs/research/multilevel-event-time-recovery.md b/docs/research/multilevel-event-time-recovery.md index 4c4de8a8..f7320eeb 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -39,15 +39,16 @@ This slice stays inside `psychometric_core`. It does not add a second invariance 33. recover the exact scalar after-t0 extra-process contribution `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` and its Eq. 5 observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:32Z; `T0TDPREDEFFECT` uses `Δt = t − t0` for both the evolution and the extra drive; `TDPREDEFFECT` after `t0` uses `t − u` for the extra drive while `μ_t` still uses `Δt`; an impulse at `u = t0` or `u = t` is not interior; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive) and refuse treating the first-occasion extra-process observed mean, `τ + λ μ_t`, the impulse-carry observed mean, the after-t0 contribution, or the evolved-plus-after-contribution latent mean as that `E(y_t)`; 34. recover the exact scalar §7.2 `asymTIPREDEFFECT` `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; Eq. 3, p. 5; Table 2, p. 12; JSS PDF opened 2026-08-21T13:08Z; expected total change in process means given a time-independent predictor; form `B z` first, then divide by `-a`; `a < 0`; a zero coefficient or zero predictor is exactly zero) and refuse treating `-B z / a` as the coefficient `B`, as `A^{-1}[e^{A Δt} − I] B z`, as `CINT`, or as `M x`; `a ≥ 0` cannot hold a finite process-mean change; 35. recover the exact scalar §7.2 `addedTIPREDVAR` `(B / a)² v` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z; stable between-subject variance accounted for by a time-independent predictor; form the unit asymptotic effect first, then square, then multiply by `v`; `v ≥ 0`; a zero coefficient or zero predictor variance is exactly zero) and refuse treating `(B / a)² v` as `TRAITVAR`, as `asymDIFFUSION`, or as `-B z / a`; -36. refuse pooling discrete lags from unequal event intervals as one coefficient; -37. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); -38. refuse the difference quotient as a continuous-time rate; -39. apply the same event-time map to CWC residuals (still not DSEM); -40. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). +36. recover the exact scalar Table 2 `asymCINT` `-κ / a` (Driver et al., 2017, Table 2, p. 12; Eq. 3, p. 5; §4.3 / p. 16; JSS PDF opened 2026-08-21T16:13Z; expected change in process means for a unit intercept; form `κ` first, then divide by `-a`; `a < 0`; a zero intercept is exactly zero) and refuse treating `-κ / a` as `κ`, as `A^{-1}[e^{A Δt} − I] κ`, as `T0MEANS`, or as `-B z / a`; `a ≥ 0` cannot hold a finite process-mean change; p. 16 `T0MEANS` stationarity includes TI predictors and is not this intercept-only map; +37. refuse pooling discrete lags from unequal event intervals as one coefficient; +38. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); +39. refuse the difference quotient as a continuous-time rate; +40. apply the same event-time map to CWC residuals (still not DSEM); +41. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). ## Claim boundary -This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. The contemporaneous `TDPREDEFFECT` impulse is not the continuous intercept, not the time-independent discrete effect, and not Voelkle et al. (2012, Eq. 14). The time-independent `TIPREDEFFECT` increment is not the continuous intercept, not the contemporaneous impulse, not Voelkle et al. (2012, Eq. 14), and not the coefficient `B`. The within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). The evolved observed mean `τ + λ μ_t` is not the contemporaneous-impulse observed mean `τ + λ(μ_t + m x)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not the impulse-carry observed mean `τ + λ(μ_t + e^{a(t−u)} m x)` when `u ≠ t`. The evolved observed mean `τ + λ μ_t` is not the impulse-carry observed mean. The carried latent mean is not `E(y_t)`. The evolved-plus-impulse latent mean is not `E(y_t)`. The evolved observed mean `τ + λ μ_t` is not the time-independent-predictor observed mean `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not that time-independent-predictor observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that time-independent-predictor observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TIPREDEFFECT` shift `t0_b z` is not the Eq. 3 process increment `A^{-1}[e^{A Δt} − I] B z`, not `κ`, and not `M x`. The Eq. 3 first-summand carry `e^{A Δt} t0_b z` is not `t0_b z` and is not that process increment. `T0TIPREDEFFECT` is the coefficient, not the first-occasion shift. The evolved observed mean `τ + λ μ_t` is not the first-occasion TI-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_b z)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion observed mean when `u ≠ t0`. The evolved-plus-T0TIPRED latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TDPREDEFFECT` shift `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `κ`. The Eq. 3 first-summand carry `e^{A Δt} t0_m x0` is not `t0_m x0` and is not that within-interval impulse carry. `T0TDPREDEFFECT` is the coefficient, not the first-occasion shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The evolved observed mean `τ + λ μ_t` is not the first-occasion TD-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_m x0)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion TD observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion TD observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion TD observed mean when `u ≠ t0`. The first-occasion TI composition `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that first-occasion TD observed mean. Same numbers as `T0TIPREDEFFECT` yield the same product; Table 3 names a different matrix. The evolved-plus-T0TDPRED latent mean is not `E(y_t)`. The §7.2 level-change `CINT` `κ = −a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. Lasting level change via that `CINT` setting requires `a < 0`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not that `CINT` setting. The Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` is not the dissipating Dirac `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`. Underflow of `e^{a Δt}` to `+0` keeps the equilibrium offset `m x`. The §7.2 extra-process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. Lasting level change via that extra process requires `ε < 0`. Precisely `ε = 0` causes computational problems in the printed specification. The extra-process observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` is not `τ + λ μ_t`, not `τ + λ(μ_t + m x)`, not the contribution, and not the evolved-plus-contribution latent mean. The extra process has `LAMBDA` 0 and is not an observed indicator. `T0TDPREDEFFECT` on the extra process uses `Δt = t − t0` for both the original-process evolution and the extra drive. `TDPREDEFFECT` after `t0` uses `t − u` with `t0 < u < t` for the extra drive while `μ_t` still uses `Δt`. The after-t0 extra-process observed mean is not the first-occasion extra-process observed mean when `u ≠ t0`. The impulse-carry `e^{a(t−u)} m x` is a Dirac on the original process and is not that `DRIFT` drive. The §7.2 `asymTIPREDEFFECT` `-B z / a` is the expected total change in process means given a time-independent predictor. It is not the coefficient `B`, not the finite-interval increment `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. Lasting asymptotic change via that map requires `a < 0`. The §7.2 `addedTIPREDVAR` `(B / a)² v` is the stable between-subject variance accounted for by that predictor. It is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`. +This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. The contemporaneous `TDPREDEFFECT` impulse is not the continuous intercept, not the time-independent discrete effect, and not Voelkle et al. (2012, Eq. 14). The time-independent `TIPREDEFFECT` increment is not the continuous intercept, not the contemporaneous impulse, not Voelkle et al. (2012, Eq. 14), and not the coefficient `B`. The within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). The evolved observed mean `τ + λ μ_t` is not the contemporaneous-impulse observed mean `τ + λ(μ_t + m x)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not the impulse-carry observed mean `τ + λ(μ_t + e^{a(t−u)} m x)` when `u ≠ t`. The evolved observed mean `τ + λ μ_t` is not the impulse-carry observed mean. The carried latent mean is not `E(y_t)`. The evolved-plus-impulse latent mean is not `E(y_t)`. The evolved observed mean `τ + λ μ_t` is not the time-independent-predictor observed mean `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not that time-independent-predictor observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that time-independent-predictor observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TIPREDEFFECT` shift `t0_b z` is not the Eq. 3 process increment `A^{-1}[e^{A Δt} − I] B z`, not `κ`, and not `M x`. The Eq. 3 first-summand carry `e^{A Δt} t0_b z` is not `t0_b z` and is not that process increment. `T0TIPREDEFFECT` is the coefficient, not the first-occasion shift. The evolved observed mean `τ + λ μ_t` is not the first-occasion TI-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_b z)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion observed mean when `u ≠ t0`. The evolved-plus-T0TIPRED latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TDPREDEFFECT` shift `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `κ`. The Eq. 3 first-summand carry `e^{A Δt} t0_m x0` is not `t0_m x0` and is not that within-interval impulse carry. `T0TDPREDEFFECT` is the coefficient, not the first-occasion shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The evolved observed mean `τ + λ μ_t` is not the first-occasion TD-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_m x0)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion TD observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion TD observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion TD observed mean when `u ≠ t0`. The first-occasion TI composition `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that first-occasion TD observed mean. Same numbers as `T0TIPREDEFFECT` yield the same product; Table 3 names a different matrix. The evolved-plus-T0TDPRED latent mean is not `E(y_t)`. The §7.2 level-change `CINT` `κ = −a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. Lasting level change via that `CINT` setting requires `a < 0`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not that `CINT` setting. The Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` is not the dissipating Dirac `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`. Underflow of `e^{a Δt}` to `+0` keeps the equilibrium offset `m x`. The §7.2 extra-process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. Lasting level change via that extra process requires `ε < 0`. Precisely `ε = 0` causes computational problems in the printed specification. The extra-process observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` is not `τ + λ μ_t`, not `τ + λ(μ_t + m x)`, not the contribution, and not the evolved-plus-contribution latent mean. The extra process has `LAMBDA` 0 and is not an observed indicator. `T0TDPREDEFFECT` on the extra process uses `Δt = t − t0` for both the original-process evolution and the extra drive. `TDPREDEFFECT` after `t0` uses `t − u` with `t0 < u < t` for the extra drive while `μ_t` still uses `Δt`. The after-t0 extra-process observed mean is not the first-occasion extra-process observed mean when `u ≠ t0`. The impulse-carry `e^{a(t−u)} m x` is a Dirac on the original process and is not that `DRIFT` drive. The §7.2 `asymTIPREDEFFECT` `-B z / a` is the expected total change in process means given a time-independent predictor. It is not the coefficient `B`, not the finite-interval increment `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. Lasting asymptotic change via that map requires `a < 0`. The §7.2 `addedTIPREDVAR` `(B / a)² v` is the stable between-subject variance accounted for by that predictor. It is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`. Table 2 `asymCINT` `-κ / a` is the intercept contribution to the stationary process mean. It is not `κ`, not the finite-interval increment `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`. Lasting asymptotic intercept change via that map requires `a < 0`. Page 16 notes that a `T0MEANS` stationarity constraint includes time-independent predictors; that composition is not this intercept-only map. ## Authoritative sources @@ -65,7 +66,7 @@ Kish, L. (1965). *Survey sampling*. John Wiley & Sons. Oud, J. H. L., & Jansen, R. A. R. G. (2000). Continuous time state space modeling of panel data by means of SEM. *Psychometrika, 65*(2), 199–215. https://doi.org/10.1007/BF02294374 (cited by Voelkle et al., 2012, Eq. 14 discussion; PDF not opened). -The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:07Z (https://www.zora.uzh.ch/handle/20.500.14742/72792; bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`). Page 16 writes discrete auto-effects as \(A^{*}(\Delta t)=\exp(A\Delta t)\) (Eq. 7). Introducing Intercepts (manuscript p. 20, Eq. 12) adds a continuous-time intercept \(b\) and writes the expected-value solution whose discrete increment is \(A^{-1}(\exp(A\Delta t)-I)b\); the scalar constant-predictor effect is \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). The next paragraph (manuscript p. 21, Eq. 14) writes the discrete effect of a **time-varying** predictor, when the sampling interval equals the constancy interval, as \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product does not depend on the predictor auto-effect. The manuscript calls Eq. 14 a first-order approximation that deteriorates as \(\Delta t\) grows, and defers unmatched sampling/constancy intervals to Oud and Jansen (2000), which is unread. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4; JSS PDF opened 2026-08-21T13:08Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\), the discrete intercept \(b_{\Delta t}=A^{-1}[A_{\Delta t}-I]b\), and the discrete process-noise covariance \(Q_{\Delta t}=\int_{0}^{\Delta t}\operatorname{expm}(A(\Delta t-\tau))LGG^{\top}L^{\top}\operatorname{expm}(A(\Delta t-\tau))^{\top}\,d\tau\). Equation 4 (p. 5) writes that the integral exhibits covariance \(Q_{\Delta t}=\operatorname{irow}(A^{\#^{-1}}[e^{A^{\#}\Delta t}-I]\operatorname{row}(Q))\) with \(A^{\#}=A\otimes I+I\otimes A\). The homogeneous-process consequence (`ξ`, `z` given) is \(Q_{\Delta t}=\operatorname{cov}(\eta_{ti}\mid\eta_{t-1,i})\) and \(\operatorname{cov}(\eta_{ti},\eta_{t-1,i})=A_{\Delta t}\operatorname{cov}(\eta_{t-1,i})\). The law of total variance on that pair is \(\operatorname{Var}(\eta_{ti})=A_{\Delta t}\operatorname{Var}(\eta_{t-1,i})A_{\Delta t}^{\top}+Q_{\Delta t}\). As \(\Delta t\to\infty\) with stable \(a<0\), Eq. 4 becomes \(-q/(2a)\). The JSS summary names that limit `asymDIFFUSION` and takes it as the total within-subject variance (p. 16). Section 4.3 (pp. 9–10) constrains a stationary `T0VAR` to that model-predicted variance and distinguishes it from a predetermined first occasion. The same section (p. 9) adds a stable trait process with `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is that time-invariant between-subject variance. Table 3 (p. 13) names `T0TIPREDEFFECT` the effect of time-independent predictors on latents at `T0` and names `TIPREDEFFECT` `B` separately. The JSS article has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). This slice takes scalar \(L=1\) (every latent subject to system noise); it does not implement the 0/1 selector matrix and is not a Kalman filter. Discrete auto-effects are strictly positive for finite real drift and interval; the inverse \(a=\ln\varphi/\Delta t\) therefore requires \(\varphi>0\). Binary64 `exp` of a large negative argument is `+0` and is refused as a discrete lag; the same underflow is a vanishing lagged covariance and is kept. Equations 3–4 of Voelkle et al. (2012) are the discouraged difference-quotient approximation. Meredith (1993) remains unread (Unpaywall/OpenAlex 2026-08-21T06:24Z: closed; Springer `content/pdf` is HTML 200, not a PDF; archive.org title search empty). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex 2026-08-21T06:24Z: closed; Springer `content/pdf` is HTML 200; ETS landing page is HTML). ERIC ED334221 is Singer and Willett (1991), *From whether to when*, not the 1991 journal article. ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title is not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex 2026-08-18T21:07Z: closed; Radboud landing and bitstream 403). +The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:07Z (https://www.zora.uzh.ch/handle/20.500.14742/72792; bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`). Page 16 writes discrete auto-effects as \(A^{*}(\Delta t)=\exp(A\Delta t)\) (Eq. 7). Introducing Intercepts (manuscript p. 20, Eq. 12) adds a continuous-time intercept \(b\) and writes the expected-value solution whose discrete increment is \(A^{-1}(\exp(A\Delta t)-I)b\); the scalar constant-predictor effect is \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). The next paragraph (manuscript p. 21, Eq. 14) writes the discrete effect of a **time-varying** predictor, when the sampling interval equals the constancy interval, as \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product does not depend on the predictor auto-effect. The manuscript calls Eq. 14 a first-order approximation that deteriorates as \(\Delta t\) grows, and defers unmatched sampling/constancy intervals to Oud and Jansen (2000), which is unread. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4; JSS PDF opened 2026-08-21T13:08Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\), the discrete intercept \(b_{\Delta t}=A^{-1}[A_{\Delta t}-I]b\), and the discrete process-noise covariance \(Q_{\Delta t}=\int_{0}^{\Delta t}\operatorname{expm}(A(\Delta t-\tau))LGG^{\top}L^{\top}\operatorname{expm}(A(\Delta t-\tau))^{\top}\,d\tau\). Equation 4 (p. 5) writes that the integral exhibits covariance \(Q_{\Delta t}=\operatorname{irow}(A^{\#^{-1}}[e^{A^{\#}\Delta t}-I]\operatorname{row}(Q))\) with \(A^{\#}=A\otimes I+I\otimes A\). The homogeneous-process consequence (`ξ`, `z` given) is \(Q_{\Delta t}=\operatorname{cov}(\eta_{ti}\mid\eta_{t-1,i})\) and \(\operatorname{cov}(\eta_{ti},\eta_{t-1,i})=A_{\Delta t}\operatorname{cov}(\eta_{t-1,i})\). The law of total variance on that pair is \(\operatorname{Var}(\eta_{ti})=A_{\Delta t}\operatorname{Var}(\eta_{t-1,i})A_{\Delta t}^{\top}+Q_{\Delta t}\). As \(\Delta t\to\infty\) with stable \(a<0\), Eq. 4 becomes \(-q/(2a)\). The JSS summary names that limit `asymDIFFUSION` and takes it as the total within-subject variance (p. 16). Section 4.3 (pp. 9–10) constrains a stationary `T0VAR` to that model-predicted variance and distinguishes it from a predetermined first occasion. The same section (p. 9) adds a stable trait process with `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is that time-invariant between-subject variance. Table 3 (p. 13) names `T0TIPREDEFFECT` the effect of time-independent predictors on latents at `T0` and names `TIPREDEFFECT` `B` separately. The JSS article has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). This slice takes scalar \(L=1\) (every latent subject to system noise); it does not implement the 0/1 selector matrix and is not a Kalman filter. Discrete auto-effects are strictly positive for finite real drift and interval; the inverse \(a=\ln\varphi/\Delta t\) therefore requires \(\varphi>0\). Binary64 `exp` of a large negative argument is `+0` and is refused as a discrete lag; the same underflow is a vanishing lagged covariance and is kept. Equations 3–4 of Voelkle et al. (2012) are the discouraged difference-quotient approximation. Meredith (1993) remains unread (Unpaywall/OpenAlex 2026-08-21T16:21Z: closed; Springer `content/pdf` is HTML 200, not a PDF; archive.org title search empty). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex 2026-08-21T16:21Z: closed; Springer `content/pdf` is HTML 200; ETS landing page is HTML). ERIC ED334221 is Singer and Willett (1991), *From whether to when*, not the 1991 journal article. ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title is not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex 2026-08-18T21:07Z: closed; Radboud landing and bitstream 403). ## Formula notes @@ -102,6 +103,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - **After-t0 extra-process observed-indicator mean.** Driver et al. (2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:32Z): `T0TDPREDEFFECT` begins the extra process at `t = 0`. `TDPREDEFFECT` begins it after `t = 0`. The interior case `t0 < u < t` drives the original process over `t − u` while `μ_t` still uses `Δt = t − t0`. The scalar composition is `E(y_t)=τ+λ(μ_t+a_{ηξ}x(e^{ε(t−u)}−e^{a(t−u)})/(ε−a))`. Form the evolved-plus-after-contribution latent mean first, then `τ+λ` of that mean. An impulse at `u = t0` is the first-occasion extra-process map. An impulse at `u = t` has not yet driven the original process. `e^{a(t−u)}mx` is a Dirac on the original process, not this `DRIFT` drive. A zero loading is exactly `τ`. The first-occasion extra-process observed mean is not this composition when `u ≠ t0`. The evolved observed mean `τ+λμ_t` is not this composition. The extra process itself is not an observed indicator. An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **Asymptotic time-independent predictor effect.** Driver et al. (2017, §7.2, pp. 20–21; Eq. 3, p. 5; Table 2, p. 12; JSS PDF opened 2026-08-21T13:08Z): Table 2 names `B` `TIPREDEFFECT`. Equation 3 maps a finite event interval as `A^{-1}[e^{AΔt}−I]Bz`. Section 7.2 names `asymTIPREDEFFECT` the expected total change in process means given an increase of 1 on a time-independent predictor. For stable `a<0` that `Δt→∞` limit is `-A^{-1}B`. The scalar map is `-Bz/a`. Form `Bz` first, then divide by `-a`. A zero coefficient or zero predictor is exactly zero. `a≥0` cannot hold a finite process-mean change. `-Bz/a` is not the coefficient `B`, not `A^{-1}[e^{AΔt}−I]Bz`, not `CINT`, and not `Mx`. An overflowing product or quotient fails closed. This is not a Kalman filter and not ctsem estimation. - **Asymptotic time-independent predictor variance.** Driver et al. (2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z): Section 7.2 names `addedTIPREDVAR` the stable between-subject variance accounted for by time-independent predictors. For predictor variance `v≥0` the scalar map is `(B/a)²v`. Form the unit asymptotic effect `-B/a` first, then square, then multiply by `v`. A zero coefficient or zero predictor variance is exactly zero. `v<0` fails closed. `(B/a)²v` is not `TRAITVAR`, not `asymDIFFUSION`, and not `-Bz/a`. The printed 2-latent `addedTIPREDVAR` 2.838 is not this scalar map. An overflowing product fails closed. This is not a Kalman filter and not ctsem estimation. +- **Asymptotic continuous intercept.** Driver et al. (2017, Table 2, p. 12; Eq. 3, p. 5; §4.3 / p. 16; JSS PDF opened 2026-08-21T16:13Z): Table 2 names `κ` `CINT` and names `asymCINT` the asymptotic (`Δt=∞`) expected change in processes for a 1 unit change in intercept. Equation 3 maps a finite event interval as `A^{-1}[e^{AΔt}−I]κ`. For stable `a<0` that `Δt→∞` limit is `-A^{-1}κ`. The scalar map is `-κ/a`. A unit intercept is `-1/a`. Form `κ` first, then divide by `-a`. A zero intercept is exactly zero. `a≥0` cannot hold a finite process-mean change. `-κ/a` is not `κ`, not `A^{-1}[e^{AΔt}−I]κ`, not `T0MEANS`, and not `-Bz/a`. Page 16 notes that a `T0MEANS` stationarity constraint includes time-independent predictors; that composition is not this intercept-only map. The printed 2-latent `CINT` values are not this scalar map. An overflowing quotient fails closed. This is not a Kalman filter and not ctsem estimation. - **Level-change discrete increment.** Driver et al. (2017, §7.2, pp. 20–21; Eq. 3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T19:50Z): Equation 3 maps `CINT` through `A^{-1}[e^{AΔt}−I]κ`. With `κ=−a m x` the scalar increment is `(e^{aΔt}−1)/a·(−a m x)=(1−e^{aΔt})m x`. Form the level-change `CINT` first, then the discrete intercept map. Underflow of `e^{aΔt}` to `+0` keeps `m x`. A zero effect or zero predictor is exactly zero. `(1−e^{aΔt})m x` is not `m x`, not `κ`, and not `A^{-1}[e^{AΔt}−I]Bz`. An overflowing product or increment fails closed. This is not a Kalman filter and not ctsem estimation. - **CWC-then-lag.** Sample cluster means are removed first. Consecutive within residuals are then fitted by least squares to \(r_{t+\Delta t}\approx\exp(a\Delta t)\,r_{t}\) on event time. Same-sign pair-wise logs initialize the scalar Newton step. Sign-flipping \(T=2\) CWC pairs have no real logarithm and fail closed. Curran and Bauer (2011, pp. 607–608) show that this person-mean subtraction on a raw autoregressive series does **not** isolate the lagged within-person effect; the helper therefore does not claim to recover the raw-process drift. - **Already-centered irregular residual.** The caller supplies lagged within residuals. The mean of \(a=\ln(r_{t+\Delta t}/r_t)/\Delta t\) is the exact scalar map. Intervals may be irregular. The helper does not center again. This is not DSEM. @@ -142,6 +144,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - Driver et al. (2017, Eq. 5 of the §7.2 after-t0 extra-process contribution; JSS PDF re-opened 2026-08-21T06:32Z) recovers a known \(E(y_t)=\tau+\lambda(\mu_t+a_{\eta\xi}x(e^{\varepsilon(t-u)}-e^{a(t-u)})/(\varepsilon-a))\) at machine-scale RMSE for \(t_0 Date: Fri, 21 Aug 2026 16:33:10 +0000 Subject: [PATCH 59/62] feat(psychometric): recover Driver p.16 stationary T0MEANS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Driver, Oud, and Voelkle (2017, p. 16) constrain T0MEANS to the model-implied values using T0MEANSbase / T0MEANSfree when the first observation is determined by the process in the same way as later observations. Those constraints include extra effects due to time-independent predictors (asymTIPREDEFFECT). For stable a < 0 the scalar composition is -κ / a + -B z / a. Form the intercept contribution first, then include the TI extra effect, then add. That constrained first-occasion mean is not free T0MEANS, not asymCINT alone, not asymTIPREDEFFECT alone, and not the finite-interval discrete latent mean. JSS PDF opened 2026-08-21T16:13Z. Still not DSEM, not a Kalman filter, and not ctsem estimation. --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 1 + CLAUDE.md | 2 +- crates/psychometric_core/src/error.rs | 46 +++ crates/psychometric_core/src/event_time.rs | 292 +++++++++++++++++- crates/psychometric_core/src/lib.rs | 17 + ...multilevel_event_time_recovery_contract.rs | 94 +++++- .../scientific_claim_boundary_contract.rs | 80 ++++- docs/TRACEABILITY.md | 2 +- docs/adr/0005-posterior-esem-dsem.md | 4 +- docs/adr/README.md | 2 +- .../multilevel-event-time-recovery.md | 15 +- docs/validation/temporal-event-foundation.md | 2 +- 13 files changed, 537 insertions(+), 22 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 6271d3ab..50a1f3d1 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -61,7 +61,7 @@ boundaries above remain the target modular MSA architecture. | `tepp_simulation` | known-truth temporal/event data generation | | `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics | | `tepp_api` | versioned DTO, schema, and export contracts | -| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; first-occasion `T0TIPREDEFFECT` shift is `t0_b z` and Eq. 3 first-summand carry is `e^{A Δt} t0_b z` (`T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_b z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), first-occasion `T0TDPREDEFFECT` shift is `t0_m x0` and Eq. 3 first-summand carry is `e^{A Δt} t0_m x0` (`T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; §7.2 level-change `CINT` is `κ = −a m x` with `a < 0` so `−κ / a = m x` (`−a m x` is not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2; Eq. 3 of that setting is `(1 − e^{a Δt}) m x`, which is not `m x`, not `κ`, and not `TIPREDEFFECT`; §7.2 extra-process contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`; identification `TDPREDEFFECT` on the extra process is 1; printed extra `DRIFT` is `−0.000001`; not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`; `ε ≥ 0` fails closed; Eq. 5 of that contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; the extra process has `LAMBDA` 0 and is not an observed indicator; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`; after-t0 extra-process `TDPREDEFFECT` is `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` while `μ_t` uses `Δt`; Eq. 5 of that after-t0 contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)`; the first-occasion extra-process observed mean is not that observed mean when `u ≠ t0`; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive; §7.2 `asymTIPREDEFFECT` is `-B z / a` for `a < 0` (`-B z / a` is not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; §7.2 `addedTIPREDVAR` is `(B / a)² v`, not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`; Table 2 `asymCINT` is `-κ / a` for `a < 0` and is not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`)), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | +| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; first-occasion `T0TIPREDEFFECT` shift is `t0_b z` and Eq. 3 first-summand carry is `e^{A Δt} t0_b z` (`T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_b z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), first-occasion `T0TDPREDEFFECT` shift is `t0_m x0` and Eq. 3 first-summand carry is `e^{A Δt} t0_m x0` (`T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; §7.2 level-change `CINT` is `κ = −a m x` with `a < 0` so `−κ / a = m x` (`−a m x` is not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2; Eq. 3 of that setting is `(1 − e^{a Δt}) m x`, which is not `m x`, not `κ`, and not `TIPREDEFFECT`; §7.2 extra-process contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`; identification `TDPREDEFFECT` on the extra process is 1; printed extra `DRIFT` is `−0.000001`; not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`; `ε ≥ 0` fails closed; Eq. 5 of that contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; the extra process has `LAMBDA` 0 and is not an observed indicator; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`; after-t0 extra-process `TDPREDEFFECT` is `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` while `μ_t` uses `Δt`; Eq. 5 of that after-t0 contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)`; the first-occasion extra-process observed mean is not that observed mean when `u ≠ t0`; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive; §7.2 `asymTIPREDEFFECT` is `-B z / a` for `a < 0` (`-B z / a` is not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; §7.2 `addedTIPREDVAR` is `(B / a)² v`, not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`; Table 2 `asymCINT` is `-κ / a` for `a < 0` and is not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`; p. 16 stationary `T0MEANS` is `-κ / a + −B z / a` and is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean)), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | No crate exposes placeholder production behavior in Task 1. This prevents an empty façade from becoming a de facto public API before its invariants and tests diff --git a/CHANGELOG.md b/CHANGELOG.md index 4193cae7..edafb740 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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; Table 2, p. 12; Eq. 3, p. 5; JSS PDF opened 2026-08-21T16:13Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar stationary `T0MEANS`. Page 16 constrains `T0MEANS` to the model-implied values using `T0MEANSbase` / `T0MEANSfree` when the first observation is determined by the process in the same way as later observations. Those constraints include extra effects due to time-independent predictors (`asymTIPREDEFFECT`). For stable `a < 0` the scalar composition is `-κ / a + −B z / a`. Form the intercept contribution first, then include the TI extra effect, then add. A zero intercept and a zero TI contribution is exactly zero. `a ≥ 0` cannot hold a finite process-mean change when either contribution is nonzero and fails closed. That constrained first-occasion mean is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean. The printed 2-latent `T0MEANS` 2.823 is not this scalar map. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-21T16:21Z: `is_oa: false`; Springer `content/pdf` is HTML 200). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Table 2, p. 12; Eq. 3, p. 5; §4.3 / p. 16; JSS PDF opened 2026-08-21T16:13Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar `asymCINT`. Table 2 names `κ` `CINT` and names `asymCINT` the asymptotic (`Δt = ∞`) expected change in processes for a 1 unit change in intercept. Equation 3 maps a finite event interval as `A^{-1}[e^{A Δt} − I] κ`. For stable `a < 0` that `Δt → ∞` limit is `-A^{-1} κ`. The scalar map is `-κ / a`. A unit intercept is `-1 / a`. Form `κ` first, then divide by `-a`. A zero intercept is exactly zero. `a ≥ 0` cannot hold a finite process-mean change and fails closed. `-κ / a` is not `κ`, not the finite-interval increment `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `asymTIPREDEFFECT` `-B z / a`. Page 16 notes that a `T0MEANS` stationarity constraint includes time-independent predictors; that composition is not this intercept-only map. The printed 2-latent `CINT` values are not this scalar map. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-21T16:21Z: `is_oa: false`; Springer `content/pdf` is HTML 200). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, §7.2, pp. 20–21; Eq. 3, p. 5; Table 2, p. 12; JSS PDF opened 2026-08-21T13:08Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar `addedTIPREDVAR`. Section 7.2 names that matrix the stable between-subject variance accounted for by time-independent predictors. For stable `a < 0` and predictor variance `v ≥ 0` the scalar map is `(B / a)² v`. Form the unit asymptotic effect `-B / a` first, then square, then multiply by `v`. A zero coefficient or zero predictor variance is exactly zero. `v < 0` fails closed. `a ≥ 0` cannot hold a finite process-mean change and fails closed. `(B / a)² v` is not `TRAITVAR`, not `asymDIFFUSION`, and not the expected total change `-B z / a`. The printed 2-latent `addedTIPREDVAR` 2.838 is not this scalar map. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-21T06:24Z: `is_oa: false`; Springer `content/pdf` is HTML 200). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, §7.2, pp. 20–21; Eq. 3, p. 5; Table 2, p. 12; JSS PDF opened 2026-08-21T13:08Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar `asymTIPREDEFFECT`. Table 2 names `B` `TIPREDEFFECT`. Equation 3 maps a finite event interval as `A^{-1}[e^{A Δt} − I] B z`. Section 7.2 names `asymTIPREDEFFECT` the expected total change in process means given an increase of 1 on a time-independent predictor. For stable `a < 0` that total change is `-A^{-1} B`. The scalar map is `-B z / a`. Form `B z` first, then divide by `-a`. A zero coefficient or zero predictor is exactly zero. `a ≥ 0` cannot hold a finite process-mean change and fails closed. `-B z / a` is not the coefficient `B`, not the finite-interval increment `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. Printed LeisureTime `TIPREDEFFECT` `−0.225` / `asymTIPREDEFFECT` `−1.673` and Happiness `0.549` / `0.219` reconstruct under this map. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-21T06:24Z: `is_oa: false`; Springer `content/pdf` is HTML 200). diff --git a/CLAUDE.md b/CLAUDE.md index 1a41632e..41ac1829 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. Putnick and Bornstein (2016, PMC5145197 opened 2026-08-19T22:15Z) require scalar invariance before latent-mean comparison; residual invariance is not a prerequisite. Two-observation series have no residual degrees of freedom (`ordinary_least_squares_fit` returns residual variance `0`) and cap at strong/scalar; they still license means. This is two-group OLS, not MGCFA. Meredith (1993) names remain unread labels. -- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. The contemporaneous time-dependent predictor impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`). Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). The §7.2 level-change form is not that impulse. The observed mean of that contemporaneous impulse is `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-impulse latent mean is not `E(y_t)`. The time-independent predictor increment is `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`). Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. `TIPREDEFFECT` is `B`, not that discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle et al. (2012, Eq. 14). The observed mean of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The within-interval time-dependent impulse carry is `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation). Form `m x` first, then `e^{a(t−u)} m x`. A zero drift is `m x` with no dissipation. Underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept. `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). An impulse at `u = t` is the contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. The observed mean of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The carried latent mean is not `E(y_t)`. The first-occasion time-independent predictor shift is `t0_b z` (Driver et al., 2017, Table 3 `T0TIPREDEFFECT`; Eq. 3 first summand). Form `t0_b z` first, then `e^{a Δt} t0_b z`. Form `μ_t` first, then add that carry. A zero drift is `t0_b z`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. `e^{A Δt} t0_b z` is not `t0_b z`. `T0TIPREDEFFECT` is the coefficient, not the shift. The observed mean of that first-occasion carry is `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The evolved-plus-carry latent mean is not `E(y_t)`. The first-occasion time-dependent predictor shift is `t0_m x0` (Driver et al., 2017, Table 3 `T0TDPREDEFFECT`; Eq. 3 first summand; JSS PDF re-opened 2026-08-20T19:10Z). Form `t0_m x0` first, then `e^{a Δt} t0_m x0`. Form `μ_t` first, then add that carry. A zero drift is `t0_m x0`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`. `e^{A Δt} t0_m x0` is not `t0_m x0`. `T0TDPREDEFFECT` is the coefficient, not the shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The observed mean of that first-occasion TD carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean. The evolved-plus-carry latent mean is not `E(y_t)`. The lasting level-change `CINT` is `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z). Form `m x` first, then multiply by `−a`. Stable `a < 0` is required so `−κ / a = m x` is an equilibrium offset. `a ≥ 0` cannot hold a new process mean. `−a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not this `CINT` setting. Equation 3 maps that intercept as `(1 − e^{a Δt}) m x` (JSS PDF re-opened 2026-08-20T19:50Z). Form the level-change `CINT` first, then the discrete intercept map. Underflow of `e^{a Δt}` to `+0` keeps `m x`. `(1 − e^{a Δt}) m x` is not `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`. The printed §7.2 lasting level change is an extra near-zero-drift latent process (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z). `T0MEANS`, `CINT`, `T0VAR`, `DIFFUSION`, and `TRAITVAR` of that process are fixed to 0; `TDPREDEFFECT` on it is fixed to 1; its `DRIFT` diagonal is very close to 0 (printed example `−0.000001`; precisely 0 causes computational problems); the original process is driven by the `DRIFT` coupling `a_{ηξ}`. After a unit identification impulse the scalar contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`). Form `a_{ηξ} x` first. A zero coupling or zero predictor is exactly zero. `ε ≥ 0` fails closed. That contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. The observed mean of that extra-process contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5 of that §7.2 contribution; JSS PDF re-opened 2026-08-21T06:12Z). The extra process has `LAMBDA` 0 and is not an observed indicator. Original indicators load on the original process after the `DRIFT` coupling. The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The contribution is not `E(y_t)`. The evolved-plus-contribution latent mean is not `E(y_t)`. `T0TDPREDEFFECT` on the extra process begins at `t = 0` and uses `Δt = t − t0` for both the original-process evolution and the extra drive. `TDPREDEFFECT` after `t0` uses `t − u` with `t0 < u < t` for the extra drive while `μ_t` still uses `Δt`. The observed mean of that after-t0 extra-process contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, Eq. 5 of that §7.2 after-t0 contribution; JSS PDF re-opened 2026-08-21T06:32Z). The first-occasion extra-process observed mean is not that observed mean when `u ≠ t0`. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is a Dirac on the original process and is not that `DRIFT` drive. An impulse at `u = t0` or `u = t` is not interior. The asymptotic time-independent predictor effect is `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z). Form `B z` first, then divide by `-a`. Stable `a < 0` is required. `a ≥ 0` cannot hold a finite process-mean change. `-B z / a` is not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. The asymptotic time-independent predictor variance is `(B / a)² v` (Driver et al., 2017, §7.2, pp. 20–21 `addedTIPREDVAR`). Form the unit asymptotic effect first, then square, then multiply by `v`. `(B / a)² v` is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`. The asymptotic continuous intercept is `-κ / a` (Driver et al., 2017, Table 2, p. 12 `asymCINT`; Eq. 3 as `Δt → ∞`; JSS PDF opened 2026-08-21T16:13Z). Form `κ` first, then divide by `-a`. Stable `a < 0` is required. `-κ / a` is not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. The contemporaneous time-dependent predictor impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`). Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). The §7.2 level-change form is not that impulse. The observed mean of that contemporaneous impulse is `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-impulse latent mean is not `E(y_t)`. The time-independent predictor increment is `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`). Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. `TIPREDEFFECT` is `B`, not that discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle et al. (2012, Eq. 14). The observed mean of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The within-interval time-dependent impulse carry is `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation). Form `m x` first, then `e^{a(t−u)} m x`. A zero drift is `m x` with no dissipation. Underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept. `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). An impulse at `u = t` is the contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. The observed mean of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The carried latent mean is not `E(y_t)`. The first-occasion time-independent predictor shift is `t0_b z` (Driver et al., 2017, Table 3 `T0TIPREDEFFECT`; Eq. 3 first summand). Form `t0_b z` first, then `e^{a Δt} t0_b z`. Form `μ_t` first, then add that carry. A zero drift is `t0_b z`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. `e^{A Δt} t0_b z` is not `t0_b z`. `T0TIPREDEFFECT` is the coefficient, not the shift. The observed mean of that first-occasion carry is `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The evolved-plus-carry latent mean is not `E(y_t)`. The first-occasion time-dependent predictor shift is `t0_m x0` (Driver et al., 2017, Table 3 `T0TDPREDEFFECT`; Eq. 3 first summand; JSS PDF re-opened 2026-08-20T19:10Z). Form `t0_m x0` first, then `e^{a Δt} t0_m x0`. Form `μ_t` first, then add that carry. A zero drift is `t0_m x0`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`. `e^{A Δt} t0_m x0` is not `t0_m x0`. `T0TDPREDEFFECT` is the coefficient, not the shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The observed mean of that first-occasion TD carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean. The evolved-plus-carry latent mean is not `E(y_t)`. The lasting level-change `CINT` is `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z). Form `m x` first, then multiply by `−a`. Stable `a < 0` is required so `−κ / a = m x` is an equilibrium offset. `a ≥ 0` cannot hold a new process mean. `−a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not this `CINT` setting. Equation 3 maps that intercept as `(1 − e^{a Δt}) m x` (JSS PDF re-opened 2026-08-20T19:50Z). Form the level-change `CINT` first, then the discrete intercept map. Underflow of `e^{a Δt}` to `+0` keeps `m x`. `(1 − e^{a Δt}) m x` is not `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`. The printed §7.2 lasting level change is an extra near-zero-drift latent process (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z). `T0MEANS`, `CINT`, `T0VAR`, `DIFFUSION`, and `TRAITVAR` of that process are fixed to 0; `TDPREDEFFECT` on it is fixed to 1; its `DRIFT` diagonal is very close to 0 (printed example `−0.000001`; precisely 0 causes computational problems); the original process is driven by the `DRIFT` coupling `a_{ηξ}`. After a unit identification impulse the scalar contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`). Form `a_{ηξ} x` first. A zero coupling or zero predictor is exactly zero. `ε ≥ 0` fails closed. That contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. The observed mean of that extra-process contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5 of that §7.2 contribution; JSS PDF re-opened 2026-08-21T06:12Z). The extra process has `LAMBDA` 0 and is not an observed indicator. Original indicators load on the original process after the `DRIFT` coupling. The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The contribution is not `E(y_t)`. The evolved-plus-contribution latent mean is not `E(y_t)`. `T0TDPREDEFFECT` on the extra process begins at `t = 0` and uses `Δt = t − t0` for both the original-process evolution and the extra drive. `TDPREDEFFECT` after `t0` uses `t − u` with `t0 < u < t` for the extra drive while `μ_t` still uses `Δt`. The observed mean of that after-t0 extra-process contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, Eq. 5 of that §7.2 after-t0 contribution; JSS PDF re-opened 2026-08-21T06:32Z). The first-occasion extra-process observed mean is not that observed mean when `u ≠ t0`. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is a Dirac on the original process and is not that `DRIFT` drive. An impulse at `u = t0` or `u = t` is not interior. The asymptotic time-independent predictor effect is `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z). Form `B z` first, then divide by `-a`. Stable `a < 0` is required. `a ≥ 0` cannot hold a finite process-mean change. `-B z / a` is not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. The asymptotic time-independent predictor variance is `(B / a)² v` (Driver et al., 2017, §7.2, pp. 20–21 `addedTIPREDVAR`). Form the unit asymptotic effect first, then square, then multiply by `v`. `(B / a)² v` is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`. The asymptotic continuous intercept is `-κ / a` (Driver et al., 2017, Table 2, p. 12 `asymCINT`; Eq. 3 as `Δt → ∞`; JSS PDF opened 2026-08-21T16:13Z). Form `κ` first, then divide by `-a`. Stable `a < 0` is required. `-κ / a` is not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`. The p. 16 stationary `T0MEANS` constraint is `-κ / a + −B z / a`. Form the intercept contribution first, then include the TI extra effect, then add. That constrained first-occasion mean is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. diff --git a/crates/psychometric_core/src/error.rs b/crates/psychometric_core/src/error.rs index 6499b8e2..a80e0ffb 100644 --- a/crates/psychometric_core/src/error.rs +++ b/crates/psychometric_core/src/error.rs @@ -372,6 +372,22 @@ pub enum PsychometricError { /// article notes that a `T0MEANS` stationarity constraint includes /// time-independent predictors; that composition is not this map. AsymptoticContinuousInterceptIsNotAsymptoticTimeIndependentEffect, + /// Driver p. 16 stationary `T0MEANS` was treated as free `T0MEANS`. + /// `-κ / a + −B z / a` is the constrained first-occasion mean, not + /// the free first-occasion latent mean. + StationaryInitialLatentMeanIsNotInitialLatentMean, + /// Driver p. 16 stationary `T0MEANS` was treated as `asymCINT`. + /// The constraint includes time-independent predictors. `-κ / a` + /// is not that composition when `B z ≠ 0`. + StationaryInitialLatentMeanIsNotAsymptoticContinuousIntercept, + /// Driver p. 16 stationary `T0MEANS` was treated as + /// `asymTIPREDEFFECT`. The constraint includes the intercept + /// contribution. `-B z / a` is not that composition when `κ ≠ 0`. + StationaryInitialLatentMeanIsNotAsymptoticTimeIndependentEffect, + /// Driver p. 16 stationary `T0MEANS` was treated as a finite- + /// interval discrete latent mean. The constrained first-occasion + /// mean is not `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`. + StationaryInitialLatentMeanIsNotDiscreteMean, } impl fmt::Display for PsychometricError { @@ -672,6 +688,18 @@ impl fmt::Display for PsychometricError { Self::AsymptoticContinuousInterceptIsNotAsymptoticTimeIndependentEffect => { "asymptotic continuous intercept is not the asymptotic time-independent predictor effect" } + Self::StationaryInitialLatentMeanIsNotInitialLatentMean => { + "stationary first-occasion latent mean is not the free first-occasion latent mean" + } + Self::StationaryInitialLatentMeanIsNotAsymptoticContinuousIntercept => { + "stationary first-occasion latent mean is not the asymptotic continuous intercept" + } + Self::StationaryInitialLatentMeanIsNotAsymptoticTimeIndependentEffect => { + "stationary first-occasion latent mean is not the asymptotic time-independent predictor effect" + } + Self::StationaryInitialLatentMeanIsNotDiscreteMean => { + "stationary first-occasion latent mean is not the finite-interval discrete latent mean" + } }; formatter.write_str(message) } @@ -1133,5 +1161,23 @@ mod tests { .to_string(), "asymptotic continuous intercept is not the asymptotic time-independent predictor effect" ); + assert_eq!( + PsychometricError::StationaryInitialLatentMeanIsNotInitialLatentMean.to_string(), + "stationary first-occasion latent mean is not the free first-occasion latent mean" + ); + assert_eq!( + PsychometricError::StationaryInitialLatentMeanIsNotAsymptoticContinuousIntercept + .to_string(), + "stationary first-occasion latent mean is not the asymptotic continuous intercept" + ); + assert_eq!( + PsychometricError::StationaryInitialLatentMeanIsNotAsymptoticTimeIndependentEffect + .to_string(), + "stationary first-occasion latent mean is not the asymptotic time-independent predictor effect" + ); + assert_eq!( + PsychometricError::StationaryInitialLatentMeanIsNotDiscreteMean.to_string(), + "stationary first-occasion latent mean is not the finite-interval discrete latent mean" + ); } } diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index c26f0dde..e35652b0 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -70,6 +70,14 @@ //! `T0MEANS`, and not `asymTIPREDEFFECT` `-B z / a`. Page 16 notes //! that a `T0MEANS` stationarity constraint includes time-independent //! predictors; that composition is not this intercept-only map. +//! Page 16 constrains `T0MEANS` to the model-implied values using +//! `T0MEANSbase` / `T0MEANSfree`. Those constraints include extra +//! effects due to time-independent predictors (`asymTIPREDEFFECT`). +//! The scalar composition is `-κ / a + −B z / a` for stable `a < 0`. +//! Form the intercept contribution first, then include the TI extra +//! effect, then add. That constrained first-occasion mean is not free +//! `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and +//! not the finite-interval discrete latent mean. //! Table 3 (p. 13) names a different matrix //! `T0TIPREDEFFECT` for time-independent predictors on latents at //! `T0`. The scalar first-occasion shift is `t0_b z`. Equation 3's @@ -2892,6 +2900,126 @@ pub fn refuse_asymptotic_continuous_intercept_as_asymptotic_time_independent_eff Err(PsychometricError::AsymptoticContinuousInterceptIsNotAsymptoticTimeIndependentEffect) } +/// Exact scalar p. 16 stationary `T0MEANS`. +/// +/// Driver, Oud, and Voelkle (2017, p. 16; Table 2, p. 12; Eq. 3, p. 5; +/// JSS PDF opened 2026-08-21T16:13Z from +/// ) +/// constrain `T0MEANS` to the model-implied values using +/// `T0MEANSbase` / `T0MEANSfree` when the first observation is +/// determined by the process in the same way as later observations. +/// Those constraints include extra effects due to time-independent +/// predictors (`asymTIPREDEFFECT`). Table 2 names `κ` `CINT` and +/// names `asymCINT` the `Δt → ∞` intercept contribution `-κ / a`. +/// For stable `a < 0` the scalar composition is +/// `-κ / a + −B z / a`. Form the intercept contribution first, then +/// include the TI extra effect, then add. A zero intercept and a zero +/// TI contribution is exactly zero. `a ≥ 0` cannot hold a finite +/// process-mean change when either contribution is nonzero and fails +/// closed. That constrained first-occasion mean is not free +/// `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and +/// not the finite-interval discrete latent mean +/// `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`. The printed 2-latent +/// `T0MEANS` 2.823 is not this scalar map. This is not a Kalman +/// filter, not a matrix `expm`, and not ctsem estimation. +/// +/// # Errors +/// +/// Returns [`PsychometricError::EventTimeRequired`] for any non-event +/// clock, [`PsychometricError::AsymptoticContinuousInterceptRequiresStableDrift`] +/// or [`PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift`] +/// when the drift is not strictly negative and the corresponding +/// contribution is nonzero, and [`PsychometricError::InvalidNumericInput`] +/// when an input is non-finite or a quotient or sum overflows. +pub fn recover_stationary_initial_latent_mean( + continuous_intercept: f64, + time_independent_effect: f64, + time_independent_predictor: f64, + log_rate: f64, + clock: LagClock, +) -> Result { + let intercept = recover_asymptotic_continuous_intercept(continuous_intercept, log_rate, clock)?; + let tipred = recover_asymptotic_time_independent_predictor_effect( + time_independent_effect, + time_independent_predictor, + log_rate, + clock, + )?; + require_finite(intercept + tipred) +} + +/// Refuse treating p. 16 stationary `T0MEANS` as free `T0MEANS`. +/// +/// `-κ / a + −B z / a` is the constrained first-occasion mean. Table 2 +/// names the free first-occasion latent mean `T0MEANS`. Those are not +/// the same map. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::StationaryInitialLatentMeanIsNotInitialLatentMean`]. +pub fn refuse_stationary_initial_latent_mean_as_initial_latent_mean( + stationary_mean: f64, + initial_latent_mean: f64, +) -> Result { + let _ = (stationary_mean, initial_latent_mean); + Err(PsychometricError::StationaryInitialLatentMeanIsNotInitialLatentMean) +} + +/// Refuse treating p. 16 stationary `T0MEANS` as `asymCINT`. +/// +/// The constraint includes time-independent predictors. `-κ / a` is +/// the intercept contribution and is not that composition when +/// `B z ≠ 0`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::StationaryInitialLatentMeanIsNotAsymptoticContinuousIntercept`]. +pub fn refuse_stationary_initial_latent_mean_as_asymptotic_continuous_intercept( + stationary_mean: f64, + asymptotic_intercept: f64, +) -> Result { + let _ = (stationary_mean, asymptotic_intercept); + Err(PsychometricError::StationaryInitialLatentMeanIsNotAsymptoticContinuousIntercept) +} + +/// Refuse treating p. 16 stationary `T0MEANS` as `asymTIPREDEFFECT`. +/// +/// The constraint includes the intercept contribution. `-B z / a` is +/// the TI extra effect and is not that composition when `κ ≠ 0`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::StationaryInitialLatentMeanIsNotAsymptoticTimeIndependentEffect`]. +pub fn refuse_stationary_initial_latent_mean_as_asymptotic_time_independent_effect( + stationary_mean: f64, + asymptotic_time_independent_effect: f64, +) -> Result { + let _ = (stationary_mean, asymptotic_time_independent_effect); + Err(PsychometricError::StationaryInitialLatentMeanIsNotAsymptoticTimeIndependentEffect) +} + +/// Refuse treating p. 16 stationary `T0MEANS` as a finite-interval +/// discrete latent mean. +/// +/// `-κ / a + −B z / a` is the `Δt → ∞` constrained first-occasion +/// mean. `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` is a finite event +/// interval and is not that limit. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::StationaryInitialLatentMeanIsNotDiscreteMean`]. +pub fn refuse_stationary_initial_latent_mean_as_discrete_mean( + stationary_mean: f64, + discrete_mean: f64, +) -> Result { + let _ = (stationary_mean, discrete_mean); + Err(PsychometricError::StationaryInitialLatentMeanIsNotDiscreteMean) +} + /// Exact scalar observed mean of a time-independent predictor. /// /// Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Eq. 3, p. 5; Table 2, @@ -4419,9 +4547,10 @@ mod tests { recover_level_change_extra_process_contribution_after, recover_local_log_rate, recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, - recover_stationary_latent_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_stationary_initial_latent_mean, recover_stationary_latent_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, @@ -4488,6 +4617,10 @@ mod tests { refuse_measurement_error_as_observed_variance, refuse_pooled_discrete_lag_across_unequal_intervals, refuse_process_noise_as_unconditional_variance, + refuse_stationary_initial_latent_mean_as_asymptotic_continuous_intercept, + refuse_stationary_initial_latent_mean_as_asymptotic_time_independent_effect, + refuse_stationary_initial_latent_mean_as_discrete_mean, + refuse_stationary_initial_latent_mean_as_initial_latent_mean, refuse_time_dependent_impulse_as_continuous_intercept, refuse_time_dependent_impulse_as_time_independent_effect, refuse_time_dependent_impulse_as_time_varying_discrete_effect, @@ -7746,6 +7879,159 @@ mod tests { ); } + #[test] + fn stationary_initial_latent_mean_recovers_driver_page_sixteen() { + // Driver et al. (2017, p. 16; Table 2, p. 12; Eq. 3) + // constrain T0MEANS to model-implied values that include + // extra effects due to time-independent predictors + // (asymTIPREDEFFECT). Reconstruct a from printed LeisureTime + // TIPREDEFFECT −0.225 / asymTIPREDEFFECT −1.673. The printed + // 2-latent T0MEANS 2.823 is not this scalar map. + let printed_effect = -0.225_f64; + let printed_asym = -1.673_f64; + let log_rate = -printed_effect / printed_asym; + let intercept = 0.3_f64; + let recovered = recover_stationary_initial_latent_mean( + intercept, + printed_effect, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("stationary T0MEANS"); + let intercept_only = + recover_asymptotic_continuous_intercept(intercept, log_rate, LagClock::EventTime) + .expect("asymCINT"); + let tipred = recover_asymptotic_time_independent_predictor_effect( + printed_effect, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("asymTIPREDEFFECT"); + assert!((recovered - (intercept_only + tipred)).abs() < 1e-12); + let synthetic = + recover_stationary_initial_latent_mean(0.3, 0.2, 1.0, -0.5, LagClock::EventTime) + .expect("synthetic"); + assert!((synthetic - 1.0).abs() < 1e-15); + let intercept_only_path = recover_stationary_initial_latent_mean( + intercept, + 0.0, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("intercept-only"); + assert!((intercept_only_path - intercept_only).abs() < 1e-15); + let tipred_only = recover_stationary_initial_latent_mean( + 0.0, + printed_effect, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("ti-only"); + assert!((tipred_only - tipred).abs() < 1e-15); + assert_eq!( + recover_stationary_initial_latent_mean(0.0, 0.0, 1.0, 0.0, LagClock::EventTime), + Ok(0.0) + ); + assert_eq!( + recover_stationary_initial_latent_mean(0.0, 0.0, 1.0, 0.5, LagClock::EventTime), + Ok(0.0) + ); + } + + #[test] + fn stationary_initial_latent_mean_is_not_t0_cint_tipred_or_discrete() { + let intercept = 0.3_f64; + let log_rate = -0.134_488_942_f64; + let recovered = recover_stationary_initial_latent_mean( + intercept, + -0.225, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("stationary T0MEANS"); + let intercept_only = + recover_asymptotic_continuous_intercept(intercept, log_rate, LagClock::EventTime) + .expect("asymCINT"); + let tipred = recover_asymptotic_time_independent_predictor_effect( + -0.225, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("asymTIPREDEFFECT"); + let discrete = + recover_discrete_latent_mean(2.823, log_rate, intercept, 1.0, LagClock::EventTime) + .expect("μ_t"); + assert!((recovered - 2.823).abs() > 1e-3); + assert!((recovered - intercept_only).abs() > 1e-3); + assert!((recovered - tipred).abs() > 1e-3); + assert!((recovered - discrete).abs() > 1e-3); + assert_eq!( + refuse_stationary_initial_latent_mean_as_initial_latent_mean(recovered, 2.823), + Err(PsychometricError::StationaryInitialLatentMeanIsNotInitialLatentMean) + ); + assert_eq!( + refuse_stationary_initial_latent_mean_as_asymptotic_continuous_intercept( + recovered, + intercept_only + ), + Err(PsychometricError::StationaryInitialLatentMeanIsNotAsymptoticContinuousIntercept) + ); + assert_eq!( + refuse_stationary_initial_latent_mean_as_asymptotic_time_independent_effect( + recovered, tipred + ), + Err(PsychometricError::StationaryInitialLatentMeanIsNotAsymptoticTimeIndependentEffect) + ); + assert_eq!( + refuse_stationary_initial_latent_mean_as_discrete_mean(recovered, discrete), + Err(PsychometricError::StationaryInitialLatentMeanIsNotDiscreteMean) + ); + } + + #[test] + fn stationary_initial_latent_mean_invalid_inputs_fail_closed() { + let intercept = 0.3_f64; + let log_rate = -0.134_488_942_f64; + assert_eq!( + recover_stationary_initial_latent_mean( + intercept, + -0.225, + 1.0, + log_rate, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_stationary_initial_latent_mean(intercept, 0.0, 1.0, 0.0, LagClock::EventTime), + Err(PsychometricError::AsymptoticContinuousInterceptRequiresStableDrift) + ); + assert_eq!( + recover_stationary_initial_latent_mean(0.0, -0.225, 1.0, 0.5, LagClock::EventTime), + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) + ); + assert_eq!( + recover_stationary_initial_latent_mean( + f64::NAN, + -0.225, + 1.0, + log_rate, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_stationary_initial_latent_mean(1e308, 1e308, 1.0, -1e-308, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + } + #[test] fn discrete_observed_mean_with_impulse_recovers_driver_equation_five() { let loading = 2.0_f64; diff --git a/crates/psychometric_core/src/lib.rs b/crates/psychometric_core/src/lib.rs index 8a951098..10cb269d 100644 --- a/crates/psychometric_core/src/lib.rs +++ b/crates/psychometric_core/src/lib.rs @@ -125,6 +125,13 @@ //! not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not //! `-B z / a`; p. 16 `T0MEANS` stationarity includes TI predictors; //! that composition is not this intercept-only map), +//! recovers the Driver p. 16 stationary `T0MEANS` as +//! `-κ / a + −B z / a` +//! (constrained first-occasion mean using `T0MEANSbase` / +//! `T0MEANSfree`; form the intercept contribution first, then include +//! the TI extra effect, then add; not free `T0MEANS`, not `asymCINT` +//! alone, not `asymTIPREDEFFECT` alone, and not the finite-interval +//! discrete latent mean), //! and refuses //! latent-mean comparison below strong invariance. @@ -261,6 +268,8 @@ pub use event_time::recover_manifest_observed_mean; pub use event_time::recover_manifest_observed_variance; /// Exact scalar observed-indicator variance `λ² Var(η) + θ + ψ`. pub use event_time::recover_manifest_trait_plus_state_observed_variance; +/// Exact scalar p. 16 stationary `T0MEANS` `-κ / a + −B z / a`. +pub use event_time::recover_stationary_initial_latent_mean; /// Exact scalar stationary within-subject variance `-q / (2 a)`. pub use event_time::recover_stationary_latent_variance; /// Exact scalar contemporaneous `TDPREDEFFECT` impulse `m x`. @@ -413,6 +422,14 @@ pub use event_time::refuse_measurement_error_as_observed_variance; pub use event_time::refuse_pooled_discrete_lag_across_unequal_intervals; /// Refuse treating Driver Eq. 3 process noise as the unconditional variance. pub use event_time::refuse_process_noise_as_unconditional_variance; +/// Refuse treating p. 16 stationary `T0MEANS` as `asymCINT`. +pub use event_time::refuse_stationary_initial_latent_mean_as_asymptotic_continuous_intercept; +/// Refuse treating p. 16 stationary `T0MEANS` as `asymTIPREDEFFECT`. +pub use event_time::refuse_stationary_initial_latent_mean_as_asymptotic_time_independent_effect; +/// Refuse treating p. 16 stationary `T0MEANS` as a finite-interval discrete mean. +pub use event_time::refuse_stationary_initial_latent_mean_as_discrete_mean; +/// Refuse treating p. 16 stationary `T0MEANS` as free `T0MEANS`. +pub use event_time::refuse_stationary_initial_latent_mean_as_initial_latent_mean; /// Refuse treating Driver Eq. 3 `TDPREDEFFECT` impulse as `CINT`. pub use event_time::refuse_time_dependent_impulse_as_continuous_intercept; /// Refuse treating Driver Eq. 3 impulse as `TIPREDEFFECT`. 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 a92cbe7e..6e7167c9 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -34,9 +34,10 @@ use psychometric_core::{ recover_level_change_extra_process_contribution_after, recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, - recover_stationary_latent_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_stationary_initial_latent_mean, recover_stationary_latent_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, @@ -100,6 +101,10 @@ use psychometric_core::{ refuse_measurement_error_as_observed_variance, refuse_pooled_discrete_lag_across_unequal_intervals, refuse_process_noise_as_unconditional_variance, + refuse_stationary_initial_latent_mean_as_asymptotic_continuous_intercept, + refuse_stationary_initial_latent_mean_as_asymptotic_time_independent_effect, + refuse_stationary_initial_latent_mean_as_discrete_mean, + refuse_stationary_initial_latent_mean_as_initial_latent_mean, refuse_time_dependent_impulse_as_continuous_intercept, refuse_time_dependent_impulse_as_time_independent_effect, refuse_time_dependent_impulse_as_time_varying_discrete_effect, @@ -4457,3 +4462,86 @@ fn asymptotic_continuous_intercept_refuses_unstable_drift_and_non_event_clocks() Ok(0.0) ); } + +#[test] +fn stationary_initial_latent_mean_recovers_driver_page_sixteen() { + let printed_effect = -0.225_f64; + let printed_asym = -1.673_f64; + let log_rate = -printed_effect / printed_asym; + let intercept = 0.3_f64; + let recovered = recover_stationary_initial_latent_mean( + intercept, + printed_effect, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("stationary T0MEANS"); + let intercept_only = + recover_asymptotic_continuous_intercept(intercept, log_rate, LagClock::EventTime) + .expect("asymCINT"); + let tipred = recover_asymptotic_time_independent_predictor_effect( + printed_effect, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("asymTIPREDEFFECT"); + let expected = intercept_only + tipred; + let error = rmse(&[expected], &[recovered]); + assert!( + error < 1e-12, + "Driver p. 16 stationary T0MEANS RMSE {error}: got {recovered}" + ); + let discrete = + recover_discrete_latent_mean(2.823, log_rate, intercept, 1.0, LagClock::EventTime) + .expect("μ_t"); + assert!( + rmse(&[recovered], &[2.823]) > error, + "T0MEANS is not stationary T0MEANS" + ); + assert!(rmse(&[recovered], &[intercept_only]) > error); + assert!(rmse(&[recovered], &[tipred]) > error); + assert!(rmse(&[recovered], &[discrete]) > error); + assert_eq!( + refuse_stationary_initial_latent_mean_as_initial_latent_mean(recovered, 2.823), + Err(PsychometricError::StationaryInitialLatentMeanIsNotInitialLatentMean) + ); + assert_eq!( + refuse_stationary_initial_latent_mean_as_asymptotic_continuous_intercept( + recovered, + intercept_only + ), + Err(PsychometricError::StationaryInitialLatentMeanIsNotAsymptoticContinuousIntercept) + ); + assert_eq!( + refuse_stationary_initial_latent_mean_as_asymptotic_time_independent_effect( + recovered, tipred + ), + Err(PsychometricError::StationaryInitialLatentMeanIsNotAsymptoticTimeIndependentEffect) + ); + assert_eq!( + refuse_stationary_initial_latent_mean_as_discrete_mean(recovered, discrete), + Err(PsychometricError::StationaryInitialLatentMeanIsNotDiscreteMean) + ); +} + +#[test] +fn stationary_initial_latent_mean_refuses_unstable_drift_and_non_event_clocks() { + assert_eq!( + recover_stationary_initial_latent_mean(0.3, -0.225, 1.0, -0.13, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_stationary_initial_latent_mean(0.3, 0.0, 1.0, 0.0, LagClock::EventTime), + Err(PsychometricError::AsymptoticContinuousInterceptRequiresStableDrift) + ); + assert_eq!( + recover_stationary_initial_latent_mean(0.0, -0.225, 1.0, 0.5, LagClock::EventTime), + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) + ); + assert_eq!( + recover_stationary_initial_latent_mean(0.0, 0.0, 1.0, 0.0, LagClock::EventTime), + Ok(0.0) + ); +} diff --git a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs index 89fbe4b6..1e6f03f9 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -29,9 +29,9 @@ use psychometric_core::{ recover_level_change_extra_process_contribution_after, recover_loading_point_estimate_mean, recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, - recover_stationary_latent_variance, recover_time_dependent_predictor_impulse, - recover_time_dependent_predictor_impulse_carry, recover_trait_plus_state_latent_variance, - recover_within_residual_event_time_log_rate, + recover_stationary_initial_latent_mean, recover_stationary_latent_variance, + recover_time_dependent_predictor_impulse, recover_time_dependent_predictor_impulse_carry, + 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, @@ -93,6 +93,10 @@ use psychometric_core::{ refuse_manifest_means_as_observed_mean, refuse_manifest_trait_variance_as_measurement_error, refuse_measurement_error_as_lagged_observed_covariance, refuse_measurement_error_as_observed_variance, refuse_process_noise_as_unconditional_variance, + refuse_stationary_initial_latent_mean_as_asymptotic_continuous_intercept, + refuse_stationary_initial_latent_mean_as_asymptotic_time_independent_effect, + refuse_stationary_initial_latent_mean_as_discrete_mean, + refuse_stationary_initial_latent_mean_as_initial_latent_mean, refuse_time_dependent_impulse_as_continuous_intercept, refuse_time_dependent_impulse_as_time_independent_effect, refuse_time_dependent_impulse_as_time_varying_discrete_effect, @@ -2248,3 +2252,73 @@ fn asymptotic_continuous_intercept_is_not_cint_increment_t0_or_tipred() { ) ); } + +#[test] +fn stationary_initial_latent_mean_is_not_t0_cint_tipred_or_discrete() { + let intercept = 0.3_f64; + let log_rate = -0.134_488_942_f64; + let recovered = recover_stationary_initial_latent_mean( + intercept, + -0.225, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("stationary T0MEANS"); + let intercept_only = + recover_asymptotic_continuous_intercept(intercept, log_rate, LagClock::EventTime) + .expect("asymCINT"); + let tipred = recover_asymptotic_time_independent_predictor_effect( + -0.225, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("asymTIPREDEFFECT"); + let discrete = + recover_discrete_latent_mean(2.823, log_rate, intercept, 1.0, LagClock::EventTime) + .expect("μ_t"); + assert!( + (recovered - 2.823).abs() > 1e-3, + "Driver et al. (2017, p. 16): constrained T0MEANS is not free T0MEANS" + ); + assert!( + (recovered - intercept_only).abs() > 1e-3, + "Driver et al. (2017, p. 16): constrained T0MEANS is not asymCINT" + ); + assert!( + (recovered - tipred).abs() > 1e-3, + "Driver et al. (2017, p. 16): constrained T0MEANS is not asymTIPREDEFFECT" + ); + assert!( + (recovered - discrete).abs() > 1e-3, + "Driver et al. (2017, p. 16): constrained T0MEANS is not μ_t" + ); + assert_eq!( + refuse_stationary_initial_latent_mean_as_initial_latent_mean(recovered, 2.823), + Err( + psychometric_core::PsychometricError::StationaryInitialLatentMeanIsNotInitialLatentMean + ) + ); + assert_eq!( + refuse_stationary_initial_latent_mean_as_asymptotic_continuous_intercept( + recovered, + intercept_only + ), + Err( + psychometric_core::PsychometricError::StationaryInitialLatentMeanIsNotAsymptoticContinuousIntercept + ) + ); + assert_eq!( + refuse_stationary_initial_latent_mean_as_asymptotic_time_independent_effect( + recovered, tipred + ), + Err( + psychometric_core::PsychometricError::StationaryInitialLatentMeanIsNotAsymptoticTimeIndependentEffect + ) + ); + assert_eq!( + refuse_stationary_initial_latent_mean_as_discrete_mean(recovered, discrete), + Err(psychometric_core::PsychometricError::StationaryInitialLatentMeanIsNotDiscreteMean) + ); +} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 56a2245d..d6730162 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -28,7 +28,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | report template/section/copied/style/modality method effects | ADR 0004/0012; PRD/TRD | simulation truth factors implemented; estimator-side method model remains future | partial | | candidate K statistical/Pareto gates + blinded LLM review | ADR 0012; research | future `model_selection` | accepted-target | | compositional topic correlation / stable clustering | ADR 0005/0012; research | future `network_analysis` | accepted-target | -| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; §7.2 level-change `CINT` is `κ = −a m x` (`a < 0`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`; Eq. 3 of that setting is `(1 − e^{a Δt}) m x`); §7.2 extra-process contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, not the dissipating Dirac; `ε ≥ 0` fails closed; Eq. 5 of that contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; extra `LAMBDA` is 0; `τ + λ μ_t` is not that observed mean; after-t0 extra-process `TDPREDEFFECT` uses `t − u` with `t0 < u < t` while `μ_t` uses `Δt`; that after-t0 observed mean is not the first-occasion extra-process observed mean; §7.2 `asymTIPREDEFFECT` is `-B z / a` for `a < 0` and is not `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; §7.2 `addedTIPREDVAR` is `(B / a)² v` and is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`; Table 2 `asymCINT` is `-κ / a` for `a < 0` and is not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z); full ESEM/DSEM remaining | partial | +| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; §7.2 level-change `CINT` is `κ = −a m x` (`a < 0`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`; Eq. 3 of that setting is `(1 − e^{a Δt}) m x`); §7.2 extra-process contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, not the dissipating Dirac; `ε ≥ 0` fails closed; Eq. 5 of that contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; extra `LAMBDA` is 0; `τ + λ μ_t` is not that observed mean; after-t0 extra-process `TDPREDEFFECT` uses `t − u` with `t0 < u < t` while `μ_t` uses `Δt`; that after-t0 observed mean is not the first-occasion extra-process observed mean; §7.2 `asymTIPREDEFFECT` is `-B z / a` for `a < 0` and is not `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; §7.2 `addedTIPREDVAR` is `(B / a)² v` and is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`; Table 2 `asymCINT` is `-κ / a` for `a < 0` and is not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`; p. 16 stationary `T0MEANS` is `-κ / a + −B z / a` and is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z); full ESEM/DSEM remaining | partial | | CPU bounded multithreading + GPU/VRAM streaming/parity | ADR 0001/0006 | future `compute_backend` | accepted-target | | TDT detection/tracking vs CHRONOS schema/prediction/temporal consistency | ADR 0016; PRD/research | future `event_intelligence` | accepted-target | | evidence-bounded LLM interpretation | ADR 0010/0012; PRD | `tepp_api` router plus future `interpretation_gateway` | partial | diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index 1452fe96..625edd12 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -1,7 +1,7 @@ # ADR 0005 — Posterior-aware ESEM/DSEM and structural interpretation **Decision status:** Accepted -**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), exact scalar contemporaneous `TDPREDEFFECT` impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; the §7.2 level-change form is not that impulse), exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of that Eq. 3 composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar time-independent `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle Eq. 14; `B` is not that discrete increment), exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of that Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of that carried latent mean; `τ + λ μ_t` is not that observed mean), exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; `T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`), exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; `T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand TD composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean), exact scalar §7.2 level-change `CINT` `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; `a < 0` so `−κ / a = m x`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2), exact scalar Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` (not `m x`, not `κ`, and not `TIPREDEFFECT`), exact scalar §7.2 extra near-zero-drift latent process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z; identification `TDPREDEFFECT` on the extra process is 1; extra `DRIFT` printed as `−0.000001`; precisely 0 causes computational problems; `ε = a` is `a_{ηξ} x Δt e^{a Δt}`; not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`; `ε ≥ 0` fails closed), exact scalar observed mean of that extra-process contribution `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z; the extra process has `LAMBDA` 0 and is not an observed indicator; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`), exact scalar after-t0 extra-process contribution `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` and Eq. 5 observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:32Z; `T0TDPREDEFFECT` uses `Δt` for both the evolution and the extra drive; `TDPREDEFFECT` after `t0` uses `t − u`; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive), exact scalar §7.2 `asymTIPREDEFFECT` `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z; expected total change in process means given a time-independent predictor; `a < 0`; not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; `a ≥ 0` fails closed), exact scalar §7.2 `addedTIPREDVAR` `(B / a)² v` (Driver et al., 2017, §7.2, pp. 20–21; stable between-subject variance accounted for by a time-independent predictor; not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`), exact scalar Table 2 `asymCINT` `-κ / a` (Driver et al., 2017, Table 2, p. 12; Eq. 3 as `Δt → ∞`; `a < 0`; not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`; `a ≥ 0` fails closed), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target +**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), exact scalar contemporaneous `TDPREDEFFECT` impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; the §7.2 level-change form is not that impulse), exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of that Eq. 3 composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar time-independent `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle Eq. 14; `B` is not that discrete increment), exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of that Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of that carried latent mean; `τ + λ μ_t` is not that observed mean), exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; `T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`), exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; `T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand TD composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean), exact scalar §7.2 level-change `CINT` `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; `a < 0` so `−κ / a = m x`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2), exact scalar Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` (not `m x`, not `κ`, and not `TIPREDEFFECT`), exact scalar §7.2 extra near-zero-drift latent process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z; identification `TDPREDEFFECT` on the extra process is 1; extra `DRIFT` printed as `−0.000001`; precisely 0 causes computational problems; `ε = a` is `a_{ηξ} x Δt e^{a Δt}`; not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`; `ε ≥ 0` fails closed), exact scalar observed mean of that extra-process contribution `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z; the extra process has `LAMBDA` 0 and is not an observed indicator; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`), exact scalar after-t0 extra-process contribution `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` and Eq. 5 observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:32Z; `T0TDPREDEFFECT` uses `Δt` for both the evolution and the extra drive; `TDPREDEFFECT` after `t0` uses `t − u`; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive), exact scalar §7.2 `asymTIPREDEFFECT` `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z; expected total change in process means given a time-independent predictor; `a < 0`; not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; `a ≥ 0` fails closed), exact scalar §7.2 `addedTIPREDVAR` `(B / a)² v` (Driver et al., 2017, §7.2, pp. 20–21; stable between-subject variance accounted for by a time-independent predictor; not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`), exact scalar Table 2 `asymCINT` `-κ / a` (Driver et al., 2017, Table 2, p. 12; Eq. 3 as `Δt → ∞`; `a < 0`; not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`; `a ≥ 0` fails closed), exact scalar p. 16 stationary `T0MEANS` `-κ / a + −B z / a` (Driver et al., 2017, p. 16; constrained first-occasion mean using `T0MEANSbase` / `T0MEANSfree`; not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target **Date:** 2026-08-05 **Supersedes:** None. ADR 0012 governs upstream topic measurement/network coordinates; this ADR governs higher-order psychometric structure and longitudinal interpretation. @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), recovers the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z; `TDPREDEFFECT` is `M`, not `CINT`; `M x` is not `A^{-1}[e^{A Δt} − I] B z`; `M x` is not Voelkle et al., 2012, Eq. 14; the level-change form is not that impulse), recovers the exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition; JSS PDF re-opened 2026-08-20T09:01Z; the evolved map `τ + λ μ_t` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-impulse latent mean is not `E(y_t)`), recovers the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z; `TIPREDEFFECT` is `B`, not `κ`; form `B z` first, then the discrete intercept map; a zero drift is `B z Δt`; `B` is not the discrete increment; `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle Eq. 14), recovers the exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; JSS PDF re-opened 2026-08-20T12:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-increment latent mean is not `E(y_t)`), recovers the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z; form `m x` first, then `e^{a(t−u)} m x`; a zero drift is `m x`; underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept; `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; an impulse at `u = t` is the contemporaneous map; an impulse at `u ≤ t0` is already in `η(t0)`), recovers the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; JSS PDF re-opened 2026-08-20T05:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the carried latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and its Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF opened 2026-08-20T15:14Z; form `t0_b z` first, then `e^{a Δt} t0_b z`; a zero drift is `t0_b z`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; `e^{A Δt} t0_b z` is not `t0_b z`; `T0TIPREDEFFECT` is the coefficient, not the shift), recovers the exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition; JSS PDF re-opened 2026-08-20T15:28Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and its Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; form `t0_m x0` first, then `e^{a Δt} t0_m x0`; a zero drift is `t0_m x0`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `T0TDPREDEFFECT` is the coefficient, not the shift; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), recovers the exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; the first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar §7.2 level-change `CINT` setting `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z; form `m x` first, then multiply by `−a`; `a < 0` so `−κ / a = m x`; `a ≥ 0` cannot hold a new process mean; `−a m x` is not the dissipating Dirac, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`; the extra near-zero-drift latent process also named in §7.2 is a different specification), recovers the exact scalar Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` (JSS PDF re-opened 2026-08-20T19:50Z; form the level-change `CINT` first, then the discrete intercept map; underflow of `e^{a Δt}` to `+0` keeps `m x`; `(1 − e^{a Δt}) m x` is not `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`), recovers the exact scalar §7.2 extra near-zero-drift latent process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z; form `a_{ηξ} x` first; `ε = a` is `a_{ηξ} x Δt e^{a Δt}`; a zero coupling or zero predictor is exactly zero; `ε ≥ 0` cannot hold a lasting extra state; that contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`), recovers the exact scalar observed mean of that extra-process contribution `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z; the extra process has `LAMBDA` 0 and is not an observed indicator; form the evolved-plus-contribution latent mean first, then `τ + λ` of that mean; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`), recovers the exact scalar after-t0 extra-process contribution `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` and its Eq. 5 observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:32Z; `T0TDPREDEFFECT` uses `Δt` for both the evolution and the extra drive; `TDPREDEFFECT` after `t0` uses `t − u` while `μ_t` still uses `Δt`; an impulse at `u = t0` or `u = t` is not interior; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive), recovers the exact scalar §7.2 `asymTIPREDEFFECT` `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z; form `B z` first, then divide by `-a`; `a < 0`; a zero coefficient or zero predictor is exactly zero; `a ≥ 0` cannot hold a finite process-mean change; `-B z / a` is not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`), recovers the exact scalar §7.2 `addedTIPREDVAR` `(B / a)² v` (Driver et al., 2017, §7.2, pp. 20–21; form the unit asymptotic effect first, then square, then multiply by `v`; `v ≥ 0`; a zero coefficient or zero predictor variance is exactly zero; `(B / a)² v` is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`), recovers the exact scalar Table 2 `asymCINT` `-κ / a` (Driver et al., 2017, Table 2, p. 12; Eq. 3 as `Δt → ∞`; JSS PDF opened 2026-08-21T16:13Z; form `κ` first, then divide by `-a`; `a < 0`; a zero intercept is exactly zero; `-κ / a` is not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), recovers the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z; `TDPREDEFFECT` is `M`, not `CINT`; `M x` is not `A^{-1}[e^{A Δt} − I] B z`; `M x` is not Voelkle et al., 2012, Eq. 14; the level-change form is not that impulse), recovers the exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition; JSS PDF re-opened 2026-08-20T09:01Z; the evolved map `τ + λ μ_t` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-impulse latent mean is not `E(y_t)`), recovers the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z; `TIPREDEFFECT` is `B`, not `κ`; form `B z` first, then the discrete intercept map; a zero drift is `B z Δt`; `B` is not the discrete increment; `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle Eq. 14), recovers the exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; JSS PDF re-opened 2026-08-20T12:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-increment latent mean is not `E(y_t)`), recovers the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z; form `m x` first, then `e^{a(t−u)} m x`; a zero drift is `m x`; underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept; `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; an impulse at `u = t` is the contemporaneous map; an impulse at `u ≤ t0` is already in `η(t0)`), recovers the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; JSS PDF re-opened 2026-08-20T05:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the carried latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and its Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF opened 2026-08-20T15:14Z; form `t0_b z` first, then `e^{a Δt} t0_b z`; a zero drift is `t0_b z`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; `e^{A Δt} t0_b z` is not `t0_b z`; `T0TIPREDEFFECT` is the coefficient, not the shift), recovers the exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition; JSS PDF re-opened 2026-08-20T15:28Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and its Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; form `t0_m x0` first, then `e^{a Δt} t0_m x0`; a zero drift is `t0_m x0`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `T0TDPREDEFFECT` is the coefficient, not the shift; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), recovers the exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; the first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar §7.2 level-change `CINT` setting `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z; form `m x` first, then multiply by `−a`; `a < 0` so `−κ / a = m x`; `a ≥ 0` cannot hold a new process mean; `−a m x` is not the dissipating Dirac, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`; the extra near-zero-drift latent process also named in §7.2 is a different specification), recovers the exact scalar Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` (JSS PDF re-opened 2026-08-20T19:50Z; form the level-change `CINT` first, then the discrete intercept map; underflow of `e^{a Δt}` to `+0` keeps `m x`; `(1 − e^{a Δt}) m x` is not `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`), recovers the exact scalar §7.2 extra near-zero-drift latent process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z; form `a_{ηξ} x` first; `ε = a` is `a_{ηξ} x Δt e^{a Δt}`; a zero coupling or zero predictor is exactly zero; `ε ≥ 0` cannot hold a lasting extra state; that contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`), recovers the exact scalar observed mean of that extra-process contribution `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z; the extra process has `LAMBDA` 0 and is not an observed indicator; form the evolved-plus-contribution latent mean first, then `τ + λ` of that mean; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`), recovers the exact scalar after-t0 extra-process contribution `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` and its Eq. 5 observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:32Z; `T0TDPREDEFFECT` uses `Δt` for both the evolution and the extra drive; `TDPREDEFFECT` after `t0` uses `t − u` while `μ_t` still uses `Δt`; an impulse at `u = t0` or `u = t` is not interior; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive), recovers the exact scalar §7.2 `asymTIPREDEFFECT` `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z; form `B z` first, then divide by `-a`; `a < 0`; a zero coefficient or zero predictor is exactly zero; `a ≥ 0` cannot hold a finite process-mean change; `-B z / a` is not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`), recovers the exact scalar §7.2 `addedTIPREDVAR` `(B / a)² v` (Driver et al., 2017, §7.2, pp. 20–21; form the unit asymptotic effect first, then square, then multiply by `v`; `v ≥ 0`; a zero coefficient or zero predictor variance is exactly zero; `(B / a)² v` is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`), recovers the exact scalar Table 2 `asymCINT` `-κ / a` (Driver et al., 2017, Table 2, p. 12; Eq. 3 as `Δt → ∞`; JSS PDF opened 2026-08-21T16:13Z; form `κ` first, then divide by `-a`; `a < 0`; a zero intercept is exactly zero; `-κ / a` is not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`), recovers the exact scalar p. 16 stationary `T0MEANS` `-κ / a + −B z / a` (Driver et al., 2017, p. 16; form the intercept contribution first, then include the TI extra effect, then add; not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. 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/adr/README.md b/docs/adr/README.md index 8fd7c2b6..a864b372 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -10,7 +10,7 @@ Read [`ADR_POLICY.md`](ADR_POLICY.md) first. **Decision status and implementatio | [0002](0002-six-clock-temporal-semantics.md) | Six-clock temporal semantics and fail-closed historical leakage prevention | Accepted | active-PR | Unmerged PR #8 is the canonical Task 3 replacement implementing typed clocks/intervals against the current protected-main lineage; conflicted PR #5 is superseded lineage. Later graph/split enforcement remains target work. | | [0003](0003-relational-event-multiple-membership.md) | Relational event ontology and time-varying cross-classified multiple membership | Accepted | partial | Weighted time-varying membership network/roles are active-PR (PR #12); full multilevel estimators, graph ontology, and persistence remain accepted-target. ADR 0016 owns event-intelligence tasks. | | [0004](0004-shared-multilingual-latent-space.md) | One shared multilingual latent space with explicit invariance status | Accepted | accepted-target | ADR 0012 owns the full topic-estimator/backend/global-topic contract. | -| [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | partial | Input gates, posterior-draw point estimates, Rubin `T` on OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects, exact scalar discrete process noise, lagged latent covariance and unconditional latent variance, stationary within-subject variance (`asymDIFFUSION`), trait-plus-state variance (`TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; Eq. 5 of the contemporaneous impulse is `τ + λ(μ_t + m x)`; `τ + λ μ_t` is not that observed mean; Eq. 5 of the time-independent predictor is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; Eq. 5 of the within-interval impulse carry is `τ + λ(μ_t + e^{a(t−u)} m x)`; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; Table 2 `asymCINT` is `-κ / a` for `a < 0` and is not `κ`, not the finite-interval increment, not `T0MEANS`, and not `-B z / a`; §7.2 `addedTIPREDVAR` is `(B / a)² v` and is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`), irregular already-centered residual lag, and strong-gated latent means are on the stacked psychometric PR; full ESEM/DSEM estimator remains accepted-target. | +| [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | partial | Input gates, posterior-draw point estimates, Rubin `T` on OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects, exact scalar discrete process noise, lagged latent covariance and unconditional latent variance, stationary within-subject variance (`asymDIFFUSION`), trait-plus-state variance (`TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; Eq. 5 of the contemporaneous impulse is `τ + λ(μ_t + m x)`; `τ + λ μ_t` is not that observed mean; Eq. 5 of the time-independent predictor is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; Eq. 5 of the within-interval impulse carry is `τ + λ(μ_t + e^{a(t−u)} m x)`; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; Table 2 `asymCINT` is `-κ / a` for `a < 0` and is not `κ`, not the finite-interval increment, not `T0MEANS`, and not `-B z / a`; p. 16 stationary `T0MEANS` is `-κ / a + −B z / a` and is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean; §7.2 `addedTIPREDVAR` is `(B / a)² v` and is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`), irregular already-centered residual lag, and strong-gated latent means are on the stacked psychometric PR; full ESEM/DSEM estimator remains accepted-target. | | [0006](0006-vram-gpu-nvidia-orchestration.md) | VRAM-adaptive GPU compute and model-credential boundary | Accepted | accepted-target | LLM orchestration policy superseded by ADR 0010; autonomous development authority governed by ADR 0015. | | [0007](0007-rust-workspace-quality-gates.md) | Explicit Rust workspace, pinned toolchains, and exact quality gates | Accepted | implemented-main | ADR 0014 governs scientific/product claim promotion beyond repository-quality tooling. | | [0008](0008-immutable-evidence-identities-digests-and-spans.md) | Immutable evidence identities, `SHA-256` digests, exact spans, and strict wire reconstruction | Accepted | implemented-main | ADR 0013 governs future persistence/reproducibility/split authority. | diff --git a/docs/research/multilevel-event-time-recovery.md b/docs/research/multilevel-event-time-recovery.md index f7320eeb..2f63968b 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -40,15 +40,16 @@ This slice stays inside `psychometric_core`. It does not add a second invariance 34. recover the exact scalar §7.2 `asymTIPREDEFFECT` `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; Eq. 3, p. 5; Table 2, p. 12; JSS PDF opened 2026-08-21T13:08Z; expected total change in process means given a time-independent predictor; form `B z` first, then divide by `-a`; `a < 0`; a zero coefficient or zero predictor is exactly zero) and refuse treating `-B z / a` as the coefficient `B`, as `A^{-1}[e^{A Δt} − I] B z`, as `CINT`, or as `M x`; `a ≥ 0` cannot hold a finite process-mean change; 35. recover the exact scalar §7.2 `addedTIPREDVAR` `(B / a)² v` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z; stable between-subject variance accounted for by a time-independent predictor; form the unit asymptotic effect first, then square, then multiply by `v`; `v ≥ 0`; a zero coefficient or zero predictor variance is exactly zero) and refuse treating `(B / a)² v` as `TRAITVAR`, as `asymDIFFUSION`, or as `-B z / a`; 36. recover the exact scalar Table 2 `asymCINT` `-κ / a` (Driver et al., 2017, Table 2, p. 12; Eq. 3, p. 5; §4.3 / p. 16; JSS PDF opened 2026-08-21T16:13Z; expected change in process means for a unit intercept; form `κ` first, then divide by `-a`; `a < 0`; a zero intercept is exactly zero) and refuse treating `-κ / a` as `κ`, as `A^{-1}[e^{A Δt} − I] κ`, as `T0MEANS`, or as `-B z / a`; `a ≥ 0` cannot hold a finite process-mean change; p. 16 `T0MEANS` stationarity includes TI predictors and is not this intercept-only map; -37. refuse pooling discrete lags from unequal event intervals as one coefficient; -38. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); -39. refuse the difference quotient as a continuous-time rate; -40. apply the same event-time map to CWC residuals (still not DSEM); -41. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). +37. recover the exact scalar p. 16 stationary `T0MEANS` `-κ / a + −B z / a` (Driver et al., 2017, p. 16; Table 2, p. 12; Eq. 3, p. 5; JSS PDF opened 2026-08-21T16:13Z; constrain `T0MEANS` to model-implied values using `T0MEANSbase` / `T0MEANSfree`; form the intercept contribution first, then include the TI extra effect, then add; a zero intercept and a zero TI contribution is exactly zero) and refuse treating that composition as free `T0MEANS`, as `asymCINT` alone, as `asymTIPREDEFFECT` alone, or as the finite-interval discrete latent mean; +38. refuse pooling discrete lags from unequal event intervals as one coefficient; +39. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); +40. refuse the difference quotient as a continuous-time rate; +41. apply the same event-time map to CWC residuals (still not DSEM); +42. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). ## Claim boundary -This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. The contemporaneous `TDPREDEFFECT` impulse is not the continuous intercept, not the time-independent discrete effect, and not Voelkle et al. (2012, Eq. 14). The time-independent `TIPREDEFFECT` increment is not the continuous intercept, not the contemporaneous impulse, not Voelkle et al. (2012, Eq. 14), and not the coefficient `B`. The within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). The evolved observed mean `τ + λ μ_t` is not the contemporaneous-impulse observed mean `τ + λ(μ_t + m x)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not the impulse-carry observed mean `τ + λ(μ_t + e^{a(t−u)} m x)` when `u ≠ t`. The evolved observed mean `τ + λ μ_t` is not the impulse-carry observed mean. The carried latent mean is not `E(y_t)`. The evolved-plus-impulse latent mean is not `E(y_t)`. The evolved observed mean `τ + λ μ_t` is not the time-independent-predictor observed mean `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not that time-independent-predictor observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that time-independent-predictor observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TIPREDEFFECT` shift `t0_b z` is not the Eq. 3 process increment `A^{-1}[e^{A Δt} − I] B z`, not `κ`, and not `M x`. The Eq. 3 first-summand carry `e^{A Δt} t0_b z` is not `t0_b z` and is not that process increment. `T0TIPREDEFFECT` is the coefficient, not the first-occasion shift. The evolved observed mean `τ + λ μ_t` is not the first-occasion TI-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_b z)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion observed mean when `u ≠ t0`. The evolved-plus-T0TIPRED latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TDPREDEFFECT` shift `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `κ`. The Eq. 3 first-summand carry `e^{A Δt} t0_m x0` is not `t0_m x0` and is not that within-interval impulse carry. `T0TDPREDEFFECT` is the coefficient, not the first-occasion shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The evolved observed mean `τ + λ μ_t` is not the first-occasion TD-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_m x0)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion TD observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion TD observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion TD observed mean when `u ≠ t0`. The first-occasion TI composition `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that first-occasion TD observed mean. Same numbers as `T0TIPREDEFFECT` yield the same product; Table 3 names a different matrix. The evolved-plus-T0TDPRED latent mean is not `E(y_t)`. The §7.2 level-change `CINT` `κ = −a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. Lasting level change via that `CINT` setting requires `a < 0`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not that `CINT` setting. The Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` is not the dissipating Dirac `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`. Underflow of `e^{a Δt}` to `+0` keeps the equilibrium offset `m x`. The §7.2 extra-process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. Lasting level change via that extra process requires `ε < 0`. Precisely `ε = 0` causes computational problems in the printed specification. The extra-process observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` is not `τ + λ μ_t`, not `τ + λ(μ_t + m x)`, not the contribution, and not the evolved-plus-contribution latent mean. The extra process has `LAMBDA` 0 and is not an observed indicator. `T0TDPREDEFFECT` on the extra process uses `Δt = t − t0` for both the original-process evolution and the extra drive. `TDPREDEFFECT` after `t0` uses `t − u` with `t0 < u < t` for the extra drive while `μ_t` still uses `Δt`. The after-t0 extra-process observed mean is not the first-occasion extra-process observed mean when `u ≠ t0`. The impulse-carry `e^{a(t−u)} m x` is a Dirac on the original process and is not that `DRIFT` drive. The §7.2 `asymTIPREDEFFECT` `-B z / a` is the expected total change in process means given a time-independent predictor. It is not the coefficient `B`, not the finite-interval increment `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. Lasting asymptotic change via that map requires `a < 0`. The §7.2 `addedTIPREDVAR` `(B / a)² v` is the stable between-subject variance accounted for by that predictor. It is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`. Table 2 `asymCINT` `-κ / a` is the intercept contribution to the stationary process mean. It is not `κ`, not the finite-interval increment `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`. Lasting asymptotic intercept change via that map requires `a < 0`. Page 16 notes that a `T0MEANS` stationarity constraint includes time-independent predictors; that composition is not this intercept-only map. +This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. The contemporaneous `TDPREDEFFECT` impulse is not the continuous intercept, not the time-independent discrete effect, and not Voelkle et al. (2012, Eq. 14). The time-independent `TIPREDEFFECT` increment is not the continuous intercept, not the contemporaneous impulse, not Voelkle et al. (2012, Eq. 14), and not the coefficient `B`. The within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). The evolved observed mean `τ + λ μ_t` is not the contemporaneous-impulse observed mean `τ + λ(μ_t + m x)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not the impulse-carry observed mean `τ + λ(μ_t + e^{a(t−u)} m x)` when `u ≠ t`. The evolved observed mean `τ + λ μ_t` is not the impulse-carry observed mean. The carried latent mean is not `E(y_t)`. The evolved-plus-impulse latent mean is not `E(y_t)`. The evolved observed mean `τ + λ μ_t` is not the time-independent-predictor observed mean `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not that time-independent-predictor observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that time-independent-predictor observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TIPREDEFFECT` shift `t0_b z` is not the Eq. 3 process increment `A^{-1}[e^{A Δt} − I] B z`, not `κ`, and not `M x`. The Eq. 3 first-summand carry `e^{A Δt} t0_b z` is not `t0_b z` and is not that process increment. `T0TIPREDEFFECT` is the coefficient, not the first-occasion shift. The evolved observed mean `τ + λ μ_t` is not the first-occasion TI-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_b z)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion observed mean when `u ≠ t0`. The evolved-plus-T0TIPRED latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TDPREDEFFECT` shift `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `κ`. The Eq. 3 first-summand carry `e^{A Δt} t0_m x0` is not `t0_m x0` and is not that within-interval impulse carry. `T0TDPREDEFFECT` is the coefficient, not the first-occasion shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The evolved observed mean `τ + λ μ_t` is not the first-occasion TD-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_m x0)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion TD observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion TD observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion TD observed mean when `u ≠ t0`. The first-occasion TI composition `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that first-occasion TD observed mean. Same numbers as `T0TIPREDEFFECT` yield the same product; Table 3 names a different matrix. The evolved-plus-T0TDPRED latent mean is not `E(y_t)`. The §7.2 level-change `CINT` `κ = −a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. Lasting level change via that `CINT` setting requires `a < 0`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not that `CINT` setting. The Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` is not the dissipating Dirac `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`. Underflow of `e^{a Δt}` to `+0` keeps the equilibrium offset `m x`. The §7.2 extra-process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. Lasting level change via that extra process requires `ε < 0`. Precisely `ε = 0` causes computational problems in the printed specification. The extra-process observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` is not `τ + λ μ_t`, not `τ + λ(μ_t + m x)`, not the contribution, and not the evolved-plus-contribution latent mean. The extra process has `LAMBDA` 0 and is not an observed indicator. `T0TDPREDEFFECT` on the extra process uses `Δt = t − t0` for both the original-process evolution and the extra drive. `TDPREDEFFECT` after `t0` uses `t − u` with `t0 < u < t` for the extra drive while `μ_t` still uses `Δt`. The after-t0 extra-process observed mean is not the first-occasion extra-process observed mean when `u ≠ t0`. The impulse-carry `e^{a(t−u)} m x` is a Dirac on the original process and is not that `DRIFT` drive. The §7.2 `asymTIPREDEFFECT` `-B z / a` is the expected total change in process means given a time-independent predictor. It is not the coefficient `B`, not the finite-interval increment `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. Lasting asymptotic change via that map requires `a < 0`. The §7.2 `addedTIPREDVAR` `(B / a)² v` is the stable between-subject variance accounted for by that predictor. It is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`. Table 2 `asymCINT` `-κ / a` is the intercept contribution to the stationary process mean. It is not `κ`, not the finite-interval increment `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`. Lasting asymptotic intercept change via that map requires `a < 0`. Page 16 notes that a `T0MEANS` stationarity constraint includes time-independent predictors; that composition is not this intercept-only map. The p. 16 constrained first-occasion mean `-κ / a + −B z / a` is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean. ## Authoritative sources @@ -104,6 +105,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - **Asymptotic time-independent predictor effect.** Driver et al. (2017, §7.2, pp. 20–21; Eq. 3, p. 5; Table 2, p. 12; JSS PDF opened 2026-08-21T13:08Z): Table 2 names `B` `TIPREDEFFECT`. Equation 3 maps a finite event interval as `A^{-1}[e^{AΔt}−I]Bz`. Section 7.2 names `asymTIPREDEFFECT` the expected total change in process means given an increase of 1 on a time-independent predictor. For stable `a<0` that `Δt→∞` limit is `-A^{-1}B`. The scalar map is `-Bz/a`. Form `Bz` first, then divide by `-a`. A zero coefficient or zero predictor is exactly zero. `a≥0` cannot hold a finite process-mean change. `-Bz/a` is not the coefficient `B`, not `A^{-1}[e^{AΔt}−I]Bz`, not `CINT`, and not `Mx`. An overflowing product or quotient fails closed. This is not a Kalman filter and not ctsem estimation. - **Asymptotic time-independent predictor variance.** Driver et al. (2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z): Section 7.2 names `addedTIPREDVAR` the stable between-subject variance accounted for by time-independent predictors. For predictor variance `v≥0` the scalar map is `(B/a)²v`. Form the unit asymptotic effect `-B/a` first, then square, then multiply by `v`. A zero coefficient or zero predictor variance is exactly zero. `v<0` fails closed. `(B/a)²v` is not `TRAITVAR`, not `asymDIFFUSION`, and not `-Bz/a`. The printed 2-latent `addedTIPREDVAR` 2.838 is not this scalar map. An overflowing product fails closed. This is not a Kalman filter and not ctsem estimation. - **Asymptotic continuous intercept.** Driver et al. (2017, Table 2, p. 12; Eq. 3, p. 5; §4.3 / p. 16; JSS PDF opened 2026-08-21T16:13Z): Table 2 names `κ` `CINT` and names `asymCINT` the asymptotic (`Δt=∞`) expected change in processes for a 1 unit change in intercept. Equation 3 maps a finite event interval as `A^{-1}[e^{AΔt}−I]κ`. For stable `a<0` that `Δt→∞` limit is `-A^{-1}κ`. The scalar map is `-κ/a`. A unit intercept is `-1/a`. Form `κ` first, then divide by `-a`. A zero intercept is exactly zero. `a≥0` cannot hold a finite process-mean change. `-κ/a` is not `κ`, not `A^{-1}[e^{AΔt}−I]κ`, not `T0MEANS`, and not `-Bz/a`. Page 16 notes that a `T0MEANS` stationarity constraint includes time-independent predictors; that composition is not this intercept-only map. The printed 2-latent `CINT` values are not this scalar map. An overflowing quotient fails closed. This is not a Kalman filter and not ctsem estimation. +- **Stationary first-occasion latent mean.** Driver et al. (2017, p. 16; Table 2, p. 12; Eq. 3, p. 5; JSS PDF opened 2026-08-21T16:13Z): when the first observation is determined by the process in the same way as later observations, `T0MEANS` is constrained to the model-implied values using `T0MEANSbase` / `T0MEANSfree`. Those constraints include extra effects due to time-independent predictors (`asymTIPREDEFFECT`). For stable `a<0` the scalar composition is `-κ/a + −Bz/a`. Form the intercept contribution first, then include the TI extra effect, then add. A zero intercept and a zero TI contribution is exactly zero. `a≥0` cannot hold a finite process-mean change when either contribution is nonzero. That constrained first-occasion mean is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not `exp(aΔt)μ_0+(exp(aΔt)−1)/a κ`. The printed 2-latent `T0MEANS` 2.823 is not this scalar map. An overflowing sum fails closed. This is not a Kalman filter and not ctsem estimation. - **Level-change discrete increment.** Driver et al. (2017, §7.2, pp. 20–21; Eq. 3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T19:50Z): Equation 3 maps `CINT` through `A^{-1}[e^{AΔt}−I]κ`. With `κ=−a m x` the scalar increment is `(e^{aΔt}−1)/a·(−a m x)=(1−e^{aΔt})m x`. Form the level-change `CINT` first, then the discrete intercept map. Underflow of `e^{aΔt}` to `+0` keeps `m x`. A zero effect or zero predictor is exactly zero. `(1−e^{aΔt})m x` is not `m x`, not `κ`, and not `A^{-1}[e^{AΔt}−I]Bz`. An overflowing product or increment fails closed. This is not a Kalman filter and not ctsem estimation. - **CWC-then-lag.** Sample cluster means are removed first. Consecutive within residuals are then fitted by least squares to \(r_{t+\Delta t}\approx\exp(a\Delta t)\,r_{t}\) on event time. Same-sign pair-wise logs initialize the scalar Newton step. Sign-flipping \(T=2\) CWC pairs have no real logarithm and fail closed. Curran and Bauer (2011, pp. 607–608) show that this person-mean subtraction on a raw autoregressive series does **not** isolate the lagged within-person effect; the helper therefore does not claim to recover the raw-process drift. - **Already-centered irregular residual.** The caller supplies lagged within residuals. The mean of \(a=\ln(r_{t+\Delta t}/r_t)/\Delta t\) is the exact scalar map. Intervals may be irregular. The helper does not center again. This is not DSEM. @@ -145,6 +147,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - Driver et al. (2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z) recovers a known `asymTIPREDEFFECT` \(-Bz/a\) at machine-scale RMSE, and that RMSE is smaller than treating the coefficient `B`, the finite-interval increment \(A^{-1}[e^{A\Delta t}-I]Bz\), `CINT`, or \(Mx\) as that total change; printed LeisureTime \(-0.225/-1.673\) and Happiness \(0.549/0.219\) reconstruct under the map; a zero coefficient or zero predictor is exactly zero; \(a\ge 0\) with a nonzero effect fails closed; a non-event clock and an overflowing product or quotient fail closed; - Driver et al. (2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z) recovers a known `addedTIPREDVAR` \((B/a)^{2}v\) at machine-scale RMSE, and that RMSE is smaller than treating `TRAITVAR`, `asymDIFFUSION`, or `-B z / a` as that variance; a zero coefficient or zero predictor variance is exactly zero; \(v<0\) fails closed; \(a\ge 0\) with a nonzero variance fails closed; a non-event clock and an overflowing product fail closed; - Driver et al. (2017, Table 2, p. 12; Eq. 3 as \(\Delta t\to\infty\); JSS PDF opened 2026-08-21T16:13Z) recovers a known `asymCINT` \(-\kappa/a\) at machine-scale RMSE, and that RMSE is smaller than treating `CINT`, the finite-interval increment \(A^{-1}[e^{A\Delta t}-I]\kappa\), `T0MEANS`, or `-B z / a` as that total change; a large finite \(\Delta t\) discrete increment converges on \(-\kappa/a\); a zero intercept is exactly zero even if \(a\ge 0\); \(a\ge 0\) with a nonzero intercept fails closed; a non-event clock and an overflowing quotient fail closed; +- Driver et al. (2017, p. 16; Table 2, p. 12; Eq. 3; JSS PDF opened 2026-08-21T16:13Z) recovers a known stationary `T0MEANS` \(-\kappa/a + -Bz/a\) at machine-scale RMSE, and that RMSE is smaller than treating free `T0MEANS`, `asymCINT` alone, `asymTIPREDEFFECT` alone, or the finite-interval discrete latent mean as that constraint; a zero intercept and a zero TI contribution is exactly zero even if \(a\ge 0\); \(a\ge 0\) with a nonzero intercept or TI contribution fails closed; a non-event clock and an overflowing sum fail closed; - pooling discrete lags from unequal intervals fails closed; - CWC-then-lag on a two-cluster decaying series has smaller computed RMSE than a level-pooled series when the latter is identified; - already-centered irregular residuals recover a known drift at machine-scale RMSE, and that RMSE is smaller than CWC of the corresponding raw autoregressive series (Curran & Bauer, 2011, pp. 607–608); diff --git a/docs/validation/temporal-event-foundation.md b/docs/validation/temporal-event-foundation.md index 315c301b..c2ef996c 100644 --- a/docs/validation/temporal-event-foundation.md +++ b/docs/validation/temporal-event-foundation.md @@ -23,7 +23,7 @@ This report tracks exact-head scientific and engineering evidence required befor | Truth corpora / manifests | `tepp_simulation` | implemented-main | — | deterministic generator tests | Task 10 / PR #18 | | Recovery metrics | `validation_core` | implemented-main | — | RMSE/bias/coverage/MC gates | Task 11 / PR #19 | | Versioned API/export contracts | `tepp_api` | implemented-main | naruon HTTP interchange | unknown-field/version/limit + naruon HTTPS interchange tests | Task 12 / PR #21; live HTTP service remaining | -| Psychometric structural input gates | `psychometric_core` | partial | stacked psychometric PR | construct-class refusal + ALR/ILR boundary + true-loading RMSE + posterior-draw point-estimate mean + Rubin `T` + CWC within/between + CWC contextual effect + event-time log-rate + constant- and time-varying-predictor discrete effects + exact scalar discrete process noise + lagged latent covariance and unconditional latent variance + stationary within-subject variance + trait-plus-state variance + observed-indicator variance + discrete latent mean (`T0MEANS`/`CINT`) + evolved observed mean (`τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`) + contemporaneous `TDPREDEFFECT` impulse (`m x`; not `CINT`, not `TIPREDEFFECT`, not Voelkle Eq. 14) + Eq. 5 of that contemporaneous impulse (`τ + λ(μ_t + m x)`; `τ + λ μ_t` is not that observed mean) + time-independent `TIPREDEFFECT` increment (`A^{-1}[e^{A Δt} − I] B z`; not `CINT`, not `M x`, not Voelkle Eq. 14, not the coefficient `B`) + Eq. 5 of that increment (`τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`; `τ + λ μ_t` is not that observed mean) + within-interval `TDPREDEFFECT` carry (`e^{A(t−u)} M x` for `t0 < u < t`; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, not Voelkle Eq. 14) + Eq. 5 of that carry (`τ + λ(μ_t + e^{a(t−u)} m x)`; `τ + λ μ_t` is not that observed mean) + §7.2 level-change `CINT` (`κ = −a m x`; Eq. 3 increment `(1 − e^{a Δt}) m x`) + §7.2 extra-process contribution (`a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; not `κ`, not the increment, not the Dirac; `ε ≥ 0` fails closed) + Eq. 5 of that extra-process contribution (`τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; extra `LAMBDA` is 0; `τ + λ μ_t` is not that observed mean) + after-t0 extra-process `TDPREDEFFECT` (`a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t`; Eq. 5 `τ + λ(μ_t + contribution(t−u))`; not the first-occasion extra-process observed mean; not the impulse-carry Dirac) + §7.2 `asymTIPREDEFFECT` (`-B z / a` for `a < 0`; not `B`, not the finite-interval increment, not `CINT`, not `M x`) + §7.2 `addedTIPREDVAR` (`(B / a)² v`; not `TRAITVAR`, not `asymDIFFUSION`, not `-B z / a`) + Table 2 `asymCINT` (`-κ / a` for `a < 0`; not `κ`, not the finite-interval increment, not `T0MEANS`, not `-B z / a`) + irregular already-centered residual lag + strong-gated latent means (n=2 residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016); full ESEM/DSEM remaining | ADR 0005; `docs/research/posterior-esem-input-gates.md`; `docs/research/multilevel-event-time-recovery.md`; `docs/research/rubin-total-variance.md`; `docs/research/strong-invariance-latent-means.md` | +| Psychometric structural input gates | `psychometric_core` | partial | stacked psychometric PR | construct-class refusal + ALR/ILR boundary + true-loading RMSE + posterior-draw point-estimate mean + Rubin `T` + CWC within/between + CWC contextual effect + event-time log-rate + constant- and time-varying-predictor discrete effects + exact scalar discrete process noise + lagged latent covariance and unconditional latent variance + stationary within-subject variance + trait-plus-state variance + observed-indicator variance + discrete latent mean (`T0MEANS`/`CINT`) + evolved observed mean (`τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`) + contemporaneous `TDPREDEFFECT` impulse (`m x`; not `CINT`, not `TIPREDEFFECT`, not Voelkle Eq. 14) + Eq. 5 of that contemporaneous impulse (`τ + λ(μ_t + m x)`; `τ + λ μ_t` is not that observed mean) + time-independent `TIPREDEFFECT` increment (`A^{-1}[e^{A Δt} − I] B z`; not `CINT`, not `M x`, not Voelkle Eq. 14, not the coefficient `B`) + Eq. 5 of that increment (`τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`; `τ + λ μ_t` is not that observed mean) + within-interval `TDPREDEFFECT` carry (`e^{A(t−u)} M x` for `t0 < u < t`; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, not Voelkle Eq. 14) + Eq. 5 of that carry (`τ + λ(μ_t + e^{a(t−u)} m x)`; `τ + λ μ_t` is not that observed mean) + §7.2 level-change `CINT` (`κ = −a m x`; Eq. 3 increment `(1 − e^{a Δt}) m x`) + §7.2 extra-process contribution (`a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; not `κ`, not the increment, not the Dirac; `ε ≥ 0` fails closed) + Eq. 5 of that extra-process contribution (`τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; extra `LAMBDA` is 0; `τ + λ μ_t` is not that observed mean) + after-t0 extra-process `TDPREDEFFECT` (`a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t`; Eq. 5 `τ + λ(μ_t + contribution(t−u))`; not the first-occasion extra-process observed mean; not the impulse-carry Dirac) + §7.2 `asymTIPREDEFFECT` (`-B z / a` for `a < 0`; not `B`, not the finite-interval increment, not `CINT`, not `M x`) + §7.2 `addedTIPREDVAR` (`(B / a)² v`; not `TRAITVAR`, not `asymDIFFUSION`, not `-B z / a`) + Table 2 `asymCINT` (`-κ / a` for `a < 0`; not `κ`, not the finite-interval increment, not `T0MEANS`, not `-B z / a`) + p. 16 stationary `T0MEANS` (`-κ / a + −B z / a`; not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, not the finite-interval discrete mean) + irregular already-centered residual lag + strong-gated latent means (n=2 residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016); full ESEM/DSEM remaining | ADR 0005; `docs/research/posterior-esem-input-gates.md`; `docs/research/multilevel-event-time-recovery.md`; `docs/research/rubin-total-variance.md`; `docs/research/strong-invariance-latent-means.md` | | Purpose-bound provider payloads | `tepp_api` | implemented-main | provider-payload minimization | expired/not-yet-valid/inverted/cross-tenant/impossible-calendar grant, mapping refusal, audited elevated re-id replay | ADR 0009; `docs/research/provider-payload-minimization.md` | | Adaptive orchestration router | `tepp_api` | accepted-target | active PR | mode selection, document-control denial, ablation, credential-free bind | ADR 0010; `docs/research/adaptive-orchestration-router.md` | | CWL modular connectors | `docs/connectors/*` | implemented-main | — | contract docs + examples | PR #22; live HTTP ports remaining | From 274556d1e467a14f16730d9cc0d31e76e4dd2161 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 20:18:02 +0000 Subject: [PATCH 60/62] =?UTF-8?q?feat(psychometric):=20recover=20Driver=20?= =?UTF-8?q?=C2=A74.3=20Eq.=205=20of=20stationary=20T0MEANS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Form the constrained first-occasion mean first, then τ + λ of that mean. E(y_0) = τ + λ(−κ/a + −Bz/a). Not free T0MEANS, not asymCINT alone, not evolved τ + λ μ_t, and not MANIFESTMEANS. --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 1 + CLAUDE.md | 2 +- crates/psychometric_core/src/error.rs | 56 +++ crates/psychometric_core/src/event_time.rs | 400 +++++++++++++++++- crates/psychometric_core/src/lib.rs | 32 +- ...multilevel_event_time_recovery_contract.rs | 162 ++++++- .../scientific_claim_boundary_contract.rs | 107 ++++- docs/TRACEABILITY.md | 2 +- docs/adr/0005-posterior-esem-dsem.md | 4 +- docs/adr/README.md | 2 +- .../multilevel-event-time-recovery.md | 14 +- docs/validation/temporal-event-foundation.md | 2 +- 13 files changed, 756 insertions(+), 30 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 50a1f3d1..28063b1d 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -61,7 +61,7 @@ boundaries above remain the target modular MSA architecture. | `tepp_simulation` | known-truth temporal/event data generation | | `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics | | `tepp_api` | versioned DTO, schema, and export contracts | -| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; first-occasion `T0TIPREDEFFECT` shift is `t0_b z` and Eq. 3 first-summand carry is `e^{A Δt} t0_b z` (`T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_b z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), first-occasion `T0TDPREDEFFECT` shift is `t0_m x0` and Eq. 3 first-summand carry is `e^{A Δt} t0_m x0` (`T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; §7.2 level-change `CINT` is `κ = −a m x` with `a < 0` so `−κ / a = m x` (`−a m x` is not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2; Eq. 3 of that setting is `(1 − e^{a Δt}) m x`, which is not `m x`, not `κ`, and not `TIPREDEFFECT`; §7.2 extra-process contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`; identification `TDPREDEFFECT` on the extra process is 1; printed extra `DRIFT` is `−0.000001`; not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`; `ε ≥ 0` fails closed; Eq. 5 of that contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; the extra process has `LAMBDA` 0 and is not an observed indicator; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`; after-t0 extra-process `TDPREDEFFECT` is `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` while `μ_t` uses `Δt`; Eq. 5 of that after-t0 contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)`; the first-occasion extra-process observed mean is not that observed mean when `u ≠ t0`; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive; §7.2 `asymTIPREDEFFECT` is `-B z / a` for `a < 0` (`-B z / a` is not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; §7.2 `addedTIPREDVAR` is `(B / a)² v`, not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`; Table 2 `asymCINT` is `-κ / a` for `a < 0` and is not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`; p. 16 stationary `T0MEANS` is `-κ / a + −B z / a` and is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean)), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | +| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; first-occasion `T0TIPREDEFFECT` shift is `t0_b z` and Eq. 3 first-summand carry is `e^{A Δt} t0_b z` (`T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_b z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), first-occasion `T0TDPREDEFFECT` shift is `t0_m x0` and Eq. 3 first-summand carry is `e^{A Δt} t0_m x0` (`T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; §7.2 level-change `CINT` is `κ = −a m x` with `a < 0` so `−κ / a = m x` (`−a m x` is not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2; Eq. 3 of that setting is `(1 − e^{a Δt}) m x`, which is not `m x`, not `κ`, and not `TIPREDEFFECT`; §7.2 extra-process contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`; identification `TDPREDEFFECT` on the extra process is 1; printed extra `DRIFT` is `−0.000001`; not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`; `ε ≥ 0` fails closed; Eq. 5 of that contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; the extra process has `LAMBDA` 0 and is not an observed indicator; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`; after-t0 extra-process `TDPREDEFFECT` is `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` while `μ_t` uses `Δt`; Eq. 5 of that after-t0 contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)`; the first-occasion extra-process observed mean is not that observed mean when `u ≠ t0`; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive; §7.2 `asymTIPREDEFFECT` is `-B z / a` for `a < 0` (`-B z / a` is not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; §7.2 `addedTIPREDVAR` is `(B / a)² v`, not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`; Table 2 `asymCINT` is `-κ / a` for `a < 0` and is not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`; p. 16 stationary `T0MEANS` is `-κ / a + −B z / a` and is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean; Eq. 5 of that constrained mean is `τ + λ(−κ / a + −B z / a)`; `τ + λ μ_0` is not that observed mean; `τ + λ(−κ / a)` is not that observed mean when `B z ≠ 0`; `τ + λ μ_t` is not that observed mean; `MANIFESTMEANS` is not `E(y_0)`; the constrained latent mean is not `E(y_0)`)), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | No crate exposes placeholder production behavior in Task 1. This prevents an empty façade from becoming a de facto public API before its invariants and tests diff --git a/CHANGELOG.md b/CHANGELOG.md index edafb740..388b4a48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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, §4.3, pp. 9–10; Eq. 5, p. 5; Table 2, p. 12; Eq. 3, p. 5; JSS PDF re-opened 2026-08-21T20:07Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar observed-indicator mean of §4.3 stationary `T0MEANS`. Section 4.3 constrains the first-occasion mean to the model-predicted mean when `stationary` includes `"T0MEANS"`. Equation 5 writes `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and `Γ ~ N(τ, Ψ)`. The constrained latent mean is `-κ / a + −B z / a`. The scalar composition is `E(y_0) = τ + λ(−κ / a + −B z / a)`. Form the stationary latent mean first, then `τ + λ` of that mean. A zero loading is exactly `τ`. A zero intercept and a zero TI contribution is exactly `τ`. Evolving from that stationary start with `CINT` and `TIPREDEFFECT` stays at the stationary mean. `τ + λ μ_0` for free `T0MEANS` is not this composition. `τ + λ(−κ / a)` is not this composition when `B z ≠ 0`. `τ + λ μ_t` is not this composition. `MANIFESTMEANS` is not `E(y_0)`. The constrained latent mean is not `E(y_0)`. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) remains unread (Unpaywall request empty this cycle). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-21T20:10Z: `is_oa: false`; Springer `content/pdf` is HTML 200). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, p. 16; Table 2, p. 12; Eq. 3, p. 5; JSS PDF opened 2026-08-21T16:13Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar stationary `T0MEANS`. Page 16 constrains `T0MEANS` to the model-implied values using `T0MEANSbase` / `T0MEANSfree` when the first observation is determined by the process in the same way as later observations. Those constraints include extra effects due to time-independent predictors (`asymTIPREDEFFECT`). For stable `a < 0` the scalar composition is `-κ / a + −B z / a`. Form the intercept contribution first, then include the TI extra effect, then add. A zero intercept and a zero TI contribution is exactly zero. `a ≥ 0` cannot hold a finite process-mean change when either contribution is nonzero and fails closed. That constrained first-occasion mean is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean. The printed 2-latent `T0MEANS` 2.823 is not this scalar map. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-21T16:21Z: `is_oa: false`; Springer `content/pdf` is HTML 200). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Table 2, p. 12; Eq. 3, p. 5; §4.3 / p. 16; JSS PDF opened 2026-08-21T16:13Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar `asymCINT`. Table 2 names `κ` `CINT` and names `asymCINT` the asymptotic (`Δt = ∞`) expected change in processes for a 1 unit change in intercept. Equation 3 maps a finite event interval as `A^{-1}[e^{A Δt} − I] κ`. For stable `a < 0` that `Δt → ∞` limit is `-A^{-1} κ`. The scalar map is `-κ / a`. A unit intercept is `-1 / a`. Form `κ` first, then divide by `-a`. A zero intercept is exactly zero. `a ≥ 0` cannot hold a finite process-mean change and fails closed. `-κ / a` is not `κ`, not the finite-interval increment `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `asymTIPREDEFFECT` `-B z / a`. Page 16 notes that a `T0MEANS` stationarity constraint includes time-independent predictors; that composition is not this intercept-only map. The printed 2-latent `CINT` values are not this scalar map. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-21T16:21Z: `is_oa: false`; Springer `content/pdf` is HTML 200). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, §7.2, pp. 20–21; Eq. 3, p. 5; Table 2, p. 12; JSS PDF opened 2026-08-21T13:08Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar `addedTIPREDVAR`. Section 7.2 names that matrix the stable between-subject variance accounted for by time-independent predictors. For stable `a < 0` and predictor variance `v ≥ 0` the scalar map is `(B / a)² v`. Form the unit asymptotic effect `-B / a` first, then square, then multiply by `v`. A zero coefficient or zero predictor variance is exactly zero. `v < 0` fails closed. `a ≥ 0` cannot hold a finite process-mean change and fails closed. `(B / a)² v` is not `TRAITVAR`, not `asymDIFFUSION`, and not the expected total change `-B z / a`. The printed 2-latent `addedTIPREDVAR` 2.838 is not this scalar map. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-21T06:24Z: `is_oa: false`; Springer `content/pdf` is HTML 200). diff --git a/CLAUDE.md b/CLAUDE.md index 41ac1829..0f2829c3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. Putnick and Bornstein (2016, PMC5145197 opened 2026-08-19T22:15Z) require scalar invariance before latent-mean comparison; residual invariance is not a prerequisite. Two-observation series have no residual degrees of freedom (`ordinary_least_squares_fit` returns residual variance `0`) and cap at strong/scalar; they still license means. This is two-group OLS, not MGCFA. Meredith (1993) names remain unread labels. -- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. The contemporaneous time-dependent predictor impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`). Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). The §7.2 level-change form is not that impulse. The observed mean of that contemporaneous impulse is `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-impulse latent mean is not `E(y_t)`. The time-independent predictor increment is `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`). Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. `TIPREDEFFECT` is `B`, not that discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle et al. (2012, Eq. 14). The observed mean of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The within-interval time-dependent impulse carry is `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation). Form `m x` first, then `e^{a(t−u)} m x`. A zero drift is `m x` with no dissipation. Underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept. `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). An impulse at `u = t` is the contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. The observed mean of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The carried latent mean is not `E(y_t)`. The first-occasion time-independent predictor shift is `t0_b z` (Driver et al., 2017, Table 3 `T0TIPREDEFFECT`; Eq. 3 first summand). Form `t0_b z` first, then `e^{a Δt} t0_b z`. Form `μ_t` first, then add that carry. A zero drift is `t0_b z`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. `e^{A Δt} t0_b z` is not `t0_b z`. `T0TIPREDEFFECT` is the coefficient, not the shift. The observed mean of that first-occasion carry is `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The evolved-plus-carry latent mean is not `E(y_t)`. The first-occasion time-dependent predictor shift is `t0_m x0` (Driver et al., 2017, Table 3 `T0TDPREDEFFECT`; Eq. 3 first summand; JSS PDF re-opened 2026-08-20T19:10Z). Form `t0_m x0` first, then `e^{a Δt} t0_m x0`. Form `μ_t` first, then add that carry. A zero drift is `t0_m x0`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`. `e^{A Δt} t0_m x0` is not `t0_m x0`. `T0TDPREDEFFECT` is the coefficient, not the shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The observed mean of that first-occasion TD carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean. The evolved-plus-carry latent mean is not `E(y_t)`. The lasting level-change `CINT` is `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z). Form `m x` first, then multiply by `−a`. Stable `a < 0` is required so `−κ / a = m x` is an equilibrium offset. `a ≥ 0` cannot hold a new process mean. `−a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not this `CINT` setting. Equation 3 maps that intercept as `(1 − e^{a Δt}) m x` (JSS PDF re-opened 2026-08-20T19:50Z). Form the level-change `CINT` first, then the discrete intercept map. Underflow of `e^{a Δt}` to `+0` keeps `m x`. `(1 − e^{a Δt}) m x` is not `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`. The printed §7.2 lasting level change is an extra near-zero-drift latent process (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z). `T0MEANS`, `CINT`, `T0VAR`, `DIFFUSION`, and `TRAITVAR` of that process are fixed to 0; `TDPREDEFFECT` on it is fixed to 1; its `DRIFT` diagonal is very close to 0 (printed example `−0.000001`; precisely 0 causes computational problems); the original process is driven by the `DRIFT` coupling `a_{ηξ}`. After a unit identification impulse the scalar contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`). Form `a_{ηξ} x` first. A zero coupling or zero predictor is exactly zero. `ε ≥ 0` fails closed. That contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. The observed mean of that extra-process contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5 of that §7.2 contribution; JSS PDF re-opened 2026-08-21T06:12Z). The extra process has `LAMBDA` 0 and is not an observed indicator. Original indicators load on the original process after the `DRIFT` coupling. The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The contribution is not `E(y_t)`. The evolved-plus-contribution latent mean is not `E(y_t)`. `T0TDPREDEFFECT` on the extra process begins at `t = 0` and uses `Δt = t − t0` for both the original-process evolution and the extra drive. `TDPREDEFFECT` after `t0` uses `t − u` with `t0 < u < t` for the extra drive while `μ_t` still uses `Δt`. The observed mean of that after-t0 extra-process contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, Eq. 5 of that §7.2 after-t0 contribution; JSS PDF re-opened 2026-08-21T06:32Z). The first-occasion extra-process observed mean is not that observed mean when `u ≠ t0`. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is a Dirac on the original process and is not that `DRIFT` drive. An impulse at `u = t0` or `u = t` is not interior. The asymptotic time-independent predictor effect is `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z). Form `B z` first, then divide by `-a`. Stable `a < 0` is required. `a ≥ 0` cannot hold a finite process-mean change. `-B z / a` is not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. The asymptotic time-independent predictor variance is `(B / a)² v` (Driver et al., 2017, §7.2, pp. 20–21 `addedTIPREDVAR`). Form the unit asymptotic effect first, then square, then multiply by `v`. `(B / a)² v` is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`. The asymptotic continuous intercept is `-κ / a` (Driver et al., 2017, Table 2, p. 12 `asymCINT`; Eq. 3 as `Δt → ∞`; JSS PDF opened 2026-08-21T16:13Z). Form `κ` first, then divide by `-a`. Stable `a < 0` is required. `-κ / a` is not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`. The p. 16 stationary `T0MEANS` constraint is `-κ / a + −B z / a`. Form the intercept contribution first, then include the TI extra effect, then add. That constrained first-occasion mean is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. The contemporaneous time-dependent predictor impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`). Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). The §7.2 level-change form is not that impulse. The observed mean of that contemporaneous impulse is `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-impulse latent mean is not `E(y_t)`. The time-independent predictor increment is `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`). Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. `TIPREDEFFECT` is `B`, not that discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle et al. (2012, Eq. 14). The observed mean of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The within-interval time-dependent impulse carry is `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation). Form `m x` first, then `e^{a(t−u)} m x`. A zero drift is `m x` with no dissipation. Underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept. `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). An impulse at `u = t` is the contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. The observed mean of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The carried latent mean is not `E(y_t)`. The first-occasion time-independent predictor shift is `t0_b z` (Driver et al., 2017, Table 3 `T0TIPREDEFFECT`; Eq. 3 first summand). Form `t0_b z` first, then `e^{a Δt} t0_b z`. Form `μ_t` first, then add that carry. A zero drift is `t0_b z`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. `e^{A Δt} t0_b z` is not `t0_b z`. `T0TIPREDEFFECT` is the coefficient, not the shift. The observed mean of that first-occasion carry is `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The evolved-plus-carry latent mean is not `E(y_t)`. The first-occasion time-dependent predictor shift is `t0_m x0` (Driver et al., 2017, Table 3 `T0TDPREDEFFECT`; Eq. 3 first summand; JSS PDF re-opened 2026-08-20T19:10Z). Form `t0_m x0` first, then `e^{a Δt} t0_m x0`. Form `μ_t` first, then add that carry. A zero drift is `t0_m x0`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`. `e^{A Δt} t0_m x0` is not `t0_m x0`. `T0TDPREDEFFECT` is the coefficient, not the shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The observed mean of that first-occasion TD carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean. The evolved-plus-carry latent mean is not `E(y_t)`. The lasting level-change `CINT` is `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z). Form `m x` first, then multiply by `−a`. Stable `a < 0` is required so `−κ / a = m x` is an equilibrium offset. `a ≥ 0` cannot hold a new process mean. `−a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not this `CINT` setting. Equation 3 maps that intercept as `(1 − e^{a Δt}) m x` (JSS PDF re-opened 2026-08-20T19:50Z). Form the level-change `CINT` first, then the discrete intercept map. Underflow of `e^{a Δt}` to `+0` keeps `m x`. `(1 − e^{a Δt}) m x` is not `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`. The printed §7.2 lasting level change is an extra near-zero-drift latent process (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z). `T0MEANS`, `CINT`, `T0VAR`, `DIFFUSION`, and `TRAITVAR` of that process are fixed to 0; `TDPREDEFFECT` on it is fixed to 1; its `DRIFT` diagonal is very close to 0 (printed example `−0.000001`; precisely 0 causes computational problems); the original process is driven by the `DRIFT` coupling `a_{ηξ}`. After a unit identification impulse the scalar contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`). Form `a_{ηξ} x` first. A zero coupling or zero predictor is exactly zero. `ε ≥ 0` fails closed. That contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. The observed mean of that extra-process contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5 of that §7.2 contribution; JSS PDF re-opened 2026-08-21T06:12Z). The extra process has `LAMBDA` 0 and is not an observed indicator. Original indicators load on the original process after the `DRIFT` coupling. The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The contribution is not `E(y_t)`. The evolved-plus-contribution latent mean is not `E(y_t)`. `T0TDPREDEFFECT` on the extra process begins at `t = 0` and uses `Δt = t − t0` for both the original-process evolution and the extra drive. `TDPREDEFFECT` after `t0` uses `t − u` with `t0 < u < t` for the extra drive while `μ_t` still uses `Δt`. The observed mean of that after-t0 extra-process contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, Eq. 5 of that §7.2 after-t0 contribution; JSS PDF re-opened 2026-08-21T06:32Z). The first-occasion extra-process observed mean is not that observed mean when `u ≠ t0`. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is a Dirac on the original process and is not that `DRIFT` drive. An impulse at `u = t0` or `u = t` is not interior. The asymptotic time-independent predictor effect is `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z). Form `B z` first, then divide by `-a`. Stable `a < 0` is required. `a ≥ 0` cannot hold a finite process-mean change. `-B z / a` is not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. The asymptotic time-independent predictor variance is `(B / a)² v` (Driver et al., 2017, §7.2, pp. 20–21 `addedTIPREDVAR`). Form the unit asymptotic effect first, then square, then multiply by `v`. `(B / a)² v` is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`. The asymptotic continuous intercept is `-κ / a` (Driver et al., 2017, Table 2, p. 12 `asymCINT`; Eq. 3 as `Δt → ∞`; JSS PDF opened 2026-08-21T16:13Z). Form `κ` first, then divide by `-a`. Stable `a < 0` is required. `-κ / a` is not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`. The p. 16 stationary `T0MEANS` constraint is `-κ / a + −B z / a`. Form the intercept contribution first, then include the TI extra effect, then add. That constrained first-occasion mean is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean. Equation 5 of that constrained mean is `τ + λ(−κ / a + −B z / a)` (Driver et al., 2017, §4.3, pp. 9–10; Eq. 5, p. 5; JSS PDF re-opened 2026-08-21T20:07Z). Form the stationary latent mean first, then `τ + λ` of that mean. `τ + λ μ_0` for free `T0MEANS` is not that composition. `τ + λ(−κ / a)` is not that composition when `B z ≠ 0`. `τ + λ μ_t` is not that composition. `MANIFESTMEANS` is not `E(y_0)`. The constrained latent mean is not `E(y_0)`. Evolving from that stationary start with `CINT` and `TIPREDEFFECT` stays at the stationary mean. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. diff --git a/crates/psychometric_core/src/error.rs b/crates/psychometric_core/src/error.rs index a80e0ffb..6affb4ab 100644 --- a/crates/psychometric_core/src/error.rs +++ b/crates/psychometric_core/src/error.rs @@ -388,6 +388,26 @@ pub enum PsychometricError { /// interval discrete latent mean. The constrained first-occasion /// mean is not `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`. StationaryInitialLatentMeanIsNotDiscreteMean, + /// Driver Eq. 5 of §4.3 stationary `T0MEANS` was treated as + /// `MANIFESTMEANS`. `τ + λ(−κ / a + −B z / a)` is not `τ`. + StationaryInitialObservedMeanIsNotManifestMeans, + /// Driver §4.3 stationary `T0MEANS` was treated as `E(y_0)`. + /// `−κ / a + −B z / a` is the constrained latent mean, not the + /// observed-indicator mean. + StationaryInitialLatentMeanIsNotObservedMean, + /// Driver Eq. 5 of a finite-interval evolved mean was treated as + /// Eq. 5 of §4.3 stationary `T0MEANS`. `τ + λ μ_t` is not + /// `τ + λ(−κ / a + −B z / a)` when the first occasion is + /// constrained. + EvolvedObservedMeanIsNotStationaryInitialObservedMean, + /// Driver Eq. 5 of `asymCINT` was treated as Eq. 5 of §4.3 + /// stationary `T0MEANS`. `τ + λ(−κ / a)` is not + /// `τ + λ(−κ / a + −B z / a)` when `B z ≠ 0`. + AsymptoticContinuousInterceptObservedMeanIsNotStationaryInitialObservedMean, + /// Driver Eq. 5 of free `T0MEANS` was treated as Eq. 5 of §4.3 + /// stationary `T0MEANS`. `τ + λ μ_0` is not + /// `τ + λ(−κ / a + −B z / a)`. + InitialObservedMeanIsNotStationaryInitialObservedMean, } impl fmt::Display for PsychometricError { @@ -700,6 +720,21 @@ impl fmt::Display for PsychometricError { Self::StationaryInitialLatentMeanIsNotDiscreteMean => { "stationary first-occasion latent mean is not the finite-interval discrete latent mean" } + Self::StationaryInitialObservedMeanIsNotManifestMeans => { + "stationary first-occasion observed mean is not the manifest mean" + } + Self::StationaryInitialLatentMeanIsNotObservedMean => { + "stationary first-occasion latent mean is not the first-occasion observed mean" + } + Self::EvolvedObservedMeanIsNotStationaryInitialObservedMean => { + "evolved observed mean is not the stationary first-occasion observed mean" + } + Self::AsymptoticContinuousInterceptObservedMeanIsNotStationaryInitialObservedMean => { + "asymptotic-intercept observed mean is not the stationary first-occasion observed mean" + } + Self::InitialObservedMeanIsNotStationaryInitialObservedMean => { + "free first-occasion observed mean is not the stationary first-occasion observed mean" + } }; formatter.write_str(message) } @@ -1179,5 +1214,26 @@ mod tests { PsychometricError::StationaryInitialLatentMeanIsNotDiscreteMean.to_string(), "stationary first-occasion latent mean is not the finite-interval discrete latent mean" ); + assert_eq!( + PsychometricError::StationaryInitialObservedMeanIsNotManifestMeans.to_string(), + "stationary first-occasion observed mean is not the manifest mean" + ); + assert_eq!( + PsychometricError::StationaryInitialLatentMeanIsNotObservedMean.to_string(), + "stationary first-occasion latent mean is not the first-occasion observed mean" + ); + assert_eq!( + PsychometricError::EvolvedObservedMeanIsNotStationaryInitialObservedMean.to_string(), + "evolved observed mean is not the stationary first-occasion observed mean" + ); + assert_eq!( + PsychometricError::AsymptoticContinuousInterceptObservedMeanIsNotStationaryInitialObservedMean + .to_string(), + "asymptotic-intercept observed mean is not the stationary first-occasion observed mean" + ); + assert_eq!( + PsychometricError::InitialObservedMeanIsNotStationaryInitialObservedMean.to_string(), + "free first-occasion observed mean is not the stationary first-occasion observed mean" + ); } } diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index e35652b0..f670497d 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -78,6 +78,12 @@ //! effect, then add. That constrained first-occasion mean is not free //! `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and //! not the finite-interval discrete latent mean. +//! Equation 5 of that constrained first-occasion mean is +//! `τ + λ(−κ / a + −B z / a)` (§4.3, pp. 9–10; Eq. 5, p. 5; JSS PDF +//! re-opened 2026-08-21T20:07Z). Form the stationary latent mean +//! first, then `τ + λ` of that mean. `τ + λ μ_0` for free `T0MEANS` +//! is not that composition. `τ + λ(−κ / a)` is not that composition +//! when `B z ≠ 0`. `τ + λ μ_t` is not that composition. //! Table 3 (p. 13) names a different matrix //! `T0TIPREDEFFECT` for time-independent predictors on latents at //! `T0`. The scalar first-occasion shift is `t0_b z`. Equation 3's @@ -3020,6 +3026,142 @@ pub fn refuse_stationary_initial_latent_mean_as_discrete_mean( Err(PsychometricError::StationaryInitialLatentMeanIsNotDiscreteMean) } +/// Exact scalar observed mean of §4.3 stationary `T0MEANS`. +/// +/// Driver, Oud, and Voelkle (2017, §4.3, pp. 9–10; Eq. 5, p. 5; +/// Table 2, p. 12; Eq. 3, p. 5; JSS PDF re-opened 2026-08-21T20:07Z +/// from +/// ) +/// constrain the first-occasion mean to the model-predicted mean +/// when `stationary` includes `"T0MEANS"`. Equation 5 writes +/// `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and +/// `Γ ~ N(τ, Ψ)`. The constrained latent mean is +/// `-κ / a + −B z / a`. The scalar composition is +/// `E(y_0) = τ + λ(−κ / a + −B z / a)`. Form the stationary latent +/// mean first, then `τ + λ` of that mean. A zero loading is exactly +/// `τ`. A zero intercept and a zero TI contribution is exactly `τ`. +/// `τ + λ μ_0` for free `T0MEANS` is not this composition. +/// `τ + λ(−κ / a)` is not this composition when `B z ≠ 0`. +/// `τ + λ μ_t` is not this composition. `MANIFESTMEANS` is not +/// `E(y_0)`. The constrained latent mean is not `E(y_0)`. This is +/// not a Kalman filter, not a matrix `expm`, and not ctsem +/// estimation. +/// +/// # Errors +/// +/// Propagates [`recover_stationary_initial_latent_mean`] and +/// [`recover_manifest_observed_mean`]. +#[allow(clippy::too_many_arguments)] +pub fn recover_stationary_initial_observed_mean( + loading: f64, + continuous_intercept: f64, + time_independent_effect: f64, + time_independent_predictor: f64, + log_rate: f64, + manifest_mean: f64, + clock: LagClock, +) -> Result { + let stationary_latent_mean = recover_stationary_initial_latent_mean( + continuous_intercept, + time_independent_effect, + time_independent_predictor, + log_rate, + clock, + )?; + recover_manifest_observed_mean(loading, stationary_latent_mean, manifest_mean) +} + +/// Refuse treating §4.3 stationary `T0MEANS` as `E(y_0)`. +/// +/// `−κ / a + −B z / a` is the constrained latent mean. Equation 5 +/// maps `E(y_0) = τ + λ` of that mean. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::StationaryInitialLatentMeanIsNotObservedMean`]. +pub fn refuse_stationary_initial_latent_mean_as_observed_mean( + stationary_latent_mean: f64, + stationary_observed_mean: f64, +) -> Result { + let _ = (stationary_latent_mean, stationary_observed_mean); + Err(PsychometricError::StationaryInitialLatentMeanIsNotObservedMean) +} + +/// Refuse treating `MANIFESTMEANS` as Eq. 5 of §4.3 stationary +/// `T0MEANS`. +/// +/// Table 2 names `τ` `MANIFESTMEANS`. `τ + λ(−κ / a + −B z / a)` is +/// not `τ` when the loading and constrained mean are nonzero. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::StationaryInitialObservedMeanIsNotManifestMeans`]. +pub fn refuse_stationary_initial_observed_mean_as_manifest_means( + stationary_observed_mean: f64, + manifest_mean: f64, +) -> Result { + let _ = (stationary_observed_mean, manifest_mean); + Err(PsychometricError::StationaryInitialObservedMeanIsNotManifestMeans) +} + +/// Refuse treating evolved `τ + λ μ_t` as Eq. 5 of §4.3 stationary +/// `T0MEANS`. +/// +/// A finite-interval evolved observed mean is not the constrained +/// first-occasion observed mean. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::EvolvedObservedMeanIsNotStationaryInitialObservedMean`]. +pub fn refuse_evolved_observed_mean_as_stationary_initial_observed_mean( + evolved_observed_mean: f64, + stationary_observed_mean: f64, +) -> Result { + let _ = (evolved_observed_mean, stationary_observed_mean); + Err(PsychometricError::EvolvedObservedMeanIsNotStationaryInitialObservedMean) +} + +/// Refuse treating `τ + λ(−κ / a)` as Eq. 5 of §4.3 stationary +/// `T0MEANS`. +/// +/// The constraint includes time-independent predictors. +/// `τ + λ(−κ / a)` is not that composition when `B z ≠ 0`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::AsymptoticContinuousInterceptObservedMeanIsNotStationaryInitialObservedMean`]. +pub fn refuse_asymptotic_continuous_intercept_observed_mean_as_stationary_initial_observed_mean( + asymptotic_intercept_observed_mean: f64, + stationary_observed_mean: f64, +) -> Result { + let _ = (asymptotic_intercept_observed_mean, stationary_observed_mean); + Err( + PsychometricError::AsymptoticContinuousInterceptObservedMeanIsNotStationaryInitialObservedMean, + ) +} + +/// Refuse treating `τ + λ μ_0` as Eq. 5 of §4.3 stationary +/// `T0MEANS`. +/// +/// Free first-occasion `T0MEANS` is not the constrained +/// first-occasion mean. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::InitialObservedMeanIsNotStationaryInitialObservedMean`]. +pub fn refuse_initial_observed_mean_as_stationary_initial_observed_mean( + initial_observed_mean: f64, + stationary_observed_mean: f64, +) -> Result { + let _ = (initial_observed_mean, stationary_observed_mean); + Err(PsychometricError::InitialObservedMeanIsNotStationaryInitialObservedMean) +} + /// Exact scalar observed mean of a time-independent predictor. /// /// Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Eq. 3, p. 5; Table 2, @@ -4547,16 +4689,17 @@ mod tests { recover_level_change_extra_process_contribution_after, recover_local_log_rate, recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, - recover_stationary_initial_latent_mean, recover_stationary_latent_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_stationary_initial_latent_mean, recover_stationary_initial_observed_mean, + recover_stationary_latent_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, refuse_asymptotic_continuous_intercept_as_continuous_intercept, refuse_asymptotic_continuous_intercept_as_discrete_increment, refuse_asymptotic_continuous_intercept_as_initial_latent_mean, + refuse_asymptotic_continuous_intercept_observed_mean_as_stationary_initial_observed_mean, refuse_asymptotic_time_independent_effect_as_coefficient, refuse_asymptotic_time_independent_effect_as_continuous_intercept, refuse_asymptotic_time_independent_effect_as_discrete_effect, @@ -4573,6 +4716,7 @@ mod tests { refuse_evolved_observed_mean_as_impulse_observed_mean, refuse_evolved_observed_mean_as_initial_time_dependent_observed_mean, refuse_evolved_observed_mean_as_initial_time_independent_observed_mean, + refuse_evolved_observed_mean_as_stationary_initial_observed_mean, refuse_evolved_observed_mean_as_time_independent_observed_mean, refuse_extra_process_contribution_as_observed_mean, refuse_extra_process_latent_mean_as_observed_mean, @@ -4589,6 +4733,7 @@ mod tests { refuse_impulse_observed_mean_as_time_independent_observed_mean, refuse_initial_latent_mean_as_evolved_mean, refuse_initial_observed_mean_as_evolved_observed_mean, + refuse_initial_observed_mean_as_stationary_initial_observed_mean, refuse_initial_time_dependent_carry_as_impulse_carry, refuse_initial_time_dependent_carry_as_initial_effect, refuse_initial_time_dependent_coefficient_as_initial_effect, @@ -4621,6 +4766,8 @@ mod tests { refuse_stationary_initial_latent_mean_as_asymptotic_time_independent_effect, refuse_stationary_initial_latent_mean_as_discrete_mean, refuse_stationary_initial_latent_mean_as_initial_latent_mean, + refuse_stationary_initial_latent_mean_as_observed_mean, + refuse_stationary_initial_observed_mean_as_manifest_means, refuse_time_dependent_impulse_as_continuous_intercept, refuse_time_dependent_impulse_as_time_independent_effect, refuse_time_dependent_impulse_as_time_varying_discrete_effect, @@ -8032,6 +8179,251 @@ mod tests { ); } + #[test] + fn stationary_initial_observed_mean_recovers_driver_equation_five_of_section_four_point_three() + { + // Driver et al. (2017, §4.3, pp. 9–10; Eq. 5, p. 5) + // constrain first-occasion means to the model-predicted + // mean. Equation 5 maps E(y_0) = τ + λ of that mean. + let printed_effect = -0.225_f64; + let printed_asym = -1.673_f64; + let log_rate = -printed_effect / printed_asym; + let intercept = 0.3_f64; + let loading = 2.0_f64; + let manifest_mean = 0.5_f64; + let recovered = recover_stationary_initial_observed_mean( + loading, + intercept, + printed_effect, + 1.0, + log_rate, + manifest_mean, + LagClock::EventTime, + ) + .expect("eq5-stationary-T0MEANS"); + let latent = recover_stationary_initial_latent_mean( + intercept, + printed_effect, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("stationary T0MEANS"); + let expected = + recover_manifest_observed_mean(loading, latent, manifest_mean).expect("τ+λμ"); + assert!((recovered - expected).abs() < 1e-12); + let intercept_only = + recover_asymptotic_continuous_intercept(intercept, log_rate, LagClock::EventTime) + .expect("asymCINT"); + let intercept_only_observed = + recover_manifest_observed_mean(loading, intercept_only, manifest_mean) + .expect("τ+λ(−κ/a)"); + assert!((recovered - intercept_only_observed).abs() > 1e-3); + let free_initial_observed = + recover_manifest_observed_mean(loading, 2.823, manifest_mean).expect("τ+λμ_0"); + assert!((recovered - free_initial_observed).abs() > 1e-3); + let evolved_from_free = recover_discrete_observed_mean( + loading, + 2.823, + log_rate, + intercept, + manifest_mean, + 1.0, + LagClock::EventTime, + ) + .expect("τ+λμ_t"); + assert!((recovered - evolved_from_free).abs() > 1e-3); + assert!((recovered - manifest_mean).abs() > 1e-3); + assert!((recovered - latent).abs() > 1e-3); + assert_eq!( + recover_stationary_initial_observed_mean( + 0.0, + intercept, + printed_effect, + 1.0, + log_rate, + manifest_mean, + LagClock::EventTime, + ), + Ok(manifest_mean) + ); + assert_eq!( + recover_stationary_initial_observed_mean( + loading, + 0.0, + 0.0, + 1.0, + 0.0, + manifest_mean, + LagClock::EventTime, + ), + Ok(manifest_mean) + ); + let evolved_from_stationary = + recover_discrete_observed_mean_with_time_independent_predictor( + loading, + latent, + log_rate, + intercept, + printed_effect, + 1.0, + manifest_mean, + 2.0, + LagClock::EventTime, + ) + .expect("invariance"); + assert!((evolved_from_stationary - recovered).abs() < 1e-12); + let evolved_latent = recover_discrete_latent_mean_with_time_independent_predictor( + latent, + log_rate, + intercept, + printed_effect, + 1.0, + 2.0, + LagClock::EventTime, + ) + .expect("stationary invariance"); + assert!((evolved_latent - latent).abs() < 1e-12); + } + + #[test] + fn stationary_initial_observed_mean_is_not_manifest_latent_evolved_or_free() { + let intercept = 0.3_f64; + let log_rate = -0.134_488_942_f64; + let loading = 2.0_f64; + let manifest_mean = 0.5_f64; + let recovered = recover_stationary_initial_observed_mean( + loading, + intercept, + -0.225, + 1.0, + log_rate, + manifest_mean, + LagClock::EventTime, + ) + .expect("eq5-stationary-T0MEANS"); + let latent = recover_stationary_initial_latent_mean( + intercept, + -0.225, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("stationary T0MEANS"); + let intercept_only = + recover_asymptotic_continuous_intercept(intercept, log_rate, LagClock::EventTime) + .expect("asymCINT"); + let intercept_only_observed = + recover_manifest_observed_mean(loading, intercept_only, manifest_mean) + .expect("τ+λ(−κ/a)"); + let free_initial_observed = + recover_manifest_observed_mean(loading, 2.823, manifest_mean).expect("τ+λμ_0"); + let evolved = recover_discrete_observed_mean( + loading, + 2.823, + log_rate, + intercept, + manifest_mean, + 1.0, + LagClock::EventTime, + ) + .expect("τ+λμ_t"); + assert_eq!( + refuse_stationary_initial_latent_mean_as_observed_mean(latent, recovered), + Err(PsychometricError::StationaryInitialLatentMeanIsNotObservedMean) + ); + assert_eq!( + refuse_stationary_initial_observed_mean_as_manifest_means(recovered, manifest_mean), + Err(PsychometricError::StationaryInitialObservedMeanIsNotManifestMeans) + ); + assert_eq!( + refuse_evolved_observed_mean_as_stationary_initial_observed_mean(evolved, recovered), + Err(PsychometricError::EvolvedObservedMeanIsNotStationaryInitialObservedMean) + ); + assert_eq!( + refuse_asymptotic_continuous_intercept_observed_mean_as_stationary_initial_observed_mean( + intercept_only_observed, + recovered + ), + Err( + PsychometricError::AsymptoticContinuousInterceptObservedMeanIsNotStationaryInitialObservedMean + ) + ); + assert_eq!( + refuse_initial_observed_mean_as_stationary_initial_observed_mean( + free_initial_observed, + recovered + ), + Err(PsychometricError::InitialObservedMeanIsNotStationaryInitialObservedMean) + ); + } + + #[test] + fn stationary_initial_observed_mean_invalid_inputs_fail_closed() { + let intercept = 0.3_f64; + let log_rate = -0.134_488_942_f64; + assert_eq!( + recover_stationary_initial_observed_mean( + 2.0, + intercept, + -0.225, + 1.0, + log_rate, + 0.5, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_stationary_initial_observed_mean( + 2.0, + intercept, + 0.0, + 1.0, + 0.0, + 0.5, + LagClock::EventTime + ), + Err(PsychometricError::AsymptoticContinuousInterceptRequiresStableDrift) + ); + assert_eq!( + recover_stationary_initial_observed_mean( + 2.0, + 0.0, + -0.225, + 1.0, + 0.5, + 0.5, + LagClock::EventTime + ), + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) + ); + assert_eq!( + recover_stationary_initial_observed_mean( + f64::NAN, + intercept, + -0.225, + 1.0, + log_rate, + 0.5, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_stationary_initial_observed_mean( + 2.0, + 1e308, + 1e308, + 1.0, + -1e-308, + 0.5, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + } + #[test] fn discrete_observed_mean_with_impulse_recovers_driver_equation_five() { let loading = 2.0_f64; diff --git a/crates/psychometric_core/src/lib.rs b/crates/psychometric_core/src/lib.rs index 10cb269d..c2f265c9 100644 --- a/crates/psychometric_core/src/lib.rs +++ b/crates/psychometric_core/src/lib.rs @@ -125,13 +125,21 @@ //! not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not //! `-B z / a`; p. 16 `T0MEANS` stationarity includes TI predictors; //! that composition is not this intercept-only map), -//! recovers the Driver p. 16 stationary `T0MEANS` as +//! recovers the Driver p. 16 / §4.3 stationary `T0MEANS` as //! `-κ / a + −B z / a` -//! (constrained first-occasion mean using `T0MEANSbase` / -//! `T0MEANSfree`; form the intercept contribution first, then include -//! the TI extra effect, then add; not free `T0MEANS`, not `asymCINT` -//! alone, not `asymTIPREDEFFECT` alone, and not the finite-interval -//! discrete latent mean), +//! (constrained first-occasion mean; form the intercept +//! contribution first, then include the TI extra effect, then add; +//! not free `T0MEANS`, not `asymCINT` alone, not +//! `asymTIPREDEFFECT` alone, and not the finite-interval discrete +//! latent mean), +//! recovers the Driver Eq. 5 of that constrained mean as +//! `τ + λ(−κ / a + −B z / a)` +//! (§4.3, pp. 9–10; Eq. 5, p. 5; JSS PDF re-opened 2026-08-21T20:07Z; +//! form the stationary latent mean first, then `τ + λ` of that mean; +//! `τ + λ μ_0` is not that observed mean; `τ + λ(−κ / a)` is not +//! that observed mean when `B z ≠ 0`; `τ + λ μ_t` is not that +//! observed mean; `MANIFESTMEANS` is not `E(y_0)`; the constrained +//! latent mean is not `E(y_0)`), //! and refuses //! latent-mean comparison below strong invariance. @@ -270,6 +278,8 @@ pub use event_time::recover_manifest_observed_variance; pub use event_time::recover_manifest_trait_plus_state_observed_variance; /// Exact scalar p. 16 stationary `T0MEANS` `-κ / a + −B z / a`. pub use event_time::recover_stationary_initial_latent_mean; +/// Exact scalar Eq. 5 of §4.3 stationary `T0MEANS` `τ + λ(−κ / a + −B z / a)`. +pub use event_time::recover_stationary_initial_observed_mean; /// Exact scalar stationary within-subject variance `-q / (2 a)`. pub use event_time::recover_stationary_latent_variance; /// Exact scalar contemporaneous `TDPREDEFFECT` impulse `m x`. @@ -294,6 +304,8 @@ pub use event_time::refuse_asymptotic_continuous_intercept_as_continuous_interce pub use event_time::refuse_asymptotic_continuous_intercept_as_discrete_increment; /// Refuse treating Table 2 `asymCINT` as `T0MEANS`. pub use event_time::refuse_asymptotic_continuous_intercept_as_initial_latent_mean; +/// Refuse treating `τ + λ(−κ / a)` as Eq. 5 of §4.3 stationary `T0MEANS`. +pub use event_time::refuse_asymptotic_continuous_intercept_observed_mean_as_stationary_initial_observed_mean; /// Refuse treating §7.2 `asymTIPREDEFFECT` as `TIPREDEFFECT` `B`. pub use event_time::refuse_asymptotic_time_independent_effect_as_coefficient; /// Refuse treating §7.2 `asymTIPREDEFFECT` as `CINT`. @@ -328,6 +340,8 @@ pub use event_time::refuse_evolved_observed_mean_as_impulse_observed_mean; pub use event_time::refuse_evolved_observed_mean_as_initial_time_dependent_observed_mean; /// Refuse treating evolved `τ + λ μ_t` as the first-occasion TI-predictor observed mean. pub use event_time::refuse_evolved_observed_mean_as_initial_time_independent_observed_mean; +/// Refuse treating evolved `τ + λ μ_t` as Eq. 5 of §4.3 stationary `T0MEANS`. +pub use event_time::refuse_evolved_observed_mean_as_stationary_initial_observed_mean; /// Refuse treating evolved `τ + λ μ_t` as the time-independent-predictor observed mean. pub use event_time::refuse_evolved_observed_mean_as_time_independent_observed_mean; /// Refuse treating the §7.2 extra-process contribution as `E(y_t)`. @@ -360,6 +374,8 @@ pub use event_time::refuse_impulse_observed_mean_as_time_independent_observed_me pub use event_time::refuse_initial_latent_mean_as_evolved_mean; /// Refuse treating first-occasion `τ + λ μ_0` as `E(y_t)`. pub use event_time::refuse_initial_observed_mean_as_evolved_observed_mean; +/// Refuse treating `τ + λ μ_0` as Eq. 5 of §4.3 stationary `T0MEANS`. +pub use event_time::refuse_initial_observed_mean_as_stationary_initial_observed_mean; /// Refuse treating the Eq. 3 `T0TDPREDEFFECT` carry as the within-interval impulse carry. pub use event_time::refuse_initial_time_dependent_carry_as_impulse_carry; /// Refuse treating the Eq. 3 `T0TDPREDEFFECT` carry as the first-occasion shift. @@ -430,6 +446,10 @@ pub use event_time::refuse_stationary_initial_latent_mean_as_asymptotic_time_ind pub use event_time::refuse_stationary_initial_latent_mean_as_discrete_mean; /// Refuse treating p. 16 stationary `T0MEANS` as free `T0MEANS`. pub use event_time::refuse_stationary_initial_latent_mean_as_initial_latent_mean; +/// Refuse treating §4.3 stationary `T0MEANS` as `E(y_0)`. +pub use event_time::refuse_stationary_initial_latent_mean_as_observed_mean; +/// Refuse treating Eq. 5 of §4.3 stationary `T0MEANS` as `MANIFESTMEANS`. +pub use event_time::refuse_stationary_initial_observed_mean_as_manifest_means; /// Refuse treating Driver Eq. 3 `TDPREDEFFECT` impulse as `CINT`. pub use event_time::refuse_time_dependent_impulse_as_continuous_intercept; /// Refuse treating Driver Eq. 3 impulse as `TIPREDEFFECT`. 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 6e7167c9..64e05254 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -34,16 +34,17 @@ use psychometric_core::{ recover_level_change_extra_process_contribution_after, recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, - recover_stationary_initial_latent_mean, recover_stationary_latent_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_stationary_initial_latent_mean, recover_stationary_initial_observed_mean, + recover_stationary_latent_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, refuse_asymptotic_continuous_intercept_as_continuous_intercept, refuse_asymptotic_continuous_intercept_as_discrete_increment, refuse_asymptotic_continuous_intercept_as_initial_latent_mean, + refuse_asymptotic_continuous_intercept_observed_mean_as_stationary_initial_observed_mean, refuse_asymptotic_time_independent_effect_as_coefficient, refuse_asymptotic_time_independent_effect_as_continuous_intercept, refuse_asymptotic_time_independent_effect_as_discrete_effect, @@ -60,6 +61,7 @@ use psychometric_core::{ refuse_evolved_observed_mean_as_impulse_observed_mean, refuse_evolved_observed_mean_as_initial_time_dependent_observed_mean, refuse_evolved_observed_mean_as_initial_time_independent_observed_mean, + refuse_evolved_observed_mean_as_stationary_initial_observed_mean, refuse_evolved_observed_mean_as_time_independent_observed_mean, refuse_extra_process_contribution_as_observed_mean, refuse_extra_process_latent_mean_as_observed_mean, @@ -76,6 +78,7 @@ use psychometric_core::{ refuse_impulse_observed_mean_as_time_independent_observed_mean, refuse_initial_latent_mean_as_evolved_mean, refuse_initial_observed_mean_as_evolved_observed_mean, + refuse_initial_observed_mean_as_stationary_initial_observed_mean, refuse_initial_time_dependent_carry_as_impulse_carry, refuse_initial_time_dependent_carry_as_initial_effect, refuse_initial_time_dependent_coefficient_as_initial_effect, @@ -105,6 +108,8 @@ use psychometric_core::{ refuse_stationary_initial_latent_mean_as_asymptotic_time_independent_effect, refuse_stationary_initial_latent_mean_as_discrete_mean, refuse_stationary_initial_latent_mean_as_initial_latent_mean, + refuse_stationary_initial_latent_mean_as_observed_mean, + refuse_stationary_initial_observed_mean_as_manifest_means, refuse_time_dependent_impulse_as_continuous_intercept, refuse_time_dependent_impulse_as_time_independent_effect, refuse_time_dependent_impulse_as_time_varying_discrete_effect, @@ -4545,3 +4550,152 @@ fn stationary_initial_latent_mean_refuses_unstable_drift_and_non_event_clocks() Ok(0.0) ); } + +#[test] +#[allow(clippy::too_many_lines)] +fn stationary_initial_observed_mean_recovers_driver_equation_five_of_section_four_point_three() { + let printed_effect = -0.225_f64; + let printed_asym = -1.673_f64; + let log_rate = -printed_effect / printed_asym; + let intercept = 0.3_f64; + let loading = 2.0_f64; + let manifest_mean = 0.5_f64; + let recovered = recover_stationary_initial_observed_mean( + loading, + intercept, + printed_effect, + 1.0, + log_rate, + manifest_mean, + LagClock::EventTime, + ) + .expect("eq5-stationary-T0MEANS"); + let latent = recover_stationary_initial_latent_mean( + intercept, + printed_effect, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("stationary T0MEANS"); + let expected = recover_manifest_observed_mean(loading, latent, manifest_mean).expect("τ+λμ"); + let error = rmse(&[expected], &[recovered]); + assert!( + error < 1e-12, + "Driver §4.3 Eq. 5 of stationary T0MEANS RMSE {error}: got {recovered}" + ); + let intercept_only = + recover_asymptotic_continuous_intercept(intercept, log_rate, LagClock::EventTime) + .expect("asymCINT"); + let intercept_only_observed = + recover_manifest_observed_mean(loading, intercept_only, manifest_mean).expect("τ+λ(−κ/a)"); + let free_initial_observed = + recover_manifest_observed_mean(loading, 2.823, manifest_mean).expect("τ+λμ_0"); + let evolved = recover_discrete_observed_mean( + loading, + 2.823, + log_rate, + intercept, + manifest_mean, + 1.0, + LagClock::EventTime, + ) + .expect("τ+λμ_t"); + assert!( + rmse(&[recovered], &[manifest_mean]) > error, + "MANIFESTMEANS is not E(y_0)" + ); + assert!(rmse(&[recovered], &[latent]) > error); + assert!(rmse(&[recovered], &[intercept_only_observed]) > error); + assert!(rmse(&[recovered], &[free_initial_observed]) > error); + assert!(rmse(&[recovered], &[evolved]) > error); + assert_eq!( + recover_stationary_initial_observed_mean( + 0.0, + intercept, + printed_effect, + 1.0, + log_rate, + manifest_mean, + LagClock::EventTime, + ), + Ok(manifest_mean) + ); + let evolved_from_stationary = recover_discrete_observed_mean_with_time_independent_predictor( + loading, + latent, + log_rate, + intercept, + printed_effect, + 1.0, + manifest_mean, + 2.0, + LagClock::EventTime, + ) + .expect("invariance"); + assert!(rmse(&[recovered], &[evolved_from_stationary]) < 1e-12); + assert_eq!( + refuse_stationary_initial_latent_mean_as_observed_mean(latent, recovered), + Err(PsychometricError::StationaryInitialLatentMeanIsNotObservedMean) + ); + assert_eq!( + refuse_stationary_initial_observed_mean_as_manifest_means(recovered, manifest_mean), + Err(PsychometricError::StationaryInitialObservedMeanIsNotManifestMeans) + ); + assert_eq!( + refuse_evolved_observed_mean_as_stationary_initial_observed_mean(evolved, recovered), + Err(PsychometricError::EvolvedObservedMeanIsNotStationaryInitialObservedMean) + ); + assert_eq!( + refuse_asymptotic_continuous_intercept_observed_mean_as_stationary_initial_observed_mean( + intercept_only_observed, + recovered + ), + Err( + PsychometricError::AsymptoticContinuousInterceptObservedMeanIsNotStationaryInitialObservedMean + ) + ); + assert_eq!( + refuse_initial_observed_mean_as_stationary_initial_observed_mean( + free_initial_observed, + recovered + ), + Err(PsychometricError::InitialObservedMeanIsNotStationaryInitialObservedMean) + ); +} + +#[test] +fn stationary_initial_observed_mean_refuses_unstable_drift_and_non_event_clocks() { + assert_eq!( + recover_stationary_initial_observed_mean( + 2.0, + 0.3, + -0.225, + 1.0, + -0.13, + 0.5, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_stationary_initial_observed_mean(2.0, 0.3, 0.0, 1.0, 0.0, 0.5, LagClock::EventTime), + Err(PsychometricError::AsymptoticContinuousInterceptRequiresStableDrift) + ); + assert_eq!( + recover_stationary_initial_observed_mean( + 2.0, + 0.0, + -0.225, + 1.0, + 0.5, + 0.5, + LagClock::EventTime + ), + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) + ); + assert_eq!( + recover_stationary_initial_observed_mean(2.0, 0.0, 0.0, 1.0, 0.0, 0.5, LagClock::EventTime), + Ok(0.5) + ); +} diff --git a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs index 1e6f03f9..18913554 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -29,15 +29,17 @@ use psychometric_core::{ recover_level_change_extra_process_contribution_after, recover_loading_point_estimate_mean, recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, - recover_stationary_initial_latent_mean, recover_stationary_latent_variance, - recover_time_dependent_predictor_impulse, recover_time_dependent_predictor_impulse_carry, - recover_trait_plus_state_latent_variance, recover_within_residual_event_time_log_rate, + recover_stationary_initial_latent_mean, recover_stationary_initial_observed_mean, + recover_stationary_latent_variance, recover_time_dependent_predictor_impulse, + recover_time_dependent_predictor_impulse_carry, 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, refuse_asymptotic_continuous_intercept_as_continuous_intercept, refuse_asymptotic_continuous_intercept_as_discrete_increment, refuse_asymptotic_continuous_intercept_as_initial_latent_mean, + refuse_asymptotic_continuous_intercept_observed_mean_as_stationary_initial_observed_mean, refuse_asymptotic_time_independent_effect_as_coefficient, refuse_asymptotic_time_independent_effect_as_continuous_intercept, refuse_asymptotic_time_independent_effect_as_discrete_effect, @@ -54,6 +56,7 @@ use psychometric_core::{ refuse_evolved_observed_mean_as_impulse_observed_mean, refuse_evolved_observed_mean_as_initial_time_dependent_observed_mean, refuse_evolved_observed_mean_as_initial_time_independent_observed_mean, + refuse_evolved_observed_mean_as_stationary_initial_observed_mean, refuse_evolved_observed_mean_as_time_independent_observed_mean, refuse_extra_process_contribution_as_observed_mean, refuse_extra_process_latent_mean_as_observed_mean, @@ -70,6 +73,7 @@ use psychometric_core::{ refuse_impulse_observed_mean_as_time_independent_observed_mean, refuse_initial_latent_mean_as_evolved_mean, refuse_initial_observed_mean_as_evolved_observed_mean, + refuse_initial_observed_mean_as_stationary_initial_observed_mean, refuse_initial_time_dependent_carry_as_impulse_carry, refuse_initial_time_dependent_carry_as_initial_effect, refuse_initial_time_dependent_coefficient_as_initial_effect, @@ -97,6 +101,8 @@ use psychometric_core::{ refuse_stationary_initial_latent_mean_as_asymptotic_time_independent_effect, refuse_stationary_initial_latent_mean_as_discrete_mean, refuse_stationary_initial_latent_mean_as_initial_latent_mean, + refuse_stationary_initial_latent_mean_as_observed_mean, + refuse_stationary_initial_observed_mean_as_manifest_means, refuse_time_dependent_impulse_as_continuous_intercept, refuse_time_dependent_impulse_as_time_independent_effect, refuse_time_dependent_impulse_as_time_varying_discrete_effect, @@ -2322,3 +2328,98 @@ fn stationary_initial_latent_mean_is_not_t0_cint_tipred_or_discrete() { Err(psychometric_core::PsychometricError::StationaryInitialLatentMeanIsNotDiscreteMean) ); } + +#[test] +fn stationary_initial_observed_mean_is_not_manifest_latent_evolved_or_free() { + let intercept = 0.3_f64; + let log_rate = -0.134_488_942_f64; + let loading = 2.0_f64; + let manifest_mean = 0.5_f64; + let recovered = recover_stationary_initial_observed_mean( + loading, + intercept, + -0.225, + 1.0, + log_rate, + manifest_mean, + LagClock::EventTime, + ) + .expect("eq5-stationary-T0MEANS"); + let latent = recover_stationary_initial_latent_mean( + intercept, + -0.225, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("stationary T0MEANS"); + let intercept_only = + recover_asymptotic_continuous_intercept(intercept, log_rate, LagClock::EventTime) + .expect("asymCINT"); + let intercept_only_observed = + recover_manifest_observed_mean(loading, intercept_only, manifest_mean).expect("τ+λ(−κ/a)"); + let free_initial_observed = + recover_manifest_observed_mean(loading, 2.823, manifest_mean).expect("τ+λμ_0"); + let evolved = recover_discrete_observed_mean( + loading, + 2.823, + log_rate, + intercept, + manifest_mean, + 1.0, + LagClock::EventTime, + ) + .expect("τ+λμ_t"); + assert!( + (recovered - manifest_mean).abs() > 1e-3, + "Driver et al. (2017, §4.3 / Eq. 5): E(y_0) is not MANIFESTMEANS" + ); + assert!( + (recovered - latent).abs() > 1e-3, + "Driver et al. (2017, §4.3 / Eq. 5): E(y_0) is not the constrained latent mean" + ); + assert!( + (recovered - intercept_only_observed).abs() > 1e-3, + "Driver et al. (2017, §4.3 / Eq. 5): E(y_0) is not τ + λ(−κ / a)" + ); + assert!( + (recovered - free_initial_observed).abs() > 1e-3, + "Driver et al. (2017, §4.3 / Eq. 5): E(y_0) is not τ + λ μ_0" + ); + assert!( + (recovered - evolved).abs() > 1e-3, + "Driver et al. (2017, §4.3 / Eq. 5): E(y_0) is not τ + λ μ_t" + ); + assert_eq!( + refuse_stationary_initial_latent_mean_as_observed_mean(latent, recovered), + Err(psychometric_core::PsychometricError::StationaryInitialLatentMeanIsNotObservedMean) + ); + assert_eq!( + refuse_stationary_initial_observed_mean_as_manifest_means(recovered, manifest_mean), + Err(psychometric_core::PsychometricError::StationaryInitialObservedMeanIsNotManifestMeans) + ); + assert_eq!( + refuse_evolved_observed_mean_as_stationary_initial_observed_mean(evolved, recovered), + Err( + psychometric_core::PsychometricError::EvolvedObservedMeanIsNotStationaryInitialObservedMean + ) + ); + assert_eq!( + refuse_asymptotic_continuous_intercept_observed_mean_as_stationary_initial_observed_mean( + intercept_only_observed, + recovered + ), + Err( + psychometric_core::PsychometricError::AsymptoticContinuousInterceptObservedMeanIsNotStationaryInitialObservedMean + ) + ); + assert_eq!( + refuse_initial_observed_mean_as_stationary_initial_observed_mean( + free_initial_observed, + recovered + ), + Err( + psychometric_core::PsychometricError::InitialObservedMeanIsNotStationaryInitialObservedMean + ) + ); +} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index d6730162..57a00f73 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -28,7 +28,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | report template/section/copied/style/modality method effects | ADR 0004/0012; PRD/TRD | simulation truth factors implemented; estimator-side method model remains future | partial | | candidate K statistical/Pareto gates + blinded LLM review | ADR 0012; research | future `model_selection` | accepted-target | | compositional topic correlation / stable clustering | ADR 0005/0012; research | future `network_analysis` | accepted-target | -| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; §7.2 level-change `CINT` is `κ = −a m x` (`a < 0`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`; Eq. 3 of that setting is `(1 − e^{a Δt}) m x`); §7.2 extra-process contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, not the dissipating Dirac; `ε ≥ 0` fails closed; Eq. 5 of that contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; extra `LAMBDA` is 0; `τ + λ μ_t` is not that observed mean; after-t0 extra-process `TDPREDEFFECT` uses `t − u` with `t0 < u < t` while `μ_t` uses `Δt`; that after-t0 observed mean is not the first-occasion extra-process observed mean; §7.2 `asymTIPREDEFFECT` is `-B z / a` for `a < 0` and is not `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; §7.2 `addedTIPREDVAR` is `(B / a)² v` and is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`; Table 2 `asymCINT` is `-κ / a` for `a < 0` and is not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`; p. 16 stationary `T0MEANS` is `-κ / a + −B z / a` and is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z); full ESEM/DSEM remaining | partial | +| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; §7.2 level-change `CINT` is `κ = −a m x` (`a < 0`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`; Eq. 3 of that setting is `(1 − e^{a Δt}) m x`); §7.2 extra-process contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, not the dissipating Dirac; `ε ≥ 0` fails closed; Eq. 5 of that contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; extra `LAMBDA` is 0; `τ + λ μ_t` is not that observed mean; after-t0 extra-process `TDPREDEFFECT` uses `t − u` with `t0 < u < t` while `μ_t` uses `Δt`; that after-t0 observed mean is not the first-occasion extra-process observed mean; §7.2 `asymTIPREDEFFECT` is `-B z / a` for `a < 0` and is not `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; §7.2 `addedTIPREDVAR` is `(B / a)² v` and is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`; Table 2 `asymCINT` is `-κ / a` for `a < 0` and is not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`; p. 16 stationary `T0MEANS` is `-κ / a + −B z / a` and is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean; Eq. 5 of that constrained mean is `τ + λ(−κ / a + −B z / a)`; `τ + λ μ_0` is not that observed mean; `MANIFESTMEANS` is not `E(y_0)`; the constrained latent mean is not `E(y_0)`), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z); full ESEM/DSEM remaining | partial | | CPU bounded multithreading + GPU/VRAM streaming/parity | ADR 0001/0006 | future `compute_backend` | accepted-target | | TDT detection/tracking vs CHRONOS schema/prediction/temporal consistency | ADR 0016; PRD/research | future `event_intelligence` | accepted-target | | evidence-bounded LLM interpretation | ADR 0010/0012; PRD | `tepp_api` router plus future `interpretation_gateway` | partial | diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index 625edd12..d645cefc 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -1,7 +1,7 @@ # ADR 0005 — Posterior-aware ESEM/DSEM and structural interpretation **Decision status:** Accepted -**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), exact scalar contemporaneous `TDPREDEFFECT` impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; the §7.2 level-change form is not that impulse), exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of that Eq. 3 composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar time-independent `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle Eq. 14; `B` is not that discrete increment), exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of that Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of that carried latent mean; `τ + λ μ_t` is not that observed mean), exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; `T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`), exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; `T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand TD composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean), exact scalar §7.2 level-change `CINT` `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; `a < 0` so `−κ / a = m x`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2), exact scalar Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` (not `m x`, not `κ`, and not `TIPREDEFFECT`), exact scalar §7.2 extra near-zero-drift latent process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z; identification `TDPREDEFFECT` on the extra process is 1; extra `DRIFT` printed as `−0.000001`; precisely 0 causes computational problems; `ε = a` is `a_{ηξ} x Δt e^{a Δt}`; not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`; `ε ≥ 0` fails closed), exact scalar observed mean of that extra-process contribution `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z; the extra process has `LAMBDA` 0 and is not an observed indicator; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`), exact scalar after-t0 extra-process contribution `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` and Eq. 5 observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:32Z; `T0TDPREDEFFECT` uses `Δt` for both the evolution and the extra drive; `TDPREDEFFECT` after `t0` uses `t − u`; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive), exact scalar §7.2 `asymTIPREDEFFECT` `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z; expected total change in process means given a time-independent predictor; `a < 0`; not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; `a ≥ 0` fails closed), exact scalar §7.2 `addedTIPREDVAR` `(B / a)² v` (Driver et al., 2017, §7.2, pp. 20–21; stable between-subject variance accounted for by a time-independent predictor; not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`), exact scalar Table 2 `asymCINT` `-κ / a` (Driver et al., 2017, Table 2, p. 12; Eq. 3 as `Δt → ∞`; `a < 0`; not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`; `a ≥ 0` fails closed), exact scalar p. 16 stationary `T0MEANS` `-κ / a + −B z / a` (Driver et al., 2017, p. 16; constrained first-occasion mean using `T0MEANSbase` / `T0MEANSfree`; not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target +**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), exact scalar contemporaneous `TDPREDEFFECT` impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; the §7.2 level-change form is not that impulse), exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of that Eq. 3 composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar time-independent `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle Eq. 14; `B` is not that discrete increment), exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of that Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of that carried latent mean; `τ + λ μ_t` is not that observed mean), exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; `T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`), exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; `T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand TD composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean), exact scalar §7.2 level-change `CINT` `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; `a < 0` so `−κ / a = m x`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2), exact scalar Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` (not `m x`, not `κ`, and not `TIPREDEFFECT`), exact scalar §7.2 extra near-zero-drift latent process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z; identification `TDPREDEFFECT` on the extra process is 1; extra `DRIFT` printed as `−0.000001`; precisely 0 causes computational problems; `ε = a` is `a_{ηξ} x Δt e^{a Δt}`; not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`; `ε ≥ 0` fails closed), exact scalar observed mean of that extra-process contribution `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z; the extra process has `LAMBDA` 0 and is not an observed indicator; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`), exact scalar after-t0 extra-process contribution `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` and Eq. 5 observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:32Z; `T0TDPREDEFFECT` uses `Δt` for both the evolution and the extra drive; `TDPREDEFFECT` after `t0` uses `t − u`; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive), exact scalar §7.2 `asymTIPREDEFFECT` `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z; expected total change in process means given a time-independent predictor; `a < 0`; not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; `a ≥ 0` fails closed), exact scalar §7.2 `addedTIPREDVAR` `(B / a)² v` (Driver et al., 2017, §7.2, pp. 20–21; stable between-subject variance accounted for by a time-independent predictor; not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`), exact scalar Table 2 `asymCINT` `-κ / a` (Driver et al., 2017, Table 2, p. 12; Eq. 3 as `Δt → ∞`; `a < 0`; not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`; `a ≥ 0` fails closed), exact scalar p. 16 stationary `T0MEANS` `-κ / a + −B z / a` (Driver et al., 2017, p. 16; constrained first-occasion mean using `T0MEANSbase` / `T0MEANSfree`; not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean), exact scalar Eq. 5 of that constrained mean `τ + λ(−κ / a + −B z / a)` (Driver et al., 2017, §4.3, pp. 9–10; Eq. 5, p. 5; JSS PDF re-opened 2026-08-21T20:07Z; form the stationary latent mean first, then `τ + λ` of that mean; `τ + λ μ_0` is not that observed mean; `τ + λ(−κ / a)` is not that observed mean when `B z ≠ 0`; `τ + λ μ_t` is not that observed mean; `MANIFESTMEANS` is not `E(y_0)`; the constrained latent mean is not `E(y_0)`), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target **Date:** 2026-08-05 **Supersedes:** None. ADR 0012 governs upstream topic measurement/network coordinates; this ADR governs higher-order psychometric structure and longitudinal interpretation. @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), recovers the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z; `TDPREDEFFECT` is `M`, not `CINT`; `M x` is not `A^{-1}[e^{A Δt} − I] B z`; `M x` is not Voelkle et al., 2012, Eq. 14; the level-change form is not that impulse), recovers the exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition; JSS PDF re-opened 2026-08-20T09:01Z; the evolved map `τ + λ μ_t` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-impulse latent mean is not `E(y_t)`), recovers the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z; `TIPREDEFFECT` is `B`, not `κ`; form `B z` first, then the discrete intercept map; a zero drift is `B z Δt`; `B` is not the discrete increment; `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle Eq. 14), recovers the exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; JSS PDF re-opened 2026-08-20T12:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-increment latent mean is not `E(y_t)`), recovers the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z; form `m x` first, then `e^{a(t−u)} m x`; a zero drift is `m x`; underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept; `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; an impulse at `u = t` is the contemporaneous map; an impulse at `u ≤ t0` is already in `η(t0)`), recovers the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; JSS PDF re-opened 2026-08-20T05:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the carried latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and its Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF opened 2026-08-20T15:14Z; form `t0_b z` first, then `e^{a Δt} t0_b z`; a zero drift is `t0_b z`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; `e^{A Δt} t0_b z` is not `t0_b z`; `T0TIPREDEFFECT` is the coefficient, not the shift), recovers the exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition; JSS PDF re-opened 2026-08-20T15:28Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and its Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; form `t0_m x0` first, then `e^{a Δt} t0_m x0`; a zero drift is `t0_m x0`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `T0TDPREDEFFECT` is the coefficient, not the shift; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), recovers the exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; the first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar §7.2 level-change `CINT` setting `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z; form `m x` first, then multiply by `−a`; `a < 0` so `−κ / a = m x`; `a ≥ 0` cannot hold a new process mean; `−a m x` is not the dissipating Dirac, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`; the extra near-zero-drift latent process also named in §7.2 is a different specification), recovers the exact scalar Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` (JSS PDF re-opened 2026-08-20T19:50Z; form the level-change `CINT` first, then the discrete intercept map; underflow of `e^{a Δt}` to `+0` keeps `m x`; `(1 − e^{a Δt}) m x` is not `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`), recovers the exact scalar §7.2 extra near-zero-drift latent process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z; form `a_{ηξ} x` first; `ε = a` is `a_{ηξ} x Δt e^{a Δt}`; a zero coupling or zero predictor is exactly zero; `ε ≥ 0` cannot hold a lasting extra state; that contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`), recovers the exact scalar observed mean of that extra-process contribution `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z; the extra process has `LAMBDA` 0 and is not an observed indicator; form the evolved-plus-contribution latent mean first, then `τ + λ` of that mean; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`), recovers the exact scalar after-t0 extra-process contribution `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` and its Eq. 5 observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:32Z; `T0TDPREDEFFECT` uses `Δt` for both the evolution and the extra drive; `TDPREDEFFECT` after `t0` uses `t − u` while `μ_t` still uses `Δt`; an impulse at `u = t0` or `u = t` is not interior; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive), recovers the exact scalar §7.2 `asymTIPREDEFFECT` `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z; form `B z` first, then divide by `-a`; `a < 0`; a zero coefficient or zero predictor is exactly zero; `a ≥ 0` cannot hold a finite process-mean change; `-B z / a` is not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`), recovers the exact scalar §7.2 `addedTIPREDVAR` `(B / a)² v` (Driver et al., 2017, §7.2, pp. 20–21; form the unit asymptotic effect first, then square, then multiply by `v`; `v ≥ 0`; a zero coefficient or zero predictor variance is exactly zero; `(B / a)² v` is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`), recovers the exact scalar Table 2 `asymCINT` `-κ / a` (Driver et al., 2017, Table 2, p. 12; Eq. 3 as `Δt → ∞`; JSS PDF opened 2026-08-21T16:13Z; form `κ` first, then divide by `-a`; `a < 0`; a zero intercept is exactly zero; `-κ / a` is not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`), recovers the exact scalar p. 16 stationary `T0MEANS` `-κ / a + −B z / a` (Driver et al., 2017, p. 16; form the intercept contribution first, then include the TI extra effect, then add; not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), recovers the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z; `TDPREDEFFECT` is `M`, not `CINT`; `M x` is not `A^{-1}[e^{A Δt} − I] B z`; `M x` is not Voelkle et al., 2012, Eq. 14; the level-change form is not that impulse), recovers the exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition; JSS PDF re-opened 2026-08-20T09:01Z; the evolved map `τ + λ μ_t` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-impulse latent mean is not `E(y_t)`), recovers the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z; `TIPREDEFFECT` is `B`, not `κ`; form `B z` first, then the discrete intercept map; a zero drift is `B z Δt`; `B` is not the discrete increment; `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle Eq. 14), recovers the exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; JSS PDF re-opened 2026-08-20T12:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-increment latent mean is not `E(y_t)`), recovers the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z; form `m x` first, then `e^{a(t−u)} m x`; a zero drift is `m x`; underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept; `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; an impulse at `u = t` is the contemporaneous map; an impulse at `u ≤ t0` is already in `η(t0)`), recovers the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; JSS PDF re-opened 2026-08-20T05:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the carried latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and its Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF opened 2026-08-20T15:14Z; form `t0_b z` first, then `e^{a Δt} t0_b z`; a zero drift is `t0_b z`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; `e^{A Δt} t0_b z` is not `t0_b z`; `T0TIPREDEFFECT` is the coefficient, not the shift), recovers the exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition; JSS PDF re-opened 2026-08-20T15:28Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and its Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; form `t0_m x0` first, then `e^{a Δt} t0_m x0`; a zero drift is `t0_m x0`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `T0TDPREDEFFECT` is the coefficient, not the shift; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), recovers the exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; the first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar §7.2 level-change `CINT` setting `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z; form `m x` first, then multiply by `−a`; `a < 0` so `−κ / a = m x`; `a ≥ 0` cannot hold a new process mean; `−a m x` is not the dissipating Dirac, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`; the extra near-zero-drift latent process also named in §7.2 is a different specification), recovers the exact scalar Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` (JSS PDF re-opened 2026-08-20T19:50Z; form the level-change `CINT` first, then the discrete intercept map; underflow of `e^{a Δt}` to `+0` keeps `m x`; `(1 − e^{a Δt}) m x` is not `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`), recovers the exact scalar §7.2 extra near-zero-drift latent process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z; form `a_{ηξ} x` first; `ε = a` is `a_{ηξ} x Δt e^{a Δt}`; a zero coupling or zero predictor is exactly zero; `ε ≥ 0` cannot hold a lasting extra state; that contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`), recovers the exact scalar observed mean of that extra-process contribution `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z; the extra process has `LAMBDA` 0 and is not an observed indicator; form the evolved-plus-contribution latent mean first, then `τ + λ` of that mean; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`), recovers the exact scalar after-t0 extra-process contribution `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` and its Eq. 5 observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:32Z; `T0TDPREDEFFECT` uses `Δt` for both the evolution and the extra drive; `TDPREDEFFECT` after `t0` uses `t − u` while `μ_t` still uses `Δt`; an impulse at `u = t0` or `u = t` is not interior; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive), recovers the exact scalar §7.2 `asymTIPREDEFFECT` `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z; form `B z` first, then divide by `-a`; `a < 0`; a zero coefficient or zero predictor is exactly zero; `a ≥ 0` cannot hold a finite process-mean change; `-B z / a` is not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`), recovers the exact scalar §7.2 `addedTIPREDVAR` `(B / a)² v` (Driver et al., 2017, §7.2, pp. 20–21; form the unit asymptotic effect first, then square, then multiply by `v`; `v ≥ 0`; a zero coefficient or zero predictor variance is exactly zero; `(B / a)² v` is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`), recovers the exact scalar Table 2 `asymCINT` `-κ / a` (Driver et al., 2017, Table 2, p. 12; Eq. 3 as `Δt → ∞`; JSS PDF opened 2026-08-21T16:13Z; form `κ` first, then divide by `-a`; `a < 0`; a zero intercept is exactly zero; `-κ / a` is not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`), recovers the exact scalar p. 16 stationary `T0MEANS` `-κ / a + −B z / a` (Driver et al., 2017, p. 16; form the intercept contribution first, then include the TI extra effect, then add; not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean), recovers the exact scalar Eq. 5 of that constrained mean `τ + λ(−κ / a + −B z / a)` (Driver et al., 2017, §4.3, pp. 9–10; Eq. 5, p. 5; JSS PDF re-opened 2026-08-21T20:07Z; form the stationary latent mean first, then `τ + λ` of that mean; `τ + λ μ_0` is not that observed mean; `τ + λ(−κ / a)` is not that observed mean when `B z ≠ 0`; `τ + λ μ_t` is not that observed mean; `MANIFESTMEANS` is not `E(y_0)`; the constrained latent mean is not `E(y_0)`), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. 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/adr/README.md b/docs/adr/README.md index a864b372..84380310 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -10,7 +10,7 @@ Read [`ADR_POLICY.md`](ADR_POLICY.md) first. **Decision status and implementatio | [0002](0002-six-clock-temporal-semantics.md) | Six-clock temporal semantics and fail-closed historical leakage prevention | Accepted | active-PR | Unmerged PR #8 is the canonical Task 3 replacement implementing typed clocks/intervals against the current protected-main lineage; conflicted PR #5 is superseded lineage. Later graph/split enforcement remains target work. | | [0003](0003-relational-event-multiple-membership.md) | Relational event ontology and time-varying cross-classified multiple membership | Accepted | partial | Weighted time-varying membership network/roles are active-PR (PR #12); full multilevel estimators, graph ontology, and persistence remain accepted-target. ADR 0016 owns event-intelligence tasks. | | [0004](0004-shared-multilingual-latent-space.md) | One shared multilingual latent space with explicit invariance status | Accepted | accepted-target | ADR 0012 owns the full topic-estimator/backend/global-topic contract. | -| [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | partial | Input gates, posterior-draw point estimates, Rubin `T` on OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects, exact scalar discrete process noise, lagged latent covariance and unconditional latent variance, stationary within-subject variance (`asymDIFFUSION`), trait-plus-state variance (`TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; Eq. 5 of the contemporaneous impulse is `τ + λ(μ_t + m x)`; `τ + λ μ_t` is not that observed mean; Eq. 5 of the time-independent predictor is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; Eq. 5 of the within-interval impulse carry is `τ + λ(μ_t + e^{a(t−u)} m x)`; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; Table 2 `asymCINT` is `-κ / a` for `a < 0` and is not `κ`, not the finite-interval increment, not `T0MEANS`, and not `-B z / a`; p. 16 stationary `T0MEANS` is `-κ / a + −B z / a` and is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean; §7.2 `addedTIPREDVAR` is `(B / a)² v` and is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`), irregular already-centered residual lag, and strong-gated latent means are on the stacked psychometric PR; full ESEM/DSEM estimator remains accepted-target. | +| [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | partial | Input gates, posterior-draw point estimates, Rubin `T` on OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects, exact scalar discrete process noise, lagged latent covariance and unconditional latent variance, stationary within-subject variance (`asymDIFFUSION`), trait-plus-state variance (`TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; Eq. 5 of the contemporaneous impulse is `τ + λ(μ_t + m x)`; `τ + λ μ_t` is not that observed mean; Eq. 5 of the time-independent predictor is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; Eq. 5 of the within-interval impulse carry is `τ + λ(μ_t + e^{a(t−u)} m x)`; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; Table 2 `asymCINT` is `-κ / a` for `a < 0` and is not `κ`, not the finite-interval increment, not `T0MEANS`, and not `-B z / a`; p. 16 stationary `T0MEANS` is `-κ / a + −B z / a` and is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean; Eq. 5 of that constrained mean is `τ + λ(−κ / a + −B z / a)` (`τ + λ μ_0` is not that observed mean; `MANIFESTMEANS` is not `E(y_0)`); §7.2 `addedTIPREDVAR` is `(B / a)² v` and is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`), irregular already-centered residual lag, and strong-gated latent means are on the stacked psychometric PR; full ESEM/DSEM estimator remains accepted-target. | | [0006](0006-vram-gpu-nvidia-orchestration.md) | VRAM-adaptive GPU compute and model-credential boundary | Accepted | accepted-target | LLM orchestration policy superseded by ADR 0010; autonomous development authority governed by ADR 0015. | | [0007](0007-rust-workspace-quality-gates.md) | Explicit Rust workspace, pinned toolchains, and exact quality gates | Accepted | implemented-main | ADR 0014 governs scientific/product claim promotion beyond repository-quality tooling. | | [0008](0008-immutable-evidence-identities-digests-and-spans.md) | Immutable evidence identities, `SHA-256` digests, exact spans, and strict wire reconstruction | Accepted | implemented-main | ADR 0013 governs future persistence/reproducibility/split authority. | diff --git a/docs/research/multilevel-event-time-recovery.md b/docs/research/multilevel-event-time-recovery.md index 2f63968b..1fdec225 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -41,15 +41,16 @@ This slice stays inside `psychometric_core`. It does not add a second invariance 35. recover the exact scalar §7.2 `addedTIPREDVAR` `(B / a)² v` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z; stable between-subject variance accounted for by a time-independent predictor; form the unit asymptotic effect first, then square, then multiply by `v`; `v ≥ 0`; a zero coefficient or zero predictor variance is exactly zero) and refuse treating `(B / a)² v` as `TRAITVAR`, as `asymDIFFUSION`, or as `-B z / a`; 36. recover the exact scalar Table 2 `asymCINT` `-κ / a` (Driver et al., 2017, Table 2, p. 12; Eq. 3, p. 5; §4.3 / p. 16; JSS PDF opened 2026-08-21T16:13Z; expected change in process means for a unit intercept; form `κ` first, then divide by `-a`; `a < 0`; a zero intercept is exactly zero) and refuse treating `-κ / a` as `κ`, as `A^{-1}[e^{A Δt} − I] κ`, as `T0MEANS`, or as `-B z / a`; `a ≥ 0` cannot hold a finite process-mean change; p. 16 `T0MEANS` stationarity includes TI predictors and is not this intercept-only map; 37. recover the exact scalar p. 16 stationary `T0MEANS` `-κ / a + −B z / a` (Driver et al., 2017, p. 16; Table 2, p. 12; Eq. 3, p. 5; JSS PDF opened 2026-08-21T16:13Z; constrain `T0MEANS` to model-implied values using `T0MEANSbase` / `T0MEANSfree`; form the intercept contribution first, then include the TI extra effect, then add; a zero intercept and a zero TI contribution is exactly zero) and refuse treating that composition as free `T0MEANS`, as `asymCINT` alone, as `asymTIPREDEFFECT` alone, or as the finite-interval discrete latent mean; -38. refuse pooling discrete lags from unequal event intervals as one coefficient; -39. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); -40. refuse the difference quotient as a continuous-time rate; -41. apply the same event-time map to CWC residuals (still not DSEM); -42. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). +38. recover the exact scalar Eq. 5 of §4.3 stationary `T0MEANS` `τ + λ(−κ / a + −B z / a)` (Driver et al., 2017, §4.3, pp. 9–10; Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-21T20:07Z; form the stationary latent mean first, then `τ + λ` of that mean; a zero loading is exactly `τ`; evolving from that stationary start with `CINT` and `TIPREDEFFECT` stays at the stationary mean) and refuse treating `τ + λ μ_0`, `τ + λ(−κ / a)` when `B z ≠ 0`, `τ + λ μ_t`, `MANIFESTMEANS`, or the constrained latent mean as `E(y_0)`; +39. refuse pooling discrete lags from unequal event intervals as one coefficient; +40. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); +41. refuse the difference quotient as a continuous-time rate; +42. apply the same event-time map to CWC residuals (still not DSEM); +43. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). ## Claim boundary -This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. The contemporaneous `TDPREDEFFECT` impulse is not the continuous intercept, not the time-independent discrete effect, and not Voelkle et al. (2012, Eq. 14). The time-independent `TIPREDEFFECT` increment is not the continuous intercept, not the contemporaneous impulse, not Voelkle et al. (2012, Eq. 14), and not the coefficient `B`. The within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). The evolved observed mean `τ + λ μ_t` is not the contemporaneous-impulse observed mean `τ + λ(μ_t + m x)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not the impulse-carry observed mean `τ + λ(μ_t + e^{a(t−u)} m x)` when `u ≠ t`. The evolved observed mean `τ + λ μ_t` is not the impulse-carry observed mean. The carried latent mean is not `E(y_t)`. The evolved-plus-impulse latent mean is not `E(y_t)`. The evolved observed mean `τ + λ μ_t` is not the time-independent-predictor observed mean `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not that time-independent-predictor observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that time-independent-predictor observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TIPREDEFFECT` shift `t0_b z` is not the Eq. 3 process increment `A^{-1}[e^{A Δt} − I] B z`, not `κ`, and not `M x`. The Eq. 3 first-summand carry `e^{A Δt} t0_b z` is not `t0_b z` and is not that process increment. `T0TIPREDEFFECT` is the coefficient, not the first-occasion shift. The evolved observed mean `τ + λ μ_t` is not the first-occasion TI-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_b z)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion observed mean when `u ≠ t0`. The evolved-plus-T0TIPRED latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TDPREDEFFECT` shift `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `κ`. The Eq. 3 first-summand carry `e^{A Δt} t0_m x0` is not `t0_m x0` and is not that within-interval impulse carry. `T0TDPREDEFFECT` is the coefficient, not the first-occasion shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The evolved observed mean `τ + λ μ_t` is not the first-occasion TD-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_m x0)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion TD observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion TD observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion TD observed mean when `u ≠ t0`. The first-occasion TI composition `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that first-occasion TD observed mean. Same numbers as `T0TIPREDEFFECT` yield the same product; Table 3 names a different matrix. The evolved-plus-T0TDPRED latent mean is not `E(y_t)`. The §7.2 level-change `CINT` `κ = −a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. Lasting level change via that `CINT` setting requires `a < 0`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not that `CINT` setting. The Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` is not the dissipating Dirac `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`. Underflow of `e^{a Δt}` to `+0` keeps the equilibrium offset `m x`. The §7.2 extra-process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. Lasting level change via that extra process requires `ε < 0`. Precisely `ε = 0` causes computational problems in the printed specification. The extra-process observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` is not `τ + λ μ_t`, not `τ + λ(μ_t + m x)`, not the contribution, and not the evolved-plus-contribution latent mean. The extra process has `LAMBDA` 0 and is not an observed indicator. `T0TDPREDEFFECT` on the extra process uses `Δt = t − t0` for both the original-process evolution and the extra drive. `TDPREDEFFECT` after `t0` uses `t − u` with `t0 < u < t` for the extra drive while `μ_t` still uses `Δt`. The after-t0 extra-process observed mean is not the first-occasion extra-process observed mean when `u ≠ t0`. The impulse-carry `e^{a(t−u)} m x` is a Dirac on the original process and is not that `DRIFT` drive. The §7.2 `asymTIPREDEFFECT` `-B z / a` is the expected total change in process means given a time-independent predictor. It is not the coefficient `B`, not the finite-interval increment `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. Lasting asymptotic change via that map requires `a < 0`. The §7.2 `addedTIPREDVAR` `(B / a)² v` is the stable between-subject variance accounted for by that predictor. It is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`. Table 2 `asymCINT` `-κ / a` is the intercept contribution to the stationary process mean. It is not `κ`, not the finite-interval increment `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`. Lasting asymptotic intercept change via that map requires `a < 0`. Page 16 notes that a `T0MEANS` stationarity constraint includes time-independent predictors; that composition is not this intercept-only map. The p. 16 constrained first-occasion mean `-κ / a + −B z / a` is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean. +This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. The contemporaneous `TDPREDEFFECT` impulse is not the continuous intercept, not the time-independent discrete effect, and not Voelkle et al. (2012, Eq. 14). The time-independent `TIPREDEFFECT` increment is not the continuous intercept, not the contemporaneous impulse, not Voelkle et al. (2012, Eq. 14), and not the coefficient `B`. The within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). The evolved observed mean `τ + λ μ_t` is not the contemporaneous-impulse observed mean `τ + λ(μ_t + m x)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not the impulse-carry observed mean `τ + λ(μ_t + e^{a(t−u)} m x)` when `u ≠ t`. The evolved observed mean `τ + λ μ_t` is not the impulse-carry observed mean. The carried latent mean is not `E(y_t)`. The evolved-plus-impulse latent mean is not `E(y_t)`. The evolved observed mean `τ + λ μ_t` is not the time-independent-predictor observed mean `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not that time-independent-predictor observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that time-independent-predictor observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TIPREDEFFECT` shift `t0_b z` is not the Eq. 3 process increment `A^{-1}[e^{A Δt} − I] B z`, not `κ`, and not `M x`. The Eq. 3 first-summand carry `e^{A Δt} t0_b z` is not `t0_b z` and is not that process increment. `T0TIPREDEFFECT` is the coefficient, not the first-occasion shift. The evolved observed mean `τ + λ μ_t` is not the first-occasion TI-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_b z)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion observed mean when `u ≠ t0`. The evolved-plus-T0TIPRED latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TDPREDEFFECT` shift `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `κ`. The Eq. 3 first-summand carry `e^{A Δt} t0_m x0` is not `t0_m x0` and is not that within-interval impulse carry. `T0TDPREDEFFECT` is the coefficient, not the first-occasion shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The evolved observed mean `τ + λ μ_t` is not the first-occasion TD-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_m x0)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion TD observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion TD observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion TD observed mean when `u ≠ t0`. The first-occasion TI composition `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that first-occasion TD observed mean. Same numbers as `T0TIPREDEFFECT` yield the same product; Table 3 names a different matrix. The evolved-plus-T0TDPRED latent mean is not `E(y_t)`. The §7.2 level-change `CINT` `κ = −a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. Lasting level change via that `CINT` setting requires `a < 0`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not that `CINT` setting. The Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` is not the dissipating Dirac `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`. Underflow of `e^{a Δt}` to `+0` keeps the equilibrium offset `m x`. The §7.2 extra-process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. Lasting level change via that extra process requires `ε < 0`. Precisely `ε = 0` causes computational problems in the printed specification. The extra-process observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` is not `τ + λ μ_t`, not `τ + λ(μ_t + m x)`, not the contribution, and not the evolved-plus-contribution latent mean. The extra process has `LAMBDA` 0 and is not an observed indicator. `T0TDPREDEFFECT` on the extra process uses `Δt = t − t0` for both the original-process evolution and the extra drive. `TDPREDEFFECT` after `t0` uses `t − u` with `t0 < u < t` for the extra drive while `μ_t` still uses `Δt`. The after-t0 extra-process observed mean is not the first-occasion extra-process observed mean when `u ≠ t0`. The impulse-carry `e^{a(t−u)} m x` is a Dirac on the original process and is not that `DRIFT` drive. The §7.2 `asymTIPREDEFFECT` `-B z / a` is the expected total change in process means given a time-independent predictor. It is not the coefficient `B`, not the finite-interval increment `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. Lasting asymptotic change via that map requires `a < 0`. The §7.2 `addedTIPREDVAR` `(B / a)² v` is the stable between-subject variance accounted for by that predictor. It is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`. Table 2 `asymCINT` `-κ / a` is the intercept contribution to the stationary process mean. It is not `κ`, not the finite-interval increment `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`. Lasting asymptotic intercept change via that map requires `a < 0`. Page 16 notes that a `T0MEANS` stationarity constraint includes time-independent predictors; that composition is not this intercept-only map. The p. 16 constrained first-occasion mean `-κ / a + −B z / a` is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean. Equation 5 of that constrained mean `τ + λ(−κ / a + −B z / a)` is not `τ + λ μ_0`, not `τ + λ(−κ / a)` when `B z ≠ 0`, not `τ + λ μ_t`, not `MANIFESTMEANS`, and not the constrained latent mean. ## Authoritative sources @@ -148,6 +149,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - Driver et al. (2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z) recovers a known `addedTIPREDVAR` \((B/a)^{2}v\) at machine-scale RMSE, and that RMSE is smaller than treating `TRAITVAR`, `asymDIFFUSION`, or `-B z / a` as that variance; a zero coefficient or zero predictor variance is exactly zero; \(v<0\) fails closed; \(a\ge 0\) with a nonzero variance fails closed; a non-event clock and an overflowing product fail closed; - Driver et al. (2017, Table 2, p. 12; Eq. 3 as \(\Delta t\to\infty\); JSS PDF opened 2026-08-21T16:13Z) recovers a known `asymCINT` \(-\kappa/a\) at machine-scale RMSE, and that RMSE is smaller than treating `CINT`, the finite-interval increment \(A^{-1}[e^{A\Delta t}-I]\kappa\), `T0MEANS`, or `-B z / a` as that total change; a large finite \(\Delta t\) discrete increment converges on \(-\kappa/a\); a zero intercept is exactly zero even if \(a\ge 0\); \(a\ge 0\) with a nonzero intercept fails closed; a non-event clock and an overflowing quotient fail closed; - Driver et al. (2017, p. 16; Table 2, p. 12; Eq. 3; JSS PDF opened 2026-08-21T16:13Z) recovers a known stationary `T0MEANS` \(-\kappa/a + -Bz/a\) at machine-scale RMSE, and that RMSE is smaller than treating free `T0MEANS`, `asymCINT` alone, `asymTIPREDEFFECT` alone, or the finite-interval discrete latent mean as that constraint; a zero intercept and a zero TI contribution is exactly zero even if \(a\ge 0\); \(a\ge 0\) with a nonzero intercept or TI contribution fails closed; a non-event clock and an overflowing sum fail closed; +- Driver et al. (2017, §4.3, pp. 9–10; Eq. 5, p. 5; JSS PDF re-opened 2026-08-21T20:07Z) recovers a known \(E(y_0)=\tau+\lambda(-\kappa/a + -Bz/a)\) at machine-scale RMSE, and that RMSE is smaller than treating \(\tau+\lambda\mu_0\), \(\tau+\lambda(-\kappa/a)\), \(\tau+\lambda\mu_t\), `MANIFESTMEANS`, or the constrained latent mean as \(E(y_0)\); a zero loading is \(\tau\); a zero intercept and a zero TI contribution is \(\tau\); evolving from that stationary start with `CINT` and `TIPREDEFFECT` stays at the stationary mean; \(a\ge 0\) with a nonzero intercept or TI contribution fails closed; a non-event clock and an overflowing product or sum fail closed; - pooling discrete lags from unequal intervals fails closed; - CWC-then-lag on a two-cluster decaying series has smaller computed RMSE than a level-pooled series when the latter is identified; - already-centered irregular residuals recover a known drift at machine-scale RMSE, and that RMSE is smaller than CWC of the corresponding raw autoregressive series (Curran & Bauer, 2011, pp. 607–608); diff --git a/docs/validation/temporal-event-foundation.md b/docs/validation/temporal-event-foundation.md index c2ef996c..fe8cc7d8 100644 --- a/docs/validation/temporal-event-foundation.md +++ b/docs/validation/temporal-event-foundation.md @@ -23,7 +23,7 @@ This report tracks exact-head scientific and engineering evidence required befor | Truth corpora / manifests | `tepp_simulation` | implemented-main | — | deterministic generator tests | Task 10 / PR #18 | | Recovery metrics | `validation_core` | implemented-main | — | RMSE/bias/coverage/MC gates | Task 11 / PR #19 | | Versioned API/export contracts | `tepp_api` | implemented-main | naruon HTTP interchange | unknown-field/version/limit + naruon HTTPS interchange tests | Task 12 / PR #21; live HTTP service remaining | -| Psychometric structural input gates | `psychometric_core` | partial | stacked psychometric PR | construct-class refusal + ALR/ILR boundary + true-loading RMSE + posterior-draw point-estimate mean + Rubin `T` + CWC within/between + CWC contextual effect + event-time log-rate + constant- and time-varying-predictor discrete effects + exact scalar discrete process noise + lagged latent covariance and unconditional latent variance + stationary within-subject variance + trait-plus-state variance + observed-indicator variance + discrete latent mean (`T0MEANS`/`CINT`) + evolved observed mean (`τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`) + contemporaneous `TDPREDEFFECT` impulse (`m x`; not `CINT`, not `TIPREDEFFECT`, not Voelkle Eq. 14) + Eq. 5 of that contemporaneous impulse (`τ + λ(μ_t + m x)`; `τ + λ μ_t` is not that observed mean) + time-independent `TIPREDEFFECT` increment (`A^{-1}[e^{A Δt} − I] B z`; not `CINT`, not `M x`, not Voelkle Eq. 14, not the coefficient `B`) + Eq. 5 of that increment (`τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`; `τ + λ μ_t` is not that observed mean) + within-interval `TDPREDEFFECT` carry (`e^{A(t−u)} M x` for `t0 < u < t`; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, not Voelkle Eq. 14) + Eq. 5 of that carry (`τ + λ(μ_t + e^{a(t−u)} m x)`; `τ + λ μ_t` is not that observed mean) + §7.2 level-change `CINT` (`κ = −a m x`; Eq. 3 increment `(1 − e^{a Δt}) m x`) + §7.2 extra-process contribution (`a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; not `κ`, not the increment, not the Dirac; `ε ≥ 0` fails closed) + Eq. 5 of that extra-process contribution (`τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; extra `LAMBDA` is 0; `τ + λ μ_t` is not that observed mean) + after-t0 extra-process `TDPREDEFFECT` (`a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t`; Eq. 5 `τ + λ(μ_t + contribution(t−u))`; not the first-occasion extra-process observed mean; not the impulse-carry Dirac) + §7.2 `asymTIPREDEFFECT` (`-B z / a` for `a < 0`; not `B`, not the finite-interval increment, not `CINT`, not `M x`) + §7.2 `addedTIPREDVAR` (`(B / a)² v`; not `TRAITVAR`, not `asymDIFFUSION`, not `-B z / a`) + Table 2 `asymCINT` (`-κ / a` for `a < 0`; not `κ`, not the finite-interval increment, not `T0MEANS`, not `-B z / a`) + p. 16 stationary `T0MEANS` (`-κ / a + −B z / a`; not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, not the finite-interval discrete mean) + irregular already-centered residual lag + strong-gated latent means (n=2 residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016); full ESEM/DSEM remaining | ADR 0005; `docs/research/posterior-esem-input-gates.md`; `docs/research/multilevel-event-time-recovery.md`; `docs/research/rubin-total-variance.md`; `docs/research/strong-invariance-latent-means.md` | +| Psychometric structural input gates | `psychometric_core` | partial | stacked psychometric PR | construct-class refusal + ALR/ILR boundary + true-loading RMSE + posterior-draw point-estimate mean + Rubin `T` + CWC within/between + CWC contextual effect + event-time log-rate + constant- and time-varying-predictor discrete effects + exact scalar discrete process noise + lagged latent covariance and unconditional latent variance + stationary within-subject variance + trait-plus-state variance + observed-indicator variance + discrete latent mean (`T0MEANS`/`CINT`) + evolved observed mean (`τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`) + contemporaneous `TDPREDEFFECT` impulse (`m x`; not `CINT`, not `TIPREDEFFECT`, not Voelkle Eq. 14) + Eq. 5 of that contemporaneous impulse (`τ + λ(μ_t + m x)`; `τ + λ μ_t` is not that observed mean) + time-independent `TIPREDEFFECT` increment (`A^{-1}[e^{A Δt} − I] B z`; not `CINT`, not `M x`, not Voelkle Eq. 14, not the coefficient `B`) + Eq. 5 of that increment (`τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`; `τ + λ μ_t` is not that observed mean) + within-interval `TDPREDEFFECT` carry (`e^{A(t−u)} M x` for `t0 < u < t`; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, not Voelkle Eq. 14) + Eq. 5 of that carry (`τ + λ(μ_t + e^{a(t−u)} m x)`; `τ + λ μ_t` is not that observed mean) + §7.2 level-change `CINT` (`κ = −a m x`; Eq. 3 increment `(1 − e^{a Δt}) m x`) + §7.2 extra-process contribution (`a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; not `κ`, not the increment, not the Dirac; `ε ≥ 0` fails closed) + Eq. 5 of that extra-process contribution (`τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; extra `LAMBDA` is 0; `τ + λ μ_t` is not that observed mean) + after-t0 extra-process `TDPREDEFFECT` (`a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t`; Eq. 5 `τ + λ(μ_t + contribution(t−u))`; not the first-occasion extra-process observed mean; not the impulse-carry Dirac) + §7.2 `asymTIPREDEFFECT` (`-B z / a` for `a < 0`; not `B`, not the finite-interval increment, not `CINT`, not `M x`) + §7.2 `addedTIPREDVAR` (`(B / a)² v`; not `TRAITVAR`, not `asymDIFFUSION`, not `-B z / a`) + Table 2 `asymCINT` (`-κ / a` for `a < 0`; not `κ`, not the finite-interval increment, not `T0MEANS`, not `-B z / a`) + p. 16 stationary `T0MEANS` (`-κ / a + −B z / a`; not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, not the finite-interval discrete mean) + Eq. 5 of that constrained mean (`τ + λ(−κ / a + −B z / a)`; `τ + λ μ_0` is not that observed mean; `τ + λ(−κ / a)` is not that observed mean when `B z ≠ 0`; `τ + λ μ_t` is not that observed mean; `MANIFESTMEANS` is not `E(y_0)`; the constrained latent mean is not `E(y_0)`) + irregular already-centered residual lag + strong-gated latent means (n=2 residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016); full ESEM/DSEM remaining | ADR 0005; `docs/research/posterior-esem-input-gates.md`; `docs/research/multilevel-event-time-recovery.md`; `docs/research/rubin-total-variance.md`; `docs/research/strong-invariance-latent-means.md` | | Purpose-bound provider payloads | `tepp_api` | implemented-main | provider-payload minimization | expired/not-yet-valid/inverted/cross-tenant/impossible-calendar grant, mapping refusal, audited elevated re-id replay | ADR 0009; `docs/research/provider-payload-minimization.md` | | Adaptive orchestration router | `tepp_api` | accepted-target | active PR | mode selection, document-control denial, ablation, credential-free bind | ADR 0010; `docs/research/adaptive-orchestration-router.md` | | CWL modular connectors | `docs/connectors/*` | implemented-main | — | contract docs + examples | PR #22; live HTTP ports remaining | From 06bffc5a8c48f7afa6664e14fd26d863164eec2b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 03:18:43 +0000 Subject: [PATCH 61/62] =?UTF-8?q?feat(psychometric):=20recover=20Driver=20?= =?UTF-8?q?=C2=A74.3=20/=20p.16=20stationary=20T0VAR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Driver, Oud, and Voelkle (2017, §4.3, pp. 9–10; p. 16) constrain T0VAR to the model-predicted variance when stationary includes "T0VAR". Page 16 names asymDIFFUSION the total within-subject variance -q/(2a). Section 4.3 adds TRAITVAR. Section 7.2 names addedTIPREDVAR the stable between-subject variance accounted for by time-independent predictors, (B/a)² v. For stable a < 0 the scalar composition is trait + -q/(2a) + (B/a)² v. Form the within-subject contribution first, then include the trait, then include the TI extra variance, then add. Trait-only variance does not require a stable drift. That constrained first-occasion variance is not free T0VAR, not asymDIFFUSION alone, not TRAITVAR alone, not addedTIPREDVAR alone, and not the finite-interval discrete latent variance. JSS PDF re-opened 2026-08-22T03:07Z. Still not DSEM, not a Kalman filter, and not ctsem estimation. --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 1 + CLAUDE.md | 2 +- crates/psychometric_core/src/error.rs | 66 +++ crates/psychometric_core/src/event_time.rs | 430 +++++++++++++++++- crates/psychometric_core/src/lib.rs | 20 + ...multilevel_event_time_recovery_contract.rs | 166 ++++++- .../scientific_claim_boundary_contract.rs | 97 +++- docs/TRACEABILITY.md | 2 +- docs/adr/0005-posterior-esem-dsem.md | 4 +- docs/adr/README.md | 2 +- .../multilevel-event-time-recovery.md | 16 +- docs/validation/temporal-event-foundation.md | 2 +- 13 files changed, 784 insertions(+), 26 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 28063b1d..4691cbb9 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -61,7 +61,7 @@ boundaries above remain the target modular MSA architecture. | `tepp_simulation` | known-truth temporal/event data generation | | `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics | | `tepp_api` | versioned DTO, schema, and export contracts | -| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; first-occasion `T0TIPREDEFFECT` shift is `t0_b z` and Eq. 3 first-summand carry is `e^{A Δt} t0_b z` (`T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_b z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), first-occasion `T0TDPREDEFFECT` shift is `t0_m x0` and Eq. 3 first-summand carry is `e^{A Δt} t0_m x0` (`T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; §7.2 level-change `CINT` is `κ = −a m x` with `a < 0` so `−κ / a = m x` (`−a m x` is not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2; Eq. 3 of that setting is `(1 − e^{a Δt}) m x`, which is not `m x`, not `κ`, and not `TIPREDEFFECT`; §7.2 extra-process contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`; identification `TDPREDEFFECT` on the extra process is 1; printed extra `DRIFT` is `−0.000001`; not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`; `ε ≥ 0` fails closed; Eq. 5 of that contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; the extra process has `LAMBDA` 0 and is not an observed indicator; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`; after-t0 extra-process `TDPREDEFFECT` is `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` while `μ_t` uses `Δt`; Eq. 5 of that after-t0 contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)`; the first-occasion extra-process observed mean is not that observed mean when `u ≠ t0`; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive; §7.2 `asymTIPREDEFFECT` is `-B z / a` for `a < 0` (`-B z / a` is not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; §7.2 `addedTIPREDVAR` is `(B / a)² v`, not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`; Table 2 `asymCINT` is `-κ / a` for `a < 0` and is not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`; p. 16 stationary `T0MEANS` is `-κ / a + −B z / a` and is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean; Eq. 5 of that constrained mean is `τ + λ(−κ / a + −B z / a)`; `τ + λ μ_0` is not that observed mean; `τ + λ(−κ / a)` is not that observed mean when `B z ≠ 0`; `τ + λ μ_t` is not that observed mean; `MANIFESTMEANS` is not `E(y_0)`; the constrained latent mean is not `E(y_0)`)), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | +| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; first-occasion `T0TIPREDEFFECT` shift is `t0_b z` and Eq. 3 first-summand carry is `e^{A Δt} t0_b z` (`T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_b z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), first-occasion `T0TDPREDEFFECT` shift is `t0_m x0` and Eq. 3 first-summand carry is `e^{A Δt} t0_m x0` (`T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; §7.2 level-change `CINT` is `κ = −a m x` with `a < 0` so `−κ / a = m x` (`−a m x` is not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2; Eq. 3 of that setting is `(1 − e^{a Δt}) m x`, which is not `m x`, not `κ`, and not `TIPREDEFFECT`; §7.2 extra-process contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`; identification `TDPREDEFFECT` on the extra process is 1; printed extra `DRIFT` is `−0.000001`; not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`; `ε ≥ 0` fails closed; Eq. 5 of that contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; the extra process has `LAMBDA` 0 and is not an observed indicator; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`; after-t0 extra-process `TDPREDEFFECT` is `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` while `μ_t` uses `Δt`; Eq. 5 of that after-t0 contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)`; the first-occasion extra-process observed mean is not that observed mean when `u ≠ t0`; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive; §7.2 `asymTIPREDEFFECT` is `-B z / a` for `a < 0` (`-B z / a` is not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; §7.2 `addedTIPREDVAR` is `(B / a)² v`, not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`; Table 2 `asymCINT` is `-κ / a` for `a < 0` and is not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`; p. 16 stationary `T0MEANS` is `-κ / a + −B z / a` and is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean; Eq. 5 of that constrained mean is `τ + λ(−κ / a + −B z / a)`; `τ + λ μ_0` is not that observed mean; `τ + λ(−κ / a)` is not that observed mean when `B z ≠ 0`; `τ + λ μ_t` is not that observed mean; `MANIFESTMEANS` is not `E(y_0)`; the constrained latent mean is not `E(y_0)`; stationary `T0VAR` is `trait + −q / (2 a) + (B / a)² v` (not free `T0VAR`, not `asymDIFFUSION` alone, not `TRAITVAR` alone, not `addedTIPREDVAR` alone, and not the finite-interval discrete latent variance)), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | No crate exposes placeholder production behavior in Task 1. This prevents an empty façade from becoming a de facto public API before its invariants and tests diff --git a/CHANGELOG.md b/CHANGELOG.md index 388b4a48..cebe0882 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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, §4.3, pp. 9–10; p. 16; Table 2, p. 12; §7.2, pp. 20–21; Eq. 4, p. 5; JSS PDF re-opened 2026-08-22T03:07Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar stationary `T0VAR`. Section 4.3 constrains the first-occasion variance to the model-predicted variance when `stationary` includes `"T0VAR"`. Page 16 names `asymDIFFUSION` the total within-subject variance `-q / (2 a)`. Section 4.3 (p. 9) adds `TRAITVAR`. Section 7.2 names `addedTIPREDVAR` the stable between-subject variance accounted for by time-independent predictors, `(B / a)² v`. The scalar composition is `trait + −q / (2 a) + (B / a)² v`. Form the within-subject contribution first, then include the trait, then include the TI extra variance, then add. A zero trait, a zero diffusion, and a zero TI contribution is exactly zero. A zero diffusion and a zero TI contribution is exactly the trait. `a ≥ 0` cannot hold a finite process variance when the diffusion or the TI contribution is nonzero and fails closed. Trait-only variance does not require a stable drift. That constrained first-occasion variance is not free `T0VAR`, not `asymDIFFUSION` alone, not `TRAITVAR` alone, not `addedTIPREDVAR` alone, and not the finite-interval discrete latent variance. The printed 2-latent `addedTIPREDVAR` 2.838 is not this scalar map. 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-22T03:07Z: request empty). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-22T03:07Z: request empty; ETS RR-88-45 PDF 404; Wiley PDF 403). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, §4.3, pp. 9–10; Eq. 5, p. 5; Table 2, p. 12; Eq. 3, p. 5; JSS PDF re-opened 2026-08-21T20:07Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar observed-indicator mean of §4.3 stationary `T0MEANS`. Section 4.3 constrains the first-occasion mean to the model-predicted mean when `stationary` includes `"T0MEANS"`. Equation 5 writes `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and `Γ ~ N(τ, Ψ)`. The constrained latent mean is `-κ / a + −B z / a`. The scalar composition is `E(y_0) = τ + λ(−κ / a + −B z / a)`. Form the stationary latent mean first, then `τ + λ` of that mean. A zero loading is exactly `τ`. A zero intercept and a zero TI contribution is exactly `τ`. Evolving from that stationary start with `CINT` and `TIPREDEFFECT` stays at the stationary mean. `τ + λ μ_0` for free `T0MEANS` is not this composition. `τ + λ(−κ / a)` is not this composition when `B z ≠ 0`. `τ + λ μ_t` is not this composition. `MANIFESTMEANS` is not `E(y_0)`. The constrained latent mean is not `E(y_0)`. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) remains unread (Unpaywall request empty this cycle). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-21T20:10Z: `is_oa: false`; Springer `content/pdf` is HTML 200). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, p. 16; Table 2, p. 12; Eq. 3, p. 5; JSS PDF opened 2026-08-21T16:13Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar stationary `T0MEANS`. Page 16 constrains `T0MEANS` to the model-implied values using `T0MEANSbase` / `T0MEANSfree` when the first observation is determined by the process in the same way as later observations. Those constraints include extra effects due to time-independent predictors (`asymTIPREDEFFECT`). For stable `a < 0` the scalar composition is `-κ / a + −B z / a`. Form the intercept contribution first, then include the TI extra effect, then add. A zero intercept and a zero TI contribution is exactly zero. `a ≥ 0` cannot hold a finite process-mean change when either contribution is nonzero and fails closed. That constrained first-occasion mean is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean. The printed 2-latent `T0MEANS` 2.823 is not this scalar map. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-21T16:21Z: `is_oa: false`; Springer `content/pdf` is HTML 200). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Table 2, p. 12; Eq. 3, p. 5; §4.3 / p. 16; JSS PDF opened 2026-08-21T16:13Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar `asymCINT`. Table 2 names `κ` `CINT` and names `asymCINT` the asymptotic (`Δt = ∞`) expected change in processes for a 1 unit change in intercept. Equation 3 maps a finite event interval as `A^{-1}[e^{A Δt} − I] κ`. For stable `a < 0` that `Δt → ∞` limit is `-A^{-1} κ`. The scalar map is `-κ / a`. A unit intercept is `-1 / a`. Form `κ` first, then divide by `-a`. A zero intercept is exactly zero. `a ≥ 0` cannot hold a finite process-mean change and fails closed. `-κ / a` is not `κ`, not the finite-interval increment `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `asymTIPREDEFFECT` `-B z / a`. Page 16 notes that a `T0MEANS` stationarity constraint includes time-independent predictors; that composition is not this intercept-only map. The printed 2-latent `CINT` values are not this scalar map. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-21T16:21Z: `is_oa: false`; Springer `content/pdf` is HTML 200). diff --git a/CLAUDE.md b/CLAUDE.md index 0f2829c3..bf7ba504 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. Putnick and Bornstein (2016, PMC5145197 opened 2026-08-19T22:15Z) require scalar invariance before latent-mean comparison; residual invariance is not a prerequisite. Two-observation series have no residual degrees of freedom (`ordinary_least_squares_fit` returns residual variance `0`) and cap at strong/scalar; they still license means. This is two-group OLS, not MGCFA. Meredith (1993) names remain unread labels. -- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. The contemporaneous time-dependent predictor impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`). Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). The §7.2 level-change form is not that impulse. The observed mean of that contemporaneous impulse is `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-impulse latent mean is not `E(y_t)`. The time-independent predictor increment is `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`). Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. `TIPREDEFFECT` is `B`, not that discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle et al. (2012, Eq. 14). The observed mean of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The within-interval time-dependent impulse carry is `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation). Form `m x` first, then `e^{a(t−u)} m x`. A zero drift is `m x` with no dissipation. Underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept. `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). An impulse at `u = t` is the contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. The observed mean of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The carried latent mean is not `E(y_t)`. The first-occasion time-independent predictor shift is `t0_b z` (Driver et al., 2017, Table 3 `T0TIPREDEFFECT`; Eq. 3 first summand). Form `t0_b z` first, then `e^{a Δt} t0_b z`. Form `μ_t` first, then add that carry. A zero drift is `t0_b z`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. `e^{A Δt} t0_b z` is not `t0_b z`. `T0TIPREDEFFECT` is the coefficient, not the shift. The observed mean of that first-occasion carry is `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The evolved-plus-carry latent mean is not `E(y_t)`. The first-occasion time-dependent predictor shift is `t0_m x0` (Driver et al., 2017, Table 3 `T0TDPREDEFFECT`; Eq. 3 first summand; JSS PDF re-opened 2026-08-20T19:10Z). Form `t0_m x0` first, then `e^{a Δt} t0_m x0`. Form `μ_t` first, then add that carry. A zero drift is `t0_m x0`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`. `e^{A Δt} t0_m x0` is not `t0_m x0`. `T0TDPREDEFFECT` is the coefficient, not the shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The observed mean of that first-occasion TD carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean. The evolved-plus-carry latent mean is not `E(y_t)`. The lasting level-change `CINT` is `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z). Form `m x` first, then multiply by `−a`. Stable `a < 0` is required so `−κ / a = m x` is an equilibrium offset. `a ≥ 0` cannot hold a new process mean. `−a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not this `CINT` setting. Equation 3 maps that intercept as `(1 − e^{a Δt}) m x` (JSS PDF re-opened 2026-08-20T19:50Z). Form the level-change `CINT` first, then the discrete intercept map. Underflow of `e^{a Δt}` to `+0` keeps `m x`. `(1 − e^{a Δt}) m x` is not `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`. The printed §7.2 lasting level change is an extra near-zero-drift latent process (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z). `T0MEANS`, `CINT`, `T0VAR`, `DIFFUSION`, and `TRAITVAR` of that process are fixed to 0; `TDPREDEFFECT` on it is fixed to 1; its `DRIFT` diagonal is very close to 0 (printed example `−0.000001`; precisely 0 causes computational problems); the original process is driven by the `DRIFT` coupling `a_{ηξ}`. After a unit identification impulse the scalar contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`). Form `a_{ηξ} x` first. A zero coupling or zero predictor is exactly zero. `ε ≥ 0` fails closed. That contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. The observed mean of that extra-process contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5 of that §7.2 contribution; JSS PDF re-opened 2026-08-21T06:12Z). The extra process has `LAMBDA` 0 and is not an observed indicator. Original indicators load on the original process after the `DRIFT` coupling. The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The contribution is not `E(y_t)`. The evolved-plus-contribution latent mean is not `E(y_t)`. `T0TDPREDEFFECT` on the extra process begins at `t = 0` and uses `Δt = t − t0` for both the original-process evolution and the extra drive. `TDPREDEFFECT` after `t0` uses `t − u` with `t0 < u < t` for the extra drive while `μ_t` still uses `Δt`. The observed mean of that after-t0 extra-process contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, Eq. 5 of that §7.2 after-t0 contribution; JSS PDF re-opened 2026-08-21T06:32Z). The first-occasion extra-process observed mean is not that observed mean when `u ≠ t0`. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is a Dirac on the original process and is not that `DRIFT` drive. An impulse at `u = t0` or `u = t` is not interior. The asymptotic time-independent predictor effect is `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z). Form `B z` first, then divide by `-a`. Stable `a < 0` is required. `a ≥ 0` cannot hold a finite process-mean change. `-B z / a` is not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. The asymptotic time-independent predictor variance is `(B / a)² v` (Driver et al., 2017, §7.2, pp. 20–21 `addedTIPREDVAR`). Form the unit asymptotic effect first, then square, then multiply by `v`. `(B / a)² v` is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`. The asymptotic continuous intercept is `-κ / a` (Driver et al., 2017, Table 2, p. 12 `asymCINT`; Eq. 3 as `Δt → ∞`; JSS PDF opened 2026-08-21T16:13Z). Form `κ` first, then divide by `-a`. Stable `a < 0` is required. `-κ / a` is not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`. The p. 16 stationary `T0MEANS` constraint is `-κ / a + −B z / a`. Form the intercept contribution first, then include the TI extra effect, then add. That constrained first-occasion mean is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean. Equation 5 of that constrained mean is `τ + λ(−κ / a + −B z / a)` (Driver et al., 2017, §4.3, pp. 9–10; Eq. 5, p. 5; JSS PDF re-opened 2026-08-21T20:07Z). Form the stationary latent mean first, then `τ + λ` of that mean. `τ + λ μ_0` for free `T0MEANS` is not that composition. `τ + λ(−κ / a)` is not that composition when `B z ≠ 0`. `τ + λ μ_t` is not that composition. `MANIFESTMEANS` is not `E(y_0)`. The constrained latent mean is not `E(y_0)`. Evolving from that stationary start with `CINT` and `TIPREDEFFECT` stays at the stationary mean. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. The contemporaneous time-dependent predictor impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`). Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). The §7.2 level-change form is not that impulse. The observed mean of that contemporaneous impulse is `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-impulse latent mean is not `E(y_t)`. The time-independent predictor increment is `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`). Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. `TIPREDEFFECT` is `B`, not that discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle et al. (2012, Eq. 14). The observed mean of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The within-interval time-dependent impulse carry is `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation). Form `m x` first, then `e^{a(t−u)} m x`. A zero drift is `m x` with no dissipation. Underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept. `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). An impulse at `u = t` is the contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. The observed mean of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The carried latent mean is not `E(y_t)`. The first-occasion time-independent predictor shift is `t0_b z` (Driver et al., 2017, Table 3 `T0TIPREDEFFECT`; Eq. 3 first summand). Form `t0_b z` first, then `e^{a Δt} t0_b z`. Form `μ_t` first, then add that carry. A zero drift is `t0_b z`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. `e^{A Δt} t0_b z` is not `t0_b z`. `T0TIPREDEFFECT` is the coefficient, not the shift. The observed mean of that first-occasion carry is `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The evolved-plus-carry latent mean is not `E(y_t)`. The first-occasion time-dependent predictor shift is `t0_m x0` (Driver et al., 2017, Table 3 `T0TDPREDEFFECT`; Eq. 3 first summand; JSS PDF re-opened 2026-08-20T19:10Z). Form `t0_m x0` first, then `e^{a Δt} t0_m x0`. Form `μ_t` first, then add that carry. A zero drift is `t0_m x0`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`. `e^{A Δt} t0_m x0` is not `t0_m x0`. `T0TDPREDEFFECT` is the coefficient, not the shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The observed mean of that first-occasion TD carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean. The evolved-plus-carry latent mean is not `E(y_t)`. The lasting level-change `CINT` is `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z). Form `m x` first, then multiply by `−a`. Stable `a < 0` is required so `−κ / a = m x` is an equilibrium offset. `a ≥ 0` cannot hold a new process mean. `−a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not this `CINT` setting. Equation 3 maps that intercept as `(1 − e^{a Δt}) m x` (JSS PDF re-opened 2026-08-20T19:50Z). Form the level-change `CINT` first, then the discrete intercept map. Underflow of `e^{a Δt}` to `+0` keeps `m x`. `(1 − e^{a Δt}) m x` is not `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`. The printed §7.2 lasting level change is an extra near-zero-drift latent process (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z). `T0MEANS`, `CINT`, `T0VAR`, `DIFFUSION`, and `TRAITVAR` of that process are fixed to 0; `TDPREDEFFECT` on it is fixed to 1; its `DRIFT` diagonal is very close to 0 (printed example `−0.000001`; precisely 0 causes computational problems); the original process is driven by the `DRIFT` coupling `a_{ηξ}`. After a unit identification impulse the scalar contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`). Form `a_{ηξ} x` first. A zero coupling or zero predictor is exactly zero. `ε ≥ 0` fails closed. That contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. The observed mean of that extra-process contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5 of that §7.2 contribution; JSS PDF re-opened 2026-08-21T06:12Z). The extra process has `LAMBDA` 0 and is not an observed indicator. Original indicators load on the original process after the `DRIFT` coupling. The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The contribution is not `E(y_t)`. The evolved-plus-contribution latent mean is not `E(y_t)`. `T0TDPREDEFFECT` on the extra process begins at `t = 0` and uses `Δt = t − t0` for both the original-process evolution and the extra drive. `TDPREDEFFECT` after `t0` uses `t − u` with `t0 < u < t` for the extra drive while `μ_t` still uses `Δt`. The observed mean of that after-t0 extra-process contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, Eq. 5 of that §7.2 after-t0 contribution; JSS PDF re-opened 2026-08-21T06:32Z). The first-occasion extra-process observed mean is not that observed mean when `u ≠ t0`. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is a Dirac on the original process and is not that `DRIFT` drive. An impulse at `u = t0` or `u = t` is not interior. The asymptotic time-independent predictor effect is `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z). Form `B z` first, then divide by `-a`. Stable `a < 0` is required. `a ≥ 0` cannot hold a finite process-mean change. `-B z / a` is not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. The asymptotic time-independent predictor variance is `(B / a)² v` (Driver et al., 2017, §7.2, pp. 20–21 `addedTIPREDVAR`). Form the unit asymptotic effect first, then square, then multiply by `v`. `(B / a)² v` is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`. The asymptotic continuous intercept is `-κ / a` (Driver et al., 2017, Table 2, p. 12 `asymCINT`; Eq. 3 as `Δt → ∞`; JSS PDF opened 2026-08-21T16:13Z). Form `κ` first, then divide by `-a`. Stable `a < 0` is required. `-κ / a` is not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`. The p. 16 stationary `T0MEANS` constraint is `-κ / a + −B z / a`. Form the intercept contribution first, then include the TI extra effect, then add. That constrained first-occasion mean is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean. Equation 5 of that constrained mean is `τ + λ(−κ / a + −B z / a)` (Driver et al., 2017, §4.3, pp. 9–10; Eq. 5, p. 5; JSS PDF re-opened 2026-08-21T20:07Z). Form the stationary latent mean first, then `τ + λ` of that mean. `τ + λ μ_0` for free `T0MEANS` is not that composition. `τ + λ(−κ / a)` is not that composition when `B z ≠ 0`. `τ + λ μ_t` is not that composition. `MANIFESTMEANS` is not `E(y_0)`. The constrained latent mean is not `E(y_0)`. The p. 16 constrained first-occasion variance `trait + −q / (2 a) + (B / a)² v` is not free `T0VAR`, not `asymDIFFUSION` alone, not `TRAITVAR` alone, not `addedTIPREDVAR` alone, and not the finite-interval discrete latent variance. Evolving from that stationary start with `CINT` and `TIPREDEFFECT` stays at the stationary mean. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. diff --git a/crates/psychometric_core/src/error.rs b/crates/psychometric_core/src/error.rs index 6affb4ab..2ed90f89 100644 --- a/crates/psychometric_core/src/error.rs +++ b/crates/psychometric_core/src/error.rs @@ -408,6 +408,30 @@ pub enum PsychometricError { /// stationary `T0MEANS`. `τ + λ μ_0` is not /// `τ + λ(−κ / a + −B z / a)`. InitialObservedMeanIsNotStationaryInitialObservedMean, + /// Driver §4.3 / p. 16 stationary `T0VAR` was treated as free + /// `T0VAR`. `trait + −q / (2 a) + (B / a)² v` is the constrained + /// first-occasion variance, not the free first-occasion latent + /// variance. + StationaryInitialLatentVarianceIsNotInitialLatentVariance, + /// Driver §4.3 / p. 16 stationary `T0VAR` was treated as + /// `asymDIFFUSION`. The constraint includes trait variance and + /// time-independent predictor variance. `-q / (2 a)` is not that + /// composition when `TRAITVAR` or `addedTIPREDVAR` is nonzero. + StationaryInitialLatentVarianceIsNotStationaryWithinSubject, + /// Driver §4.3 / p. 16 stationary `T0VAR` was treated as + /// `TRAITVAR`. The constraint includes the within-subject + /// process variance and time-independent predictor variance. + StationaryInitialLatentVarianceIsNotTraitVariance, + /// Driver §4.3 / p. 16 stationary `T0VAR` was treated as + /// `addedTIPREDVAR`. The constraint includes trait variance and + /// `asymDIFFUSION`. `(B / a)² v` is not that composition when + /// those contributions are nonzero. + StationaryInitialLatentVarianceIsNotAsymptoticTimeIndependentVariance, + /// Driver §4.3 / p. 16 stationary `T0VAR` was treated as a + /// finite-interval discrete latent variance. + /// `exp(2 a Δt) p + Q_Δt` is not the constrained first-occasion + /// variance. + StationaryInitialLatentVarianceIsNotDiscreteVariance, } impl fmt::Display for PsychometricError { @@ -735,6 +759,21 @@ impl fmt::Display for PsychometricError { Self::InitialObservedMeanIsNotStationaryInitialObservedMean => { "free first-occasion observed mean is not the stationary first-occasion observed mean" } + Self::StationaryInitialLatentVarianceIsNotInitialLatentVariance => { + "stationary first-occasion latent variance is not the free first-occasion latent variance" + } + Self::StationaryInitialLatentVarianceIsNotStationaryWithinSubject => { + "stationary first-occasion latent variance is not the asymptotic within-subject variance" + } + Self::StationaryInitialLatentVarianceIsNotTraitVariance => { + "stationary first-occasion latent variance is not the trait variance" + } + Self::StationaryInitialLatentVarianceIsNotAsymptoticTimeIndependentVariance => { + "stationary first-occasion latent variance is not the asymptotic time-independent predictor variance" + } + Self::StationaryInitialLatentVarianceIsNotDiscreteVariance => { + "stationary first-occasion latent variance is not the finite-interval discrete latent variance" + } }; formatter.write_str(message) } @@ -1236,4 +1275,31 @@ mod tests { "free first-occasion observed mean is not the stationary first-occasion observed mean" ); } + + #[test] + fn stationary_initial_latent_variance_boundary_messages_are_stable() { + assert_eq!( + PsychometricError::StationaryInitialLatentVarianceIsNotInitialLatentVariance + .to_string(), + "stationary first-occasion latent variance is not the free first-occasion latent variance" + ); + assert_eq!( + PsychometricError::StationaryInitialLatentVarianceIsNotStationaryWithinSubject + .to_string(), + "stationary first-occasion latent variance is not the asymptotic within-subject variance" + ); + assert_eq!( + PsychometricError::StationaryInitialLatentVarianceIsNotTraitVariance.to_string(), + "stationary first-occasion latent variance is not the trait variance" + ); + assert_eq!( + PsychometricError::StationaryInitialLatentVarianceIsNotAsymptoticTimeIndependentVariance + .to_string(), + "stationary first-occasion latent variance is not the asymptotic time-independent predictor variance" + ); + assert_eq!( + PsychometricError::StationaryInitialLatentVarianceIsNotDiscreteVariance.to_string(), + "stationary first-occasion latent variance is not the finite-interval discrete latent variance" + ); + } } diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index f670497d..535c46b4 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -84,6 +84,22 @@ //! first, then `τ + λ` of that mean. `τ + λ μ_0` for free `T0MEANS` //! is not that composition. `τ + λ(−κ / a)` is not that composition //! when `B z ≠ 0`. `τ + λ μ_t` is not that composition. +//! Section 4.3 (pp. 9–10; JSS PDF re-opened 2026-08-22T03:07Z) +//! constrains `T0VAR` to the model-predicted variance when +//! `stationary` includes `"T0VAR"`. Page 16 names `asymDIFFUSION` +//! the total within-subject variance `-q / (2 a)`. Section 4.3 +//! (p. 9) adds `TRAITVAR`. Section 7.2 (pp. 20–21) names +//! `addedTIPREDVAR` the stable between-subject variance accounted +//! for by time-independent predictors, `(B / a)² v`. The scalar +//! composition is `trait + −q / (2 a) + (B / a)² v` for stable +//! `a < 0` when the process or TI contribution is nonzero. Form the +//! within-subject contribution first, then include the trait, then +//! include the TI extra variance, then add. That constrained +//! first-occasion variance is not free `T0VAR`, not +//! `asymDIFFUSION` alone, not `TRAITVAR` alone, not +//! `addedTIPREDVAR` alone, and not the finite-interval discrete +//! latent variance `exp(2 a Δt) p + Q_Δt`. The printed 2-latent +//! `addedTIPREDVAR` 2.838 is not this scalar map. //! Table 3 (p. 13) names a different matrix //! `T0TIPREDEFFECT` for time-independent predictors on latents at //! `T0`. The scalar first-occasion shift is `t0_b z`. Equation 3's @@ -3162,6 +3178,171 @@ pub fn refuse_initial_observed_mean_as_stationary_initial_observed_mean( Err(PsychometricError::InitialObservedMeanIsNotStationaryInitialObservedMean) } +/// Exact scalar §4.3 / p. 16 stationary `T0VAR`. +/// +/// Driver, Oud, and Voelkle (2017, §4.3, pp. 9–10; p. 16; Table 2, +/// p. 12; §7.2, pp. 20–21; Eq. 4, p. 5; JSS PDF re-opened +/// 2026-08-22T03:07Z from +/// ) +/// constrain `T0VAR` to the model-predicted variance when +/// `stationary` includes `"T0VAR"`. Section 4.3 writes that the +/// first-occasion variances are constrained according to the model +/// predicted variances across all time points. Page 16 names +/// `asymDIFFUSION` the total within-subject variance as `Δt → ∞`. +/// For stable `a < 0` that scalar is `-q / (2 a)`. Section 4.3 +/// (p. 9) adds a stable trait process with `DRIFT` and `DIFFUSION` +/// fixed to zero (`TRAITVAR`). Section 7.2 names `addedTIPREDVAR` +/// the stable between-subject variance accounted for by +/// time-independent predictors; the scalar map is `(B / a)² v`. +/// The constrained first-occasion variance is +/// `trait + −q / (2 a) + (B / a)² v`. Form the within-subject +/// contribution first, then include the trait, then include the TI +/// extra variance, then add. A zero trait, a zero diffusion, and a +/// zero TI contribution is exactly zero. A zero diffusion and a +/// zero TI contribution is exactly the trait. `a ≥ 0` cannot hold a +/// finite process variance when the diffusion or the TI +/// contribution is nonzero and fails closed. Trait-only variance +/// does not require a stable drift. That constrained +/// first-occasion variance is not free `T0VAR`, not +/// `asymDIFFUSION` alone, not `TRAITVAR` alone, not +/// `addedTIPREDVAR` alone, and not the finite-interval discrete +/// latent variance `exp(2 a Δt) p + Q_Δt`. The printed 2-latent +/// `addedTIPREDVAR` 2.838 is not this scalar map. This is not a +/// Kalman filter, not a matrix `expm`, and not ctsem estimation. +/// +/// # Errors +/// +/// Returns [`PsychometricError::EventTimeRequired`] for any +/// non-event clock, +/// [`PsychometricError::StationaryVarianceRequiresStableDrift`] +/// when the diffusion is nonzero and the drift is not strictly +/// negative, +/// [`PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift`] +/// when the TI contribution is nonzero and the drift is not +/// strictly negative, and +/// [`PsychometricError::InvalidNumericInput`] when an input is +/// non-finite, a variance is negative, or a product or sum +/// overflows. +pub fn recover_stationary_initial_latent_variance( + trait_variance: f64, + continuous_diffusion: f64, + time_independent_effect: f64, + predictor_variance: f64, + log_rate: f64, + clock: LagClock, +) -> Result { + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + let state = if continuous_diffusion == 0.0 { + 0.0 + } else { + recover_stationary_latent_variance(continuous_diffusion, log_rate, clock)? + }; + let trait_plus_state = recover_trait_plus_state_latent_variance(trait_variance, state)?; + let added = recover_asymptotic_time_independent_predictor_variance( + time_independent_effect, + predictor_variance, + log_rate, + clock, + )?; + require_finite(trait_plus_state + added) +} + +/// Refuse treating §4.3 / p. 16 stationary `T0VAR` as free `T0VAR`. +/// +/// `trait + −q / (2 a) + (B / a)² v` is the constrained +/// first-occasion variance. Table 2 names the free first-occasion +/// latent variance `T0VAR`. Those are not the same map. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::StationaryInitialLatentVarianceIsNotInitialLatentVariance`]. +pub fn refuse_stationary_initial_latent_variance_as_initial_latent_variance( + stationary_variance: f64, + initial_latent_variance: f64, +) -> Result { + let _ = (stationary_variance, initial_latent_variance); + Err(PsychometricError::StationaryInitialLatentVarianceIsNotInitialLatentVariance) +} + +/// Refuse treating §4.3 / p. 16 stationary `T0VAR` as +/// `asymDIFFUSION`. +/// +/// The constraint includes trait variance and time-independent +/// predictor variance. `-q / (2 a)` is the within-subject +/// contribution and is not that composition when `TRAITVAR` or +/// `addedTIPREDVAR` is nonzero. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::StationaryInitialLatentVarianceIsNotStationaryWithinSubject`]. +pub fn refuse_stationary_initial_latent_variance_as_stationary_within_subject( + stationary_t0_variance: f64, + asymptotic_within_subject: f64, +) -> Result { + let _ = (stationary_t0_variance, asymptotic_within_subject); + Err(PsychometricError::StationaryInitialLatentVarianceIsNotStationaryWithinSubject) +} + +/// Refuse treating §4.3 / p. 16 stationary `T0VAR` as `TRAITVAR`. +/// +/// The constraint includes the within-subject process variance and +/// time-independent predictor variance. `TRAITVAR` is not that +/// composition when those contributions are nonzero. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::StationaryInitialLatentVarianceIsNotTraitVariance`]. +pub fn refuse_stationary_initial_latent_variance_as_trait_variance( + stationary_t0_variance: f64, + trait_variance: f64, +) -> Result { + let _ = (stationary_t0_variance, trait_variance); + Err(PsychometricError::StationaryInitialLatentVarianceIsNotTraitVariance) +} + +/// Refuse treating §4.3 / p. 16 stationary `T0VAR` as +/// `addedTIPREDVAR`. +/// +/// The constraint includes trait variance and `asymDIFFUSION`. +/// `(B / a)² v` is the TI extra variance and is not that +/// composition when those contributions are nonzero. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::StationaryInitialLatentVarianceIsNotAsymptoticTimeIndependentVariance`]. +pub fn refuse_stationary_initial_latent_variance_as_asymptotic_time_independent_variance( + stationary_t0_variance: f64, + added_predictor_variance: f64, +) -> Result { + let _ = (stationary_t0_variance, added_predictor_variance); + Err(PsychometricError::StationaryInitialLatentVarianceIsNotAsymptoticTimeIndependentVariance) +} + +/// Refuse treating §4.3 / p. 16 stationary `T0VAR` as a +/// finite-interval discrete latent variance. +/// +/// `trait + −q / (2 a) + (B / a)² v` is the `Δt → ∞` constrained +/// first-occasion variance. `exp(2 a Δt) p + Q_Δt` is a finite +/// event interval and is not that limit. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::StationaryInitialLatentVarianceIsNotDiscreteVariance`]. +pub fn refuse_stationary_initial_latent_variance_as_discrete_variance( + stationary_t0_variance: f64, + discrete_variance: f64, +) -> Result { + let _ = (stationary_t0_variance, discrete_variance); + Err(PsychometricError::StationaryInitialLatentVarianceIsNotDiscreteVariance) +} + /// Exact scalar observed mean of a time-independent predictor. /// /// Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Eq. 3, p. 5; Table 2, @@ -4689,10 +4870,11 @@ mod tests { recover_level_change_extra_process_contribution_after, recover_local_log_rate, recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, - recover_stationary_initial_latent_mean, recover_stationary_initial_observed_mean, - recover_stationary_latent_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_stationary_initial_latent_mean, recover_stationary_initial_latent_variance, + recover_stationary_initial_observed_mean, recover_stationary_latent_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, @@ -4767,6 +4949,11 @@ mod tests { refuse_stationary_initial_latent_mean_as_discrete_mean, refuse_stationary_initial_latent_mean_as_initial_latent_mean, refuse_stationary_initial_latent_mean_as_observed_mean, + refuse_stationary_initial_latent_variance_as_asymptotic_time_independent_variance, + refuse_stationary_initial_latent_variance_as_discrete_variance, + refuse_stationary_initial_latent_variance_as_initial_latent_variance, + refuse_stationary_initial_latent_variance_as_stationary_within_subject, + refuse_stationary_initial_latent_variance_as_trait_variance, refuse_stationary_initial_observed_mean_as_manifest_means, refuse_time_dependent_impulse_as_continuous_intercept, refuse_time_dependent_impulse_as_time_independent_effect, @@ -8424,6 +8611,241 @@ mod tests { ); } + #[test] + fn stationary_initial_latent_variance_recovers_driver_section_four_point_three() { + // Driver et al. (2017, §4.3, pp. 9–10; p. 16) constrain T0VAR + // to model-predicted variances. The scalar composition is + // trait + −q / (2 a) + (B / a)² v. Reconstruct a from printed + // LeisureTime TIPREDEFFECT −0.225 / asymTIPREDEFFECT −1.673. + // The printed 2-latent addedTIPREDVAR 2.838 is not this + // scalar map. + let printed_effect = -0.225_f64; + let printed_asym = -1.673_f64; + let log_rate = -printed_effect / printed_asym; + let trait_variance = 1.0_f64; + let diffusion = 0.4_f64; + let predictor_variance = 1.0_f64; + let recovered = recover_stationary_initial_latent_variance( + trait_variance, + diffusion, + printed_effect, + predictor_variance, + log_rate, + LagClock::EventTime, + ) + .expect("stationary T0VAR"); + let state = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + let trait_plus_state = + recover_trait_plus_state_latent_variance(trait_variance, state).expect("trait+state"); + let added = recover_asymptotic_time_independent_predictor_variance( + printed_effect, + predictor_variance, + log_rate, + LagClock::EventTime, + ) + .expect("addedTIPREDVAR"); + assert!((recovered - (trait_plus_state + added)).abs() < 1e-12); + let state_only = recover_stationary_initial_latent_variance( + 0.0, + diffusion, + 0.0, + predictor_variance, + log_rate, + LagClock::EventTime, + ) + .expect("state-only"); + assert!((state_only - state).abs() < 1e-15); + let trait_only = recover_stationary_initial_latent_variance( + trait_variance, + 0.0, + 0.0, + predictor_variance, + 0.0, + LagClock::EventTime, + ) + .expect("trait-only"); + assert!((trait_only - trait_variance).abs() < 1e-15); + let added_only = recover_stationary_initial_latent_variance( + 0.0, + 0.0, + printed_effect, + predictor_variance, + log_rate, + LagClock::EventTime, + ) + .expect("ti-only"); + assert!((added_only - added).abs() < 1e-15); + assert_eq!( + recover_stationary_initial_latent_variance( + 0.0, + 0.0, + 0.0, + predictor_variance, + 0.0, + LagClock::EventTime + ), + Ok(0.0) + ); + assert_eq!( + recover_stationary_initial_latent_variance( + 0.0, + 0.0, + 0.0, + predictor_variance, + 0.5, + LagClock::EventTime + ), + Ok(0.0) + ); + } + + #[test] + fn stationary_initial_latent_variance_is_not_t0_state_trait_tipred_or_discrete() { + let trait_variance = 1.0_f64; + let diffusion = 0.4_f64; + let log_rate = -0.134_488_942_f64; + let recovered = recover_stationary_initial_latent_variance( + trait_variance, + diffusion, + -0.225, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("stationary T0VAR"); + let state = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + let added = recover_asymptotic_time_independent_predictor_variance( + -0.225, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("addedTIPREDVAR"); + let discrete = recover_discrete_latent_variance( + recovered, + diffusion, + log_rate, + 1.0, + LagClock::EventTime, + ) + .expect("Var(η_t)"); + assert!((recovered - 2.0).abs() > 1e-3); + assert!((recovered - state).abs() > 1e-3); + assert!((recovered - trait_variance).abs() > 1e-3); + assert!((recovered - added).abs() > 1e-3); + assert!((recovered - discrete).abs() > 1e-3); + assert!((recovered - 2.838).abs() > 1e-3); + assert_eq!( + refuse_stationary_initial_latent_variance_as_initial_latent_variance(recovered, 2.0), + Err(PsychometricError::StationaryInitialLatentVarianceIsNotInitialLatentVariance) + ); + assert_eq!( + refuse_stationary_initial_latent_variance_as_stationary_within_subject( + recovered, state + ), + Err(PsychometricError::StationaryInitialLatentVarianceIsNotStationaryWithinSubject) + ); + assert_eq!( + refuse_stationary_initial_latent_variance_as_trait_variance(recovered, trait_variance), + Err(PsychometricError::StationaryInitialLatentVarianceIsNotTraitVariance) + ); + assert_eq!( + refuse_stationary_initial_latent_variance_as_asymptotic_time_independent_variance( + recovered, added + ), + Err( + PsychometricError::StationaryInitialLatentVarianceIsNotAsymptoticTimeIndependentVariance + ) + ); + assert_eq!( + refuse_stationary_initial_latent_variance_as_discrete_variance(recovered, discrete), + Err(PsychometricError::StationaryInitialLatentVarianceIsNotDiscreteVariance) + ); + } + + #[test] + fn stationary_initial_latent_variance_invalid_inputs_fail_closed() { + assert_eq!( + recover_stationary_initial_latent_variance( + 1.0, + 0.4, + -0.225, + 1.0, + -0.13, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_stationary_initial_latent_variance( + 0.0, + 0.4, + 0.0, + 1.0, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + recover_stationary_initial_latent_variance( + 0.0, + 0.0, + -0.225, + 1.0, + 0.5, + LagClock::EventTime + ), + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) + ); + assert_eq!( + recover_stationary_initial_latent_variance( + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + LagClock::EventTime + ), + Ok(0.0) + ); + assert_eq!( + recover_stationary_initial_latent_variance( + f64::NAN, + 0.4, + 0.0, + 0.0, + -0.5, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_stationary_initial_latent_variance( + f64::MAX, + f64::MAX, + 0.0, + 0.0, + -0.5, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_stationary_initial_latent_variance( + f64::MAX, + 0.0, + 1.0, + f64::MAX, + -1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + } + #[test] fn discrete_observed_mean_with_impulse_recovers_driver_equation_five() { let loading = 2.0_f64; diff --git a/crates/psychometric_core/src/lib.rs b/crates/psychometric_core/src/lib.rs index c2f265c9..aef364fa 100644 --- a/crates/psychometric_core/src/lib.rs +++ b/crates/psychometric_core/src/lib.rs @@ -140,6 +140,14 @@ //! that observed mean when `B z ≠ 0`; `τ + λ μ_t` is not that //! observed mean; `MANIFESTMEANS` is not `E(y_0)`; the constrained //! latent mean is not `E(y_0)`), +//! recovers the Driver §4.3 / p. 16 stationary `T0VAR` as +//! `trait + −q / (2 a) + (B / a)² v` +//! (JSS PDF re-opened 2026-08-22T03:07Z; constrained first-occasion +//! variance; form the within-subject contribution first, then +//! include the trait, then include the TI extra variance, then add; +//! not free `T0VAR`, not `asymDIFFUSION` alone, not `TRAITVAR` +//! alone, not `addedTIPREDVAR` alone, and not the finite-interval +//! discrete latent variance), //! and refuses //! latent-mean comparison below strong invariance. @@ -278,6 +286,8 @@ pub use event_time::recover_manifest_observed_variance; pub use event_time::recover_manifest_trait_plus_state_observed_variance; /// Exact scalar p. 16 stationary `T0MEANS` `-κ / a + −B z / a`. pub use event_time::recover_stationary_initial_latent_mean; +/// Exact scalar §4.3 / p. 16 stationary `T0VAR` `trait + −q / (2 a) + (B / a)² v`. +pub use event_time::recover_stationary_initial_latent_variance; /// Exact scalar Eq. 5 of §4.3 stationary `T0MEANS` `τ + λ(−κ / a + −B z / a)`. pub use event_time::recover_stationary_initial_observed_mean; /// Exact scalar stationary within-subject variance `-q / (2 a)`. @@ -448,6 +458,16 @@ pub use event_time::refuse_stationary_initial_latent_mean_as_discrete_mean; pub use event_time::refuse_stationary_initial_latent_mean_as_initial_latent_mean; /// Refuse treating §4.3 stationary `T0MEANS` as `E(y_0)`. pub use event_time::refuse_stationary_initial_latent_mean_as_observed_mean; +/// Refuse treating §4.3 / p. 16 stationary `T0VAR` as `addedTIPREDVAR`. +pub use event_time::refuse_stationary_initial_latent_variance_as_asymptotic_time_independent_variance; +/// Refuse treating §4.3 / p. 16 stationary `T0VAR` as a finite-interval discrete variance. +pub use event_time::refuse_stationary_initial_latent_variance_as_discrete_variance; +/// Refuse treating §4.3 / p. 16 stationary `T0VAR` as free `T0VAR`. +pub use event_time::refuse_stationary_initial_latent_variance_as_initial_latent_variance; +/// Refuse treating §4.3 / p. 16 stationary `T0VAR` as `asymDIFFUSION`. +pub use event_time::refuse_stationary_initial_latent_variance_as_stationary_within_subject; +/// Refuse treating §4.3 / p. 16 stationary `T0VAR` as `TRAITVAR`. +pub use event_time::refuse_stationary_initial_latent_variance_as_trait_variance; /// Refuse treating Eq. 5 of §4.3 stationary `T0MEANS` as `MANIFESTMEANS`. pub use event_time::refuse_stationary_initial_observed_mean_as_manifest_means; /// Refuse treating Driver Eq. 3 `TDPREDEFFECT` impulse as `CINT`. 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 64e05254..693e375c 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -34,10 +34,11 @@ use psychometric_core::{ recover_level_change_extra_process_contribution_after, recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, - recover_stationary_initial_latent_mean, recover_stationary_initial_observed_mean, - recover_stationary_latent_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_stationary_initial_latent_mean, recover_stationary_initial_latent_variance, + recover_stationary_initial_observed_mean, recover_stationary_latent_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, @@ -109,6 +110,11 @@ use psychometric_core::{ refuse_stationary_initial_latent_mean_as_discrete_mean, refuse_stationary_initial_latent_mean_as_initial_latent_mean, refuse_stationary_initial_latent_mean_as_observed_mean, + refuse_stationary_initial_latent_variance_as_asymptotic_time_independent_variance, + refuse_stationary_initial_latent_variance_as_discrete_variance, + refuse_stationary_initial_latent_variance_as_initial_latent_variance, + refuse_stationary_initial_latent_variance_as_stationary_within_subject, + refuse_stationary_initial_latent_variance_as_trait_variance, refuse_stationary_initial_observed_mean_as_manifest_means, refuse_time_dependent_impulse_as_continuous_intercept, refuse_time_dependent_impulse_as_time_independent_effect, @@ -4699,3 +4705,155 @@ fn stationary_initial_observed_mean_refuses_unstable_drift_and_non_event_clocks( Ok(0.5) ); } + +#[test] +fn stationary_initial_latent_variance_recovers_driver_section_four_point_three() { + let printed_effect = -0.225_f64; + let printed_asym = -1.673_f64; + let log_rate = -printed_effect / printed_asym; + let trait_variance = 1.0_f64; + let diffusion = 0.4_f64; + let predictor_variance = 1.0_f64; + let recovered = recover_stationary_initial_latent_variance( + trait_variance, + diffusion, + printed_effect, + predictor_variance, + log_rate, + LagClock::EventTime, + ) + .expect("stationary T0VAR"); + let state = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + let trait_plus_state = + recover_trait_plus_state_latent_variance(trait_variance, state).expect("trait+state"); + let added = recover_asymptotic_time_independent_predictor_variance( + printed_effect, + predictor_variance, + log_rate, + LagClock::EventTime, + ) + .expect("addedTIPREDVAR"); + let expected = trait_plus_state + added; + let error = rmse(&[expected], &[recovered]); + assert!( + error < 1e-12, + "Driver §4.3 stationary T0VAR RMSE {error}: got {recovered}" + ); + let discrete = + recover_discrete_latent_variance(recovered, diffusion, log_rate, 1.0, LagClock::EventTime) + .expect("Var(η_t)"); + let free_t0 = 2.0_f64; + assert!(rmse(&[recovered], &[free_t0]) > error); + assert!(rmse(&[recovered], &[state]) > error); + assert!(rmse(&[recovered], &[trait_variance]) > error); + assert!(rmse(&[recovered], &[added]) > error); + assert!(rmse(&[recovered], &[discrete]) > error); + assert!(rmse(&[recovered], &[2.838]) > error); + assert_eq!( + recover_stationary_initial_latent_variance( + 0.0, + 0.0, + 0.0, + predictor_variance, + log_rate, + LagClock::EventTime, + ), + Ok(0.0) + ); + assert_eq!( + recover_stationary_initial_latent_variance( + trait_variance, + 0.0, + 0.0, + predictor_variance, + 0.0, + LagClock::EventTime, + ), + Ok(trait_variance) + ); + assert_eq!( + refuse_stationary_initial_latent_variance_as_initial_latent_variance(recovered, free_t0), + Err(PsychometricError::StationaryInitialLatentVarianceIsNotInitialLatentVariance) + ); + assert_eq!( + refuse_stationary_initial_latent_variance_as_stationary_within_subject(recovered, state), + Err(PsychometricError::StationaryInitialLatentVarianceIsNotStationaryWithinSubject) + ); + assert_eq!( + refuse_stationary_initial_latent_variance_as_trait_variance(recovered, trait_variance), + Err(PsychometricError::StationaryInitialLatentVarianceIsNotTraitVariance) + ); + assert_eq!( + refuse_stationary_initial_latent_variance_as_asymptotic_time_independent_variance( + recovered, added + ), + Err( + PsychometricError::StationaryInitialLatentVarianceIsNotAsymptoticTimeIndependentVariance + ) + ); + assert_eq!( + refuse_stationary_initial_latent_variance_as_discrete_variance(recovered, discrete), + Err(PsychometricError::StationaryInitialLatentVarianceIsNotDiscreteVariance) + ); +} + +#[test] +fn stationary_initial_latent_variance_refuses_unstable_drift_and_non_event_clocks() { + assert_eq!( + recover_stationary_initial_latent_variance( + 1.0, + 0.4, + -0.225, + 1.0, + -0.13, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_stationary_initial_latent_variance(0.0, 0.4, 0.0, 1.0, 0.0, LagClock::EventTime), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + recover_stationary_initial_latent_variance(0.0, 0.0, -0.225, 1.0, 0.5, LagClock::EventTime), + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) + ); + assert_eq!( + recover_stationary_initial_latent_variance(0.0, 0.0, 0.0, 1.0, 0.0, LagClock::EventTime), + Ok(0.0) + ); + assert_eq!( + recover_stationary_initial_latent_variance( + f64::NAN, + 0.4, + 0.0, + 0.0, + -0.5, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_stationary_initial_latent_variance( + f64::MAX, + f64::MAX, + 0.0, + 0.0, + -0.5, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_stationary_initial_latent_variance( + f64::MAX, + 0.0, + 1.0, + f64::MAX, + -1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); +} diff --git a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs index 18913554..e576c397 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -29,10 +29,10 @@ use psychometric_core::{ recover_level_change_extra_process_contribution_after, recover_loading_point_estimate_mean, recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, - recover_stationary_initial_latent_mean, recover_stationary_initial_observed_mean, - recover_stationary_latent_variance, recover_time_dependent_predictor_impulse, - recover_time_dependent_predictor_impulse_carry, recover_trait_plus_state_latent_variance, - recover_within_residual_event_time_log_rate, + recover_stationary_initial_latent_mean, recover_stationary_initial_latent_variance, + recover_stationary_initial_observed_mean, recover_stationary_latent_variance, + recover_time_dependent_predictor_impulse, recover_time_dependent_predictor_impulse_carry, + 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, @@ -102,6 +102,11 @@ use psychometric_core::{ refuse_stationary_initial_latent_mean_as_discrete_mean, refuse_stationary_initial_latent_mean_as_initial_latent_mean, refuse_stationary_initial_latent_mean_as_observed_mean, + refuse_stationary_initial_latent_variance_as_asymptotic_time_independent_variance, + refuse_stationary_initial_latent_variance_as_discrete_variance, + refuse_stationary_initial_latent_variance_as_initial_latent_variance, + refuse_stationary_initial_latent_variance_as_stationary_within_subject, + refuse_stationary_initial_latent_variance_as_trait_variance, refuse_stationary_initial_observed_mean_as_manifest_means, refuse_time_dependent_impulse_as_continuous_intercept, refuse_time_dependent_impulse_as_time_independent_effect, @@ -2423,3 +2428,87 @@ fn stationary_initial_observed_mean_is_not_manifest_latent_evolved_or_free() { ) ); } + +#[test] +fn stationary_initial_latent_variance_is_not_t0_state_trait_tipred_or_discrete() { + let trait_variance = 1.0_f64; + let diffusion = 0.4_f64; + let log_rate = -0.134_488_942_f64; + let recovered = recover_stationary_initial_latent_variance( + trait_variance, + diffusion, + -0.225, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("stationary T0VAR"); + let state = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + let added = recover_asymptotic_time_independent_predictor_variance( + -0.225, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("addedTIPREDVAR"); + let discrete = + recover_discrete_latent_variance(recovered, diffusion, log_rate, 1.0, LagClock::EventTime) + .expect("Var(η_t)"); + assert!( + (recovered - 2.0).abs() > 1e-3, + "Driver et al. (2017, §4.3 / p. 16): constrained T0VAR is not free T0VAR" + ); + assert!( + (recovered - state).abs() > 1e-3, + "Driver et al. (2017, §4.3 / p. 16): constrained T0VAR is not asymDIFFUSION" + ); + assert!( + (recovered - trait_variance).abs() > 1e-3, + "Driver et al. (2017, §4.3 / p. 16): constrained T0VAR is not TRAITVAR" + ); + assert!( + (recovered - added).abs() > 1e-3, + "Driver et al. (2017, §4.3 / p. 16): constrained T0VAR is not addedTIPREDVAR" + ); + assert!( + (recovered - discrete).abs() > 1e-3, + "Driver et al. (2017, §4.3 / p. 16): constrained T0VAR is not Var(η_t)" + ); + assert!( + (recovered - 2.838).abs() > 1e-3, + "Driver et al. (2017, §7.2): printed 2-latent addedTIPREDVAR is not this scalar map" + ); + assert_eq!( + refuse_stationary_initial_latent_variance_as_initial_latent_variance(recovered, 2.0), + Err( + psychometric_core::PsychometricError::StationaryInitialLatentVarianceIsNotInitialLatentVariance + ) + ); + assert_eq!( + refuse_stationary_initial_latent_variance_as_stationary_within_subject(recovered, state), + Err( + psychometric_core::PsychometricError::StationaryInitialLatentVarianceIsNotStationaryWithinSubject + ) + ); + assert_eq!( + refuse_stationary_initial_latent_variance_as_trait_variance(recovered, trait_variance), + Err( + psychometric_core::PsychometricError::StationaryInitialLatentVarianceIsNotTraitVariance + ) + ); + assert_eq!( + refuse_stationary_initial_latent_variance_as_asymptotic_time_independent_variance( + recovered, added + ), + Err( + psychometric_core::PsychometricError::StationaryInitialLatentVarianceIsNotAsymptoticTimeIndependentVariance + ) + ); + assert_eq!( + refuse_stationary_initial_latent_variance_as_discrete_variance(recovered, discrete), + Err( + psychometric_core::PsychometricError::StationaryInitialLatentVarianceIsNotDiscreteVariance + ) + ); +} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 57a00f73..e100a36c 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -28,7 +28,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | report template/section/copied/style/modality method effects | ADR 0004/0012; PRD/TRD | simulation truth factors implemented; estimator-side method model remains future | partial | | candidate K statistical/Pareto gates + blinded LLM review | ADR 0012; research | future `model_selection` | accepted-target | | compositional topic correlation / stable clustering | ADR 0005/0012; research | future `network_analysis` | accepted-target | -| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; §7.2 level-change `CINT` is `κ = −a m x` (`a < 0`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`; Eq. 3 of that setting is `(1 − e^{a Δt}) m x`); §7.2 extra-process contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, not the dissipating Dirac; `ε ≥ 0` fails closed; Eq. 5 of that contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; extra `LAMBDA` is 0; `τ + λ μ_t` is not that observed mean; after-t0 extra-process `TDPREDEFFECT` uses `t − u` with `t0 < u < t` while `μ_t` uses `Δt`; that after-t0 observed mean is not the first-occasion extra-process observed mean; §7.2 `asymTIPREDEFFECT` is `-B z / a` for `a < 0` and is not `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; §7.2 `addedTIPREDVAR` is `(B / a)² v` and is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`; Table 2 `asymCINT` is `-κ / a` for `a < 0` and is not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`; p. 16 stationary `T0MEANS` is `-κ / a + −B z / a` and is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean; Eq. 5 of that constrained mean is `τ + λ(−κ / a + −B z / a)`; `τ + λ μ_0` is not that observed mean; `MANIFESTMEANS` is not `E(y_0)`; the constrained latent mean is not `E(y_0)`), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z); full ESEM/DSEM remaining | partial | +| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; §7.2 level-change `CINT` is `κ = −a m x` (`a < 0`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`; Eq. 3 of that setting is `(1 − e^{a Δt}) m x`); §7.2 extra-process contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, not the dissipating Dirac; `ε ≥ 0` fails closed; Eq. 5 of that contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; extra `LAMBDA` is 0; `τ + λ μ_t` is not that observed mean; after-t0 extra-process `TDPREDEFFECT` uses `t − u` with `t0 < u < t` while `μ_t` uses `Δt`; that after-t0 observed mean is not the first-occasion extra-process observed mean; §7.2 `asymTIPREDEFFECT` is `-B z / a` for `a < 0` and is not `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; §7.2 `addedTIPREDVAR` is `(B / a)² v` and is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`; Table 2 `asymCINT` is `-κ / a` for `a < 0` and is not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`; p. 16 stationary `T0MEANS` is `-κ / a + −B z / a` and is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean; Eq. 5 of that constrained mean is `τ + λ(−κ / a + −B z / a)`; `τ + λ μ_0` is not that observed mean; `MANIFESTMEANS` is not `E(y_0)`; the constrained latent mean is not `E(y_0)`; stationary `T0VAR` is `trait + −q / (2 a) + (B / a)² v` (not free `T0VAR`, not `asymDIFFUSION` alone, not `TRAITVAR` alone, not `addedTIPREDVAR` alone, and not the finite-interval discrete latent variance), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z); full ESEM/DSEM remaining | partial | | CPU bounded multithreading + GPU/VRAM streaming/parity | ADR 0001/0006 | future `compute_backend` | accepted-target | | TDT detection/tracking vs CHRONOS schema/prediction/temporal consistency | ADR 0016; PRD/research | future `event_intelligence` | accepted-target | | evidence-bounded LLM interpretation | ADR 0010/0012; PRD | `tepp_api` router plus future `interpretation_gateway` | partial | diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index d645cefc..7cb1d38e 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -1,7 +1,7 @@ # ADR 0005 — Posterior-aware ESEM/DSEM and structural interpretation **Decision status:** Accepted -**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), exact scalar contemporaneous `TDPREDEFFECT` impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; the §7.2 level-change form is not that impulse), exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of that Eq. 3 composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar time-independent `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle Eq. 14; `B` is not that discrete increment), exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of that Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of that carried latent mean; `τ + λ μ_t` is not that observed mean), exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; `T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`), exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; `T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand TD composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean), exact scalar §7.2 level-change `CINT` `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; `a < 0` so `−κ / a = m x`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2), exact scalar Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` (not `m x`, not `κ`, and not `TIPREDEFFECT`), exact scalar §7.2 extra near-zero-drift latent process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z; identification `TDPREDEFFECT` on the extra process is 1; extra `DRIFT` printed as `−0.000001`; precisely 0 causes computational problems; `ε = a` is `a_{ηξ} x Δt e^{a Δt}`; not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`; `ε ≥ 0` fails closed), exact scalar observed mean of that extra-process contribution `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z; the extra process has `LAMBDA` 0 and is not an observed indicator; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`), exact scalar after-t0 extra-process contribution `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` and Eq. 5 observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:32Z; `T0TDPREDEFFECT` uses `Δt` for both the evolution and the extra drive; `TDPREDEFFECT` after `t0` uses `t − u`; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive), exact scalar §7.2 `asymTIPREDEFFECT` `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z; expected total change in process means given a time-independent predictor; `a < 0`; not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; `a ≥ 0` fails closed), exact scalar §7.2 `addedTIPREDVAR` `(B / a)² v` (Driver et al., 2017, §7.2, pp. 20–21; stable between-subject variance accounted for by a time-independent predictor; not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`), exact scalar Table 2 `asymCINT` `-κ / a` (Driver et al., 2017, Table 2, p. 12; Eq. 3 as `Δt → ∞`; `a < 0`; not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`; `a ≥ 0` fails closed), exact scalar p. 16 stationary `T0MEANS` `-κ / a + −B z / a` (Driver et al., 2017, p. 16; constrained first-occasion mean using `T0MEANSbase` / `T0MEANSfree`; not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean), exact scalar Eq. 5 of that constrained mean `τ + λ(−κ / a + −B z / a)` (Driver et al., 2017, §4.3, pp. 9–10; Eq. 5, p. 5; JSS PDF re-opened 2026-08-21T20:07Z; form the stationary latent mean first, then `τ + λ` of that mean; `τ + λ μ_0` is not that observed mean; `τ + λ(−κ / a)` is not that observed mean when `B z ≠ 0`; `τ + λ μ_t` is not that observed mean; `MANIFESTMEANS` is not `E(y_0)`; the constrained latent mean is not `E(y_0)`), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target +**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), exact scalar contemporaneous `TDPREDEFFECT` impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; the §7.2 level-change form is not that impulse), exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of that Eq. 3 composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar time-independent `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle Eq. 14; `B` is not that discrete increment), exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of that Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of that carried latent mean; `τ + λ μ_t` is not that observed mean), exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; `T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`), exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; `T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand TD composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean), exact scalar §7.2 level-change `CINT` `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; `a < 0` so `−κ / a = m x`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2), exact scalar Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` (not `m x`, not `κ`, and not `TIPREDEFFECT`), exact scalar §7.2 extra near-zero-drift latent process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z; identification `TDPREDEFFECT` on the extra process is 1; extra `DRIFT` printed as `−0.000001`; precisely 0 causes computational problems; `ε = a` is `a_{ηξ} x Δt e^{a Δt}`; not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`; `ε ≥ 0` fails closed), exact scalar observed mean of that extra-process contribution `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z; the extra process has `LAMBDA` 0 and is not an observed indicator; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`), exact scalar after-t0 extra-process contribution `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` and Eq. 5 observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:32Z; `T0TDPREDEFFECT` uses `Δt` for both the evolution and the extra drive; `TDPREDEFFECT` after `t0` uses `t − u`; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive), exact scalar §7.2 `asymTIPREDEFFECT` `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z; expected total change in process means given a time-independent predictor; `a < 0`; not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; `a ≥ 0` fails closed), exact scalar §7.2 `addedTIPREDVAR` `(B / a)² v` (Driver et al., 2017, §7.2, pp. 20–21; stable between-subject variance accounted for by a time-independent predictor; not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`), exact scalar Table 2 `asymCINT` `-κ / a` (Driver et al., 2017, Table 2, p. 12; Eq. 3 as `Δt → ∞`; `a < 0`; not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`; `a ≥ 0` fails closed), exact scalar p. 16 stationary `T0MEANS` `-κ / a + −B z / a` (Driver et al., 2017, p. 16; constrained first-occasion mean using `T0MEANSbase` / `T0MEANSfree`; not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean), exact scalar Eq. 5 of that constrained mean `τ + λ(−κ / a + −B z / a)` (Driver et al., 2017, §4.3, pp. 9–10; Eq. 5, p. 5; JSS PDF re-opened 2026-08-21T20:07Z; form the stationary latent mean first, then `τ + λ` of that mean; `τ + λ μ_0` is not that observed mean; `τ + λ(−κ / a)` is not that observed mean when `B z ≠ 0`; `τ + λ μ_t` is not that observed mean; `MANIFESTMEANS` is not `E(y_0)`; the constrained latent mean is not `E(y_0)`), exact scalar §4.3 / p. 16 stationary `T0VAR` `trait + −q / (2 a) + (B / a)² v` (Driver et al., 2017, §4.3, pp. 9–10; p. 16; JSS PDF re-opened 2026-08-22T03:07Z; not free `T0VAR`, not `asymDIFFUSION` alone, not `TRAITVAR` alone, not `addedTIPREDVAR` alone, and not the finite-interval discrete latent variance), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target **Date:** 2026-08-05 **Supersedes:** None. ADR 0012 governs upstream topic measurement/network coordinates; this ADR governs higher-order psychometric structure and longitudinal interpretation. @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), recovers the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z; `TDPREDEFFECT` is `M`, not `CINT`; `M x` is not `A^{-1}[e^{A Δt} − I] B z`; `M x` is not Voelkle et al., 2012, Eq. 14; the level-change form is not that impulse), recovers the exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition; JSS PDF re-opened 2026-08-20T09:01Z; the evolved map `τ + λ μ_t` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-impulse latent mean is not `E(y_t)`), recovers the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z; `TIPREDEFFECT` is `B`, not `κ`; form `B z` first, then the discrete intercept map; a zero drift is `B z Δt`; `B` is not the discrete increment; `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle Eq. 14), recovers the exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; JSS PDF re-opened 2026-08-20T12:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-increment latent mean is not `E(y_t)`), recovers the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z; form `m x` first, then `e^{a(t−u)} m x`; a zero drift is `m x`; underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept; `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; an impulse at `u = t` is the contemporaneous map; an impulse at `u ≤ t0` is already in `η(t0)`), recovers the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; JSS PDF re-opened 2026-08-20T05:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the carried latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and its Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF opened 2026-08-20T15:14Z; form `t0_b z` first, then `e^{a Δt} t0_b z`; a zero drift is `t0_b z`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; `e^{A Δt} t0_b z` is not `t0_b z`; `T0TIPREDEFFECT` is the coefficient, not the shift), recovers the exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition; JSS PDF re-opened 2026-08-20T15:28Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and its Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; form `t0_m x0` first, then `e^{a Δt} t0_m x0`; a zero drift is `t0_m x0`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `T0TDPREDEFFECT` is the coefficient, not the shift; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), recovers the exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; the first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar §7.2 level-change `CINT` setting `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z; form `m x` first, then multiply by `−a`; `a < 0` so `−κ / a = m x`; `a ≥ 0` cannot hold a new process mean; `−a m x` is not the dissipating Dirac, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`; the extra near-zero-drift latent process also named in §7.2 is a different specification), recovers the exact scalar Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` (JSS PDF re-opened 2026-08-20T19:50Z; form the level-change `CINT` first, then the discrete intercept map; underflow of `e^{a Δt}` to `+0` keeps `m x`; `(1 − e^{a Δt}) m x` is not `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`), recovers the exact scalar §7.2 extra near-zero-drift latent process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z; form `a_{ηξ} x` first; `ε = a` is `a_{ηξ} x Δt e^{a Δt}`; a zero coupling or zero predictor is exactly zero; `ε ≥ 0` cannot hold a lasting extra state; that contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`), recovers the exact scalar observed mean of that extra-process contribution `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z; the extra process has `LAMBDA` 0 and is not an observed indicator; form the evolved-plus-contribution latent mean first, then `τ + λ` of that mean; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`), recovers the exact scalar after-t0 extra-process contribution `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` and its Eq. 5 observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:32Z; `T0TDPREDEFFECT` uses `Δt` for both the evolution and the extra drive; `TDPREDEFFECT` after `t0` uses `t − u` while `μ_t` still uses `Δt`; an impulse at `u = t0` or `u = t` is not interior; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive), recovers the exact scalar §7.2 `asymTIPREDEFFECT` `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z; form `B z` first, then divide by `-a`; `a < 0`; a zero coefficient or zero predictor is exactly zero; `a ≥ 0` cannot hold a finite process-mean change; `-B z / a` is not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`), recovers the exact scalar §7.2 `addedTIPREDVAR` `(B / a)² v` (Driver et al., 2017, §7.2, pp. 20–21; form the unit asymptotic effect first, then square, then multiply by `v`; `v ≥ 0`; a zero coefficient or zero predictor variance is exactly zero; `(B / a)² v` is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`), recovers the exact scalar Table 2 `asymCINT` `-κ / a` (Driver et al., 2017, Table 2, p. 12; Eq. 3 as `Δt → ∞`; JSS PDF opened 2026-08-21T16:13Z; form `κ` first, then divide by `-a`; `a < 0`; a zero intercept is exactly zero; `-κ / a` is not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`), recovers the exact scalar p. 16 stationary `T0MEANS` `-κ / a + −B z / a` (Driver et al., 2017, p. 16; form the intercept contribution first, then include the TI extra effect, then add; not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean), recovers the exact scalar Eq. 5 of that constrained mean `τ + λ(−κ / a + −B z / a)` (Driver et al., 2017, §4.3, pp. 9–10; Eq. 5, p. 5; JSS PDF re-opened 2026-08-21T20:07Z; form the stationary latent mean first, then `τ + λ` of that mean; `τ + λ μ_0` is not that observed mean; `τ + λ(−κ / a)` is not that observed mean when `B z ≠ 0`; `τ + λ μ_t` is not that observed mean; `MANIFESTMEANS` is not `E(y_0)`; the constrained latent mean is not `E(y_0)`), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), recovers the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z; `TDPREDEFFECT` is `M`, not `CINT`; `M x` is not `A^{-1}[e^{A Δt} − I] B z`; `M x` is not Voelkle et al., 2012, Eq. 14; the level-change form is not that impulse), recovers the exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition; JSS PDF re-opened 2026-08-20T09:01Z; the evolved map `τ + λ μ_t` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-impulse latent mean is not `E(y_t)`), recovers the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z; `TIPREDEFFECT` is `B`, not `κ`; form `B z` first, then the discrete intercept map; a zero drift is `B z Δt`; `B` is not the discrete increment; `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle Eq. 14), recovers the exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; JSS PDF re-opened 2026-08-20T12:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-increment latent mean is not `E(y_t)`), recovers the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z; form `m x` first, then `e^{a(t−u)} m x`; a zero drift is `m x`; underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept; `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; an impulse at `u = t` is the contemporaneous map; an impulse at `u ≤ t0` is already in `η(t0)`), recovers the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; JSS PDF re-opened 2026-08-20T05:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the carried latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and its Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF opened 2026-08-20T15:14Z; form `t0_b z` first, then `e^{a Δt} t0_b z`; a zero drift is `t0_b z`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; `e^{A Δt} t0_b z` is not `t0_b z`; `T0TIPREDEFFECT` is the coefficient, not the shift), recovers the exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition; JSS PDF re-opened 2026-08-20T15:28Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and its Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; form `t0_m x0` first, then `e^{a Δt} t0_m x0`; a zero drift is `t0_m x0`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `T0TDPREDEFFECT` is the coefficient, not the shift; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), recovers the exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; the first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar §7.2 level-change `CINT` setting `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z; form `m x` first, then multiply by `−a`; `a < 0` so `−κ / a = m x`; `a ≥ 0` cannot hold a new process mean; `−a m x` is not the dissipating Dirac, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`; the extra near-zero-drift latent process also named in §7.2 is a different specification), recovers the exact scalar Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` (JSS PDF re-opened 2026-08-20T19:50Z; form the level-change `CINT` first, then the discrete intercept map; underflow of `e^{a Δt}` to `+0` keeps `m x`; `(1 − e^{a Δt}) m x` is not `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`), recovers the exact scalar §7.2 extra near-zero-drift latent process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z; form `a_{ηξ} x` first; `ε = a` is `a_{ηξ} x Δt e^{a Δt}`; a zero coupling or zero predictor is exactly zero; `ε ≥ 0` cannot hold a lasting extra state; that contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`), recovers the exact scalar observed mean of that extra-process contribution `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z; the extra process has `LAMBDA` 0 and is not an observed indicator; form the evolved-plus-contribution latent mean first, then `τ + λ` of that mean; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`), recovers the exact scalar after-t0 extra-process contribution `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` and its Eq. 5 observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:32Z; `T0TDPREDEFFECT` uses `Δt` for both the evolution and the extra drive; `TDPREDEFFECT` after `t0` uses `t − u` while `μ_t` still uses `Δt`; an impulse at `u = t0` or `u = t` is not interior; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive), recovers the exact scalar §7.2 `asymTIPREDEFFECT` `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z; form `B z` first, then divide by `-a`; `a < 0`; a zero coefficient or zero predictor is exactly zero; `a ≥ 0` cannot hold a finite process-mean change; `-B z / a` is not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`), recovers the exact scalar §7.2 `addedTIPREDVAR` `(B / a)² v` (Driver et al., 2017, §7.2, pp. 20–21; form the unit asymptotic effect first, then square, then multiply by `v`; `v ≥ 0`; a zero coefficient or zero predictor variance is exactly zero; `(B / a)² v` is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`), recovers the exact scalar Table 2 `asymCINT` `-κ / a` (Driver et al., 2017, Table 2, p. 12; Eq. 3 as `Δt → ∞`; JSS PDF opened 2026-08-21T16:13Z; form `κ` first, then divide by `-a`; `a < 0`; a zero intercept is exactly zero; `-κ / a` is not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`), recovers the exact scalar p. 16 stationary `T0MEANS` `-κ / a + −B z / a` (Driver et al., 2017, p. 16; form the intercept contribution first, then include the TI extra effect, then add; not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean), recovers the exact scalar Eq. 5 of that constrained mean `τ + λ(−κ / a + −B z / a)` (Driver et al., 2017, §4.3, pp. 9–10; Eq. 5, p. 5; JSS PDF re-opened 2026-08-21T20:07Z; form the stationary latent mean first, then `τ + λ` of that mean; `τ + λ μ_0` is not that observed mean; `τ + λ(−κ / a)` is not that observed mean when `B z ≠ 0`; `τ + λ μ_t` is not that observed mean; `MANIFESTMEANS` is not `E(y_0)`; the constrained latent mean is not `E(y_0)`), recovers the exact scalar §4.3 / p. 16 stationary `T0VAR` `trait + −q / (2 a) + (B / a)² v` (Driver et al., 2017, §4.3, pp. 9–10; p. 16; JSS PDF re-opened 2026-08-22T03:07Z; form the within-subject contribution first, then include the trait, then include the TI extra variance, then add; not free `T0VAR`, not `asymDIFFUSION` alone, not `TRAITVAR` alone, not `addedTIPREDVAR` alone, and not the finite-interval discrete latent variance), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. 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/adr/README.md b/docs/adr/README.md index 84380310..958e94a3 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -10,7 +10,7 @@ Read [`ADR_POLICY.md`](ADR_POLICY.md) first. **Decision status and implementatio | [0002](0002-six-clock-temporal-semantics.md) | Six-clock temporal semantics and fail-closed historical leakage prevention | Accepted | active-PR | Unmerged PR #8 is the canonical Task 3 replacement implementing typed clocks/intervals against the current protected-main lineage; conflicted PR #5 is superseded lineage. Later graph/split enforcement remains target work. | | [0003](0003-relational-event-multiple-membership.md) | Relational event ontology and time-varying cross-classified multiple membership | Accepted | partial | Weighted time-varying membership network/roles are active-PR (PR #12); full multilevel estimators, graph ontology, and persistence remain accepted-target. ADR 0016 owns event-intelligence tasks. | | [0004](0004-shared-multilingual-latent-space.md) | One shared multilingual latent space with explicit invariance status | Accepted | accepted-target | ADR 0012 owns the full topic-estimator/backend/global-topic contract. | -| [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | partial | Input gates, posterior-draw point estimates, Rubin `T` on OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects, exact scalar discrete process noise, lagged latent covariance and unconditional latent variance, stationary within-subject variance (`asymDIFFUSION`), trait-plus-state variance (`TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; Eq. 5 of the contemporaneous impulse is `τ + λ(μ_t + m x)`; `τ + λ μ_t` is not that observed mean; Eq. 5 of the time-independent predictor is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; Eq. 5 of the within-interval impulse carry is `τ + λ(μ_t + e^{a(t−u)} m x)`; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; Table 2 `asymCINT` is `-κ / a` for `a < 0` and is not `κ`, not the finite-interval increment, not `T0MEANS`, and not `-B z / a`; p. 16 stationary `T0MEANS` is `-κ / a + −B z / a` and is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean; Eq. 5 of that constrained mean is `τ + λ(−κ / a + −B z / a)` (`τ + λ μ_0` is not that observed mean; `MANIFESTMEANS` is not `E(y_0)`); §7.2 `addedTIPREDVAR` is `(B / a)² v` and is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`), irregular already-centered residual lag, and strong-gated latent means are on the stacked psychometric PR; full ESEM/DSEM estimator remains accepted-target. | +| [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | partial | Input gates, posterior-draw point estimates, Rubin `T` on OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects, exact scalar discrete process noise, lagged latent covariance and unconditional latent variance, stationary within-subject variance (`asymDIFFUSION`), trait-plus-state variance (`TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; Eq. 5 of the contemporaneous impulse is `τ + λ(μ_t + m x)`; `τ + λ μ_t` is not that observed mean; Eq. 5 of the time-independent predictor is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; Eq. 5 of the within-interval impulse carry is `τ + λ(μ_t + e^{a(t−u)} m x)`; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; Table 2 `asymCINT` is `-κ / a` for `a < 0` and is not `κ`, not the finite-interval increment, not `T0MEANS`, and not `-B z / a`; p. 16 stationary `T0MEANS` is `-κ / a + −B z / a` and is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean; Eq. 5 of that constrained mean is `τ + λ(−κ / a + −B z / a)` (`τ + λ μ_0` is not that observed mean; `MANIFESTMEANS` is not `E(y_0)`); stationary `T0VAR` is `trait + −q / (2 a) + (B / a)² v` (not free `T0VAR`, not `asymDIFFUSION` alone, not `TRAITVAR` alone, not `addedTIPREDVAR` alone); §7.2 `addedTIPREDVAR` is `(B / a)² v` and is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`), irregular already-centered residual lag, and strong-gated latent means are on the stacked psychometric PR; full ESEM/DSEM estimator remains accepted-target. | | [0006](0006-vram-gpu-nvidia-orchestration.md) | VRAM-adaptive GPU compute and model-credential boundary | Accepted | accepted-target | LLM orchestration policy superseded by ADR 0010; autonomous development authority governed by ADR 0015. | | [0007](0007-rust-workspace-quality-gates.md) | Explicit Rust workspace, pinned toolchains, and exact quality gates | Accepted | implemented-main | ADR 0014 governs scientific/product claim promotion beyond repository-quality tooling. | | [0008](0008-immutable-evidence-identities-digests-and-spans.md) | Immutable evidence identities, `SHA-256` digests, exact spans, and strict wire reconstruction | Accepted | implemented-main | ADR 0013 governs future persistence/reproducibility/split authority. | diff --git a/docs/research/multilevel-event-time-recovery.md b/docs/research/multilevel-event-time-recovery.md index 1fdec225..a46e48e1 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -42,15 +42,16 @@ This slice stays inside `psychometric_core`. It does not add a second invariance 36. recover the exact scalar Table 2 `asymCINT` `-κ / a` (Driver et al., 2017, Table 2, p. 12; Eq. 3, p. 5; §4.3 / p. 16; JSS PDF opened 2026-08-21T16:13Z; expected change in process means for a unit intercept; form `κ` first, then divide by `-a`; `a < 0`; a zero intercept is exactly zero) and refuse treating `-κ / a` as `κ`, as `A^{-1}[e^{A Δt} − I] κ`, as `T0MEANS`, or as `-B z / a`; `a ≥ 0` cannot hold a finite process-mean change; p. 16 `T0MEANS` stationarity includes TI predictors and is not this intercept-only map; 37. recover the exact scalar p. 16 stationary `T0MEANS` `-κ / a + −B z / a` (Driver et al., 2017, p. 16; Table 2, p. 12; Eq. 3, p. 5; JSS PDF opened 2026-08-21T16:13Z; constrain `T0MEANS` to model-implied values using `T0MEANSbase` / `T0MEANSfree`; form the intercept contribution first, then include the TI extra effect, then add; a zero intercept and a zero TI contribution is exactly zero) and refuse treating that composition as free `T0MEANS`, as `asymCINT` alone, as `asymTIPREDEFFECT` alone, or as the finite-interval discrete latent mean; 38. recover the exact scalar Eq. 5 of §4.3 stationary `T0MEANS` `τ + λ(−κ / a + −B z / a)` (Driver et al., 2017, §4.3, pp. 9–10; Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-21T20:07Z; form the stationary latent mean first, then `τ + λ` of that mean; a zero loading is exactly `τ`; evolving from that stationary start with `CINT` and `TIPREDEFFECT` stays at the stationary mean) and refuse treating `τ + λ μ_0`, `τ + λ(−κ / a)` when `B z ≠ 0`, `τ + λ μ_t`, `MANIFESTMEANS`, or the constrained latent mean as `E(y_0)`; -39. refuse pooling discrete lags from unequal event intervals as one coefficient; -40. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); -41. refuse the difference quotient as a continuous-time rate; -42. apply the same event-time map to CWC residuals (still not DSEM); -43. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). +39. recover the exact scalar §4.3 / p. 16 stationary `T0VAR` `trait + −q / (2 a) + (B / a)² v` (Driver et al., 2017, §4.3, pp. 9–10; p. 16; Table 2, p. 12; §7.2, pp. 20–21; Eq. 4, p. 5; JSS PDF re-opened 2026-08-22T03:07Z; form the within-subject contribution first, then include the trait, then include the TI extra variance, then add; a zero trait, a zero diffusion, and a zero TI contribution is exactly zero; a zero diffusion and a zero TI contribution is exactly the trait) and refuse treating that composition as free `T0VAR`, as `asymDIFFUSION` alone, as `TRAITVAR` alone, as `addedTIPREDVAR` alone, or as the finite-interval discrete latent variance; +40. refuse pooling discrete lags from unequal event intervals as one coefficient; +41. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); +42. refuse the difference quotient as a continuous-time rate; +43. apply the same event-time map to CWC residuals (still not DSEM); +44. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). ## Claim boundary -This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. The contemporaneous `TDPREDEFFECT` impulse is not the continuous intercept, not the time-independent discrete effect, and not Voelkle et al. (2012, Eq. 14). The time-independent `TIPREDEFFECT` increment is not the continuous intercept, not the contemporaneous impulse, not Voelkle et al. (2012, Eq. 14), and not the coefficient `B`. The within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). The evolved observed mean `τ + λ μ_t` is not the contemporaneous-impulse observed mean `τ + λ(μ_t + m x)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not the impulse-carry observed mean `τ + λ(μ_t + e^{a(t−u)} m x)` when `u ≠ t`. The evolved observed mean `τ + λ μ_t` is not the impulse-carry observed mean. The carried latent mean is not `E(y_t)`. The evolved-plus-impulse latent mean is not `E(y_t)`. The evolved observed mean `τ + λ μ_t` is not the time-independent-predictor observed mean `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not that time-independent-predictor observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that time-independent-predictor observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TIPREDEFFECT` shift `t0_b z` is not the Eq. 3 process increment `A^{-1}[e^{A Δt} − I] B z`, not `κ`, and not `M x`. The Eq. 3 first-summand carry `e^{A Δt} t0_b z` is not `t0_b z` and is not that process increment. `T0TIPREDEFFECT` is the coefficient, not the first-occasion shift. The evolved observed mean `τ + λ μ_t` is not the first-occasion TI-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_b z)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion observed mean when `u ≠ t0`. The evolved-plus-T0TIPRED latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TDPREDEFFECT` shift `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `κ`. The Eq. 3 first-summand carry `e^{A Δt} t0_m x0` is not `t0_m x0` and is not that within-interval impulse carry. `T0TDPREDEFFECT` is the coefficient, not the first-occasion shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The evolved observed mean `τ + λ μ_t` is not the first-occasion TD-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_m x0)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion TD observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion TD observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion TD observed mean when `u ≠ t0`. The first-occasion TI composition `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that first-occasion TD observed mean. Same numbers as `T0TIPREDEFFECT` yield the same product; Table 3 names a different matrix. The evolved-plus-T0TDPRED latent mean is not `E(y_t)`. The §7.2 level-change `CINT` `κ = −a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. Lasting level change via that `CINT` setting requires `a < 0`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not that `CINT` setting. The Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` is not the dissipating Dirac `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`. Underflow of `e^{a Δt}` to `+0` keeps the equilibrium offset `m x`. The §7.2 extra-process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. Lasting level change via that extra process requires `ε < 0`. Precisely `ε = 0` causes computational problems in the printed specification. The extra-process observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` is not `τ + λ μ_t`, not `τ + λ(μ_t + m x)`, not the contribution, and not the evolved-plus-contribution latent mean. The extra process has `LAMBDA` 0 and is not an observed indicator. `T0TDPREDEFFECT` on the extra process uses `Δt = t − t0` for both the original-process evolution and the extra drive. `TDPREDEFFECT` after `t0` uses `t − u` with `t0 < u < t` for the extra drive while `μ_t` still uses `Δt`. The after-t0 extra-process observed mean is not the first-occasion extra-process observed mean when `u ≠ t0`. The impulse-carry `e^{a(t−u)} m x` is a Dirac on the original process and is not that `DRIFT` drive. The §7.2 `asymTIPREDEFFECT` `-B z / a` is the expected total change in process means given a time-independent predictor. It is not the coefficient `B`, not the finite-interval increment `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. Lasting asymptotic change via that map requires `a < 0`. The §7.2 `addedTIPREDVAR` `(B / a)² v` is the stable between-subject variance accounted for by that predictor. It is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`. Table 2 `asymCINT` `-κ / a` is the intercept contribution to the stationary process mean. It is not `κ`, not the finite-interval increment `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`. Lasting asymptotic intercept change via that map requires `a < 0`. Page 16 notes that a `T0MEANS` stationarity constraint includes time-independent predictors; that composition is not this intercept-only map. The p. 16 constrained first-occasion mean `-κ / a + −B z / a` is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean. Equation 5 of that constrained mean `τ + λ(−κ / a + −B z / a)` is not `τ + λ μ_0`, not `τ + λ(−κ / a)` when `B z ≠ 0`, not `τ + λ μ_t`, not `MANIFESTMEANS`, and not the constrained latent mean. +This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. The contemporaneous `TDPREDEFFECT` impulse is not the continuous intercept, not the time-independent discrete effect, and not Voelkle et al. (2012, Eq. 14). The time-independent `TIPREDEFFECT` increment is not the continuous intercept, not the contemporaneous impulse, not Voelkle et al. (2012, Eq. 14), and not the coefficient `B`. The within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). The evolved observed mean `τ + λ μ_t` is not the contemporaneous-impulse observed mean `τ + λ(μ_t + m x)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not the impulse-carry observed mean `τ + λ(μ_t + e^{a(t−u)} m x)` when `u ≠ t`. The evolved observed mean `τ + λ μ_t` is not the impulse-carry observed mean. The carried latent mean is not `E(y_t)`. The evolved-plus-impulse latent mean is not `E(y_t)`. The evolved observed mean `τ + λ μ_t` is not the time-independent-predictor observed mean `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not that time-independent-predictor observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that time-independent-predictor observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TIPREDEFFECT` shift `t0_b z` is not the Eq. 3 process increment `A^{-1}[e^{A Δt} − I] B z`, not `κ`, and not `M x`. The Eq. 3 first-summand carry `e^{A Δt} t0_b z` is not `t0_b z` and is not that process increment. `T0TIPREDEFFECT` is the coefficient, not the first-occasion shift. The evolved observed mean `τ + λ μ_t` is not the first-occasion TI-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_b z)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion observed mean when `u ≠ t0`. The evolved-plus-T0TIPRED latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TDPREDEFFECT` shift `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `κ`. The Eq. 3 first-summand carry `e^{A Δt} t0_m x0` is not `t0_m x0` and is not that within-interval impulse carry. `T0TDPREDEFFECT` is the coefficient, not the first-occasion shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The evolved observed mean `τ + λ μ_t` is not the first-occasion TD-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_m x0)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion TD observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion TD observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion TD observed mean when `u ≠ t0`. The first-occasion TI composition `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that first-occasion TD observed mean. Same numbers as `T0TIPREDEFFECT` yield the same product; Table 3 names a different matrix. The evolved-plus-T0TDPRED latent mean is not `E(y_t)`. The §7.2 level-change `CINT` `κ = −a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. Lasting level change via that `CINT` setting requires `a < 0`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not that `CINT` setting. The Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` is not the dissipating Dirac `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`. Underflow of `e^{a Δt}` to `+0` keeps the equilibrium offset `m x`. The §7.2 extra-process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. Lasting level change via that extra process requires `ε < 0`. Precisely `ε = 0` causes computational problems in the printed specification. The extra-process observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` is not `τ + λ μ_t`, not `τ + λ(μ_t + m x)`, not the contribution, and not the evolved-plus-contribution latent mean. The extra process has `LAMBDA` 0 and is not an observed indicator. `T0TDPREDEFFECT` on the extra process uses `Δt = t − t0` for both the original-process evolution and the extra drive. `TDPREDEFFECT` after `t0` uses `t − u` with `t0 < u < t` for the extra drive while `μ_t` still uses `Δt`. The after-t0 extra-process observed mean is not the first-occasion extra-process observed mean when `u ≠ t0`. The impulse-carry `e^{a(t−u)} m x` is a Dirac on the original process and is not that `DRIFT` drive. The §7.2 `asymTIPREDEFFECT` `-B z / a` is the expected total change in process means given a time-independent predictor. It is not the coefficient `B`, not the finite-interval increment `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. Lasting asymptotic change via that map requires `a < 0`. The §7.2 `addedTIPREDVAR` `(B / a)² v` is the stable between-subject variance accounted for by that predictor. It is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`. Table 2 `asymCINT` `-κ / a` is the intercept contribution to the stationary process mean. It is not `κ`, not the finite-interval increment `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`. Lasting asymptotic intercept change via that map requires `a < 0`. Page 16 notes that a `T0MEANS` stationarity constraint includes time-independent predictors; that composition is not this intercept-only map. The p. 16 constrained first-occasion mean `-κ / a + −B z / a` is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean. Equation 5 of that constrained mean `τ + λ(−κ / a + −B z / a)` is not `τ + λ μ_0`, not `τ + λ(−κ / a)` when `B z ≠ 0`, not `τ + λ μ_t`, not `MANIFESTMEANS`, and not the constrained latent mean. The p. 16 constrained first-occasion variance `trait + −q / (2 a) + (B / a)² v` is not free `T0VAR`, not `asymDIFFUSION` alone, not `TRAITVAR` alone, not `addedTIPREDVAR` alone, and not the finite-interval discrete latent variance. ## Authoritative sources @@ -68,7 +69,7 @@ Kish, L. (1965). *Survey sampling*. John Wiley & Sons. Oud, J. H. L., & Jansen, R. A. R. G. (2000). Continuous time state space modeling of panel data by means of SEM. *Psychometrika, 65*(2), 199–215. https://doi.org/10.1007/BF02294374 (cited by Voelkle et al., 2012, Eq. 14 discussion; PDF not opened). -The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:07Z (https://www.zora.uzh.ch/handle/20.500.14742/72792; bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`). Page 16 writes discrete auto-effects as \(A^{*}(\Delta t)=\exp(A\Delta t)\) (Eq. 7). Introducing Intercepts (manuscript p. 20, Eq. 12) adds a continuous-time intercept \(b\) and writes the expected-value solution whose discrete increment is \(A^{-1}(\exp(A\Delta t)-I)b\); the scalar constant-predictor effect is \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). The next paragraph (manuscript p. 21, Eq. 14) writes the discrete effect of a **time-varying** predictor, when the sampling interval equals the constancy interval, as \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product does not depend on the predictor auto-effect. The manuscript calls Eq. 14 a first-order approximation that deteriorates as \(\Delta t\) grows, and defers unmatched sampling/constancy intervals to Oud and Jansen (2000), which is unread. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4; JSS PDF opened 2026-08-21T13:08Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\), the discrete intercept \(b_{\Delta t}=A^{-1}[A_{\Delta t}-I]b\), and the discrete process-noise covariance \(Q_{\Delta t}=\int_{0}^{\Delta t}\operatorname{expm}(A(\Delta t-\tau))LGG^{\top}L^{\top}\operatorname{expm}(A(\Delta t-\tau))^{\top}\,d\tau\). Equation 4 (p. 5) writes that the integral exhibits covariance \(Q_{\Delta t}=\operatorname{irow}(A^{\#^{-1}}[e^{A^{\#}\Delta t}-I]\operatorname{row}(Q))\) with \(A^{\#}=A\otimes I+I\otimes A\). The homogeneous-process consequence (`ξ`, `z` given) is \(Q_{\Delta t}=\operatorname{cov}(\eta_{ti}\mid\eta_{t-1,i})\) and \(\operatorname{cov}(\eta_{ti},\eta_{t-1,i})=A_{\Delta t}\operatorname{cov}(\eta_{t-1,i})\). The law of total variance on that pair is \(\operatorname{Var}(\eta_{ti})=A_{\Delta t}\operatorname{Var}(\eta_{t-1,i})A_{\Delta t}^{\top}+Q_{\Delta t}\). As \(\Delta t\to\infty\) with stable \(a<0\), Eq. 4 becomes \(-q/(2a)\). The JSS summary names that limit `asymDIFFUSION` and takes it as the total within-subject variance (p. 16). Section 4.3 (pp. 9–10) constrains a stationary `T0VAR` to that model-predicted variance and distinguishes it from a predetermined first occasion. The same section (p. 9) adds a stable trait process with `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is that time-invariant between-subject variance. Table 3 (p. 13) names `T0TIPREDEFFECT` the effect of time-independent predictors on latents at `T0` and names `TIPREDEFFECT` `B` separately. The JSS article has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). This slice takes scalar \(L=1\) (every latent subject to system noise); it does not implement the 0/1 selector matrix and is not a Kalman filter. Discrete auto-effects are strictly positive for finite real drift and interval; the inverse \(a=\ln\varphi/\Delta t\) therefore requires \(\varphi>0\). Binary64 `exp` of a large negative argument is `+0` and is refused as a discrete lag; the same underflow is a vanishing lagged covariance and is kept. Equations 3–4 of Voelkle et al. (2012) are the discouraged difference-quotient approximation. Meredith (1993) remains unread (Unpaywall/OpenAlex 2026-08-21T16:21Z: closed; Springer `content/pdf` is HTML 200, not a PDF; archive.org title search empty). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex 2026-08-21T16:21Z: closed; Springer `content/pdf` is HTML 200; ETS landing page is HTML). ERIC ED334221 is Singer and Willett (1991), *From whether to when*, not the 1991 journal article. ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title is not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex 2026-08-18T21:07Z: closed; Radboud landing and bitstream 403). +The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:07Z (https://www.zora.uzh.ch/handle/20.500.14742/72792; bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`). Page 16 writes discrete auto-effects as \(A^{*}(\Delta t)=\exp(A\Delta t)\) (Eq. 7). Introducing Intercepts (manuscript p. 20, Eq. 12) adds a continuous-time intercept \(b\) and writes the expected-value solution whose discrete increment is \(A^{-1}(\exp(A\Delta t)-I)b\); the scalar constant-predictor effect is \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). The next paragraph (manuscript p. 21, Eq. 14) writes the discrete effect of a **time-varying** predictor, when the sampling interval equals the constancy interval, as \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product does not depend on the predictor auto-effect. The manuscript calls Eq. 14 a first-order approximation that deteriorates as \(\Delta t\) grows, and defers unmatched sampling/constancy intervals to Oud and Jansen (2000), which is unread. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4; JSS PDF opened 2026-08-21T13:08Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\), the discrete intercept \(b_{\Delta t}=A^{-1}[A_{\Delta t}-I]b\), and the discrete process-noise covariance \(Q_{\Delta t}=\int_{0}^{\Delta t}\operatorname{expm}(A(\Delta t-\tau))LGG^{\top}L^{\top}\operatorname{expm}(A(\Delta t-\tau))^{\top}\,d\tau\). Equation 4 (p. 5) writes that the integral exhibits covariance \(Q_{\Delta t}=\operatorname{irow}(A^{\#^{-1}}[e^{A^{\#}\Delta t}-I]\operatorname{row}(Q))\) with \(A^{\#}=A\otimes I+I\otimes A\). The homogeneous-process consequence (`ξ`, `z` given) is \(Q_{\Delta t}=\operatorname{cov}(\eta_{ti}\mid\eta_{t-1,i})\) and \(\operatorname{cov}(\eta_{ti},\eta_{t-1,i})=A_{\Delta t}\operatorname{cov}(\eta_{t-1,i})\). The law of total variance on that pair is \(\operatorname{Var}(\eta_{ti})=A_{\Delta t}\operatorname{Var}(\eta_{t-1,i})A_{\Delta t}^{\top}+Q_{\Delta t}\). As \(\Delta t\to\infty\) with stable \(a<0\), Eq. 4 becomes \(-q/(2a)\). The JSS summary names that limit `asymDIFFUSION` and takes it as the total within-subject variance (p. 16). Section 4.3 (pp. 9–10) constrains a stationary `T0VAR` to that model-predicted variance and distinguishes it from a predetermined first occasion. The same section (p. 9) adds a stable trait process with `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is that time-invariant between-subject variance. Table 3 (p. 13) names `T0TIPREDEFFECT` the effect of time-independent predictors on latents at `T0` and names `TIPREDEFFECT` `B` separately. The JSS article has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). This slice takes scalar \(L=1\) (every latent subject to system noise); it does not implement the 0/1 selector matrix and is not a Kalman filter. Discrete auto-effects are strictly positive for finite real drift and interval; the inverse \(a=\ln\varphi/\Delta t\) therefore requires \(\varphi>0\). Binary64 `exp` of a large negative argument is `+0` and is refused as a discrete lag; the same underflow is a vanishing lagged covariance and is kept. Equations 3–4 of Voelkle et al. (2012) are the discouraged difference-quotient approximation. Meredith (1993) remains unread (Unpaywall/OpenAlex 2026-08-22T03:07Z: request empty; Springer `content/pdf` is HTML 200, not a PDF; archive.org title search empty). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex 2026-08-22T03:07Z: request empty; Springer `content/pdf` is HTML 200; ETS landing page is HTML; ETS RR-88-45 PDF 404; Wiley PDF 403). ERIC ED334221 is Singer and Willett (1991), *From whether to when*, not the 1991 journal article. ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title is not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex 2026-08-18T21:07Z: closed; Radboud landing and bitstream 403). ## Formula notes @@ -107,6 +108,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - **Asymptotic time-independent predictor variance.** Driver et al. (2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z): Section 7.2 names `addedTIPREDVAR` the stable between-subject variance accounted for by time-independent predictors. For predictor variance `v≥0` the scalar map is `(B/a)²v`. Form the unit asymptotic effect `-B/a` first, then square, then multiply by `v`. A zero coefficient or zero predictor variance is exactly zero. `v<0` fails closed. `(B/a)²v` is not `TRAITVAR`, not `asymDIFFUSION`, and not `-Bz/a`. The printed 2-latent `addedTIPREDVAR` 2.838 is not this scalar map. An overflowing product fails closed. This is not a Kalman filter and not ctsem estimation. - **Asymptotic continuous intercept.** Driver et al. (2017, Table 2, p. 12; Eq. 3, p. 5; §4.3 / p. 16; JSS PDF opened 2026-08-21T16:13Z): Table 2 names `κ` `CINT` and names `asymCINT` the asymptotic (`Δt=∞`) expected change in processes for a 1 unit change in intercept. Equation 3 maps a finite event interval as `A^{-1}[e^{AΔt}−I]κ`. For stable `a<0` that `Δt→∞` limit is `-A^{-1}κ`. The scalar map is `-κ/a`. A unit intercept is `-1/a`. Form `κ` first, then divide by `-a`. A zero intercept is exactly zero. `a≥0` cannot hold a finite process-mean change. `-κ/a` is not `κ`, not `A^{-1}[e^{AΔt}−I]κ`, not `T0MEANS`, and not `-Bz/a`. Page 16 notes that a `T0MEANS` stationarity constraint includes time-independent predictors; that composition is not this intercept-only map. The printed 2-latent `CINT` values are not this scalar map. An overflowing quotient fails closed. This is not a Kalman filter and not ctsem estimation. - **Stationary first-occasion latent mean.** Driver et al. (2017, p. 16; Table 2, p. 12; Eq. 3, p. 5; JSS PDF opened 2026-08-21T16:13Z): when the first observation is determined by the process in the same way as later observations, `T0MEANS` is constrained to the model-implied values using `T0MEANSbase` / `T0MEANSfree`. Those constraints include extra effects due to time-independent predictors (`asymTIPREDEFFECT`). For stable `a<0` the scalar composition is `-κ/a + −Bz/a`. Form the intercept contribution first, then include the TI extra effect, then add. A zero intercept and a zero TI contribution is exactly zero. `a≥0` cannot hold a finite process-mean change when either contribution is nonzero. That constrained first-occasion mean is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not `exp(aΔt)μ_0+(exp(aΔt)−1)/a κ`. The printed 2-latent `T0MEANS` 2.823 is not this scalar map. An overflowing sum fails closed. This is not a Kalman filter and not ctsem estimation. +- **Stationary first-occasion latent variance.** Driver et al. (2017, §4.3, pp. 9–10; p. 16; Table 2, p. 12; §7.2, pp. 20–21; Eq. 4, p. 5; JSS PDF re-opened 2026-08-22T03:07Z): when `stationary` includes `"T0VAR"`, the first-occasion variance is constrained to the model-predicted variance across all time points. Page 16 names `asymDIFFUSION` the total within-subject variance `-q/(2a)`. Section 4.3 (p. 9) adds `TRAITVAR`. Section 7.2 names `addedTIPREDVAR` the stable between-subject variance accounted for by time-independent predictors, `(B/a)²v`. The scalar composition is `trait + −q/(2a) + (B/a)²v`. Form the within-subject contribution first, then include the trait, then include the TI extra variance, then add. A zero trait, a zero diffusion, and a zero TI contribution is exactly zero. A zero diffusion and a zero TI contribution is exactly the trait. `a≥0` cannot hold a finite process variance when the diffusion or the TI contribution is nonzero. Trait-only variance does not require a stable drift. That constrained first-occasion variance is not free `T0VAR`, not `asymDIFFUSION` alone, not `TRAITVAR` alone, not `addedTIPREDVAR` alone, and not `exp(2aΔt)p+Q_Δt`. The printed 2-latent `addedTIPREDVAR` 2.838 is not this scalar map. An overflowing sum fails closed. This is not a Kalman filter and not ctsem estimation. - **Level-change discrete increment.** Driver et al. (2017, §7.2, pp. 20–21; Eq. 3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T19:50Z): Equation 3 maps `CINT` through `A^{-1}[e^{AΔt}−I]κ`. With `κ=−a m x` the scalar increment is `(e^{aΔt}−1)/a·(−a m x)=(1−e^{aΔt})m x`. Form the level-change `CINT` first, then the discrete intercept map. Underflow of `e^{aΔt}` to `+0` keeps `m x`. A zero effect or zero predictor is exactly zero. `(1−e^{aΔt})m x` is not `m x`, not `κ`, and not `A^{-1}[e^{AΔt}−I]Bz`. An overflowing product or increment fails closed. This is not a Kalman filter and not ctsem estimation. - **CWC-then-lag.** Sample cluster means are removed first. Consecutive within residuals are then fitted by least squares to \(r_{t+\Delta t}\approx\exp(a\Delta t)\,r_{t}\) on event time. Same-sign pair-wise logs initialize the scalar Newton step. Sign-flipping \(T=2\) CWC pairs have no real logarithm and fail closed. Curran and Bauer (2011, pp. 607–608) show that this person-mean subtraction on a raw autoregressive series does **not** isolate the lagged within-person effect; the helper therefore does not claim to recover the raw-process drift. - **Already-centered irregular residual.** The caller supplies lagged within residuals. The mean of \(a=\ln(r_{t+\Delta t}/r_t)/\Delta t\) is the exact scalar map. Intervals may be irregular. The helper does not center again. This is not DSEM. diff --git a/docs/validation/temporal-event-foundation.md b/docs/validation/temporal-event-foundation.md index fe8cc7d8..973028e5 100644 --- a/docs/validation/temporal-event-foundation.md +++ b/docs/validation/temporal-event-foundation.md @@ -23,7 +23,7 @@ This report tracks exact-head scientific and engineering evidence required befor | Truth corpora / manifests | `tepp_simulation` | implemented-main | — | deterministic generator tests | Task 10 / PR #18 | | Recovery metrics | `validation_core` | implemented-main | — | RMSE/bias/coverage/MC gates | Task 11 / PR #19 | | Versioned API/export contracts | `tepp_api` | implemented-main | naruon HTTP interchange | unknown-field/version/limit + naruon HTTPS interchange tests | Task 12 / PR #21; live HTTP service remaining | -| Psychometric structural input gates | `psychometric_core` | partial | stacked psychometric PR | construct-class refusal + ALR/ILR boundary + true-loading RMSE + posterior-draw point-estimate mean + Rubin `T` + CWC within/between + CWC contextual effect + event-time log-rate + constant- and time-varying-predictor discrete effects + exact scalar discrete process noise + lagged latent covariance and unconditional latent variance + stationary within-subject variance + trait-plus-state variance + observed-indicator variance + discrete latent mean (`T0MEANS`/`CINT`) + evolved observed mean (`τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`) + contemporaneous `TDPREDEFFECT` impulse (`m x`; not `CINT`, not `TIPREDEFFECT`, not Voelkle Eq. 14) + Eq. 5 of that contemporaneous impulse (`τ + λ(μ_t + m x)`; `τ + λ μ_t` is not that observed mean) + time-independent `TIPREDEFFECT` increment (`A^{-1}[e^{A Δt} − I] B z`; not `CINT`, not `M x`, not Voelkle Eq. 14, not the coefficient `B`) + Eq. 5 of that increment (`τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`; `τ + λ μ_t` is not that observed mean) + within-interval `TDPREDEFFECT` carry (`e^{A(t−u)} M x` for `t0 < u < t`; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, not Voelkle Eq. 14) + Eq. 5 of that carry (`τ + λ(μ_t + e^{a(t−u)} m x)`; `τ + λ μ_t` is not that observed mean) + §7.2 level-change `CINT` (`κ = −a m x`; Eq. 3 increment `(1 − e^{a Δt}) m x`) + §7.2 extra-process contribution (`a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; not `κ`, not the increment, not the Dirac; `ε ≥ 0` fails closed) + Eq. 5 of that extra-process contribution (`τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; extra `LAMBDA` is 0; `τ + λ μ_t` is not that observed mean) + after-t0 extra-process `TDPREDEFFECT` (`a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t`; Eq. 5 `τ + λ(μ_t + contribution(t−u))`; not the first-occasion extra-process observed mean; not the impulse-carry Dirac) + §7.2 `asymTIPREDEFFECT` (`-B z / a` for `a < 0`; not `B`, not the finite-interval increment, not `CINT`, not `M x`) + §7.2 `addedTIPREDVAR` (`(B / a)² v`; not `TRAITVAR`, not `asymDIFFUSION`, not `-B z / a`) + Table 2 `asymCINT` (`-κ / a` for `a < 0`; not `κ`, not the finite-interval increment, not `T0MEANS`, not `-B z / a`) + p. 16 stationary `T0MEANS` (`-κ / a + −B z / a`; not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, not the finite-interval discrete mean) + Eq. 5 of that constrained mean (`τ + λ(−κ / a + −B z / a)`; `τ + λ μ_0` is not that observed mean; `τ + λ(−κ / a)` is not that observed mean when `B z ≠ 0`; `τ + λ μ_t` is not that observed mean; `MANIFESTMEANS` is not `E(y_0)`; the constrained latent mean is not `E(y_0)`) + irregular already-centered residual lag + strong-gated latent means (n=2 residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016); full ESEM/DSEM remaining | ADR 0005; `docs/research/posterior-esem-input-gates.md`; `docs/research/multilevel-event-time-recovery.md`; `docs/research/rubin-total-variance.md`; `docs/research/strong-invariance-latent-means.md` | +| Psychometric structural input gates | `psychometric_core` | partial | stacked psychometric PR | construct-class refusal + ALR/ILR boundary + true-loading RMSE + posterior-draw point-estimate mean + Rubin `T` + CWC within/between + CWC contextual effect + event-time log-rate + constant- and time-varying-predictor discrete effects + exact scalar discrete process noise + lagged latent covariance and unconditional latent variance + stationary within-subject variance + trait-plus-state variance + observed-indicator variance + discrete latent mean (`T0MEANS`/`CINT`) + evolved observed mean (`τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`) + contemporaneous `TDPREDEFFECT` impulse (`m x`; not `CINT`, not `TIPREDEFFECT`, not Voelkle Eq. 14) + Eq. 5 of that contemporaneous impulse (`τ + λ(μ_t + m x)`; `τ + λ μ_t` is not that observed mean) + time-independent `TIPREDEFFECT` increment (`A^{-1}[e^{A Δt} − I] B z`; not `CINT`, not `M x`, not Voelkle Eq. 14, not the coefficient `B`) + Eq. 5 of that increment (`τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`; `τ + λ μ_t` is not that observed mean) + within-interval `TDPREDEFFECT` carry (`e^{A(t−u)} M x` for `t0 < u < t`; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, not Voelkle Eq. 14) + Eq. 5 of that carry (`τ + λ(μ_t + e^{a(t−u)} m x)`; `τ + λ μ_t` is not that observed mean) + §7.2 level-change `CINT` (`κ = −a m x`; Eq. 3 increment `(1 − e^{a Δt}) m x`) + §7.2 extra-process contribution (`a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; not `κ`, not the increment, not the Dirac; `ε ≥ 0` fails closed) + Eq. 5 of that extra-process contribution (`τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; extra `LAMBDA` is 0; `τ + λ μ_t` is not that observed mean) + after-t0 extra-process `TDPREDEFFECT` (`a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t`; Eq. 5 `τ + λ(μ_t + contribution(t−u))`; not the first-occasion extra-process observed mean; not the impulse-carry Dirac) + §7.2 `asymTIPREDEFFECT` (`-B z / a` for `a < 0`; not `B`, not the finite-interval increment, not `CINT`, not `M x`) + §7.2 `addedTIPREDVAR` (`(B / a)² v`; not `TRAITVAR`, not `asymDIFFUSION`, not `-B z / a`) + Table 2 `asymCINT` (`-κ / a` for `a < 0`; not `κ`, not the finite-interval increment, not `T0MEANS`, not `-B z / a`) + p. 16 stationary `T0MEANS` (`-κ / a + −B z / a`; not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, not the finite-interval discrete mean) + Eq. 5 of that constrained mean (`τ + λ(−κ / a + −B z / a)`; `τ + λ μ_0` is not that observed mean; `τ + λ(−κ / a)` is not that observed mean when `B z ≠ 0`; `τ + λ μ_t` is not that observed mean; `MANIFESTMEANS` is not `E(y_0)`; the constrained latent mean is not `E(y_0)`) + stationary `T0VAR` (`trait + −q / (2 a) + (B / a)² v`; not free `T0VAR`, not `asymDIFFUSION` alone, not `TRAITVAR` alone, not `addedTIPREDVAR` alone) + irregular already-centered residual lag + strong-gated latent means (n=2 residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016); full ESEM/DSEM remaining | ADR 0005; `docs/research/posterior-esem-input-gates.md`; `docs/research/multilevel-event-time-recovery.md`; `docs/research/rubin-total-variance.md`; `docs/research/strong-invariance-latent-means.md` | | Purpose-bound provider payloads | `tepp_api` | implemented-main | provider-payload minimization | expired/not-yet-valid/inverted/cross-tenant/impossible-calendar grant, mapping refusal, audited elevated re-id replay | ADR 0009; `docs/research/provider-payload-minimization.md` | | Adaptive orchestration router | `tepp_api` | accepted-target | active PR | mode selection, document-control denial, ablation, credential-free bind | ADR 0010; `docs/research/adaptive-orchestration-router.md` | | CWL modular connectors | `docs/connectors/*` | implemented-main | — | contract docs + examples | PR #22; live HTTP ports remaining | From 9fe82a9c9aeeff8eed800b1a7d6052e359e86ed8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 03:25:36 +0000 Subject: [PATCH 62/62] feat(psychometric): recover Driver Eq. 5 of stationary T0VAR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Form the constrained first-occasion variance first, then λ² p + θ + ψ. Var(y_0) = λ²(trait + −q/(2a) + (B/a)² v) + θ + ψ. Not free T0VAR, not λ² asymDIFFUSION + θ, not evolved λ² Var(η_t) + θ, and not MANIFESTVAR. JSS PDF re-opened 2026-08-22T03:20Z. Still not DSEM, not a Kalman filter, and not ctsem estimation. --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 1 + CLAUDE.md | 2 +- crates/psychometric_core/src/error.rs | 64 +++ crates/psychometric_core/src/event_time.rs | 444 +++++++++++++++++- crates/psychometric_core/src/lib.rs | 20 + ...multilevel_event_time_recovery_contract.rs | 186 +++++++- .../scientific_claim_boundary_contract.rs | 114 ++++- docs/TRACEABILITY.md | 2 +- docs/adr/0005-posterior-esem-dsem.md | 4 +- docs/adr/README.md | 2 +- .../multilevel-event-time-recovery.md | 15 +- docs/validation/temporal-event-foundation.md | 2 +- 13 files changed, 833 insertions(+), 25 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 4691cbb9..bf5815e3 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -61,7 +61,7 @@ boundaries above remain the target modular MSA architecture. | `tepp_simulation` | known-truth temporal/event data generation | | `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics | | `tepp_api` | versioned DTO, schema, and export contracts | -| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; first-occasion `T0TIPREDEFFECT` shift is `t0_b z` and Eq. 3 first-summand carry is `e^{A Δt} t0_b z` (`T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_b z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), first-occasion `T0TDPREDEFFECT` shift is `t0_m x0` and Eq. 3 first-summand carry is `e^{A Δt} t0_m x0` (`T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; §7.2 level-change `CINT` is `κ = −a m x` with `a < 0` so `−κ / a = m x` (`−a m x` is not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2; Eq. 3 of that setting is `(1 − e^{a Δt}) m x`, which is not `m x`, not `κ`, and not `TIPREDEFFECT`; §7.2 extra-process contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`; identification `TDPREDEFFECT` on the extra process is 1; printed extra `DRIFT` is `−0.000001`; not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`; `ε ≥ 0` fails closed; Eq. 5 of that contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; the extra process has `LAMBDA` 0 and is not an observed indicator; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`; after-t0 extra-process `TDPREDEFFECT` is `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` while `μ_t` uses `Δt`; Eq. 5 of that after-t0 contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)`; the first-occasion extra-process observed mean is not that observed mean when `u ≠ t0`; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive; §7.2 `asymTIPREDEFFECT` is `-B z / a` for `a < 0` (`-B z / a` is not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; §7.2 `addedTIPREDVAR` is `(B / a)² v`, not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`; Table 2 `asymCINT` is `-κ / a` for `a < 0` and is not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`; p. 16 stationary `T0MEANS` is `-κ / a + −B z / a` and is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean; Eq. 5 of that constrained mean is `τ + λ(−κ / a + −B z / a)`; `τ + λ μ_0` is not that observed mean; `τ + λ(−κ / a)` is not that observed mean when `B z ≠ 0`; `τ + λ μ_t` is not that observed mean; `MANIFESTMEANS` is not `E(y_0)`; the constrained latent mean is not `E(y_0)`; stationary `T0VAR` is `trait + −q / (2 a) + (B / a)² v` (not free `T0VAR`, not `asymDIFFUSION` alone, not `TRAITVAR` alone, not `addedTIPREDVAR` alone, and not the finite-interval discrete latent variance)), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | +| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; first-occasion `T0TIPREDEFFECT` shift is `t0_b z` and Eq. 3 first-summand carry is `e^{A Δt} t0_b z` (`T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_b z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), first-occasion `T0TDPREDEFFECT` shift is `t0_m x0` and Eq. 3 first-summand carry is `e^{A Δt} t0_m x0` (`T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; §7.2 level-change `CINT` is `κ = −a m x` with `a < 0` so `−κ / a = m x` (`−a m x` is not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2; Eq. 3 of that setting is `(1 − e^{a Δt}) m x`, which is not `m x`, not `κ`, and not `TIPREDEFFECT`; §7.2 extra-process contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`; identification `TDPREDEFFECT` on the extra process is 1; printed extra `DRIFT` is `−0.000001`; not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`; `ε ≥ 0` fails closed; Eq. 5 of that contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; the extra process has `LAMBDA` 0 and is not an observed indicator; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`; after-t0 extra-process `TDPREDEFFECT` is `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` while `μ_t` uses `Δt`; Eq. 5 of that after-t0 contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)`; the first-occasion extra-process observed mean is not that observed mean when `u ≠ t0`; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive; §7.2 `asymTIPREDEFFECT` is `-B z / a` for `a < 0` (`-B z / a` is not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; §7.2 `addedTIPREDVAR` is `(B / a)² v`, not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`; Table 2 `asymCINT` is `-κ / a` for `a < 0` and is not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`; p. 16 stationary `T0MEANS` is `-κ / a + −B z / a` and is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean; Eq. 5 of that constrained mean is `τ + λ(−κ / a + −B z / a)`; `τ + λ μ_0` is not that observed mean; `τ + λ(−κ / a)` is not that observed mean when `B z ≠ 0`; `τ + λ μ_t` is not that observed mean; `MANIFESTMEANS` is not `E(y_0)`; the constrained latent mean is not `E(y_0)`; stationary `T0VAR` is `trait + −q / (2 a) + (B / a)² v` (not free `T0VAR`, not `asymDIFFUSION` alone, not `TRAITVAR` alone, not `addedTIPREDVAR` alone, and not the finite-interval discrete latent variance. Eq. 5 of that constrained variance is `λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ` (JSS PDF re-opened 2026-08-22T03:20Z; form the stationary latent variance first, then `λ² p + θ + ψ`; `λ² p_0` is not that observed variance; `λ²(−q / (2 a)) + θ` is not that observed variance when `TRAITVAR` or `addedTIPREDVAR` is nonzero; `MANIFESTVAR` is not `Var(y_0)`; the constrained latent variance is not `Var(y_0)`))), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | No crate exposes placeholder production behavior in Task 1. This prevents an empty façade from becoming a de facto public API before its invariants and tests diff --git a/CHANGELOG.md b/CHANGELOG.md index cebe0882..e22a2898 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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, §4.3, pp. 9–10; Eq. 5, p. 5; Table 2, p. 12; p. 16; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-22T03:20Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar observed-indicator variance of §4.3 stationary `T0VAR`. Section 4.3 constrains the first-occasion variance to the model-predicted variance when `stationary` includes `"T0VAR"`. Equation 5 writes `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and `Γ ~ N(τ, Ψ)`. The constrained latent variance is `trait + −q / (2 a) + (B / a)² v`. The scalar composition is `Var(y_0) = λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ`. Form the stationary latent variance first, then `λ² p + θ + ψ`. A zero loading is exactly `θ + ψ`. A zero trait, a zero diffusion, and a zero TI contribution is exactly `θ + ψ`. `λ² p_0` for free `T0VAR` is not this composition. `λ²(−q / (2 a)) + θ` is not this composition when `TRAITVAR` or `addedTIPREDVAR` is nonzero. Evolving the constrained variance as if it were all state is not this composition when the trait or TI contribution is nonzero. `MANIFESTVAR` is not `Var(y_0)`. The constrained latent variance is not `Var(y_0)`. `TRAITVAR` is latent and is scaled by `λ²`; `MANIFESTTRAITVAR` is not. 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-22T03:20Z: `is_oa: false`). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-22T03:20Z: `is_oa: false`; Springer `content/pdf` is HTML 200). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, §4.3, pp. 9–10; p. 16; Table 2, p. 12; §7.2, pp. 20–21; Eq. 4, p. 5; JSS PDF re-opened 2026-08-22T03:07Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar stationary `T0VAR`. Section 4.3 constrains the first-occasion variance to the model-predicted variance when `stationary` includes `"T0VAR"`. Page 16 names `asymDIFFUSION` the total within-subject variance `-q / (2 a)`. Section 4.3 (p. 9) adds `TRAITVAR`. Section 7.2 names `addedTIPREDVAR` the stable between-subject variance accounted for by time-independent predictors, `(B / a)² v`. The scalar composition is `trait + −q / (2 a) + (B / a)² v`. Form the within-subject contribution first, then include the trait, then include the TI extra variance, then add. A zero trait, a zero diffusion, and a zero TI contribution is exactly zero. A zero diffusion and a zero TI contribution is exactly the trait. `a ≥ 0` cannot hold a finite process variance when the diffusion or the TI contribution is nonzero and fails closed. Trait-only variance does not require a stable drift. That constrained first-occasion variance is not free `T0VAR`, not `asymDIFFUSION` alone, not `TRAITVAR` alone, not `addedTIPREDVAR` alone, and not the finite-interval discrete latent variance. The printed 2-latent `addedTIPREDVAR` 2.838 is not this scalar map. 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-22T03:07Z: request empty). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-22T03:07Z: request empty; ETS RR-88-45 PDF 404; Wiley PDF 403). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, §4.3, pp. 9–10; Eq. 5, p. 5; Table 2, p. 12; Eq. 3, p. 5; JSS PDF re-opened 2026-08-21T20:07Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar observed-indicator mean of §4.3 stationary `T0MEANS`. Section 4.3 constrains the first-occasion mean to the model-predicted mean when `stationary` includes `"T0MEANS"`. Equation 5 writes `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and `Γ ~ N(τ, Ψ)`. The constrained latent mean is `-κ / a + −B z / a`. The scalar composition is `E(y_0) = τ + λ(−κ / a + −B z / a)`. Form the stationary latent mean first, then `τ + λ` of that mean. A zero loading is exactly `τ`. A zero intercept and a zero TI contribution is exactly `τ`. Evolving from that stationary start with `CINT` and `TIPREDEFFECT` stays at the stationary mean. `τ + λ μ_0` for free `T0MEANS` is not this composition. `τ + λ(−κ / a)` is not this composition when `B z ≠ 0`. `τ + λ μ_t` is not this composition. `MANIFESTMEANS` is not `E(y_0)`. The constrained latent mean is not `E(y_0)`. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) remains unread (Unpaywall request empty this cycle). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-21T20:10Z: `is_oa: false`; Springer `content/pdf` is HTML 200). - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, p. 16; Table 2, p. 12; Eq. 3, p. 5; JSS PDF opened 2026-08-21T16:13Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar stationary `T0MEANS`. Page 16 constrains `T0MEANS` to the model-implied values using `T0MEANSbase` / `T0MEANSfree` when the first observation is determined by the process in the same way as later observations. Those constraints include extra effects due to time-independent predictors (`asymTIPREDEFFECT`). For stable `a < 0` the scalar composition is `-κ / a + −B z / a`. Form the intercept contribution first, then include the TI extra effect, then add. A zero intercept and a zero TI contribution is exactly zero. `a ≥ 0` cannot hold a finite process-mean change when either contribution is nonzero and fails closed. That constrained first-occasion mean is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean. The printed 2-latent `T0MEANS` 2.823 is not this scalar map. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) and Mislevy (1991, *Psychometrika, 56*, 177–196) remain unread (Unpaywall 2026-08-21T16:21Z: `is_oa: false`; Springer `content/pdf` is HTML 200). diff --git a/CLAUDE.md b/CLAUDE.md index bf7ba504..b00df4d5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. Putnick and Bornstein (2016, PMC5145197 opened 2026-08-19T22:15Z) require scalar invariance before latent-mean comparison; residual invariance is not a prerequisite. Two-observation series have no residual degrees of freedom (`ordinary_least_squares_fit` returns residual variance `0`) and cap at strong/scalar; they still license means. This is two-group OLS, not MGCFA. Meredith (1993) names remain unread labels. -- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. The contemporaneous time-dependent predictor impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`). Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). The §7.2 level-change form is not that impulse. The observed mean of that contemporaneous impulse is `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-impulse latent mean is not `E(y_t)`. The time-independent predictor increment is `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`). Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. `TIPREDEFFECT` is `B`, not that discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle et al. (2012, Eq. 14). The observed mean of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The within-interval time-dependent impulse carry is `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation). Form `m x` first, then `e^{a(t−u)} m x`. A zero drift is `m x` with no dissipation. Underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept. `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). An impulse at `u = t` is the contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. The observed mean of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The carried latent mean is not `E(y_t)`. The first-occasion time-independent predictor shift is `t0_b z` (Driver et al., 2017, Table 3 `T0TIPREDEFFECT`; Eq. 3 first summand). Form `t0_b z` first, then `e^{a Δt} t0_b z`. Form `μ_t` first, then add that carry. A zero drift is `t0_b z`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. `e^{A Δt} t0_b z` is not `t0_b z`. `T0TIPREDEFFECT` is the coefficient, not the shift. The observed mean of that first-occasion carry is `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The evolved-plus-carry latent mean is not `E(y_t)`. The first-occasion time-dependent predictor shift is `t0_m x0` (Driver et al., 2017, Table 3 `T0TDPREDEFFECT`; Eq. 3 first summand; JSS PDF re-opened 2026-08-20T19:10Z). Form `t0_m x0` first, then `e^{a Δt} t0_m x0`. Form `μ_t` first, then add that carry. A zero drift is `t0_m x0`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`. `e^{A Δt} t0_m x0` is not `t0_m x0`. `T0TDPREDEFFECT` is the coefficient, not the shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The observed mean of that first-occasion TD carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean. The evolved-plus-carry latent mean is not `E(y_t)`. The lasting level-change `CINT` is `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z). Form `m x` first, then multiply by `−a`. Stable `a < 0` is required so `−κ / a = m x` is an equilibrium offset. `a ≥ 0` cannot hold a new process mean. `−a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not this `CINT` setting. Equation 3 maps that intercept as `(1 − e^{a Δt}) m x` (JSS PDF re-opened 2026-08-20T19:50Z). Form the level-change `CINT` first, then the discrete intercept map. Underflow of `e^{a Δt}` to `+0` keeps `m x`. `(1 − e^{a Δt}) m x` is not `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`. The printed §7.2 lasting level change is an extra near-zero-drift latent process (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z). `T0MEANS`, `CINT`, `T0VAR`, `DIFFUSION`, and `TRAITVAR` of that process are fixed to 0; `TDPREDEFFECT` on it is fixed to 1; its `DRIFT` diagonal is very close to 0 (printed example `−0.000001`; precisely 0 causes computational problems); the original process is driven by the `DRIFT` coupling `a_{ηξ}`. After a unit identification impulse the scalar contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`). Form `a_{ηξ} x` first. A zero coupling or zero predictor is exactly zero. `ε ≥ 0` fails closed. That contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. The observed mean of that extra-process contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5 of that §7.2 contribution; JSS PDF re-opened 2026-08-21T06:12Z). The extra process has `LAMBDA` 0 and is not an observed indicator. Original indicators load on the original process after the `DRIFT` coupling. The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The contribution is not `E(y_t)`. The evolved-plus-contribution latent mean is not `E(y_t)`. `T0TDPREDEFFECT` on the extra process begins at `t = 0` and uses `Δt = t − t0` for both the original-process evolution and the extra drive. `TDPREDEFFECT` after `t0` uses `t − u` with `t0 < u < t` for the extra drive while `μ_t` still uses `Δt`. The observed mean of that after-t0 extra-process contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, Eq. 5 of that §7.2 after-t0 contribution; JSS PDF re-opened 2026-08-21T06:32Z). The first-occasion extra-process observed mean is not that observed mean when `u ≠ t0`. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is a Dirac on the original process and is not that `DRIFT` drive. An impulse at `u = t0` or `u = t` is not interior. The asymptotic time-independent predictor effect is `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z). Form `B z` first, then divide by `-a`. Stable `a < 0` is required. `a ≥ 0` cannot hold a finite process-mean change. `-B z / a` is not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. The asymptotic time-independent predictor variance is `(B / a)² v` (Driver et al., 2017, §7.2, pp. 20–21 `addedTIPREDVAR`). Form the unit asymptotic effect first, then square, then multiply by `v`. `(B / a)² v` is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`. The asymptotic continuous intercept is `-κ / a` (Driver et al., 2017, Table 2, p. 12 `asymCINT`; Eq. 3 as `Δt → ∞`; JSS PDF opened 2026-08-21T16:13Z). Form `κ` first, then divide by `-a`. Stable `a < 0` is required. `-κ / a` is not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`. The p. 16 stationary `T0MEANS` constraint is `-κ / a + −B z / a`. Form the intercept contribution first, then include the TI extra effect, then add. That constrained first-occasion mean is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean. Equation 5 of that constrained mean is `τ + λ(−κ / a + −B z / a)` (Driver et al., 2017, §4.3, pp. 9–10; Eq. 5, p. 5; JSS PDF re-opened 2026-08-21T20:07Z). Form the stationary latent mean first, then `τ + λ` of that mean. `τ + λ μ_0` for free `T0MEANS` is not that composition. `τ + λ(−κ / a)` is not that composition when `B z ≠ 0`. `τ + λ μ_t` is not that composition. `MANIFESTMEANS` is not `E(y_0)`. The constrained latent mean is not `E(y_0)`. The p. 16 constrained first-occasion variance `trait + −q / (2 a) + (B / a)² v` is not free `T0VAR`, not `asymDIFFUSION` alone, not `TRAITVAR` alone, not `addedTIPREDVAR` alone, and not the finite-interval discrete latent variance. Evolving from that stationary start with `CINT` and `TIPREDEFFECT` stays at the stationary mean. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. The contemporaneous time-dependent predictor impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`). Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). The §7.2 level-change form is not that impulse. The observed mean of that contemporaneous impulse is `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-impulse latent mean is not `E(y_t)`. The time-independent predictor increment is `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`). Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. `TIPREDEFFECT` is `B`, not that discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle et al. (2012, Eq. 14). The observed mean of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The within-interval time-dependent impulse carry is `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation). Form `m x` first, then `e^{a(t−u)} m x`. A zero drift is `m x` with no dissipation. Underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept. `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). An impulse at `u = t` is the contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. The observed mean of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The carried latent mean is not `E(y_t)`. The first-occasion time-independent predictor shift is `t0_b z` (Driver et al., 2017, Table 3 `T0TIPREDEFFECT`; Eq. 3 first summand). Form `t0_b z` first, then `e^{a Δt} t0_b z`. Form `μ_t` first, then add that carry. A zero drift is `t0_b z`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. `e^{A Δt} t0_b z` is not `t0_b z`. `T0TIPREDEFFECT` is the coefficient, not the shift. The observed mean of that first-occasion carry is `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The evolved-plus-carry latent mean is not `E(y_t)`. The first-occasion time-dependent predictor shift is `t0_m x0` (Driver et al., 2017, Table 3 `T0TDPREDEFFECT`; Eq. 3 first summand; JSS PDF re-opened 2026-08-20T19:10Z). Form `t0_m x0` first, then `e^{a Δt} t0_m x0`. Form `μ_t` first, then add that carry. A zero drift is `t0_m x0`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`. `e^{A Δt} t0_m x0` is not `t0_m x0`. `T0TDPREDEFFECT` is the coefficient, not the shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The observed mean of that first-occasion TD carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean. The evolved-plus-carry latent mean is not `E(y_t)`. The lasting level-change `CINT` is `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z). Form `m x` first, then multiply by `−a`. Stable `a < 0` is required so `−κ / a = m x` is an equilibrium offset. `a ≥ 0` cannot hold a new process mean. `−a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not this `CINT` setting. Equation 3 maps that intercept as `(1 − e^{a Δt}) m x` (JSS PDF re-opened 2026-08-20T19:50Z). Form the level-change `CINT` first, then the discrete intercept map. Underflow of `e^{a Δt}` to `+0` keeps `m x`. `(1 − e^{a Δt}) m x` is not `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`. The printed §7.2 lasting level change is an extra near-zero-drift latent process (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z). `T0MEANS`, `CINT`, `T0VAR`, `DIFFUSION`, and `TRAITVAR` of that process are fixed to 0; `TDPREDEFFECT` on it is fixed to 1; its `DRIFT` diagonal is very close to 0 (printed example `−0.000001`; precisely 0 causes computational problems); the original process is driven by the `DRIFT` coupling `a_{ηξ}`. After a unit identification impulse the scalar contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`). Form `a_{ηξ} x` first. A zero coupling or zero predictor is exactly zero. `ε ≥ 0` fails closed. That contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. The observed mean of that extra-process contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5 of that §7.2 contribution; JSS PDF re-opened 2026-08-21T06:12Z). The extra process has `LAMBDA` 0 and is not an observed indicator. Original indicators load on the original process after the `DRIFT` coupling. The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The contribution is not `E(y_t)`. The evolved-plus-contribution latent mean is not `E(y_t)`. `T0TDPREDEFFECT` on the extra process begins at `t = 0` and uses `Δt = t − t0` for both the original-process evolution and the extra drive. `TDPREDEFFECT` after `t0` uses `t − u` with `t0 < u < t` for the extra drive while `μ_t` still uses `Δt`. The observed mean of that after-t0 extra-process contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, Eq. 5 of that §7.2 after-t0 contribution; JSS PDF re-opened 2026-08-21T06:32Z). The first-occasion extra-process observed mean is not that observed mean when `u ≠ t0`. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is a Dirac on the original process and is not that `DRIFT` drive. An impulse at `u = t0` or `u = t` is not interior. The asymptotic time-independent predictor effect is `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z). Form `B z` first, then divide by `-a`. Stable `a < 0` is required. `a ≥ 0` cannot hold a finite process-mean change. `-B z / a` is not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. The asymptotic time-independent predictor variance is `(B / a)² v` (Driver et al., 2017, §7.2, pp. 20–21 `addedTIPREDVAR`). Form the unit asymptotic effect first, then square, then multiply by `v`. `(B / a)² v` is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`. The asymptotic continuous intercept is `-κ / a` (Driver et al., 2017, Table 2, p. 12 `asymCINT`; Eq. 3 as `Δt → ∞`; JSS PDF opened 2026-08-21T16:13Z). Form `κ` first, then divide by `-a`. Stable `a < 0` is required. `-κ / a` is not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`. The p. 16 stationary `T0MEANS` constraint is `-κ / a + −B z / a`. Form the intercept contribution first, then include the TI extra effect, then add. That constrained first-occasion mean is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean. Equation 5 of that constrained mean is `τ + λ(−κ / a + −B z / a)` (Driver et al., 2017, §4.3, pp. 9–10; Eq. 5, p. 5; JSS PDF re-opened 2026-08-21T20:07Z). Form the stationary latent mean first, then `τ + λ` of that mean. `τ + λ μ_0` for free `T0MEANS` is not that composition. `τ + λ(−κ / a)` is not that composition when `B z ≠ 0`. `τ + λ μ_t` is not that composition. `MANIFESTMEANS` is not `E(y_0)`. The constrained latent mean is not `E(y_0)`. The p. 16 constrained first-occasion variance `trait + −q / (2 a) + (B / a)² v` is not free `T0VAR`, not `asymDIFFUSION` alone, not `TRAITVAR` alone, not `addedTIPREDVAR` alone, and not the finite-interval discrete latent variance. Eq. 5 of that constrained variance is `λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ` (JSS PDF re-opened 2026-08-22T03:20Z; form the stationary latent variance first, then `λ² p + θ + ψ`; `λ² p_0` is not that observed variance; `λ²(−q / (2 a)) + θ` is not that observed variance when `TRAITVAR` or `addedTIPREDVAR` is nonzero; `MANIFESTVAR` is not `Var(y_0)`; the constrained latent variance is not `Var(y_0)`). Evolving from that stationary start with `CINT` and `TIPREDEFFECT` stays at the stationary mean. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. diff --git a/crates/psychometric_core/src/error.rs b/crates/psychometric_core/src/error.rs index 2ed90f89..93bed629 100644 --- a/crates/psychometric_core/src/error.rs +++ b/crates/psychometric_core/src/error.rs @@ -432,6 +432,28 @@ pub enum PsychometricError { /// `exp(2 a Δt) p + Q_Δt` is not the constrained first-occasion /// variance. StationaryInitialLatentVarianceIsNotDiscreteVariance, + /// Driver Eq. 5 of §4.3 stationary `T0VAR` was treated as + /// `MANIFESTVAR`. `λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ` + /// is not `θ`. + StationaryInitialObservedVarianceIsNotMeasurementError, + /// Driver §4.3 stationary `T0VAR` was treated as `Var(y_0)`. + /// `trait + −q / (2 a) + (B / a)² v` is the constrained latent + /// variance, not the observed-indicator variance. + StationaryInitialLatentVarianceIsNotObservedVariance, + /// Driver Eq. 5 of a finite-interval evolved variance was treated + /// as Eq. 5 of §4.3 stationary `T0VAR`. `λ² Var(η_t) + θ` is not + /// `λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ` when the first + /// occasion is constrained. + EvolvedObservedVarianceIsNotStationaryInitialObservedVariance, + /// Driver Eq. 5 of `asymDIFFUSION` was treated as Eq. 5 of §4.3 + /// stationary `T0VAR`. `λ²(−q / (2 a)) + θ` is not + /// `λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ` when `TRAITVAR` + /// or `addedTIPREDVAR` is nonzero. + StationaryWithinSubjectObservedVarianceIsNotStationaryInitialObservedVariance, + /// Driver Eq. 5 of free `T0VAR` was treated as Eq. 5 of §4.3 + /// stationary `T0VAR`. `λ² p_0 + θ` is not + /// `λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ`. + InitialObservedVarianceIsNotStationaryInitialObservedVariance, } impl fmt::Display for PsychometricError { @@ -774,6 +796,21 @@ impl fmt::Display for PsychometricError { Self::StationaryInitialLatentVarianceIsNotDiscreteVariance => { "stationary first-occasion latent variance is not the finite-interval discrete latent variance" } + Self::StationaryInitialObservedVarianceIsNotMeasurementError => { + "stationary first-occasion observed variance is not the measurement-error variance" + } + Self::StationaryInitialLatentVarianceIsNotObservedVariance => { + "stationary first-occasion latent variance is not the first-occasion observed variance" + } + Self::EvolvedObservedVarianceIsNotStationaryInitialObservedVariance => { + "evolved observed variance is not the stationary first-occasion observed variance" + } + Self::StationaryWithinSubjectObservedVarianceIsNotStationaryInitialObservedVariance => { + "asymptotic-within-subject observed variance is not the stationary first-occasion observed variance" + } + Self::InitialObservedVarianceIsNotStationaryInitialObservedVariance => { + "free first-occasion observed variance is not the stationary first-occasion observed variance" + } }; formatter.write_str(message) } @@ -1302,4 +1339,31 @@ mod tests { "stationary first-occasion latent variance is not the finite-interval discrete latent variance" ); } + + #[test] + fn stationary_initial_observed_variance_boundary_messages_are_stable() { + assert_eq!( + PsychometricError::StationaryInitialObservedVarianceIsNotMeasurementError.to_string(), + "stationary first-occasion observed variance is not the measurement-error variance" + ); + assert_eq!( + PsychometricError::StationaryInitialLatentVarianceIsNotObservedVariance.to_string(), + "stationary first-occasion latent variance is not the first-occasion observed variance" + ); + assert_eq!( + PsychometricError::EvolvedObservedVarianceIsNotStationaryInitialObservedVariance + .to_string(), + "evolved observed variance is not the stationary first-occasion observed variance" + ); + assert_eq!( + PsychometricError::StationaryWithinSubjectObservedVarianceIsNotStationaryInitialObservedVariance + .to_string(), + "asymptotic-within-subject observed variance is not the stationary first-occasion observed variance" + ); + assert_eq!( + PsychometricError::InitialObservedVarianceIsNotStationaryInitialObservedVariance + .to_string(), + "free first-occasion observed variance is not the stationary first-occasion observed variance" + ); + } } diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index 535c46b4..db317607 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -100,6 +100,14 @@ //! `addedTIPREDVAR` alone, and not the finite-interval discrete //! latent variance `exp(2 a Δt) p + Q_Δt`. The printed 2-latent //! `addedTIPREDVAR` 2.838 is not this scalar map. +//! Equation 5 of that constrained first-occasion variance is +//! `λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ` (§4.3, pp. 9–10; +//! Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-22T03:20Z). +//! Form the stationary latent variance first, then `λ² p + θ + ψ`. +//! `λ² p` for free `T0VAR` is not that composition. +//! `λ²(−q / (2 a)) + θ` is not that composition when `TRAITVAR` or +//! `addedTIPREDVAR` is nonzero. `MANIFESTVAR` is not `Var(y_0)`. +//! The constrained latent variance is not `Var(y_0)`. //! Table 3 (p. 13) names a different matrix //! `T0TIPREDEFFECT` for time-independent predictors on latents at //! `T0`. The scalar first-occasion shift is `t0_b z`. Equation 3's @@ -3343,6 +3351,160 @@ pub fn refuse_stationary_initial_latent_variance_as_discrete_variance( Err(PsychometricError::StationaryInitialLatentVarianceIsNotDiscreteVariance) } +/// Exact scalar Eq. 5 of §4.3 / p. 16 stationary `T0VAR`. +/// +/// Driver, Oud, and Voelkle (2017, §4.3, pp. 9–10; Eq. 5, p. 5; +/// Table 2, p. 12; p. 16; §7.2, pp. 20–21; JSS PDF re-opened +/// 2026-08-22T03:20Z from +/// ) +/// constrain `T0VAR` to the model-predicted variance when +/// `stationary` includes `"T0VAR"`. Equation 5 writes +/// `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and +/// `Γ ~ N(τ, Ψ)`. The constrained latent variance is +/// `trait + −q / (2 a) + (B / a)² v`. The scalar composition is +/// `Var(y_0) = λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ`. Form +/// the stationary latent variance first, then `λ² p + θ + ψ`. A +/// zero loading is exactly `θ + ψ`. A zero trait, a zero diffusion, +/// and a zero TI contribution is exactly `θ + ψ`. `λ² p_0` for +/// free `T0VAR` is not this composition. `λ²(−q / (2 a)) + θ` is +/// not this composition when `TRAITVAR` or `addedTIPREDVAR` is +/// nonzero. Evolving the constrained variance as if it were all +/// state is not this composition when the trait or TI contribution +/// is nonzero. `MANIFESTVAR` is not `Var(y_0)`. The constrained +/// latent variance is not `Var(y_0)`. `TRAITVAR` is latent and is +/// scaled by `λ²`; `MANIFESTTRAITVAR` is not. This is not a Kalman +/// filter, not a matrix `expm`, and not ctsem estimation. +/// +/// # Errors +/// +/// Propagates [`recover_stationary_initial_latent_variance`] and +/// [`recover_manifest_trait_plus_state_observed_variance`]. +#[allow(clippy::too_many_arguments)] +pub fn recover_stationary_initial_observed_variance( + loading: f64, + trait_variance: f64, + continuous_diffusion: f64, + time_independent_effect: f64, + predictor_variance: f64, + log_rate: f64, + measurement_error_variance: f64, + manifest_trait_variance: f64, + clock: LagClock, +) -> Result { + let stationary_latent_variance = recover_stationary_initial_latent_variance( + trait_variance, + continuous_diffusion, + time_independent_effect, + predictor_variance, + log_rate, + clock, + )?; + recover_manifest_trait_plus_state_observed_variance( + loading, + stationary_latent_variance, + measurement_error_variance, + manifest_trait_variance, + ) +} + +/// Refuse treating §4.3 stationary `T0VAR` as `Var(y_0)`. +/// +/// `trait + −q / (2 a) + (B / a)² v` is the constrained latent +/// variance. Equation 5 maps `Var(y_0) = λ²` of that variance plus +/// `θ + ψ`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::StationaryInitialLatentVarianceIsNotObservedVariance`]. +pub fn refuse_stationary_initial_latent_variance_as_observed_variance( + stationary_latent_variance: f64, + stationary_observed_variance: f64, +) -> Result { + let _ = (stationary_latent_variance, stationary_observed_variance); + Err(PsychometricError::StationaryInitialLatentVarianceIsNotObservedVariance) +} + +/// Refuse treating `MANIFESTVAR` as Eq. 5 of §4.3 stationary +/// `T0VAR`. +/// +/// Table 2 names `θ` `MANIFESTVAR`. +/// `λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ` is not `θ` when +/// the loading and constrained variance are nonzero. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::StationaryInitialObservedVarianceIsNotMeasurementError`]. +pub fn refuse_stationary_initial_observed_variance_as_measurement_error( + stationary_observed_variance: f64, + measurement_error_variance: f64, +) -> Result { + let _ = (stationary_observed_variance, measurement_error_variance); + Err(PsychometricError::StationaryInitialObservedVarianceIsNotMeasurementError) +} + +/// Refuse treating evolved `λ² Var(η_t) + θ` as Eq. 5 of §4.3 +/// stationary `T0VAR`. +/// +/// Evolving the constrained first-occasion variance as if it were +/// all state is not `λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ` +/// when the trait or TI contribution is nonzero. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::EvolvedObservedVarianceIsNotStationaryInitialObservedVariance`]. +pub fn refuse_evolved_observed_variance_as_stationary_initial_observed_variance( + evolved_observed_variance: f64, + stationary_observed_variance: f64, +) -> Result { + let _ = (evolved_observed_variance, stationary_observed_variance); + Err(PsychometricError::EvolvedObservedVarianceIsNotStationaryInitialObservedVariance) +} + +/// Refuse treating Eq. 5 of `asymDIFFUSION` as Eq. 5 of §4.3 +/// stationary `T0VAR`. +/// +/// `λ²(−q / (2 a)) + θ` is the within-subject observed contribution +/// and is not that composition when `TRAITVAR` or `addedTIPREDVAR` +/// is nonzero. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::StationaryWithinSubjectObservedVarianceIsNotStationaryInitialObservedVariance`]. +pub fn refuse_stationary_within_subject_observed_variance_as_stationary_initial_observed_variance( + within_subject_observed_variance: f64, + stationary_observed_variance: f64, +) -> Result { + let _ = ( + within_subject_observed_variance, + stationary_observed_variance, + ); + Err( + PsychometricError::StationaryWithinSubjectObservedVarianceIsNotStationaryInitialObservedVariance, + ) +} + +/// Refuse treating Eq. 5 of free `T0VAR` as Eq. 5 of §4.3 +/// stationary `T0VAR`. +/// +/// `λ² p_0 + θ` is the free first-occasion observed variance. +/// `λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ` is not that map. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::InitialObservedVarianceIsNotStationaryInitialObservedVariance`]. +pub fn refuse_initial_observed_variance_as_stationary_initial_observed_variance( + free_initial_observed_variance: f64, + stationary_observed_variance: f64, +) -> Result { + let _ = (free_initial_observed_variance, stationary_observed_variance); + Err(PsychometricError::InitialObservedVarianceIsNotStationaryInitialObservedVariance) +} + /// Exact scalar observed mean of a time-independent predictor. /// /// Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Eq. 3, p. 5; Table 2, @@ -4871,10 +5033,10 @@ mod tests { recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, recover_stationary_initial_latent_mean, recover_stationary_initial_latent_variance, - recover_stationary_initial_observed_mean, recover_stationary_latent_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_stationary_initial_observed_mean, recover_stationary_initial_observed_variance, + recover_stationary_latent_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, @@ -4900,6 +5062,7 @@ mod tests { refuse_evolved_observed_mean_as_initial_time_independent_observed_mean, refuse_evolved_observed_mean_as_stationary_initial_observed_mean, refuse_evolved_observed_mean_as_time_independent_observed_mean, + refuse_evolved_observed_variance_as_stationary_initial_observed_variance, refuse_extra_process_contribution_as_observed_mean, refuse_extra_process_latent_mean_as_observed_mean, refuse_extra_process_observed_mean_as_after_extra_process_observed_mean, @@ -4916,6 +5079,7 @@ mod tests { refuse_initial_latent_mean_as_evolved_mean, refuse_initial_observed_mean_as_evolved_observed_mean, refuse_initial_observed_mean_as_stationary_initial_observed_mean, + refuse_initial_observed_variance_as_stationary_initial_observed_variance, refuse_initial_time_dependent_carry_as_impulse_carry, refuse_initial_time_dependent_carry_as_initial_effect, refuse_initial_time_dependent_coefficient_as_initial_effect, @@ -4952,9 +5116,12 @@ mod tests { refuse_stationary_initial_latent_variance_as_asymptotic_time_independent_variance, refuse_stationary_initial_latent_variance_as_discrete_variance, refuse_stationary_initial_latent_variance_as_initial_latent_variance, + refuse_stationary_initial_latent_variance_as_observed_variance, refuse_stationary_initial_latent_variance_as_stationary_within_subject, refuse_stationary_initial_latent_variance_as_trait_variance, refuse_stationary_initial_observed_mean_as_manifest_means, + refuse_stationary_initial_observed_variance_as_measurement_error, + refuse_stationary_within_subject_observed_variance_as_stationary_initial_observed_variance, refuse_time_dependent_impulse_as_continuous_intercept, refuse_time_dependent_impulse_as_time_independent_effect, refuse_time_dependent_impulse_as_time_varying_discrete_effect, @@ -8846,6 +9013,275 @@ mod tests { ); } + #[test] + fn stationary_initial_observed_variance_recovers_driver_equation_five_of_section_four_point_three() + { + // Driver et al. (2017, §4.3, pp. 9–10; Eq. 5, p. 5) + // constrain first-occasion variances to the model-predicted + // variance. Equation 5 maps Var(y_0) = λ² of that variance + // plus θ + ψ. + let printed_effect = -0.225_f64; + let printed_asym = -1.673_f64; + let log_rate = -printed_effect / printed_asym; + let trait_variance = 1.0_f64; + let diffusion = 0.4_f64; + let loading = 2.0_f64; + let measurement_error = 0.5_f64; + let manifest_trait = 0.1_f64; + let recovered = recover_stationary_initial_observed_variance( + loading, + trait_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + measurement_error, + manifest_trait, + LagClock::EventTime, + ) + .expect("eq5-stationary-T0VAR"); + let latent = recover_stationary_initial_latent_variance( + trait_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("stationary T0VAR"); + let expected = recover_manifest_trait_plus_state_observed_variance( + loading, + latent, + measurement_error, + manifest_trait, + ) + .expect("λ²p+θ+ψ"); + assert!((recovered - expected).abs() < 1e-12); + let state = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + let state_only_observed = + recover_manifest_observed_variance(loading, state, measurement_error) + .expect("λ²(−q/2a)+θ"); + assert!((recovered - state_only_observed).abs() > 1e-3); + let free_initial_observed = + recover_manifest_observed_variance(loading, 2.0, measurement_error).expect("λ²p_0+θ"); + assert!((recovered - free_initial_observed).abs() > 1e-3); + let discrete = + recover_discrete_latent_variance(latent, diffusion, log_rate, 1.0, LagClock::EventTime) + .expect("Var(η_t)"); + let evolved = recover_manifest_observed_variance(loading, discrete, measurement_error) + .expect("λ²Var(η_t)+θ"); + assert!((recovered - evolved).abs() > 1e-3); + assert!((recovered - measurement_error).abs() > 1e-3); + assert!((recovered - latent).abs() > 1e-3); + assert_eq!( + recover_stationary_initial_observed_variance( + 0.0, + trait_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + measurement_error, + manifest_trait, + LagClock::EventTime, + ), + Ok(measurement_error + manifest_trait) + ); + assert_eq!( + recover_stationary_initial_observed_variance( + loading, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + measurement_error, + 0.0, + LagClock::EventTime, + ), + Ok(measurement_error) + ); + let zero_manifest_trait = recover_stationary_initial_observed_variance( + loading, + trait_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + measurement_error, + 0.0, + LagClock::EventTime, + ) + .expect("ψ=0"); + let expected_zero_psi = + recover_manifest_observed_variance(loading, latent, measurement_error).expect("λ²p+θ"); + assert!((zero_manifest_trait - expected_zero_psi).abs() < 1e-12); + } + + #[test] + fn stationary_initial_observed_variance_is_not_manifest_latent_evolved_or_free() { + let trait_variance = 1.0_f64; + let diffusion = 0.4_f64; + let log_rate = -0.134_488_942_f64; + let loading = 2.0_f64; + let measurement_error = 0.5_f64; + let recovered = recover_stationary_initial_observed_variance( + loading, + trait_variance, + diffusion, + -0.225, + 1.0, + log_rate, + measurement_error, + 0.1, + LagClock::EventTime, + ) + .expect("eq5-stationary-T0VAR"); + let latent = recover_stationary_initial_latent_variance( + trait_variance, + diffusion, + -0.225, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("stationary T0VAR"); + let state = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + let state_only_observed = + recover_manifest_observed_variance(loading, state, measurement_error) + .expect("λ²(−q/2a)+θ"); + let free_initial_observed = + recover_manifest_observed_variance(loading, 2.0, measurement_error).expect("λ²p_0+θ"); + let discrete = + recover_discrete_latent_variance(latent, diffusion, log_rate, 1.0, LagClock::EventTime) + .expect("Var(η_t)"); + let evolved = recover_manifest_observed_variance(loading, discrete, measurement_error) + .expect("λ²Var(η_t)+θ"); + assert_eq!( + refuse_stationary_initial_latent_variance_as_observed_variance(latent, recovered), + Err(PsychometricError::StationaryInitialLatentVarianceIsNotObservedVariance) + ); + assert_eq!( + refuse_stationary_initial_observed_variance_as_measurement_error( + recovered, + measurement_error + ), + Err(PsychometricError::StationaryInitialObservedVarianceIsNotMeasurementError) + ); + assert_eq!( + refuse_evolved_observed_variance_as_stationary_initial_observed_variance( + evolved, recovered + ), + Err(PsychometricError::EvolvedObservedVarianceIsNotStationaryInitialObservedVariance) + ); + assert_eq!( + refuse_stationary_within_subject_observed_variance_as_stationary_initial_observed_variance( + state_only_observed, + recovered + ), + Err( + PsychometricError::StationaryWithinSubjectObservedVarianceIsNotStationaryInitialObservedVariance + ) + ); + assert_eq!( + refuse_initial_observed_variance_as_stationary_initial_observed_variance( + free_initial_observed, + recovered + ), + Err(PsychometricError::InitialObservedVarianceIsNotStationaryInitialObservedVariance) + ); + } + + #[test] + fn stationary_initial_observed_variance_invalid_inputs_fail_closed() { + assert_eq!( + recover_stationary_initial_observed_variance( + 2.0, + 1.0, + 0.4, + -0.225, + 1.0, + -0.13, + 0.5, + 0.1, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_stationary_initial_observed_variance( + 2.0, + 0.0, + 0.4, + 0.0, + 1.0, + 0.0, + 0.5, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + recover_stationary_initial_observed_variance( + 2.0, + 0.0, + 0.0, + -0.225, + 1.0, + 0.5, + 0.5, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) + ); + assert_eq!( + recover_stationary_initial_observed_variance( + 2.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.5, + 0.1, + LagClock::EventTime + ), + Ok(0.6) + ); + assert_eq!( + recover_stationary_initial_observed_variance( + f64::NAN, + 1.0, + 0.4, + 0.0, + 0.0, + -0.5, + 0.5, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_stationary_initial_observed_variance( + 2.0, + f64::MAX, + f64::MAX, + 0.0, + 0.0, + -0.5, + 0.5, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + } + #[test] fn discrete_observed_mean_with_impulse_recovers_driver_equation_five() { let loading = 2.0_f64; diff --git a/crates/psychometric_core/src/lib.rs b/crates/psychometric_core/src/lib.rs index aef364fa..8432095b 100644 --- a/crates/psychometric_core/src/lib.rs +++ b/crates/psychometric_core/src/lib.rs @@ -148,6 +148,14 @@ //! not free `T0VAR`, not `asymDIFFUSION` alone, not `TRAITVAR` //! alone, not `addedTIPREDVAR` alone, and not the finite-interval //! discrete latent variance), +//! recovers the Driver Eq. 5 of that constrained variance as +//! `λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ` +//! (§4.3, pp. 9–10; Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened +//! 2026-08-22T03:20Z; form the stationary latent variance first, +//! then `λ² p + θ + ψ`; `λ² p_0` is not that observed variance; +//! `λ²(−q / (2 a)) + θ` is not that observed variance when +//! `TRAITVAR` or `addedTIPREDVAR` is nonzero; `MANIFESTVAR` is not +//! `Var(y_0)`; the constrained latent variance is not `Var(y_0)`), //! and refuses //! latent-mean comparison below strong invariance. @@ -290,6 +298,8 @@ pub use event_time::recover_stationary_initial_latent_mean; pub use event_time::recover_stationary_initial_latent_variance; /// Exact scalar Eq. 5 of §4.3 stationary `T0MEANS` `τ + λ(−κ / a + −B z / a)`. pub use event_time::recover_stationary_initial_observed_mean; +/// Exact scalar Eq. 5 of §4.3 stationary `T0VAR` `λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ`. +pub use event_time::recover_stationary_initial_observed_variance; /// Exact scalar stationary within-subject variance `-q / (2 a)`. pub use event_time::recover_stationary_latent_variance; /// Exact scalar contemporaneous `TDPREDEFFECT` impulse `m x`. @@ -354,6 +364,8 @@ pub use event_time::refuse_evolved_observed_mean_as_initial_time_independent_obs pub use event_time::refuse_evolved_observed_mean_as_stationary_initial_observed_mean; /// Refuse treating evolved `τ + λ μ_t` as the time-independent-predictor observed mean. pub use event_time::refuse_evolved_observed_mean_as_time_independent_observed_mean; +/// Refuse treating evolved `λ² Var(η_t) + θ` as Eq. 5 of §4.3 stationary `T0VAR`. +pub use event_time::refuse_evolved_observed_variance_as_stationary_initial_observed_variance; /// Refuse treating the §7.2 extra-process contribution as `E(y_t)`. pub use event_time::refuse_extra_process_contribution_as_observed_mean; /// Refuse treating the evolved-plus-contribution latent mean as `E(y_t)`. @@ -386,6 +398,8 @@ pub use event_time::refuse_initial_latent_mean_as_evolved_mean; pub use event_time::refuse_initial_observed_mean_as_evolved_observed_mean; /// Refuse treating `τ + λ μ_0` as Eq. 5 of §4.3 stationary `T0MEANS`. pub use event_time::refuse_initial_observed_mean_as_stationary_initial_observed_mean; +/// Refuse treating `λ² p_0 + θ` as Eq. 5 of §4.3 stationary `T0VAR`. +pub use event_time::refuse_initial_observed_variance_as_stationary_initial_observed_variance; /// Refuse treating the Eq. 3 `T0TDPREDEFFECT` carry as the within-interval impulse carry. pub use event_time::refuse_initial_time_dependent_carry_as_impulse_carry; /// Refuse treating the Eq. 3 `T0TDPREDEFFECT` carry as the first-occasion shift. @@ -464,12 +478,18 @@ pub use event_time::refuse_stationary_initial_latent_variance_as_asymptotic_time pub use event_time::refuse_stationary_initial_latent_variance_as_discrete_variance; /// Refuse treating §4.3 / p. 16 stationary `T0VAR` as free `T0VAR`. pub use event_time::refuse_stationary_initial_latent_variance_as_initial_latent_variance; +/// Refuse treating §4.3 stationary `T0VAR` as `Var(y_0)`. +pub use event_time::refuse_stationary_initial_latent_variance_as_observed_variance; /// Refuse treating §4.3 / p. 16 stationary `T0VAR` as `asymDIFFUSION`. pub use event_time::refuse_stationary_initial_latent_variance_as_stationary_within_subject; /// Refuse treating §4.3 / p. 16 stationary `T0VAR` as `TRAITVAR`. pub use event_time::refuse_stationary_initial_latent_variance_as_trait_variance; /// Refuse treating Eq. 5 of §4.3 stationary `T0MEANS` as `MANIFESTMEANS`. pub use event_time::refuse_stationary_initial_observed_mean_as_manifest_means; +/// Refuse treating Eq. 5 of §4.3 stationary `T0VAR` as `MANIFESTVAR`. +pub use event_time::refuse_stationary_initial_observed_variance_as_measurement_error; +/// Refuse treating Eq. 5 of `asymDIFFUSION` as Eq. 5 of §4.3 stationary `T0VAR`. +pub use event_time::refuse_stationary_within_subject_observed_variance_as_stationary_initial_observed_variance; /// Refuse treating Driver Eq. 3 `TDPREDEFFECT` impulse as `CINT`. pub use event_time::refuse_time_dependent_impulse_as_continuous_intercept; /// Refuse treating Driver Eq. 3 impulse as `TIPREDEFFECT`. 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 693e375c..999a235e 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -35,10 +35,10 @@ use psychometric_core::{ recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, recover_stationary_initial_latent_mean, recover_stationary_initial_latent_variance, - recover_stationary_initial_observed_mean, recover_stationary_latent_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_stationary_initial_observed_mean, recover_stationary_initial_observed_variance, + recover_stationary_latent_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, @@ -64,6 +64,7 @@ use psychometric_core::{ refuse_evolved_observed_mean_as_initial_time_independent_observed_mean, refuse_evolved_observed_mean_as_stationary_initial_observed_mean, refuse_evolved_observed_mean_as_time_independent_observed_mean, + refuse_evolved_observed_variance_as_stationary_initial_observed_variance, refuse_extra_process_contribution_as_observed_mean, refuse_extra_process_latent_mean_as_observed_mean, refuse_extra_process_observed_mean_as_after_extra_process_observed_mean, @@ -80,6 +81,7 @@ use psychometric_core::{ refuse_initial_latent_mean_as_evolved_mean, refuse_initial_observed_mean_as_evolved_observed_mean, refuse_initial_observed_mean_as_stationary_initial_observed_mean, + refuse_initial_observed_variance_as_stationary_initial_observed_variance, refuse_initial_time_dependent_carry_as_impulse_carry, refuse_initial_time_dependent_carry_as_initial_effect, refuse_initial_time_dependent_coefficient_as_initial_effect, @@ -113,9 +115,12 @@ use psychometric_core::{ refuse_stationary_initial_latent_variance_as_asymptotic_time_independent_variance, refuse_stationary_initial_latent_variance_as_discrete_variance, refuse_stationary_initial_latent_variance_as_initial_latent_variance, + refuse_stationary_initial_latent_variance_as_observed_variance, refuse_stationary_initial_latent_variance_as_stationary_within_subject, refuse_stationary_initial_latent_variance_as_trait_variance, refuse_stationary_initial_observed_mean_as_manifest_means, + refuse_stationary_initial_observed_variance_as_measurement_error, + refuse_stationary_within_subject_observed_variance_as_stationary_initial_observed_variance, refuse_time_dependent_impulse_as_continuous_intercept, refuse_time_dependent_impulse_as_time_independent_effect, refuse_time_dependent_impulse_as_time_varying_discrete_effect, @@ -4857,3 +4862,176 @@ fn stationary_initial_latent_variance_refuses_unstable_drift_and_non_event_clock Err(PsychometricError::InvalidNumericInput) ); } + +#[test] +#[allow(clippy::too_many_lines)] +fn stationary_initial_observed_variance_recovers_driver_equation_five_of_section_four_point_three() +{ + let printed_effect = -0.225_f64; + let printed_asym = -1.673_f64; + let log_rate = -printed_effect / printed_asym; + let trait_variance = 1.0_f64; + let diffusion = 0.4_f64; + let loading = 2.0_f64; + let measurement_error = 0.5_f64; + let manifest_trait = 0.1_f64; + let recovered = recover_stationary_initial_observed_variance( + loading, + trait_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + measurement_error, + manifest_trait, + LagClock::EventTime, + ) + .expect("eq5-stationary-T0VAR"); + let latent = recover_stationary_initial_latent_variance( + trait_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("stationary T0VAR"); + let expected = recover_manifest_trait_plus_state_observed_variance( + loading, + latent, + measurement_error, + manifest_trait, + ) + .expect("λ²p+θ+ψ"); + let error = rmse(&[expected], &[recovered]); + assert!( + error < 1e-12, + "Driver §4.3 Eq. 5 of stationary T0VAR RMSE {error}: got {recovered}" + ); + let state = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + let state_only_observed = + recover_manifest_observed_variance(loading, state, measurement_error).expect("λ²(−q/2a)+θ"); + let free_initial_observed = + recover_manifest_observed_variance(loading, 2.0, measurement_error).expect("λ²p_0+θ"); + let discrete = + recover_discrete_latent_variance(latent, diffusion, log_rate, 1.0, LagClock::EventTime) + .expect("Var(η_t)"); + let evolved = recover_manifest_observed_variance(loading, discrete, measurement_error) + .expect("λ²Var(η_t)+θ"); + assert!( + rmse(&[recovered], &[measurement_error]) > error, + "MANIFESTVAR is not Var(y_0)" + ); + assert!(rmse(&[recovered], &[latent]) > error); + assert!(rmse(&[recovered], &[state_only_observed]) > error); + assert!(rmse(&[recovered], &[free_initial_observed]) > error); + assert!(rmse(&[recovered], &[evolved]) > error); + assert_eq!( + recover_stationary_initial_observed_variance( + 0.0, + trait_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + measurement_error, + manifest_trait, + LagClock::EventTime, + ), + Ok(measurement_error + manifest_trait) + ); + assert_eq!( + refuse_stationary_initial_latent_variance_as_observed_variance(latent, recovered), + Err(PsychometricError::StationaryInitialLatentVarianceIsNotObservedVariance) + ); + assert_eq!( + refuse_stationary_initial_observed_variance_as_measurement_error( + recovered, + measurement_error + ), + Err(PsychometricError::StationaryInitialObservedVarianceIsNotMeasurementError) + ); + assert_eq!( + refuse_evolved_observed_variance_as_stationary_initial_observed_variance( + evolved, recovered + ), + Err(PsychometricError::EvolvedObservedVarianceIsNotStationaryInitialObservedVariance) + ); + assert_eq!( + refuse_stationary_within_subject_observed_variance_as_stationary_initial_observed_variance( + state_only_observed, + recovered + ), + Err( + PsychometricError::StationaryWithinSubjectObservedVarianceIsNotStationaryInitialObservedVariance + ) + ); + assert_eq!( + refuse_initial_observed_variance_as_stationary_initial_observed_variance( + free_initial_observed, + recovered + ), + Err(PsychometricError::InitialObservedVarianceIsNotStationaryInitialObservedVariance) + ); +} + +#[test] +fn stationary_initial_observed_variance_refuses_unstable_drift_and_non_event_clocks() { + assert_eq!( + recover_stationary_initial_observed_variance( + 2.0, + 1.0, + 0.4, + -0.225, + 1.0, + -0.13, + 0.5, + 0.1, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_stationary_initial_observed_variance( + 2.0, + 0.0, + 0.4, + 0.0, + 1.0, + 0.0, + 0.5, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + recover_stationary_initial_observed_variance( + 2.0, + 0.0, + 0.0, + -0.225, + 1.0, + 0.5, + 0.5, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) + ); + assert_eq!( + recover_stationary_initial_observed_variance( + 2.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.5, + 0.1, + LagClock::EventTime + ), + Ok(0.6) + ); +} diff --git a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs index e576c397..b6482170 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -30,9 +30,10 @@ use psychometric_core::{ recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, recover_stationary_initial_latent_mean, recover_stationary_initial_latent_variance, - recover_stationary_initial_observed_mean, recover_stationary_latent_variance, - recover_time_dependent_predictor_impulse, recover_time_dependent_predictor_impulse_carry, - recover_trait_plus_state_latent_variance, recover_within_residual_event_time_log_rate, + recover_stationary_initial_observed_mean, recover_stationary_initial_observed_variance, + recover_stationary_latent_variance, recover_time_dependent_predictor_impulse, + recover_time_dependent_predictor_impulse_carry, 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 +59,7 @@ use psychometric_core::{ refuse_evolved_observed_mean_as_initial_time_independent_observed_mean, refuse_evolved_observed_mean_as_stationary_initial_observed_mean, refuse_evolved_observed_mean_as_time_independent_observed_mean, + refuse_evolved_observed_variance_as_stationary_initial_observed_variance, refuse_extra_process_contribution_as_observed_mean, refuse_extra_process_latent_mean_as_observed_mean, refuse_extra_process_observed_mean_as_after_extra_process_observed_mean, @@ -74,6 +76,7 @@ use psychometric_core::{ refuse_initial_latent_mean_as_evolved_mean, refuse_initial_observed_mean_as_evolved_observed_mean, refuse_initial_observed_mean_as_stationary_initial_observed_mean, + refuse_initial_observed_variance_as_stationary_initial_observed_variance, refuse_initial_time_dependent_carry_as_impulse_carry, refuse_initial_time_dependent_carry_as_initial_effect, refuse_initial_time_dependent_coefficient_as_initial_effect, @@ -105,9 +108,12 @@ use psychometric_core::{ refuse_stationary_initial_latent_variance_as_asymptotic_time_independent_variance, refuse_stationary_initial_latent_variance_as_discrete_variance, refuse_stationary_initial_latent_variance_as_initial_latent_variance, + refuse_stationary_initial_latent_variance_as_observed_variance, refuse_stationary_initial_latent_variance_as_stationary_within_subject, refuse_stationary_initial_latent_variance_as_trait_variance, refuse_stationary_initial_observed_mean_as_manifest_means, + refuse_stationary_initial_observed_variance_as_measurement_error, + refuse_stationary_within_subject_observed_variance_as_stationary_initial_observed_variance, refuse_time_dependent_impulse_as_continuous_intercept, refuse_time_dependent_impulse_as_time_independent_effect, refuse_time_dependent_impulse_as_time_varying_discrete_effect, @@ -2512,3 +2518,105 @@ fn stationary_initial_latent_variance_is_not_t0_state_trait_tipred_or_discrete() ) ); } + +#[test] +fn stationary_initial_observed_variance_is_not_manifest_latent_evolved_or_free() { + let trait_variance = 1.0_f64; + let diffusion = 0.4_f64; + let log_rate = -0.134_488_942_f64; + let loading = 2.0_f64; + let measurement_error = 0.5_f64; + let recovered = recover_stationary_initial_observed_variance( + loading, + trait_variance, + diffusion, + -0.225, + 1.0, + log_rate, + measurement_error, + 0.1, + LagClock::EventTime, + ) + .expect("eq5-stationary-T0VAR"); + let latent = recover_stationary_initial_latent_variance( + trait_variance, + diffusion, + -0.225, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("stationary T0VAR"); + let state = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + let state_only_observed = + recover_manifest_observed_variance(loading, state, measurement_error).expect("λ²(−q/2a)+θ"); + let free_initial_observed = + recover_manifest_observed_variance(loading, 2.0, measurement_error).expect("λ²p_0+θ"); + let discrete = + recover_discrete_latent_variance(latent, diffusion, log_rate, 1.0, LagClock::EventTime) + .expect("Var(η_t)"); + let evolved = recover_manifest_observed_variance(loading, discrete, measurement_error) + .expect("λ²Var(η_t)+θ"); + assert!( + (recovered - measurement_error).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 of §4.3 T0VAR): Var(y_0) is not MANIFESTVAR" + ); + assert!( + (recovered - latent).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 of §4.3 T0VAR): Var(y_0) is not constrained T0VAR" + ); + assert!( + (recovered - state_only_observed).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 of §4.3 T0VAR): Var(y_0) is not λ² asymDIFFUSION + θ" + ); + assert!( + (recovered - free_initial_observed).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 of §4.3 T0VAR): Var(y_0) is not λ² free T0VAR + θ" + ); + assert!( + (recovered - evolved).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 of §4.3 T0VAR): Var(y_0) is not λ² Var(η_t) + θ" + ); + assert_eq!( + refuse_stationary_initial_latent_variance_as_observed_variance(latent, recovered), + Err( + psychometric_core::PsychometricError::StationaryInitialLatentVarianceIsNotObservedVariance + ) + ); + assert_eq!( + refuse_stationary_initial_observed_variance_as_measurement_error( + recovered, + measurement_error + ), + Err( + psychometric_core::PsychometricError::StationaryInitialObservedVarianceIsNotMeasurementError + ) + ); + assert_eq!( + refuse_evolved_observed_variance_as_stationary_initial_observed_variance( + evolved, recovered + ), + Err( + psychometric_core::PsychometricError::EvolvedObservedVarianceIsNotStationaryInitialObservedVariance + ) + ); + assert_eq!( + refuse_stationary_within_subject_observed_variance_as_stationary_initial_observed_variance( + state_only_observed, + recovered + ), + Err( + psychometric_core::PsychometricError::StationaryWithinSubjectObservedVarianceIsNotStationaryInitialObservedVariance + ) + ); + assert_eq!( + refuse_initial_observed_variance_as_stationary_initial_observed_variance( + free_initial_observed, + recovered + ), + Err( + psychometric_core::PsychometricError::InitialObservedVarianceIsNotStationaryInitialObservedVariance + ) + ); +} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index e100a36c..6b94477a 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -28,7 +28,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | report template/section/copied/style/modality method effects | ADR 0004/0012; PRD/TRD | simulation truth factors implemented; estimator-side method model remains future | partial | | candidate K statistical/Pareto gates + blinded LLM review | ADR 0012; research | future `model_selection` | accepted-target | | compositional topic correlation / stable clustering | ADR 0005/0012; research | future `network_analysis` | accepted-target | -| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; §7.2 level-change `CINT` is `κ = −a m x` (`a < 0`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`; Eq. 3 of that setting is `(1 − e^{a Δt}) m x`); §7.2 extra-process contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, not the dissipating Dirac; `ε ≥ 0` fails closed; Eq. 5 of that contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; extra `LAMBDA` is 0; `τ + λ μ_t` is not that observed mean; after-t0 extra-process `TDPREDEFFECT` uses `t − u` with `t0 < u < t` while `μ_t` uses `Δt`; that after-t0 observed mean is not the first-occasion extra-process observed mean; §7.2 `asymTIPREDEFFECT` is `-B z / a` for `a < 0` and is not `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; §7.2 `addedTIPREDVAR` is `(B / a)² v` and is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`; Table 2 `asymCINT` is `-κ / a` for `a < 0` and is not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`; p. 16 stationary `T0MEANS` is `-κ / a + −B z / a` and is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean; Eq. 5 of that constrained mean is `τ + λ(−κ / a + −B z / a)`; `τ + λ μ_0` is not that observed mean; `MANIFESTMEANS` is not `E(y_0)`; the constrained latent mean is not `E(y_0)`; stationary `T0VAR` is `trait + −q / (2 a) + (B / a)² v` (not free `T0VAR`, not `asymDIFFUSION` alone, not `TRAITVAR` alone, not `addedTIPREDVAR` alone, and not the finite-interval discrete latent variance), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z); full ESEM/DSEM remaining | partial | +| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; §7.2 level-change `CINT` is `κ = −a m x` (`a < 0`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`; Eq. 3 of that setting is `(1 − e^{a Δt}) m x`); §7.2 extra-process contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, not the dissipating Dirac; `ε ≥ 0` fails closed; Eq. 5 of that contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; extra `LAMBDA` is 0; `τ + λ μ_t` is not that observed mean; after-t0 extra-process `TDPREDEFFECT` uses `t − u` with `t0 < u < t` while `μ_t` uses `Δt`; that after-t0 observed mean is not the first-occasion extra-process observed mean; §7.2 `asymTIPREDEFFECT` is `-B z / a` for `a < 0` and is not `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; §7.2 `addedTIPREDVAR` is `(B / a)² v` and is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`; Table 2 `asymCINT` is `-κ / a` for `a < 0` and is not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`; p. 16 stationary `T0MEANS` is `-κ / a + −B z / a` and is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean; Eq. 5 of that constrained mean is `τ + λ(−κ / a + −B z / a)`; `τ + λ μ_0` is not that observed mean; `MANIFESTMEANS` is not `E(y_0)`; the constrained latent mean is not `E(y_0)`; stationary `T0VAR` is `trait + −q / (2 a) + (B / a)² v` (not free `T0VAR`, not `asymDIFFUSION` alone, not `TRAITVAR` alone, not `addedTIPREDVAR` alone, and not the finite-interval discrete latent variance. Eq. 5 of that constrained variance is `λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ` (JSS PDF re-opened 2026-08-22T03:20Z; form the stationary latent variance first, then `λ² p + θ + ψ`; `λ² p_0` is not that observed variance; `λ²(−q / (2 a)) + θ` is not that observed variance when `TRAITVAR` or `addedTIPREDVAR` is nonzero; `MANIFESTVAR` is not `Var(y_0)`; the constrained latent variance is not `Var(y_0)`)), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z); full ESEM/DSEM remaining | partial | | CPU bounded multithreading + GPU/VRAM streaming/parity | ADR 0001/0006 | future `compute_backend` | accepted-target | | TDT detection/tracking vs CHRONOS schema/prediction/temporal consistency | ADR 0016; PRD/research | future `event_intelligence` | accepted-target | | evidence-bounded LLM interpretation | ADR 0010/0012; PRD | `tepp_api` router plus future `interpretation_gateway` | partial | diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index 7cb1d38e..77729972 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -1,7 +1,7 @@ # ADR 0005 — Posterior-aware ESEM/DSEM and structural interpretation **Decision status:** Accepted -**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), exact scalar contemporaneous `TDPREDEFFECT` impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; the §7.2 level-change form is not that impulse), exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of that Eq. 3 composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar time-independent `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle Eq. 14; `B` is not that discrete increment), exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of that Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of that carried latent mean; `τ + λ μ_t` is not that observed mean), exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; `T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`), exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; `T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand TD composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean), exact scalar §7.2 level-change `CINT` `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; `a < 0` so `−κ / a = m x`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2), exact scalar Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` (not `m x`, not `κ`, and not `TIPREDEFFECT`), exact scalar §7.2 extra near-zero-drift latent process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z; identification `TDPREDEFFECT` on the extra process is 1; extra `DRIFT` printed as `−0.000001`; precisely 0 causes computational problems; `ε = a` is `a_{ηξ} x Δt e^{a Δt}`; not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`; `ε ≥ 0` fails closed), exact scalar observed mean of that extra-process contribution `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z; the extra process has `LAMBDA` 0 and is not an observed indicator; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`), exact scalar after-t0 extra-process contribution `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` and Eq. 5 observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:32Z; `T0TDPREDEFFECT` uses `Δt` for both the evolution and the extra drive; `TDPREDEFFECT` after `t0` uses `t − u`; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive), exact scalar §7.2 `asymTIPREDEFFECT` `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z; expected total change in process means given a time-independent predictor; `a < 0`; not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; `a ≥ 0` fails closed), exact scalar §7.2 `addedTIPREDVAR` `(B / a)² v` (Driver et al., 2017, §7.2, pp. 20–21; stable between-subject variance accounted for by a time-independent predictor; not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`), exact scalar Table 2 `asymCINT` `-κ / a` (Driver et al., 2017, Table 2, p. 12; Eq. 3 as `Δt → ∞`; `a < 0`; not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`; `a ≥ 0` fails closed), exact scalar p. 16 stationary `T0MEANS` `-κ / a + −B z / a` (Driver et al., 2017, p. 16; constrained first-occasion mean using `T0MEANSbase` / `T0MEANSfree`; not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean), exact scalar Eq. 5 of that constrained mean `τ + λ(−κ / a + −B z / a)` (Driver et al., 2017, §4.3, pp. 9–10; Eq. 5, p. 5; JSS PDF re-opened 2026-08-21T20:07Z; form the stationary latent mean first, then `τ + λ` of that mean; `τ + λ μ_0` is not that observed mean; `τ + λ(−κ / a)` is not that observed mean when `B z ≠ 0`; `τ + λ μ_t` is not that observed mean; `MANIFESTMEANS` is not `E(y_0)`; the constrained latent mean is not `E(y_0)`), exact scalar §4.3 / p. 16 stationary `T0VAR` `trait + −q / (2 a) + (B / a)² v` (Driver et al., 2017, §4.3, pp. 9–10; p. 16; JSS PDF re-opened 2026-08-22T03:07Z; not free `T0VAR`, not `asymDIFFUSION` alone, not `TRAITVAR` alone, not `addedTIPREDVAR` alone, and not the finite-interval discrete latent variance), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target +**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), exact scalar contemporaneous `TDPREDEFFECT` impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; the §7.2 level-change form is not that impulse), exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of that Eq. 3 composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar time-independent `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle Eq. 14; `B` is not that discrete increment), exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of that Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of that carried latent mean; `τ + λ μ_t` is not that observed mean), exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; `T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`), exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; `T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand TD composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean), exact scalar §7.2 level-change `CINT` `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; `a < 0` so `−κ / a = m x`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2), exact scalar Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` (not `m x`, not `κ`, and not `TIPREDEFFECT`), exact scalar §7.2 extra near-zero-drift latent process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z; identification `TDPREDEFFECT` on the extra process is 1; extra `DRIFT` printed as `−0.000001`; precisely 0 causes computational problems; `ε = a` is `a_{ηξ} x Δt e^{a Δt}`; not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`; `ε ≥ 0` fails closed), exact scalar observed mean of that extra-process contribution `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z; the extra process has `LAMBDA` 0 and is not an observed indicator; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`), exact scalar after-t0 extra-process contribution `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` and Eq. 5 observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:32Z; `T0TDPREDEFFECT` uses `Δt` for both the evolution and the extra drive; `TDPREDEFFECT` after `t0` uses `t − u`; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive), exact scalar §7.2 `asymTIPREDEFFECT` `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z; expected total change in process means given a time-independent predictor; `a < 0`; not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; `a ≥ 0` fails closed), exact scalar §7.2 `addedTIPREDVAR` `(B / a)² v` (Driver et al., 2017, §7.2, pp. 20–21; stable between-subject variance accounted for by a time-independent predictor; not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`), exact scalar Table 2 `asymCINT` `-κ / a` (Driver et al., 2017, Table 2, p. 12; Eq. 3 as `Δt → ∞`; `a < 0`; not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`; `a ≥ 0` fails closed), exact scalar p. 16 stationary `T0MEANS` `-κ / a + −B z / a` (Driver et al., 2017, p. 16; constrained first-occasion mean using `T0MEANSbase` / `T0MEANSfree`; not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean), exact scalar Eq. 5 of that constrained mean `τ + λ(−κ / a + −B z / a)` (Driver et al., 2017, §4.3, pp. 9–10; Eq. 5, p. 5; JSS PDF re-opened 2026-08-21T20:07Z; form the stationary latent mean first, then `τ + λ` of that mean; `τ + λ μ_0` is not that observed mean; `τ + λ(−κ / a)` is not that observed mean when `B z ≠ 0`; `τ + λ μ_t` is not that observed mean; `MANIFESTMEANS` is not `E(y_0)`; the constrained latent mean is not `E(y_0)`), exact scalar §4.3 / p. 16 stationary `T0VAR` `trait + −q / (2 a) + (B / a)² v` (Driver et al., 2017, §4.3, pp. 9–10; p. 16; JSS PDF re-opened 2026-08-22T03:07Z; not free `T0VAR`, not `asymDIFFUSION` alone, not `TRAITVAR` alone, not `addedTIPREDVAR` alone, and not the finite-interval discrete latent variance. Eq. 5 of that constrained variance is `λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ` (JSS PDF re-opened 2026-08-22T03:20Z; form the stationary latent variance first, then `λ² p + θ + ψ`; `λ² p_0` is not that observed variance; `λ²(−q / (2 a)) + θ` is not that observed variance when `TRAITVAR` or `addedTIPREDVAR` is nonzero; `MANIFESTVAR` is not `Var(y_0)`; the constrained latent variance is not `Var(y_0)`)), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target **Date:** 2026-08-05 **Supersedes:** None. ADR 0012 governs upstream topic measurement/network coordinates; this ADR governs higher-order psychometric structure and longitudinal interpretation. @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), recovers the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z; `TDPREDEFFECT` is `M`, not `CINT`; `M x` is not `A^{-1}[e^{A Δt} − I] B z`; `M x` is not Voelkle et al., 2012, Eq. 14; the level-change form is not that impulse), recovers the exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition; JSS PDF re-opened 2026-08-20T09:01Z; the evolved map `τ + λ μ_t` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-impulse latent mean is not `E(y_t)`), recovers the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z; `TIPREDEFFECT` is `B`, not `κ`; form `B z` first, then the discrete intercept map; a zero drift is `B z Δt`; `B` is not the discrete increment; `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle Eq. 14), recovers the exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; JSS PDF re-opened 2026-08-20T12:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-increment latent mean is not `E(y_t)`), recovers the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z; form `m x` first, then `e^{a(t−u)} m x`; a zero drift is `m x`; underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept; `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; an impulse at `u = t` is the contemporaneous map; an impulse at `u ≤ t0` is already in `η(t0)`), recovers the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; JSS PDF re-opened 2026-08-20T05:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the carried latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and its Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF opened 2026-08-20T15:14Z; form `t0_b z` first, then `e^{a Δt} t0_b z`; a zero drift is `t0_b z`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; `e^{A Δt} t0_b z` is not `t0_b z`; `T0TIPREDEFFECT` is the coefficient, not the shift), recovers the exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition; JSS PDF re-opened 2026-08-20T15:28Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and its Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; form `t0_m x0` first, then `e^{a Δt} t0_m x0`; a zero drift is `t0_m x0`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `T0TDPREDEFFECT` is the coefficient, not the shift; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), recovers the exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; the first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar §7.2 level-change `CINT` setting `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z; form `m x` first, then multiply by `−a`; `a < 0` so `−κ / a = m x`; `a ≥ 0` cannot hold a new process mean; `−a m x` is not the dissipating Dirac, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`; the extra near-zero-drift latent process also named in §7.2 is a different specification), recovers the exact scalar Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` (JSS PDF re-opened 2026-08-20T19:50Z; form the level-change `CINT` first, then the discrete intercept map; underflow of `e^{a Δt}` to `+0` keeps `m x`; `(1 − e^{a Δt}) m x` is not `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`), recovers the exact scalar §7.2 extra near-zero-drift latent process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z; form `a_{ηξ} x` first; `ε = a` is `a_{ηξ} x Δt e^{a Δt}`; a zero coupling or zero predictor is exactly zero; `ε ≥ 0` cannot hold a lasting extra state; that contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`), recovers the exact scalar observed mean of that extra-process contribution `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z; the extra process has `LAMBDA` 0 and is not an observed indicator; form the evolved-plus-contribution latent mean first, then `τ + λ` of that mean; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`), recovers the exact scalar after-t0 extra-process contribution `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` and its Eq. 5 observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:32Z; `T0TDPREDEFFECT` uses `Δt` for both the evolution and the extra drive; `TDPREDEFFECT` after `t0` uses `t − u` while `μ_t` still uses `Δt`; an impulse at `u = t0` or `u = t` is not interior; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive), recovers the exact scalar §7.2 `asymTIPREDEFFECT` `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z; form `B z` first, then divide by `-a`; `a < 0`; a zero coefficient or zero predictor is exactly zero; `a ≥ 0` cannot hold a finite process-mean change; `-B z / a` is not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`), recovers the exact scalar §7.2 `addedTIPREDVAR` `(B / a)² v` (Driver et al., 2017, §7.2, pp. 20–21; form the unit asymptotic effect first, then square, then multiply by `v`; `v ≥ 0`; a zero coefficient or zero predictor variance is exactly zero; `(B / a)² v` is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`), recovers the exact scalar Table 2 `asymCINT` `-κ / a` (Driver et al., 2017, Table 2, p. 12; Eq. 3 as `Δt → ∞`; JSS PDF opened 2026-08-21T16:13Z; form `κ` first, then divide by `-a`; `a < 0`; a zero intercept is exactly zero; `-κ / a` is not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`), recovers the exact scalar p. 16 stationary `T0MEANS` `-κ / a + −B z / a` (Driver et al., 2017, p. 16; form the intercept contribution first, then include the TI extra effect, then add; not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean), recovers the exact scalar Eq. 5 of that constrained mean `τ + λ(−κ / a + −B z / a)` (Driver et al., 2017, §4.3, pp. 9–10; Eq. 5, p. 5; JSS PDF re-opened 2026-08-21T20:07Z; form the stationary latent mean first, then `τ + λ` of that mean; `τ + λ μ_0` is not that observed mean; `τ + λ(−κ / a)` is not that observed mean when `B z ≠ 0`; `τ + λ μ_t` is not that observed mean; `MANIFESTMEANS` is not `E(y_0)`; the constrained latent mean is not `E(y_0)`), recovers the exact scalar §4.3 / p. 16 stationary `T0VAR` `trait + −q / (2 a) + (B / a)² v` (Driver et al., 2017, §4.3, pp. 9–10; p. 16; JSS PDF re-opened 2026-08-22T03:07Z; form the within-subject contribution first, then include the trait, then include the TI extra variance, then add; not free `T0VAR`, not `asymDIFFUSION` alone, not `TRAITVAR` alone, not `addedTIPREDVAR` alone, and not the finite-interval discrete latent variance), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; when `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), recovers the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z; `TDPREDEFFECT` is `M`, not `CINT`; `M x` is not `A^{-1}[e^{A Δt} − I] B z`; `M x` is not Voelkle et al., 2012, Eq. 14; the level-change form is not that impulse), recovers the exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition; JSS PDF re-opened 2026-08-20T09:01Z; the evolved map `τ + λ μ_t` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-impulse latent mean is not `E(y_t)`), recovers the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z; `TIPREDEFFECT` is `B`, not `κ`; form `B z` first, then the discrete intercept map; a zero drift is `B z Δt`; `B` is not the discrete increment; `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle Eq. 14), recovers the exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; JSS PDF re-opened 2026-08-20T12:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-increment latent mean is not `E(y_t)`), recovers the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z; form `m x` first, then `e^{a(t−u)} m x`; a zero drift is `m x`; underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept; `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; an impulse at `u = t` is the contemporaneous map; an impulse at `u ≤ t0` is already in `η(t0)`), recovers the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; JSS PDF re-opened 2026-08-20T05:12Z; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`; `MANIFESTMEANS` is not `E(y_t)`; the carried latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and its Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF opened 2026-08-20T15:14Z; form `t0_b z` first, then `e^{a Δt} t0_b z`; a zero drift is `t0_b z`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; `e^{A Δt} t0_b z` is not `t0_b z`; `T0TIPREDEFFECT` is the coefficient, not the shift), recovers the exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition; JSS PDF re-opened 2026-08-20T15:28Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and its Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; form `t0_m x0` first, then `e^{a Δt} t0_m x0`; a zero drift is `t0_m x0`; underflow of `e^{a Δt}` to `+0` is a vanishing carry and is kept; `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `T0TDPREDEFFECT` is the coefficient, not the shift; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), recovers the exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z; the evolved map `τ + λ μ_t` is not that observed mean; the process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`; the first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar §7.2 level-change `CINT` setting `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z; form `m x` first, then multiply by `−a`; `a < 0` so `−κ / a = m x`; `a ≥ 0` cannot hold a new process mean; `−a m x` is not the dissipating Dirac, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`; the extra near-zero-drift latent process also named in §7.2 is a different specification), recovers the exact scalar Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` (JSS PDF re-opened 2026-08-20T19:50Z; form the level-change `CINT` first, then the discrete intercept map; underflow of `e^{a Δt}` to `+0` keeps `m x`; `(1 − e^{a Δt}) m x` is not `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`), recovers the exact scalar §7.2 extra near-zero-drift latent process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z; form `a_{ηξ} x` first; `ε = a` is `a_{ηξ} x Δt e^{a Δt}`; a zero coupling or zero predictor is exactly zero; `ε ≥ 0` cannot hold a lasting extra state; that contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`), recovers the exact scalar observed mean of that extra-process contribution `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z; the extra process has `LAMBDA` 0 and is not an observed indicator; form the evolved-plus-contribution latent mean first, then `τ + λ` of that mean; the evolved map `τ + λ μ_t` is not that observed mean; the contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`), recovers the exact scalar after-t0 extra-process contribution `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` and its Eq. 5 observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:32Z; `T0TDPREDEFFECT` uses `Δt` for both the evolution and the extra drive; `TDPREDEFFECT` after `t0` uses `t − u` while `μ_t` still uses `Δt`; an impulse at `u = t0` or `u = t` is not interior; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive), recovers the exact scalar §7.2 `asymTIPREDEFFECT` `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z; form `B z` first, then divide by `-a`; `a < 0`; a zero coefficient or zero predictor is exactly zero; `a ≥ 0` cannot hold a finite process-mean change; `-B z / a` is not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`), recovers the exact scalar §7.2 `addedTIPREDVAR` `(B / a)² v` (Driver et al., 2017, §7.2, pp. 20–21; form the unit asymptotic effect first, then square, then multiply by `v`; `v ≥ 0`; a zero coefficient or zero predictor variance is exactly zero; `(B / a)² v` is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`), recovers the exact scalar Table 2 `asymCINT` `-κ / a` (Driver et al., 2017, Table 2, p. 12; Eq. 3 as `Δt → ∞`; JSS PDF opened 2026-08-21T16:13Z; form `κ` first, then divide by `-a`; `a < 0`; a zero intercept is exactly zero; `-κ / a` is not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`), recovers the exact scalar p. 16 stationary `T0MEANS` `-κ / a + −B z / a` (Driver et al., 2017, p. 16; form the intercept contribution first, then include the TI extra effect, then add; not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean), recovers the exact scalar Eq. 5 of that constrained mean `τ + λ(−κ / a + −B z / a)` (Driver et al., 2017, §4.3, pp. 9–10; Eq. 5, p. 5; JSS PDF re-opened 2026-08-21T20:07Z; form the stationary latent mean first, then `τ + λ` of that mean; `τ + λ μ_0` is not that observed mean; `τ + λ(−κ / a)` is not that observed mean when `B z ≠ 0`; `τ + λ μ_t` is not that observed mean; `MANIFESTMEANS` is not `E(y_0)`; the constrained latent mean is not `E(y_0)`), recovers the exact scalar §4.3 / p. 16 stationary `T0VAR` `trait + −q / (2 a) + (B / a)² v` (Driver et al., 2017, §4.3, pp. 9–10; p. 16; JSS PDF re-opened 2026-08-22T03:07Z; form the within-subject contribution first, then include the trait, then include the TI extra variance, then add; not free `T0VAR`, not `asymDIFFUSION` alone, not `TRAITVAR` alone, not `addedTIPREDVAR` alone, and not the finite-interval discrete latent variance), recovers the exact scalar Eq. 5 of that constrained variance `λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ` (Driver et al., 2017, §4.3, pp. 9–10; Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-22T03:20Z; form the stationary latent variance first, then `λ² p + θ + ψ`; `λ² p_0` is not that observed variance; `λ²(−q / (2 a)) + θ` is not that observed variance when `TRAITVAR` or `addedTIPREDVAR` is nonzero; `MANIFESTVAR` is not `Var(y_0)`; the constrained latent variance is not `Var(y_0)`), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. 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/adr/README.md b/docs/adr/README.md index 958e94a3..b23150c7 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -10,7 +10,7 @@ Read [`ADR_POLICY.md`](ADR_POLICY.md) first. **Decision status and implementatio | [0002](0002-six-clock-temporal-semantics.md) | Six-clock temporal semantics and fail-closed historical leakage prevention | Accepted | active-PR | Unmerged PR #8 is the canonical Task 3 replacement implementing typed clocks/intervals against the current protected-main lineage; conflicted PR #5 is superseded lineage. Later graph/split enforcement remains target work. | | [0003](0003-relational-event-multiple-membership.md) | Relational event ontology and time-varying cross-classified multiple membership | Accepted | partial | Weighted time-varying membership network/roles are active-PR (PR #12); full multilevel estimators, graph ontology, and persistence remain accepted-target. ADR 0016 owns event-intelligence tasks. | | [0004](0004-shared-multilingual-latent-space.md) | One shared multilingual latent space with explicit invariance status | Accepted | accepted-target | ADR 0012 owns the full topic-estimator/backend/global-topic contract. | -| [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | partial | Input gates, posterior-draw point estimates, Rubin `T` on OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects, exact scalar discrete process noise, lagged latent covariance and unconditional latent variance, stationary within-subject variance (`asymDIFFUSION`), trait-plus-state variance (`TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; Eq. 5 of the contemporaneous impulse is `τ + λ(μ_t + m x)`; `τ + λ μ_t` is not that observed mean; Eq. 5 of the time-independent predictor is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; Eq. 5 of the within-interval impulse carry is `τ + λ(μ_t + e^{a(t−u)} m x)`; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; Table 2 `asymCINT` is `-κ / a` for `a < 0` and is not `κ`, not the finite-interval increment, not `T0MEANS`, and not `-B z / a`; p. 16 stationary `T0MEANS` is `-κ / a + −B z / a` and is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean; Eq. 5 of that constrained mean is `τ + λ(−κ / a + −B z / a)` (`τ + λ μ_0` is not that observed mean; `MANIFESTMEANS` is not `E(y_0)`); stationary `T0VAR` is `trait + −q / (2 a) + (B / a)² v` (not free `T0VAR`, not `asymDIFFUSION` alone, not `TRAITVAR` alone, not `addedTIPREDVAR` alone); §7.2 `addedTIPREDVAR` is `(B / a)² v` and is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`), irregular already-centered residual lag, and strong-gated latent means are on the stacked psychometric PR; full ESEM/DSEM estimator remains accepted-target. | +| [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | partial | Input gates, posterior-draw point estimates, Rubin `T` on OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects, exact scalar discrete process noise, lagged latent covariance and unconditional latent variance, stationary within-subject variance (`asymDIFFUSION`), trait-plus-state variance (`TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; Eq. 5 of the contemporaneous impulse is `τ + λ(μ_t + m x)`; `τ + λ μ_t` is not that observed mean; Eq. 5 of the time-independent predictor is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; Eq. 5 of the within-interval impulse carry is `τ + λ(μ_t + e^{a(t−u)} m x)`; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; Table 2 `asymCINT` is `-κ / a` for `a < 0` and is not `κ`, not the finite-interval increment, not `T0MEANS`, and not `-B z / a`; p. 16 stationary `T0MEANS` is `-κ / a + −B z / a` and is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean; Eq. 5 of that constrained mean is `τ + λ(−κ / a + −B z / a)` (`τ + λ μ_0` is not that observed mean; `MANIFESTMEANS` is not `E(y_0)`); stationary `T0VAR` is `trait + −q / (2 a) + (B / a)² v` (not free `T0VAR`, not `asymDIFFUSION` alone, not `TRAITVAR` alone, not `addedTIPREDVAR` alone; Eq. 5 is `λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ` (`λ² p_0` is not `Var(y_0)`; `λ²(−q / (2 a)) + θ` is not `Var(y_0)` when trait or TI is nonzero; `MANIFESTVAR` is not `Var(y_0)`; the constrained latent variance is not `Var(y_0)`)); §7.2 `addedTIPREDVAR` is `(B / a)² v` and is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`), irregular already-centered residual lag, and strong-gated latent means are on the stacked psychometric PR; full ESEM/DSEM estimator remains accepted-target. | | [0006](0006-vram-gpu-nvidia-orchestration.md) | VRAM-adaptive GPU compute and model-credential boundary | Accepted | accepted-target | LLM orchestration policy superseded by ADR 0010; autonomous development authority governed by ADR 0015. | | [0007](0007-rust-workspace-quality-gates.md) | Explicit Rust workspace, pinned toolchains, and exact quality gates | Accepted | implemented-main | ADR 0014 governs scientific/product claim promotion beyond repository-quality tooling. | | [0008](0008-immutable-evidence-identities-digests-and-spans.md) | Immutable evidence identities, `SHA-256` digests, exact spans, and strict wire reconstruction | Accepted | implemented-main | ADR 0013 governs future persistence/reproducibility/split authority. | diff --git a/docs/research/multilevel-event-time-recovery.md b/docs/research/multilevel-event-time-recovery.md index a46e48e1..9f0ef89c 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -43,15 +43,16 @@ This slice stays inside `psychometric_core`. It does not add a second invariance 37. recover the exact scalar p. 16 stationary `T0MEANS` `-κ / a + −B z / a` (Driver et al., 2017, p. 16; Table 2, p. 12; Eq. 3, p. 5; JSS PDF opened 2026-08-21T16:13Z; constrain `T0MEANS` to model-implied values using `T0MEANSbase` / `T0MEANSfree`; form the intercept contribution first, then include the TI extra effect, then add; a zero intercept and a zero TI contribution is exactly zero) and refuse treating that composition as free `T0MEANS`, as `asymCINT` alone, as `asymTIPREDEFFECT` alone, or as the finite-interval discrete latent mean; 38. recover the exact scalar Eq. 5 of §4.3 stationary `T0MEANS` `τ + λ(−κ / a + −B z / a)` (Driver et al., 2017, §4.3, pp. 9–10; Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-21T20:07Z; form the stationary latent mean first, then `τ + λ` of that mean; a zero loading is exactly `τ`; evolving from that stationary start with `CINT` and `TIPREDEFFECT` stays at the stationary mean) and refuse treating `τ + λ μ_0`, `τ + λ(−κ / a)` when `B z ≠ 0`, `τ + λ μ_t`, `MANIFESTMEANS`, or the constrained latent mean as `E(y_0)`; 39. recover the exact scalar §4.3 / p. 16 stationary `T0VAR` `trait + −q / (2 a) + (B / a)² v` (Driver et al., 2017, §4.3, pp. 9–10; p. 16; Table 2, p. 12; §7.2, pp. 20–21; Eq. 4, p. 5; JSS PDF re-opened 2026-08-22T03:07Z; form the within-subject contribution first, then include the trait, then include the TI extra variance, then add; a zero trait, a zero diffusion, and a zero TI contribution is exactly zero; a zero diffusion and a zero TI contribution is exactly the trait) and refuse treating that composition as free `T0VAR`, as `asymDIFFUSION` alone, as `TRAITVAR` alone, as `addedTIPREDVAR` alone, or as the finite-interval discrete latent variance; -40. refuse pooling discrete lags from unequal event intervals as one coefficient; -41. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); -42. refuse the difference quotient as a continuous-time rate; -43. apply the same event-time map to CWC residuals (still not DSEM); -44. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). +40. recover the exact scalar Eq. 5 of §4.3 stationary `T0VAR` `λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ` (Driver et al., 2017, §4.3, pp. 9–10; Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-22T03:20Z; form the stationary latent variance first, then `λ² p + θ + ψ`; a zero loading is exactly `θ + ψ`; a zero trait, a zero diffusion, and a zero TI contribution is exactly `θ + ψ`) and refuse treating `λ² p_0 + θ`, `λ²(−q / (2 a)) + θ` when `TRAITVAR` or `addedTIPREDVAR` is nonzero, `λ² Var(η_t) + θ`, `MANIFESTVAR`, or the constrained latent variance as `Var(y_0)`; +41. refuse pooling discrete lags from unequal event intervals as one coefficient; +42. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); +43. refuse the difference quotient as a continuous-time rate; +44. apply the same event-time map to CWC residuals (still not DSEM); +45. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). ## Claim boundary -This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. The contemporaneous `TDPREDEFFECT` impulse is not the continuous intercept, not the time-independent discrete effect, and not Voelkle et al. (2012, Eq. 14). The time-independent `TIPREDEFFECT` increment is not the continuous intercept, not the contemporaneous impulse, not Voelkle et al. (2012, Eq. 14), and not the coefficient `B`. The within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). The evolved observed mean `τ + λ μ_t` is not the contemporaneous-impulse observed mean `τ + λ(μ_t + m x)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not the impulse-carry observed mean `τ + λ(μ_t + e^{a(t−u)} m x)` when `u ≠ t`. The evolved observed mean `τ + λ μ_t` is not the impulse-carry observed mean. The carried latent mean is not `E(y_t)`. The evolved-plus-impulse latent mean is not `E(y_t)`. The evolved observed mean `τ + λ μ_t` is not the time-independent-predictor observed mean `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not that time-independent-predictor observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that time-independent-predictor observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TIPREDEFFECT` shift `t0_b z` is not the Eq. 3 process increment `A^{-1}[e^{A Δt} − I] B z`, not `κ`, and not `M x`. The Eq. 3 first-summand carry `e^{A Δt} t0_b z` is not `t0_b z` and is not that process increment. `T0TIPREDEFFECT` is the coefficient, not the first-occasion shift. The evolved observed mean `τ + λ μ_t` is not the first-occasion TI-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_b z)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion observed mean when `u ≠ t0`. The evolved-plus-T0TIPRED latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TDPREDEFFECT` shift `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `κ`. The Eq. 3 first-summand carry `e^{A Δt} t0_m x0` is not `t0_m x0` and is not that within-interval impulse carry. `T0TDPREDEFFECT` is the coefficient, not the first-occasion shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The evolved observed mean `τ + λ μ_t` is not the first-occasion TD-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_m x0)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion TD observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion TD observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion TD observed mean when `u ≠ t0`. The first-occasion TI composition `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that first-occasion TD observed mean. Same numbers as `T0TIPREDEFFECT` yield the same product; Table 3 names a different matrix. The evolved-plus-T0TDPRED latent mean is not `E(y_t)`. The §7.2 level-change `CINT` `κ = −a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. Lasting level change via that `CINT` setting requires `a < 0`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not that `CINT` setting. The Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` is not the dissipating Dirac `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`. Underflow of `e^{a Δt}` to `+0` keeps the equilibrium offset `m x`. The §7.2 extra-process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. Lasting level change via that extra process requires `ε < 0`. Precisely `ε = 0` causes computational problems in the printed specification. The extra-process observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` is not `τ + λ μ_t`, not `τ + λ(μ_t + m x)`, not the contribution, and not the evolved-plus-contribution latent mean. The extra process has `LAMBDA` 0 and is not an observed indicator. `T0TDPREDEFFECT` on the extra process uses `Δt = t − t0` for both the original-process evolution and the extra drive. `TDPREDEFFECT` after `t0` uses `t − u` with `t0 < u < t` for the extra drive while `μ_t` still uses `Δt`. The after-t0 extra-process observed mean is not the first-occasion extra-process observed mean when `u ≠ t0`. The impulse-carry `e^{a(t−u)} m x` is a Dirac on the original process and is not that `DRIFT` drive. The §7.2 `asymTIPREDEFFECT` `-B z / a` is the expected total change in process means given a time-independent predictor. It is not the coefficient `B`, not the finite-interval increment `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. Lasting asymptotic change via that map requires `a < 0`. The §7.2 `addedTIPREDVAR` `(B / a)² v` is the stable between-subject variance accounted for by that predictor. It is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`. Table 2 `asymCINT` `-κ / a` is the intercept contribution to the stationary process mean. It is not `κ`, not the finite-interval increment `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`. Lasting asymptotic intercept change via that map requires `a < 0`. Page 16 notes that a `T0MEANS` stationarity constraint includes time-independent predictors; that composition is not this intercept-only map. The p. 16 constrained first-occasion mean `-κ / a + −B z / a` is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean. Equation 5 of that constrained mean `τ + λ(−κ / a + −B z / a)` is not `τ + λ μ_0`, not `τ + λ(−κ / a)` when `B z ≠ 0`, not `τ + λ μ_t`, not `MANIFESTMEANS`, and not the constrained latent mean. The p. 16 constrained first-occasion variance `trait + −q / (2 a) + (B / a)² v` is not free `T0VAR`, not `asymDIFFUSION` alone, not `TRAITVAR` alone, not `addedTIPREDVAR` alone, and not the finite-interval discrete latent variance. +This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. The contemporaneous `TDPREDEFFECT` impulse is not the continuous intercept, not the time-independent discrete effect, and not Voelkle et al. (2012, Eq. 14). The time-independent `TIPREDEFFECT` increment is not the continuous intercept, not the contemporaneous impulse, not Voelkle et al. (2012, Eq. 14), and not the coefficient `B`. The within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). The evolved observed mean `τ + λ μ_t` is not the contemporaneous-impulse observed mean `τ + λ(μ_t + m x)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not the impulse-carry observed mean `τ + λ(μ_t + e^{a(t−u)} m x)` when `u ≠ t`. The evolved observed mean `τ + λ μ_t` is not the impulse-carry observed mean. The carried latent mean is not `E(y_t)`. The evolved-plus-impulse latent mean is not `E(y_t)`. The evolved observed mean `τ + λ μ_t` is not the time-independent-predictor observed mean `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not that time-independent-predictor observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that time-independent-predictor observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TIPREDEFFECT` shift `t0_b z` is not the Eq. 3 process increment `A^{-1}[e^{A Δt} − I] B z`, not `κ`, and not `M x`. The Eq. 3 first-summand carry `e^{A Δt} t0_b z` is not `t0_b z` and is not that process increment. `T0TIPREDEFFECT` is the coefficient, not the first-occasion shift. The evolved observed mean `τ + λ μ_t` is not the first-occasion TI-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_b z)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion observed mean when `u ≠ t0`. The evolved-plus-T0TIPRED latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TDPREDEFFECT` shift `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `κ`. The Eq. 3 first-summand carry `e^{A Δt} t0_m x0` is not `t0_m x0` and is not that within-interval impulse carry. `T0TDPREDEFFECT` is the coefficient, not the first-occasion shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The evolved observed mean `τ + λ μ_t` is not the first-occasion TD-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_m x0)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion TD observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion TD observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion TD observed mean when `u ≠ t0`. The first-occasion TI composition `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that first-occasion TD observed mean. Same numbers as `T0TIPREDEFFECT` yield the same product; Table 3 names a different matrix. The evolved-plus-T0TDPRED latent mean is not `E(y_t)`. The §7.2 level-change `CINT` `κ = −a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. Lasting level change via that `CINT` setting requires `a < 0`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not that `CINT` setting. The Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` is not the dissipating Dirac `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`. Underflow of `e^{a Δt}` to `+0` keeps the equilibrium offset `m x`. The §7.2 extra-process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. Lasting level change via that extra process requires `ε < 0`. Precisely `ε = 0` causes computational problems in the printed specification. The extra-process observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` is not `τ + λ μ_t`, not `τ + λ(μ_t + m x)`, not the contribution, and not the evolved-plus-contribution latent mean. The extra process has `LAMBDA` 0 and is not an observed indicator. `T0TDPREDEFFECT` on the extra process uses `Δt = t − t0` for both the original-process evolution and the extra drive. `TDPREDEFFECT` after `t0` uses `t − u` with `t0 < u < t` for the extra drive while `μ_t` still uses `Δt`. The after-t0 extra-process observed mean is not the first-occasion extra-process observed mean when `u ≠ t0`. The impulse-carry `e^{a(t−u)} m x` is a Dirac on the original process and is not that `DRIFT` drive. The §7.2 `asymTIPREDEFFECT` `-B z / a` is the expected total change in process means given a time-independent predictor. It is not the coefficient `B`, not the finite-interval increment `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. Lasting asymptotic change via that map requires `a < 0`. The §7.2 `addedTIPREDVAR` `(B / a)² v` is the stable between-subject variance accounted for by that predictor. It is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`. Table 2 `asymCINT` `-κ / a` is the intercept contribution to the stationary process mean. It is not `κ`, not the finite-interval increment `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`. Lasting asymptotic intercept change via that map requires `a < 0`. Page 16 notes that a `T0MEANS` stationarity constraint includes time-independent predictors; that composition is not this intercept-only map. The p. 16 constrained first-occasion mean `-κ / a + −B z / a` is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean. Equation 5 of that constrained mean `τ + λ(−κ / a + −B z / a)` is not `τ + λ μ_0`, not `τ + λ(−κ / a)` when `B z ≠ 0`, not `τ + λ μ_t`, not `MANIFESTMEANS`, and not the constrained latent mean. The p. 16 constrained first-occasion variance `trait + −q / (2 a) + (B / a)² v` is not free `T0VAR`, not `asymDIFFUSION` alone, not `TRAITVAR` alone, not `addedTIPREDVAR` alone, and not the finite-interval discrete latent variance. Equation 5 of that constrained variance `λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ` is not `λ² p_0 + θ`, not `λ²(−q / (2 a)) + θ` when `TRAITVAR` or `addedTIPREDVAR` is nonzero, not `λ² Var(η_t) + θ` when the first occasion is constrained, not `MANIFESTVAR`, and not the constrained latent variance. ## Authoritative sources @@ -69,7 +70,7 @@ Kish, L. (1965). *Survey sampling*. John Wiley & Sons. Oud, J. H. L., & Jansen, R. A. R. G. (2000). Continuous time state space modeling of panel data by means of SEM. *Psychometrika, 65*(2), 199–215. https://doi.org/10.1007/BF02294374 (cited by Voelkle et al., 2012, Eq. 14 discussion; PDF not opened). -The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:07Z (https://www.zora.uzh.ch/handle/20.500.14742/72792; bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`). Page 16 writes discrete auto-effects as \(A^{*}(\Delta t)=\exp(A\Delta t)\) (Eq. 7). Introducing Intercepts (manuscript p. 20, Eq. 12) adds a continuous-time intercept \(b\) and writes the expected-value solution whose discrete increment is \(A^{-1}(\exp(A\Delta t)-I)b\); the scalar constant-predictor effect is \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). The next paragraph (manuscript p. 21, Eq. 14) writes the discrete effect of a **time-varying** predictor, when the sampling interval equals the constancy interval, as \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product does not depend on the predictor auto-effect. The manuscript calls Eq. 14 a first-order approximation that deteriorates as \(\Delta t\) grows, and defers unmatched sampling/constancy intervals to Oud and Jansen (2000), which is unread. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4; JSS PDF opened 2026-08-21T13:08Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\), the discrete intercept \(b_{\Delta t}=A^{-1}[A_{\Delta t}-I]b\), and the discrete process-noise covariance \(Q_{\Delta t}=\int_{0}^{\Delta t}\operatorname{expm}(A(\Delta t-\tau))LGG^{\top}L^{\top}\operatorname{expm}(A(\Delta t-\tau))^{\top}\,d\tau\). Equation 4 (p. 5) writes that the integral exhibits covariance \(Q_{\Delta t}=\operatorname{irow}(A^{\#^{-1}}[e^{A^{\#}\Delta t}-I]\operatorname{row}(Q))\) with \(A^{\#}=A\otimes I+I\otimes A\). The homogeneous-process consequence (`ξ`, `z` given) is \(Q_{\Delta t}=\operatorname{cov}(\eta_{ti}\mid\eta_{t-1,i})\) and \(\operatorname{cov}(\eta_{ti},\eta_{t-1,i})=A_{\Delta t}\operatorname{cov}(\eta_{t-1,i})\). The law of total variance on that pair is \(\operatorname{Var}(\eta_{ti})=A_{\Delta t}\operatorname{Var}(\eta_{t-1,i})A_{\Delta t}^{\top}+Q_{\Delta t}\). As \(\Delta t\to\infty\) with stable \(a<0\), Eq. 4 becomes \(-q/(2a)\). The JSS summary names that limit `asymDIFFUSION` and takes it as the total within-subject variance (p. 16). Section 4.3 (pp. 9–10) constrains a stationary `T0VAR` to that model-predicted variance and distinguishes it from a predetermined first occasion. The same section (p. 9) adds a stable trait process with `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is that time-invariant between-subject variance. Table 3 (p. 13) names `T0TIPREDEFFECT` the effect of time-independent predictors on latents at `T0` and names `TIPREDEFFECT` `B` separately. The JSS article has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). This slice takes scalar \(L=1\) (every latent subject to system noise); it does not implement the 0/1 selector matrix and is not a Kalman filter. Discrete auto-effects are strictly positive for finite real drift and interval; the inverse \(a=\ln\varphi/\Delta t\) therefore requires \(\varphi>0\). Binary64 `exp` of a large negative argument is `+0` and is refused as a discrete lag; the same underflow is a vanishing lagged covariance and is kept. Equations 3–4 of Voelkle et al. (2012) are the discouraged difference-quotient approximation. Meredith (1993) remains unread (Unpaywall/OpenAlex 2026-08-22T03:07Z: request empty; Springer `content/pdf` is HTML 200, not a PDF; archive.org title search empty). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex 2026-08-22T03:07Z: request empty; Springer `content/pdf` is HTML 200; ETS landing page is HTML; ETS RR-88-45 PDF 404; Wiley PDF 403). ERIC ED334221 is Singer and Willett (1991), *From whether to when*, not the 1991 journal article. ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title is not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex 2026-08-18T21:07Z: closed; Radboud landing and bitstream 403). +The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:07Z (https://www.zora.uzh.ch/handle/20.500.14742/72792; bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`). Page 16 writes discrete auto-effects as \(A^{*}(\Delta t)=\exp(A\Delta t)\) (Eq. 7). Introducing Intercepts (manuscript p. 20, Eq. 12) adds a continuous-time intercept \(b\) and writes the expected-value solution whose discrete increment is \(A^{-1}(\exp(A\Delta t)-I)b\); the scalar constant-predictor effect is \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). The next paragraph (manuscript p. 21, Eq. 14) writes the discrete effect of a **time-varying** predictor, when the sampling interval equals the constancy interval, as \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product does not depend on the predictor auto-effect. The manuscript calls Eq. 14 a first-order approximation that deteriorates as \(\Delta t\) grows, and defers unmatched sampling/constancy intervals to Oud and Jansen (2000), which is unread. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4; JSS PDF opened 2026-08-21T13:08Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\), the discrete intercept \(b_{\Delta t}=A^{-1}[A_{\Delta t}-I]b\), and the discrete process-noise covariance \(Q_{\Delta t}=\int_{0}^{\Delta t}\operatorname{expm}(A(\Delta t-\tau))LGG^{\top}L^{\top}\operatorname{expm}(A(\Delta t-\tau))^{\top}\,d\tau\). Equation 4 (p. 5) writes that the integral exhibits covariance \(Q_{\Delta t}=\operatorname{irow}(A^{\#^{-1}}[e^{A^{\#}\Delta t}-I]\operatorname{row}(Q))\) with \(A^{\#}=A\otimes I+I\otimes A\). The homogeneous-process consequence (`ξ`, `z` given) is \(Q_{\Delta t}=\operatorname{cov}(\eta_{ti}\mid\eta_{t-1,i})\) and \(\operatorname{cov}(\eta_{ti},\eta_{t-1,i})=A_{\Delta t}\operatorname{cov}(\eta_{t-1,i})\). The law of total variance on that pair is \(\operatorname{Var}(\eta_{ti})=A_{\Delta t}\operatorname{Var}(\eta_{t-1,i})A_{\Delta t}^{\top}+Q_{\Delta t}\). As \(\Delta t\to\infty\) with stable \(a<0\), Eq. 4 becomes \(-q/(2a)\). The JSS summary names that limit `asymDIFFUSION` and takes it as the total within-subject variance (p. 16). Section 4.3 (pp. 9–10) constrains a stationary `T0VAR` to that model-predicted variance and distinguishes it from a predetermined first occasion. The same section (p. 9) adds a stable trait process with `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is that time-invariant between-subject variance. Table 3 (p. 13) names `T0TIPREDEFFECT` the effect of time-independent predictors on latents at `T0` and names `TIPREDEFFECT` `B` separately. The JSS article has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). This slice takes scalar \(L=1\) (every latent subject to system noise); it does not implement the 0/1 selector matrix and is not a Kalman filter. Discrete auto-effects are strictly positive for finite real drift and interval; the inverse \(a=\ln\varphi/\Delta t\) therefore requires \(\varphi>0\). Binary64 `exp` of a large negative argument is `+0` and is refused as a discrete lag; the same underflow is a vanishing lagged covariance and is kept. Equations 3–4 of Voelkle et al. (2012) are the discouraged difference-quotient approximation. Meredith (1993) remains unread (Unpaywall/OpenAlex 2026-08-22T03:20Z: `is_oa: false`; Springer `content/pdf` is HTML 200, not a PDF; archive.org title search empty). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex 2026-08-22T03:20Z: `is_oa: false`; Springer `content/pdf` is HTML 200; ETS landing page is HTML; ETS RR-88-45 PDF 404; Wiley PDF 403). ERIC ED334221 is Singer and Willett (1991), *From whether to when*, not the 1991 journal article. ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title is not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex 2026-08-18T21:07Z: closed; Radboud landing and bitstream 403). ## Formula notes diff --git a/docs/validation/temporal-event-foundation.md b/docs/validation/temporal-event-foundation.md index 973028e5..7ca0e7ce 100644 --- a/docs/validation/temporal-event-foundation.md +++ b/docs/validation/temporal-event-foundation.md @@ -23,7 +23,7 @@ This report tracks exact-head scientific and engineering evidence required befor | Truth corpora / manifests | `tepp_simulation` | implemented-main | — | deterministic generator tests | Task 10 / PR #18 | | Recovery metrics | `validation_core` | implemented-main | — | RMSE/bias/coverage/MC gates | Task 11 / PR #19 | | Versioned API/export contracts | `tepp_api` | implemented-main | naruon HTTP interchange | unknown-field/version/limit + naruon HTTPS interchange tests | Task 12 / PR #21; live HTTP service remaining | -| Psychometric structural input gates | `psychometric_core` | partial | stacked psychometric PR | construct-class refusal + ALR/ILR boundary + true-loading RMSE + posterior-draw point-estimate mean + Rubin `T` + CWC within/between + CWC contextual effect + event-time log-rate + constant- and time-varying-predictor discrete effects + exact scalar discrete process noise + lagged latent covariance and unconditional latent variance + stationary within-subject variance + trait-plus-state variance + observed-indicator variance + discrete latent mean (`T0MEANS`/`CINT`) + evolved observed mean (`τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`) + contemporaneous `TDPREDEFFECT` impulse (`m x`; not `CINT`, not `TIPREDEFFECT`, not Voelkle Eq. 14) + Eq. 5 of that contemporaneous impulse (`τ + λ(μ_t + m x)`; `τ + λ μ_t` is not that observed mean) + time-independent `TIPREDEFFECT` increment (`A^{-1}[e^{A Δt} − I] B z`; not `CINT`, not `M x`, not Voelkle Eq. 14, not the coefficient `B`) + Eq. 5 of that increment (`τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`; `τ + λ μ_t` is not that observed mean) + within-interval `TDPREDEFFECT` carry (`e^{A(t−u)} M x` for `t0 < u < t`; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, not Voelkle Eq. 14) + Eq. 5 of that carry (`τ + λ(μ_t + e^{a(t−u)} m x)`; `τ + λ μ_t` is not that observed mean) + §7.2 level-change `CINT` (`κ = −a m x`; Eq. 3 increment `(1 − e^{a Δt}) m x`) + §7.2 extra-process contribution (`a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; not `κ`, not the increment, not the Dirac; `ε ≥ 0` fails closed) + Eq. 5 of that extra-process contribution (`τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; extra `LAMBDA` is 0; `τ + λ μ_t` is not that observed mean) + after-t0 extra-process `TDPREDEFFECT` (`a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t`; Eq. 5 `τ + λ(μ_t + contribution(t−u))`; not the first-occasion extra-process observed mean; not the impulse-carry Dirac) + §7.2 `asymTIPREDEFFECT` (`-B z / a` for `a < 0`; not `B`, not the finite-interval increment, not `CINT`, not `M x`) + §7.2 `addedTIPREDVAR` (`(B / a)² v`; not `TRAITVAR`, not `asymDIFFUSION`, not `-B z / a`) + Table 2 `asymCINT` (`-κ / a` for `a < 0`; not `κ`, not the finite-interval increment, not `T0MEANS`, not `-B z / a`) + p. 16 stationary `T0MEANS` (`-κ / a + −B z / a`; not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, not the finite-interval discrete mean) + Eq. 5 of that constrained mean (`τ + λ(−κ / a + −B z / a)`; `τ + λ μ_0` is not that observed mean; `τ + λ(−κ / a)` is not that observed mean when `B z ≠ 0`; `τ + λ μ_t` is not that observed mean; `MANIFESTMEANS` is not `E(y_0)`; the constrained latent mean is not `E(y_0)`) + stationary `T0VAR` (`trait + −q / (2 a) + (B / a)² v`; not free `T0VAR`, not `asymDIFFUSION` alone, not `TRAITVAR` alone, not `addedTIPREDVAR` alone) + irregular already-centered residual lag + strong-gated latent means (n=2 residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016); full ESEM/DSEM remaining | ADR 0005; `docs/research/posterior-esem-input-gates.md`; `docs/research/multilevel-event-time-recovery.md`; `docs/research/rubin-total-variance.md`; `docs/research/strong-invariance-latent-means.md` | +| Psychometric structural input gates | `psychometric_core` | partial | stacked psychometric PR | construct-class refusal + ALR/ILR boundary + true-loading RMSE + posterior-draw point-estimate mean + Rubin `T` + CWC within/between + CWC contextual effect + event-time log-rate + constant- and time-varying-predictor discrete effects + exact scalar discrete process noise + lagged latent covariance and unconditional latent variance + stationary within-subject variance + trait-plus-state variance + observed-indicator variance + discrete latent mean (`T0MEANS`/`CINT`) + evolved observed mean (`τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`) + contemporaneous `TDPREDEFFECT` impulse (`m x`; not `CINT`, not `TIPREDEFFECT`, not Voelkle Eq. 14) + Eq. 5 of that contemporaneous impulse (`τ + λ(μ_t + m x)`; `τ + λ μ_t` is not that observed mean) + time-independent `TIPREDEFFECT` increment (`A^{-1}[e^{A Δt} − I] B z`; not `CINT`, not `M x`, not Voelkle Eq. 14, not the coefficient `B`) + Eq. 5 of that increment (`τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`; `τ + λ μ_t` is not that observed mean) + within-interval `TDPREDEFFECT` carry (`e^{A(t−u)} M x` for `t0 < u < t`; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, not Voelkle Eq. 14) + Eq. 5 of that carry (`τ + λ(μ_t + e^{a(t−u)} m x)`; `τ + λ μ_t` is not that observed mean) + §7.2 level-change `CINT` (`κ = −a m x`; Eq. 3 increment `(1 − e^{a Δt}) m x`) + §7.2 extra-process contribution (`a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; not `κ`, not the increment, not the Dirac; `ε ≥ 0` fails closed) + Eq. 5 of that extra-process contribution (`τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; extra `LAMBDA` is 0; `τ + λ μ_t` is not that observed mean) + after-t0 extra-process `TDPREDEFFECT` (`a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t`; Eq. 5 `τ + λ(μ_t + contribution(t−u))`; not the first-occasion extra-process observed mean; not the impulse-carry Dirac) + §7.2 `asymTIPREDEFFECT` (`-B z / a` for `a < 0`; not `B`, not the finite-interval increment, not `CINT`, not `M x`) + §7.2 `addedTIPREDVAR` (`(B / a)² v`; not `TRAITVAR`, not `asymDIFFUSION`, not `-B z / a`) + Table 2 `asymCINT` (`-κ / a` for `a < 0`; not `κ`, not the finite-interval increment, not `T0MEANS`, not `-B z / a`) + p. 16 stationary `T0MEANS` (`-κ / a + −B z / a`; not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, not the finite-interval discrete mean) + Eq. 5 of that constrained mean (`τ + λ(−κ / a + −B z / a)`; `τ + λ μ_0` is not that observed mean; `τ + λ(−κ / a)` is not that observed mean when `B z ≠ 0`; `τ + λ μ_t` is not that observed mean; `MANIFESTMEANS` is not `E(y_0)`; the constrained latent mean is not `E(y_0)`) + stationary `T0VAR` (`trait + −q / (2 a) + (B / a)² v`; not free `T0VAR`, not `asymDIFFUSION` alone, not `TRAITVAR` alone, not `addedTIPREDVAR` alone; Eq. 5 is `λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ` (`λ² p_0` is not `Var(y_0)`; `λ²(−q / (2 a)) + θ` is not `Var(y_0)` when trait or TI is nonzero; `MANIFESTVAR` is not `Var(y_0)`; the constrained latent variance is not `Var(y_0)`)) + Eq. 5 of that constrained variance (`λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ`; `MANIFESTVAR` is not `Var(y_0)`) + irregular already-centered residual lag + strong-gated latent means (n=2 residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016); full ESEM/DSEM remaining | ADR 0005; `docs/research/posterior-esem-input-gates.md`; `docs/research/multilevel-event-time-recovery.md`; `docs/research/rubin-total-variance.md`; `docs/research/strong-invariance-latent-means.md` | | Purpose-bound provider payloads | `tepp_api` | implemented-main | provider-payload minimization | expired/not-yet-valid/inverted/cross-tenant/impossible-calendar grant, mapping refusal, audited elevated re-id replay | ADR 0009; `docs/research/provider-payload-minimization.md` | | Adaptive orchestration router | `tepp_api` | accepted-target | active PR | mode selection, document-control denial, ablation, credential-free bind | ADR 0010; `docs/research/adaptive-orchestration-router.md` | | CWL modular connectors | `docs/connectors/*` | implemented-main | — | contract docs + examples | PR #22; live HTTP ports remaining |