Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
48c6c34
feat: add Rust-owned longitudinal OLS and AR state layer
cursoragent Aug 17, 2026
70a83b6
test: recover OLS parameters with valid respondent identifiers
cursoragent Aug 17, 2026
eafb302
docs: index ADR-0016/0017 so the longitudinal PR stays merge-safe
cursoragent Aug 17, 2026
d463f27
Revert "docs: index ADR-0016/0017 so the longitudinal PR stays merge-…
cursoragent Aug 17, 2026
95b552f
test: cover unused worker shards and real observation scalars
cursoragent Aug 17, 2026
2ee0c87
ci: retrigger org CodeQL after GitHub API 503
cursoragent Aug 17, 2026
17bebdc
Add joint MAP hierarchical CT-AR Rasch slice (stacked on #976) (#982)
seonghobae Aug 18, 2026
7c99591
test(longitudinal): align single-occasion fit error
seonghobae Aug 18, 2026
e87502c
test(multilevel): require longitudinal state binding registration
seonghobae Aug 18, 2026
ccc5a54
docs(architecture): keep proposed longitudinal ADRs non-shipped
seonghobae Aug 18, 2026
ab1f26a
fix(multilevel): bound simulator arrays before copying
seonghobae Aug 18, 2026
21ead6b
docs(multilevel): distinguish OLS from discrete AR spacing
seonghobae Aug 18, 2026
34842e7
test(longitudinal): expose hostile execution-control callbacks
seonghobae Aug 18, 2026
230c5d3
fix(longitudinal): harden execution-control boundary
seonghobae Aug 18, 2026
b7718d1
chore(longitudinal): leave aggregate changelog to release serialization
seonghobae Aug 18, 2026
ed9868c
fix(longitudinal): align bounded gradients and Hessian evidence
seonghobae Aug 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions crates/fast-mlsirm-py/src/multilevel_bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
//! counts, and its input validation are owned by
//! `mlsirm_core::multilevel::weighted_contextual_effect`.

use mlsirm_core::longitudinal::fit_longitudinal_state as core_fit_longitudinal_state;
use mlsirm_core::multilevel::weighted_contextual_effect as core_weighted_contextual_effect;
use numpy::{PyArray1, PyReadonlyArray1, ToPyArray};
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use pyo3::types::PyDict;
use pyo3::types::PyModule;
use pyo3::wrap_pyfunction;

Expand Down Expand Up @@ -87,9 +89,74 @@ fn py_weighted_contextual_effect<'py>(
Ok(result.to_pyarray(py))
}

/// Fit the Rust-owned repeated-measurement state layer.
///
/// Parameters
/// ----------
/// row_offsets : numpy.ndarray[uint64]
/// CSR-style respondent pointer, length ``n_respondents + 1``.
/// sequence_indices : numpy.ndarray[uint64]
/// Discrete occasion indices aligned with ``values``.
/// time_offsets_milliseconds : numpy.ndarray[int64]
/// Exact millisecond offsets aligned with ``values``.
/// values : numpy.ndarray[float64]
/// Observed states; ``NaN`` marks a missing occasion.
/// state_kind : str
/// Compatibility wire label for the requested state predictor.
/// ar_coefficient : float or None
/// Caller-supplied discrete AR coefficient, or ``None`` for OLS trends.
/// worker_count : int
/// Number of deterministic worker threads (``>= 1``).
///
/// Returns
/// -------
/// dict
/// Predicted states, respondent intercepts/slopes, RMSE, and counts.
#[pyfunction(name = "fit_longitudinal_state")]
fn py_fit_longitudinal_state<'py>(
py: Python<'py>,
row_offsets: PyReadonlyArray1<'_, u64>,
sequence_indices: PyReadonlyArray1<'_, u64>,
time_offsets_milliseconds: PyReadonlyArray1<'_, i64>,
values: PyReadonlyArray1<'_, f64>,
state_kind: &str,
ar_coefficient: Option<f64>,
worker_count: usize,
) -> PyResult<Bound<'py, PyDict>> {
let row_offsets = checked_usize_values(row_offsets.as_slice()?, "row_offsets")?;
let sequence_indices = checked_usize_values(sequence_indices.as_slice()?, "sequence_indices")?;
let time_offsets = time_offsets_milliseconds.as_slice()?.to_vec();
let values = values.as_slice()?.to_vec();
let state_kind = state_kind.to_owned();
let fit = py
.detach(move || {
core_fit_longitudinal_state(
&row_offsets,
&sequence_indices,
&time_offsets,
&values,
&state_kind,
ar_coefficient,
worker_count,
)
})
.map_err(PyValueError::new_err)?;
let result = PyDict::new(py);
result.set_item("state", fit.state.to_pyarray(py))?;
result.set_item("intercepts", fit.intercepts.to_pyarray(py))?;
result.set_item("slopes", fit.slopes.to_pyarray(py))?;
result.set_item("ar_coefficient", fit.ar_coefficient)?;
result.set_item("rmse", fit.rmse)?;
result.set_item("observed_count", fit.observed_count)?;
result.set_item("transition_count", fit.transition_count)?;
result.set_item("engine", "rust_cpu_multithreaded")?;
Ok(result)
}

#[pymodule]
#[pyo3(name = "_multilevel_core")]
fn fast_mlsirm_multilevel_core(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(py_weighted_contextual_effect, m)?)?;
m.add_function(wrap_pyfunction!(py_fit_longitudinal_state, m)?)?;
Ok(())
}
1 change: 1 addition & 0 deletions crates/mlsirm-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub mod gtheory;
pub mod ksirt;
pub mod linking;
pub mod lltm;
pub mod longitudinal;
pub mod marginal;
pub mod mhrm;
pub mod mixed;
Expand Down
Loading
Loading