lme-python 0.1.7__tar.gz → 0.1.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {lme_python-0.1.7 → lme_python-0.1.8}/.github/workflows/ci.yml +26 -4
- {lme_python-0.1.7 → lme_python-0.1.8}/.github/workflows/python-release.yml +3 -2
- {lme_python-0.1.7 → lme_python-0.1.8}/.github/workflows/repo-metadata.yml +11 -1
- lme_python-0.1.8/AGENTS.md +173 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/CHANGELOG.md +15 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/CONTRIBUTING.md +59 -5
- {lme_python-0.1.7 → lme_python-0.1.8}/Cargo.lock +5 -5
- {lme_python-0.1.7 → lme_python-0.1.8}/Cargo.toml +9 -2
- {lme_python-0.1.7 → lme_python-0.1.8}/GUIDE.md +57 -3
- {lme_python-0.1.7 → lme_python-0.1.8}/PKG-INFO +1 -1
- {lme_python-0.1.7 → lme_python-0.1.8}/README.md +6 -5
- lme_python-0.1.8/RELEASING.md +255 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/REPO_COMPLETION_BY_AREA.md +14 -14
- lme_python-0.1.8/Taskfile.yml +108 -0
- lme_python-0.1.8/lefthook.yml +40 -0
- lme_python-0.1.8/mise.toml +16 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/pyproject.toml +13 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/python/Cargo.lock +20 -52
- {lme_python-0.1.7 → lme_python-0.1.8}/python/Cargo.toml +2 -2
- {lme_python-0.1.7 → lme_python-0.1.8}/python/PYTHON_GUIDE.md +39 -12
- {lme_python-0.1.7 → lme_python-0.1.8}/python/examples/glmer_cbpp.py +5 -4
- {lme_python-0.1.7 → lme_python-0.1.8}/python/examples/glmer_grouseticks.py +15 -14
- {lme_python-0.1.7 → lme_python-0.1.8}/python/examples/lm_basics.py +2 -2
- {lme_python-0.1.7 → lme_python-0.1.8}/python/examples/lmer_sleepstudy.py +9 -7
- {lme_python-0.1.7 → lme_python-0.1.8}/python/examples/model_comparison.py +22 -20
- {lme_python-0.1.7 → lme_python-0.1.8}/python/examples/plotting_demo/compare_plots.py +22 -14
- {lme_python-0.1.7 → lme_python-0.1.8}/python/examples/plotting_demo/find_rscript.py +1 -2
- {lme_python-0.1.7 → lme_python-0.1.8}/python/examples/plotting_demo/numeric_overlay.py +1 -3
- {lme_python-0.1.7 → lme_python-0.1.8}/python/examples/plotting_demo/plot_demo.py +9 -3
- {lme_python-0.1.7 → lme_python-0.1.8}/python/examples/plotting_demo/run_all.py +10 -3
- {lme_python-0.1.7 → lme_python-0.1.8}/python/examples/verification_project/parity.py +17 -21
- lme_python-0.1.8/python/lme_python.pyi +150 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/python/requirements-ci.txt +5 -5
- {lme_python-0.1.7 → lme_python-0.1.8}/python/src/lib.rs +612 -77
- lme_python-0.1.8/python/tests/test_api_parity.py +138 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/python/tests/test_basic.py +97 -66
- lme_python-0.1.8/scripts/ci/README.md +27 -0
- lme_python-0.1.8/scripts/ci/lme_ci.py +404 -0
- lme_python-0.1.8/scripts/ci/run.sh +30 -0
- lme_python-0.1.8/scripts/local_ci.ps1 +4 -0
- lme_python-0.1.8/scripts/local_ci.sh +4 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/scripts/sync_github_repo_metadata.py +35 -4
- {lme_python-0.1.7 → lme_python-0.1.8}/src/anova.rs +6 -169
- lme_python-0.1.8/src/contrast.rs +266 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/src/ddf.rs +11 -5
- {lme_python-0.1.7 → lme_python-0.1.8}/src/glmm_math.rs +27 -7
- {lme_python-0.1.7 → lme_python-0.1.8}/src/kenward_roger.rs +11 -1
- lme_python-0.1.8/src/kr_modcomp.rs +327 -0
- lme_python-0.1.8/src/kr_vcov_adj.rs +296 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/src/lib.rs +31 -2
- {lme_python-0.1.7 → lme_python-0.1.8}/src/model_matrix.rs +1 -2
- lme_python-0.1.8/src/nlmm/fit.rs +528 -0
- lme_python-0.1.8/src/nlmm/formula.rs +124 -0
- lme_python-0.1.8/src/nlmm/mod.rs +37 -0
- lme_python-0.1.8/src/nlmm/sslogis.rs +37 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/src/optimizer.rs +7 -2
- {lme_python-0.1.7 → lme_python-0.1.8}/tests/data/golden_parity_manifest.json +28 -2
- lme_python-0.1.8/tests/data/orange.csv +36 -0
- lme_python-0.1.8/tests/data/orange_nlmer.json +15 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/tests/data/pastes_cask_reml.json +5 -1
- lme_python-0.1.8/tests/test_contrast.rs +83 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_failure_modes.rs +14 -1
- lme_python-0.1.8/tests/test_glmm_weighted.rs +64 -0
- lme_python-0.1.8/tests/test_kr_modcomp_pastes.rs +51 -0
- lme_python-0.1.8/tests/test_nlmm_orange.rs +54 -0
- lme_python-0.1.7/RELEASING.md +0 -208
- lme_python-0.1.7/scripts/local_ci.ps1 +0 -85
- lme_python-0.1.7/scripts/local_ci.sh +0 -86
- {lme_python-0.1.7 → lme_python-0.1.8}/.github/workflows/audit.yml +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/.github/workflows/benchmarks.yml +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/.github/workflows/crate-publish-dry-run.yml +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/.github/workflows/fuzz-smoke.yml +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/.gitignore +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/BENCHMARKS.md +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/LICENSE +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/benches/bench_load_production.rs +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/benches/bench_math.rs +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/COMPARISONS.md +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/categorical_anova.R +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/categorical_anova.jl +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/categorical_anova.py +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/categorical_anova.rs +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/comparison_chart.png +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/glmm_cbpp.R +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/glmm_cbpp.jl +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/glmm_cbpp.py +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/glmm_cbpp.rs +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/glmm_gaussian.R +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/glmm_gaussian.jl +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/glmm_gaussian.py +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/glmm_gaussian.rs +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/glmm_grouseticks.R +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/glmm_grouseticks.jl +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/glmm_grouseticks.py +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/glmm_grouseticks.rs +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/lmer_weighted.R +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/lmer_weighted.jl +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/lmer_weighted.py +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/lmer_weighted.rs +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/lmm_dyestuff.R +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/lmm_dyestuff.jl +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/lmm_dyestuff.py +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/lmm_dyestuff.rs +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/lmm_pastes.R +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/lmm_pastes.jl +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/lmm_pastes.py +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/lmm_pastes.rs +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/lmm_penicillin.R +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/lmm_penicillin.jl +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/lmm_penicillin.py +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/lmm_penicillin.rs +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/sleepstudy.R +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/sleepstudy.jl +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/sleepstudy.py +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/sleepstudy.rs +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/sleepstudy_ml.R +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/sleepstudy_ml.jl +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/sleepstudy_ml.py +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/sleepstudy_ml.rs +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/docs/benchmarks/app.js +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/docs/benchmarks/index.html +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/docs/benchmarks/styles.css +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/docs/index.html +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/python/examples/plotting_demo/.gitignore +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/python/examples/plotting_demo/README.md +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/python/examples/plotting_demo/figure_specs.py +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/python/examples/plotting_demo/paths.py +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/python/examples/plotting_demo/plot_r.R +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/python/examples/verification_project/README.md +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/python/examples/verification_project/conftest.py +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/python/examples/verification_project/paths.py +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/python/examples/verification_project/run.py +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/python/examples/verification_project/test_parity.py +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/scripts/ast_explorations/test_fiasto.rs +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/scripts/ast_explorations/test_fiasto_offset.rs +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/scripts/build_benchmark_site.py +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/scripts/dump_dyestuff.R +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/scripts/dump_pastes.R +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/scripts/make_penicillin_csv.py +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/scripts/plot_comparisons.py +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/scripts/run_cross_language_benchmarks.py +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/src/anova_contrasts.rs +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/src/family.rs +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/src/formula.rs +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/src/math.rs +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/src/robust.rs +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/src/satterthwaite.rs +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/tests/categorical_anova_test.rs +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/tests/data/cbpp_binary.csv +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/tests/data/dyestuff.csv +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/tests/data/glmm_binomial.json +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/tests/data/glmm_poisson.json +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/tests/data/grouseticks.csv +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/tests/data/intercept_only.json +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/tests/data/mock_crossed.json +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/tests/data/mock_ml.json +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/tests/data/pastes.csv +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/tests/data/penicillin.csv +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/tests/data/penicillin.json +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/tests/data/random_slopes.json +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/tests/data/sleepstudy.csv +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/tests/generate_mock_data.py +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/tests/generate_test_data.R +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_anova.rs +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_anova_types.rs +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_conditional_real.rs +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_confint_simulate.rs +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_coverage_edge_cases.rs +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_coverage_gaps.rs +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_crossed_mock.rs +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_e2e_lmer.rs +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_edge_cases.rs +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_errors.rs +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_formula.rs +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_formula_stress.rs +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_gaps.rs +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_glmm.rs +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_golden_parity.rs +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_intercept_only.rs +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_kenward_roger.rs +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_ml_optimization.rs +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_no_intercept.rs +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_numerical_parity.rs +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_predict.rs +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_production_load.rs +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_random_slopes.rs +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_robust.rs +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_sandwich_math.R +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_satterthwaite.rs +0 -0
- {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_statistical_identities.rs +0 -0
|
@@ -23,6 +23,28 @@ env:
|
|
|
23
23
|
CARGO_TERM_COLOR: always
|
|
24
24
|
|
|
25
25
|
jobs:
|
|
26
|
+
rust-lint:
|
|
27
|
+
name: Rust fmt & clippy
|
|
28
|
+
runs-on: ubuntu-latest
|
|
29
|
+
steps:
|
|
30
|
+
- uses: actions/checkout@v6
|
|
31
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
32
|
+
- name: Install OpenBLAS
|
|
33
|
+
run: sudo apt-get update && sudo apt-get install -y libopenblas-dev
|
|
34
|
+
- name: Cache cargo registry & build
|
|
35
|
+
uses: actions/cache@v5
|
|
36
|
+
with:
|
|
37
|
+
path: |
|
|
38
|
+
~/.cargo/registry
|
|
39
|
+
~/.cargo/git
|
|
40
|
+
target
|
|
41
|
+
key: ubuntu-lint-${{ hashFiles('**/Cargo.lock') }}
|
|
42
|
+
restore-keys: ubuntu-lint-
|
|
43
|
+
- name: rustfmt --check
|
|
44
|
+
run: python3 scripts/ci/lme_ci.py fmt-check
|
|
45
|
+
- name: clippy
|
|
46
|
+
run: python3 scripts/ci/lme_ci.py clippy
|
|
47
|
+
|
|
26
48
|
test:
|
|
27
49
|
name: Test (${{ matrix.os }})
|
|
28
50
|
runs-on: ${{ matrix.os }}
|
|
@@ -37,10 +59,6 @@ jobs:
|
|
|
37
59
|
- name: Install Rust toolchain
|
|
38
60
|
uses: dtolnay/rust-toolchain@stable
|
|
39
61
|
|
|
40
|
-
- name: Install OpenBLAS (macOS)
|
|
41
|
-
if: matrix.os == 'macos-latest'
|
|
42
|
-
run: brew install openblas
|
|
43
|
-
|
|
44
62
|
- name: Install OpenBLAS (Ubuntu)
|
|
45
63
|
if: matrix.os == 'ubuntu-latest'
|
|
46
64
|
run: sudo apt-get update && sudo apt-get install -y libopenblas-dev
|
|
@@ -55,6 +73,10 @@ jobs:
|
|
|
55
73
|
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
|
56
74
|
restore-keys: ${{ runner.os }}-cargo-
|
|
57
75
|
|
|
76
|
+
- name: Set OpenBLAS target (macOS)
|
|
77
|
+
if: matrix.os == 'macos-latest'
|
|
78
|
+
run: echo "OPENBLAS_TARGET=ARMV8" >> "$GITHUB_ENV"
|
|
79
|
+
|
|
58
80
|
- name: Build
|
|
59
81
|
run: cargo build --verbose --locked
|
|
60
82
|
|
|
@@ -82,8 +82,9 @@ jobs:
|
|
|
82
82
|
- uses: actions/setup-python@v6
|
|
83
83
|
with:
|
|
84
84
|
python-version: '3.10'
|
|
85
|
-
- name:
|
|
86
|
-
|
|
85
|
+
- name: Set OpenBLAS target (Apple Silicon wheel)
|
|
86
|
+
if: matrix.target == 'aarch64'
|
|
87
|
+
run: echo "OPENBLAS_TARGET=ARMV8" >> "$GITHUB_ENV"
|
|
87
88
|
- name: Build wheels
|
|
88
89
|
uses: PyO3/maturin-action@v1
|
|
89
90
|
with:
|
|
@@ -24,10 +24,20 @@ jobs:
|
|
|
24
24
|
run: |
|
|
25
25
|
if [ -z "$REPO_ADMIN_TOKEN" ]; then
|
|
26
26
|
echo "REPO_ADMIN_TOKEN is not set."
|
|
27
|
-
echo "Create a fine-grained PAT
|
|
27
|
+
echo "Create a fine-grained PAT with repository Administration: Read and write."
|
|
28
|
+
echo "Add it under Settings → Secrets and variables → Actions → REPO_ADMIN_TOKEN."
|
|
28
29
|
exit 1
|
|
29
30
|
fi
|
|
30
31
|
|
|
32
|
+
- name: Dry-run metadata payload
|
|
33
|
+
run: python scripts/sync_github_repo_metadata.py --dry-run
|
|
34
|
+
|
|
35
|
+
- name: Verify admin token is valid
|
|
36
|
+
env:
|
|
37
|
+
REPO_ADMIN_TOKEN: ${{ secrets.REPO_ADMIN_TOKEN }}
|
|
38
|
+
GITHUB_REPOSITORY: ${{ github.repository }}
|
|
39
|
+
run: python scripts/sync_github_repo_metadata.py --verify-token
|
|
40
|
+
|
|
31
41
|
- name: Sync repository metadata
|
|
32
42
|
env:
|
|
33
43
|
REPO_ADMIN_TOKEN: ${{ secrets.REPO_ADMIN_TOKEN }}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# Agent and contributor pre-flights
|
|
2
|
+
|
|
3
|
+
This repo uses **four tiers** of checks (one more than a typical Task + bash-hook setup). Pick the tier that matches how much you changed.
|
|
4
|
+
|
|
5
|
+
## Architecture (vs duplicated shell scripts)
|
|
6
|
+
|
|
7
|
+
```mermaid
|
|
8
|
+
flowchart TD
|
|
9
|
+
subgraph entry [Entry points]
|
|
10
|
+
task[Taskfile.yml]
|
|
11
|
+
lh[Lefthook]
|
|
12
|
+
gha[GitHub Actions]
|
|
13
|
+
legacy[scripts/local_ci.*]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
subgraph core [Single implementation]
|
|
17
|
+
lme["scripts/ci/lme_ci.py"]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
subgraph tools [Pinned tooling via mise.toml]
|
|
21
|
+
mise[mise install]
|
|
22
|
+
uv[uv — Python venv + Ruff]
|
|
23
|
+
lk[lefthook — git hooks]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
task --> lme
|
|
27
|
+
lh --> lme
|
|
28
|
+
gha --> lme
|
|
29
|
+
legacy --> lme
|
|
30
|
+
mise --> uv
|
|
31
|
+
mise --> lk
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Why this is better than parallel `.ps1` / `.sh` trees:**
|
|
35
|
+
|
|
36
|
+
| Piece | Role |
|
|
37
|
+
|---|---|
|
|
38
|
+
| [`scripts/ci/lme_ci.py`](lme_ci.py) | One cross-platform implementation (stdlib Python 3.10+) |
|
|
39
|
+
| [`lefthook.yml`](../lefthook.yml) | Declarative hooks, **parallel** pre-commit, `stage_fixed` auto-staging |
|
|
40
|
+
| [`mise.toml`](../mise.toml) | Pins Rust, Python 3.11, uv, lefthook, task — reproducible dev env |
|
|
41
|
+
| [`uv`](https://docs.astral.sh/uv/) | Fast venv creation (`uv venv --python 3.11`), no repo-root `.venv` backup dance |
|
|
42
|
+
| **Ruff** | `python/tests` + `python/examples` on `task lint`; staged auto-fix on commit |
|
|
43
|
+
| **pre-push hook** | Automatic `preflight` (lint + `cargo check --all-targets` + `cargo audit` + repo metadata dry-run) |
|
|
44
|
+
|
|
45
|
+
[`Taskfile.yml`](../Taskfile.yml) is a thin alias layer; it does not duplicate logic.
|
|
46
|
+
|
|
47
|
+
## 0. One-time setup
|
|
48
|
+
|
|
49
|
+
```powershell
|
|
50
|
+
mise install # rust, python 3.11, uv, lefthook, task
|
|
51
|
+
task setup # mise install + lefthook install
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Or install tools manually, then:
|
|
55
|
+
|
|
56
|
+
```powershell
|
|
57
|
+
task hooks:install
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## 1. On `git commit` (Lefthook — parallel, staged-only)
|
|
61
|
+
|
|
62
|
+
[`lefthook.yml`](../lefthook.yml) runs only when matching files are staged:
|
|
63
|
+
|
|
64
|
+
| Staged | Command | Notes |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| `**/*.rs` | `fmt` then `clippy` | `stage_fixed: true` re-stages rustfmt fixes |
|
|
67
|
+
| `python/**/*.py` | `ruff-staged --fix` | Ruff check + format; auto-stages fixes |
|
|
68
|
+
| `Cargo.toml`, `Cargo.lock`, `python/Cargo.toml`, `python/Cargo.lock` | `cargo check --all-targets` | Catches link/BLAS issues on your platform when manifests change |
|
|
69
|
+
| `Cargo.toml`, `scripts/sync_github_repo_metadata.py` | `repo-metadata` dry-run (+ token verify if `REPO_ADMIN_TOKEN` set) | Validates About-box payload before GHA metadata sync |
|
|
70
|
+
|
|
71
|
+
Runs **in parallel** where safe (Ruff vs Rust fmt). Clippy waits on fmt (`priority`); manifest `check` runs after clippy (`priority` 3).
|
|
72
|
+
|
|
73
|
+
**Not** on commit: full `cargo test`, maturin/pytest, `pip-audit`.
|
|
74
|
+
|
|
75
|
+
Bypass once: `git commit --no-verify`.
|
|
76
|
+
|
|
77
|
+
## 2. On `git push` (Lefthook pre-push)
|
|
78
|
+
|
|
79
|
+
Runs **`preflight`** in parallel with itself as one command:
|
|
80
|
+
|
|
81
|
+
1. `lint` — Rust fmt/clippy + Ruff
|
|
82
|
+
2. `cargo check --workspace --all-targets --locked` — compile graph including tests/examples
|
|
83
|
+
3. `cargo audit` — root crate + `python/` (same as GHA audit job, without `pip-audit`)
|
|
84
|
+
4. `repo-metadata` — dry-run from `Cargo.toml`; **`--verify-token`** when `REPO_ADMIN_TOKEN` is in the environment
|
|
85
|
+
|
|
86
|
+
Install `cargo-audit` once: `cargo install cargo-audit` (pinned in GHA as 0.22.1).
|
|
87
|
+
|
|
88
|
+
To catch **expired metadata tokens** before push (optional):
|
|
89
|
+
|
|
90
|
+
```powershell
|
|
91
|
+
$env:REPO_ADMIN_TOKEN = "<fine-grained PAT with Administration: Read and write>"
|
|
92
|
+
task repo-metadata
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
If GHA reports `401 Bad credentials` on **Sync Repository Metadata**, rotate the secret: **Settings → Secrets and variables → Actions → `REPO_ADMIN_TOKEN`**.
|
|
96
|
+
|
|
97
|
+
Bypass: `git push --no-verify`.
|
|
98
|
+
|
|
99
|
+
**macOS Apple Silicon BLAS** (`openblas-static` vs MKL) is only exercised on `macos-latest` in GHA — Windows/Linux preflights cannot catch that matrix cell. After changing `Cargo.toml` BLAS target tables or release workflows, run `task ci` or wait for the macOS CI job.
|
|
100
|
+
|
|
101
|
+
## 3. Before finishing work (manual)
|
|
102
|
+
|
|
103
|
+
### Quick path
|
|
104
|
+
|
|
105
|
+
```powershell
|
|
106
|
+
task lint
|
|
107
|
+
task preflight # same as pre-push hook (without pip-audit)
|
|
108
|
+
task audit # full security audit incl. pip-audit
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Runs Rust (`fmt --check` + clippy) and Python Ruff on `python/tests` and `python/examples`.
|
|
112
|
+
|
|
113
|
+
If you changed **Python bindings**:
|
|
114
|
+
|
|
115
|
+
```powershell
|
|
116
|
+
task python
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Rust-only
|
|
120
|
+
|
|
121
|
+
```powershell
|
|
122
|
+
task rust
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Python-only
|
|
126
|
+
|
|
127
|
+
```powershell
|
|
128
|
+
task lint:python
|
|
129
|
+
task python
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Direct runner (no Task):
|
|
133
|
+
|
|
134
|
+
```powershell
|
|
135
|
+
python scripts/ci/lme_ci.py lint
|
|
136
|
+
python scripts/ci/lme_ci.py python --reuse-venv
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## 4. Before PR / large refactors
|
|
140
|
+
|
|
141
|
+
```powershell
|
|
142
|
+
task # full core CI mirror
|
|
143
|
+
task ci:fast # reuse python/.venv, skip wheel-reinstall pytest
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Equivalent:
|
|
147
|
+
|
|
148
|
+
```powershell
|
|
149
|
+
python scripts/ci/lme_ci.py ci
|
|
150
|
+
python scripts/ci/lme_ci.py ci --reuse-venv --skip-wheel-reinstall
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**CI-only:** multi-OS matrix, Python 3.10/3.12/3.13, production-load gates, `cargo audit` / `pip-audit`.
|
|
154
|
+
|
|
155
|
+
## `lme_ci.py` commands
|
|
156
|
+
|
|
157
|
+
| Command | Purpose |
|
|
158
|
+
|---|---|
|
|
159
|
+
| `ci` | Full core CI |
|
|
160
|
+
| `lint` | Rust + Python static checks |
|
|
161
|
+
| `preflight` | Pre-push: lint + check + cargo audit + repo metadata |
|
|
162
|
+
| `audit` | cargo audit + pip-audit (GHA mirror) |
|
|
163
|
+
| `repo-metadata` | Dry-run GitHub About sync; verify token if `REPO_ADMIN_TOKEN` set |
|
|
164
|
+
| `rust-lint` | `fmt --check` + clippy |
|
|
165
|
+
| `ruff-lint` | Ruff on `python/tests` + `python/examples` |
|
|
166
|
+
| `rust-all` | Rust slice without Python bindings tests |
|
|
167
|
+
| `python` | Maturin + pytest (+ wheel pass) |
|
|
168
|
+
| `ruff-staged --fix` | Staged Python lint/format |
|
|
169
|
+
| `hooks-install` | `lefthook install` |
|
|
170
|
+
|
|
171
|
+
Run `python scripts/ci/lme_ci.py --help` for the full list.
|
|
172
|
+
|
|
173
|
+
See also [`CONTRIBUTING.md`](../CONTRIBUTING.md).
|
|
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.8] - 2026-07-01
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Python bindings pushed to ~95% Rust parity: `lm_matrix`, `contrast_matrix`, structured results (`PyConfintResult`, `PyFixedEffectsAnova`, `PyContrastTest`, `PyLikelihoodRatioAnova`, `PySimulateResult`), extra [`PyLmeFit`](python/src/lib.rs) getters (`b`, `u`, `beta_se`, `fixed_term_assign`, `categorical_levels`, `v_beta_unscaled`), and [`lme_python.pyi`](python/lme_python.pyi). Prior: `nlmer`, `glmer_weighted`, `contrast_matrix_from_names`, `test_contrast_vs`, metadata getters. Tests in [`python/tests/test_api_parity.py`](python/tests/test_api_parity.py).
|
|
15
|
+
- Weighted GLMMs: [`glmer_weighted`](src/lib.rs) with prior observation weights in PIRLS / Laplace deviance (delegates to [`lmer_weighted`](src/lib.rs) for Gaussian). Python: `lme_python.glmer_weighted(..., weights=...)`. Tests: [`tests/test_glmm_weighted.rs`](tests/test_glmm_weighted.rs).
|
|
16
|
+
- Nonlinear mixed models (`nlmer`-style): [`nlmer`](src/nlmm/mod.rs) with three-part formulas (`response ~ SSlogis(cov, Asym, xmid, scal) ~ Asym|group`), penalized Gauss–Newton inner loop, and golden-section profiling of a single random-effect standard deviation. Parity test [`tests/test_nlmm_orange.rs`](tests/test_nlmm_orange.rs) against `lme4::nlmer(Orange)` (ML, random effect on `Asym` only).
|
|
17
|
+
- User-defined fixed-effects contrast tests: [`LmeFit::test_contrast`](src/contrast.rs), [`test_contrast_vs`](src/contrast.rs), and [`contrast_matrix_from_names`](src/contrast.rs) ([`tests/test_contrast.rs`](tests/test_contrast.rs)). Python: `fit.test_contrast(l_matrix)`.
|
|
18
|
+
- Kenward–Roger multi-DoF ANOVA via `pbkrtest::KRmodcomp` / `.KR_adjust` ([`src/kr_modcomp.rs`](src/kr_modcomp.rs)) and structural `vcovAdj16` ([`src/kr_vcov_adj.rs`](src/kr_vcov_adj.rs)), wired from [`src/anova.rs`](src/anova.rs). Parity test [`tests/test_kr_modcomp_pastes.rs`](tests/test_kr_modcomp_pastes.rs); golden case `pastes_cask_multi_dof_reml` now checks KR ANOVA.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Kenward–Roger multi-DoF rows no longer use marginal-df pooling only; they use full `KRmodcomp` when `vcovAdj` differs from `vcov`, with pooling when adjustment is negligible (e.g. pastes batch RE).
|
|
23
|
+
- CI and local dev tooling: single cross-platform [`scripts/ci/lme_ci.py`](scripts/ci/lme_ci.py) runner (Task, Lefthook, GitHub Actions, legacy `local_ci` scripts); [`Taskfile.yml`](Taskfile.yml), [`lefthook.yml`](lefthook.yml), [`mise.toml`](mise.toml) pins; Ruff on `python/tests` and `python/examples`. See [`AGENTS.md`](AGENTS.md).
|
|
24
|
+
|
|
10
25
|
## [0.1.7] - 2026-05-18
|
|
11
26
|
|
|
12
27
|
### Added
|
|
@@ -11,9 +11,33 @@ This repository contains:
|
|
|
11
11
|
|
|
12
12
|
## Local setup
|
|
13
13
|
|
|
14
|
+
### Recommended toolchain
|
|
15
|
+
|
|
16
|
+
Install [mise](https://mise.jdx.dev) and run once per clone:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
mise install
|
|
20
|
+
task setup # installs pinned tools + lefthook git hooks
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
[`mise.toml`](mise.toml) pins Rust (stable), Python 3.11, [uv](https://docs.astral.sh/uv/), [lefthook](https://lefthook.dev/), and [go-task](https://taskfile.dev).
|
|
24
|
+
|
|
25
|
+
See [AGENTS.md](AGENTS.md) for the four-tier pre-flight model (Lefthook commit/push hooks → `task lint` / `task preflight` → `task ci`).
|
|
26
|
+
|
|
27
|
+
Install **`cargo-audit`** for the pre-push hook: `cargo install cargo-audit` (GHA pins 0.22.1).
|
|
28
|
+
|
|
29
|
+
### CI runner
|
|
30
|
+
|
|
31
|
+
All checks share one implementation: [`scripts/ci/lme_ci.py`](scripts/ci/lme_ci.py). Task, Lefthook, GitHub Actions, and [`scripts/local_ci.sh`](scripts/local_ci.sh) call into it — no duplicated PowerShell/bash logic.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
python3 scripts/ci/lme_ci.py ci
|
|
35
|
+
python3 scripts/ci/lme_ci.py rust-lint
|
|
36
|
+
```
|
|
37
|
+
|
|
14
38
|
### Rust
|
|
15
39
|
|
|
16
|
-
Install a stable Rust toolchain via [rustup](https://rustup.rs).
|
|
40
|
+
Install a stable Rust toolchain via [rustup](https://rustup.rs) (or `mise install`).
|
|
17
41
|
|
|
18
42
|
Useful commands:
|
|
19
43
|
|
|
@@ -27,14 +51,35 @@ cargo clippy --locked -- -D warnings
|
|
|
27
51
|
cargo doc --no-deps --locked
|
|
28
52
|
```
|
|
29
53
|
|
|
30
|
-
|
|
54
|
+
Or via Task:
|
|
55
|
+
|
|
56
|
+
```powershell
|
|
57
|
+
task lint # fmt --check + clippy + Ruff (python/tests + examples)
|
|
58
|
+
task preflight # pre-push hook: lint + check + cargo audit + repo-metadata dry-run
|
|
59
|
+
task audit # cargo audit + pip-audit (GHA security audit mirror)
|
|
60
|
+
task rust # full Rust slice (no Python)
|
|
61
|
+
task # full core CI mirror
|
|
62
|
+
task ci:fast # reuse python/.venv, skip wheel-reinstall pytest
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
To run the same **core** checks as [GitHub Actions CI](.github/workflows/ci.yml) locally:
|
|
31
66
|
|
|
32
67
|
```bash
|
|
33
|
-
|
|
68
|
+
task ci
|
|
69
|
+
# or
|
|
70
|
+
python3 scripts/ci/lme_ci.py ci
|
|
34
71
|
```
|
|
35
72
|
|
|
36
73
|
```powershell
|
|
37
|
-
|
|
74
|
+
task ci
|
|
75
|
+
# or
|
|
76
|
+
python scripts/ci/lme_ci.py ci
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Git hooks (parallel pre-commit + pre-push **preflight**) via Lefthook:
|
|
80
|
+
|
|
81
|
+
```powershell
|
|
82
|
+
task hooks:install
|
|
38
83
|
```
|
|
39
84
|
|
|
40
85
|
### Python bindings
|
|
@@ -66,7 +111,7 @@ pip-compile pyproject.toml --extra dev -o requirements-ci.txt --allow-unsafe --s
|
|
|
66
111
|
|
|
67
112
|
Use `pytest tests/` so only [`python/tests/`](python/tests/) runs (same as [`.github/workflows/ci.yml`](.github/workflows/ci.yml)); `pytest` alone also collects optional demos under `python/examples/`.
|
|
68
113
|
|
|
69
|
-
The
|
|
114
|
+
The [`scripts/local_ci.sh`](scripts/local_ci.sh) / [`scripts/local_ci.ps1`](scripts/local_ci.ps1) helpers use **uv** to create `python/.venv` with Python **3.11** explicitly, then `pip install -r requirements-ci.txt`, `maturin develop`, and `pytest tests/`. CI also runs that binding flow on **Ubuntu only** for Python **3.10**, **3.12**, and **3.13** (see job `python-bindings-versions` in [`.github/workflows/ci.yml`](.github/workflows/ci.yml)). If you use **CPython 3.14** in your own venv, set `PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1` before `maturin develop` (see [python/PYTHON_GUIDE.md](python/PYTHON_GUIDE.md)).
|
|
70
115
|
|
|
71
116
|
## Working on numerical changes
|
|
72
117
|
|
|
@@ -111,6 +156,15 @@ The GitHub About box is synced from `Cargo.toml` using:
|
|
|
111
156
|
|
|
112
157
|
If you change `package.description`, `homepage`, `keywords`, or `categories`, the metadata workflow will update the GitHub repository metadata on the next run.
|
|
113
158
|
|
|
159
|
+
Preflight locally:
|
|
160
|
+
|
|
161
|
+
```powershell
|
|
162
|
+
task repo-metadata # dry-run; verifies token if REPO_ADMIN_TOKEN is set
|
|
163
|
+
python scripts/sync_github_repo_metadata.py --dry-run
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The workflow needs a valid **`REPO_ADMIN_TOKEN`** repository secret (fine-grained PAT with **Administration: Read and write** on this repo). If CI fails with `401 Bad credentials`, create a new PAT and update **Settings → Secrets and variables → Actions → REPO_ADMIN_TOKEN**.
|
|
167
|
+
|
|
114
168
|
## Pull request expectations
|
|
115
169
|
|
|
116
170
|
Keep changes focused. For non-trivial changes, include:
|
|
@@ -1528,7 +1528,7 @@ checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
|
|
|
1528
1528
|
|
|
1529
1529
|
[[package]]
|
|
1530
1530
|
name = "lme-rs"
|
|
1531
|
-
version = "0.1.
|
|
1531
|
+
version = "0.1.8"
|
|
1532
1532
|
dependencies = [
|
|
1533
1533
|
"anyhow",
|
|
1534
1534
|
"argmin",
|
|
@@ -2948,9 +2948,9 @@ dependencies = [
|
|
|
2948
2948
|
|
|
2949
2949
|
[[package]]
|
|
2950
2950
|
name = "rustls"
|
|
2951
|
-
version = "0.23.
|
|
2951
|
+
version = "0.23.41"
|
|
2952
2952
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
2953
|
-
checksum = "
|
|
2953
|
+
checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f"
|
|
2954
2954
|
dependencies = [
|
|
2955
2955
|
"log",
|
|
2956
2956
|
"once_cell",
|
|
@@ -2972,9 +2972,9 @@ dependencies = [
|
|
|
2972
2972
|
|
|
2973
2973
|
[[package]]
|
|
2974
2974
|
name = "rustls-webpki"
|
|
2975
|
-
version = "0.103.
|
|
2975
|
+
version = "0.103.13"
|
|
2976
2976
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
2977
|
-
checksum = "
|
|
2977
|
+
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
|
|
2978
2978
|
dependencies = [
|
|
2979
2979
|
"ring",
|
|
2980
2980
|
"rustls-pki-types",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "lme-rs"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.8"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
authors = ["Martin Huck <x4g4p3x@users.noreply.github.com>"]
|
|
6
6
|
description = "Rust port of R's lme4: linear mixed-effects models with 1:1 numerical compatibility"
|
|
@@ -89,8 +89,15 @@ path = "comparisons/lmer_weighted.rs"
|
|
|
89
89
|
name = "glmm_gaussian"
|
|
90
90
|
path = "comparisons/glmm_gaussian.rs"
|
|
91
91
|
|
|
92
|
+
# x86_64 (Linux, Windows, macOS x86_64 wheels via cross-compile): static Intel MKL.
|
|
92
93
|
[target.'cfg(target_arch = "x86_64")'.dependencies]
|
|
93
94
|
ndarray-linalg = { version = "0.17", features = ["intel-mkl-static"] }
|
|
94
95
|
|
|
95
|
-
|
|
96
|
+
# macOS Apple Silicon: static OpenBLAS (MKL does not link on aarch64-apple-darwin;
|
|
97
|
+
# Homebrew OpenBLAS pulls libgcc dylibs maturin cannot repair).
|
|
98
|
+
[target.'cfg(all(target_arch = "aarch64", target_os = "macos"))'.dependencies]
|
|
99
|
+
ndarray-linalg = { version = "0.17", features = ["openblas-static"] }
|
|
100
|
+
|
|
101
|
+
# Other non-x86_64 (e.g. Linux aarch64 manylinux wheels): static OpenBLAS.
|
|
102
|
+
[target.'cfg(all(not(target_arch = "x86_64"), not(target_os = "macos")))'.dependencies]
|
|
96
103
|
ndarray-linalg = { version = "0.17", features = ["openblas-static"] }
|
|
@@ -21,7 +21,7 @@ This guide covers the practical Rust workflow for fitting linear and generalized
|
|
|
21
21
|
|
|
22
22
|
```toml
|
|
23
23
|
[dependencies]
|
|
24
|
-
lme-rs = "0.1.
|
|
24
|
+
lme-rs = "0.1.8"
|
|
25
25
|
polars = { version = "0.46", features = ["csv"] }
|
|
26
26
|
anyhow = "1"
|
|
27
27
|
```
|
|
@@ -134,6 +134,39 @@ let weights = Array1::from_vec(vec![1.0; df.height()]);
|
|
|
134
134
|
let fit = lmer_weighted("y ~ x + (1 | group)", &df, true, Some(weights))?;
|
|
135
135
|
```
|
|
136
136
|
|
|
137
|
+
### `nlmer()` for nonlinear mixed models
|
|
138
|
+
|
|
139
|
+
`nlmer` fits Gaussian nonlinear mixed models with a three-part formula (response, nonlinear mean, random part):
|
|
140
|
+
|
|
141
|
+
```text
|
|
142
|
+
circumference ~ SSlogis(age, Asym, xmid, scal) ~ Asym|Tree
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
```rust
|
|
146
|
+
use lme_rs::nlmer;
|
|
147
|
+
use std::collections::HashMap;
|
|
148
|
+
|
|
149
|
+
let mut start = HashMap::new();
|
|
150
|
+
start.insert("Asym".to_string(), 200.0);
|
|
151
|
+
start.insert("xmid".to_string(), 725.0);
|
|
152
|
+
start.insert("scal".to_string(), 350.0);
|
|
153
|
+
|
|
154
|
+
// ML (matches R `nlmer` default); pass `true` for REML profiling
|
|
155
|
+
let fit = nlmer(
|
|
156
|
+
"circumference ~ SSlogis(age, Asym, xmid, scal) ~ Asym|Tree",
|
|
157
|
+
&df,
|
|
158
|
+
start,
|
|
159
|
+
false,
|
|
160
|
+
)?;
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Current limitations:
|
|
164
|
+
|
|
165
|
+
- Only `SSlogis` is implemented as the mean function.
|
|
166
|
+
- Only one random effect, on a single nonlinear parameter (the canonical Orange-tree example).
|
|
167
|
+
- `predict()` is not wired for NLMM fits yet; use `fitted` / `residuals` on the training data.
|
|
168
|
+
- Log-likelihood and residual variance can differ slightly from `lme4` because this release uses a Laplace / penalized Gauss–Newton path (`nAGQ = 0` style), not full adaptive quadrature.
|
|
169
|
+
|
|
137
170
|
### `glmer()` for generalized linear mixed models
|
|
138
171
|
|
|
139
172
|
```rust
|
|
@@ -169,6 +202,24 @@ Built-in family support through the public enum:
|
|
|
169
202
|
|
|
170
203
|
At the public API level, `glmer()` currently dispatches through these built-in families and their default links.
|
|
171
204
|
|
|
205
|
+
### `glmer_weighted()` for prior observation weights
|
|
206
|
+
|
|
207
|
+
```rust
|
|
208
|
+
use lme_rs::{family::Family, glmer_weighted};
|
|
209
|
+
use ndarray::Array1;
|
|
210
|
+
|
|
211
|
+
let w = Array1::from_vec(vec![1.0; df.height()]);
|
|
212
|
+
let fit = glmer_weighted(
|
|
213
|
+
"y ~ period2 + period3 + period4 + (1 | herd)",
|
|
214
|
+
&df,
|
|
215
|
+
Family::Binomial,
|
|
216
|
+
1,
|
|
217
|
+
Some(w),
|
|
218
|
+
)?;
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Weights must be strictly positive and match the number of rows. For `Family::Gaussian`, fitting delegates to `lmer_weighted` with ML.
|
|
222
|
+
|
|
172
223
|
### Understanding model output
|
|
173
224
|
|
|
174
225
|
The `Display` implementation intentionally mirrors the shape of R output.
|
|
@@ -297,8 +348,9 @@ Current scope:
|
|
|
297
348
|
- Type **II** and Type **III** tables (`anova_typed` / `AnovaType`)
|
|
298
349
|
- 1-DoF marginal tests for continuous predictors; joint multi-DoF Wald F-tests for categorical predictors when dummy columns are grouped in the fit
|
|
299
350
|
- Multi-DoF Satterthwaite denominator df follows **`lmerTest::contestMD()`** (orthogonal contrast directions + `get_Fstat_ddf()` pooling); call `with_satterthwaite()` before `anova()` so the vcov Jacobian is available
|
|
300
|
-
- Kenward–Roger multi-DoF rows
|
|
351
|
+
- Kenward–Roger multi-DoF rows use `pbkrtest::KRmodcomp` / `.KR_adjust` via [`kr_modcomp`](src/kr_modcomp.rs) and `vcovAdj16` via [`kr_vcov_adj`](src/kr_vcov_adj.rs); when `vcovAdj` ≈ `vcov`, DenDF matches marginal KR pooling (pastes `cask` reference)
|
|
301
352
|
- Golden regression for categorical multi-DoF ANOVA: manifest case `pastes_cask_multi_dof_reml` in [`tests/data/golden_parity_manifest.json`](tests/data/golden_parity_manifest.json) (pastes / `cask`)
|
|
353
|
+
- **User-defined contrasts:** any **q × p** matrix `L` via [`LmeFit::test_contrast`](src/contrast.rs) / [`test_contrast_vs`](src/contrast.rs) (`lmerTest::contestMD` / `KRmodcomp`); helpers [`contrast_matrix`](src/contrast.rs) and [`contrast_matrix_from_names`](src/contrast.rs). Python: `fit.test_contrast(l_matrix, ddf_method=...)`.
|
|
302
354
|
|
|
303
355
|
## Model Comparison
|
|
304
356
|
|
|
@@ -328,7 +380,7 @@ For GLMMs, `lme-rs` computes the optimization target from a Laplace-approximated
|
|
|
328
380
|
|
|
329
381
|
### ANOVA scope
|
|
330
382
|
|
|
331
|
-
Fixed-effects ANOVA supports Type **II** and Type **III** (`AnovaType`). Type II uses `lmerTest`-style contrasts (marginal for non-contained terms, Doolittle reordering for contained terms). Continuous terms use 1-DoF tests where applicable; categorical predictors use joint multi-DoF Wald rows. Arbitrary
|
|
383
|
+
Fixed-effects ANOVA supports Type **II** and Type **III** (`AnovaType`). Type II uses `lmerTest`-style contrasts (marginal for non-contained terms, Doolittle reordering for contained terms). Continuous terms use 1-DoF tests where applicable; categorical predictors use joint multi-DoF Wald rows. Arbitrary **q × p** contrast matrices are supported via [`test_contrast`](src/contrast.rs).
|
|
332
384
|
|
|
333
385
|
### Kenward-Roger status
|
|
334
386
|
|
|
@@ -381,7 +433,9 @@ For concrete parity outputs, use the scripts and datasets in `comparisons/` and
|
|
|
381
433
|
| `lm(y, x)` | fixed-effects-only linear regression |
|
|
382
434
|
| `lmer(formula, data, reml)` | linear mixed model |
|
|
383
435
|
| `lmer_weighted(formula, data, reml, weights)` | weighted linear mixed model |
|
|
436
|
+
| `nlmer(formula, data, start, reml)` | nonlinear mixed model (`SSlogis`, one RE parameter) |
|
|
384
437
|
| `glmer(formula, data, family)` | generalized linear mixed model |
|
|
438
|
+
| `glmer_weighted(formula, data, family, n_agq, weights)` | GLMM with prior observation weights |
|
|
385
439
|
| `anova(fit_a, fit_b)` | likelihood ratio test between nested models |
|
|
386
440
|
|
|
387
441
|
### `LmeFit` methods
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
|
|
12
12
|
- `lm()` for fixed-effects-only linear models
|
|
13
13
|
- `lmer()` and `lmer_weighted()` for linear mixed models
|
|
14
|
-
- `
|
|
14
|
+
- `nlmer()` for nonlinear mixed models (`SSlogis` mean; random effect on one nonlinear parameter, e.g. Orange-tree growth)
|
|
15
|
+
- `glmer()` and `glmer_weighted()` for binomial, poisson, gaussian, and gamma mixed models
|
|
15
16
|
- Wilkinson formulas with nested and crossed random effects
|
|
16
17
|
- Population-level and conditional prediction APIs
|
|
17
18
|
- Wald confidence intervals, parametric simulation, robust standard errors, Satterthwaite degrees of freedom, and Kenward-Roger denominator degrees of freedom
|
|
@@ -64,9 +65,9 @@ For a subjective, area-by-area view of what is implemented versus still open (in
|
|
|
64
65
|
|
|
65
66
|
- Numerical parity is the goal for the covered LMM and GLMM workflows, but the guarantee is scoped to the models and examples exercised by the repository tests and comparison fixtures.
|
|
66
67
|
- `glmer()` uses a Laplace approximation. Absolute AIC, BIC, and log-likelihood values can differ from R because `lme-rs` optimizes a deviance expression that omits data-dependent constants. Coefficients and variance parameters are the quantities to compare.
|
|
67
|
-
- Fixed-effects ANOVA supports **Type II** and **Type III** (`anova_typed` / `AnovaType`). Continuous fixed effects use 1-DoF tests where applicable; categorical predictors encoded as multiple dummies use **joint multi-DoF Wald F-tests**, with multi-DoF Satterthwaite denominator df following **`lmerTest::contestMD()`** (see [GUIDE.md](GUIDE.md) and [comparisons/COMPARISONS.md](comparisons/COMPARISONS.md) §4). Arbitrary user-defined contrast matrices are
|
|
68
|
+
- Fixed-effects ANOVA supports **Type II** and **Type III** (`anova_typed` / `AnovaType`). Continuous fixed effects use 1-DoF tests where applicable; categorical predictors encoded as multiple dummies use **joint multi-DoF Wald F-tests**, with multi-DoF Satterthwaite denominator df following **`lmerTest::contestMD()`** (see [GUIDE.md](GUIDE.md) and [comparisons/COMPARISONS.md](comparisons/COMPARISONS.md) §4). Arbitrary user-defined **q × p** contrast matrices are supported via `test_contrast()` (Rust) / `fit.test_contrast()` (Python).
|
|
68
69
|
- `with_kenward_roger()` produces denominator degrees of freedom that match R's `pbkrtest` to within the precision of numerical differentiation on the covered LMM models.
|
|
69
|
-
- The
|
|
70
|
+
- The Python bindings mirror the Rust API (`lm`, `lm_matrix`, `lmer`, `glmer`, `nlmer`, contrasts, ANOVA, prediction, simulation) with structured result types and [`lme_python.pyi`](python/lme_python.pyi) stubs.
|
|
70
71
|
- Built-in GLMM families currently use their default links through the public `glmer()` API.
|
|
71
72
|
|
|
72
73
|
## Documentation map
|
|
@@ -97,6 +98,6 @@ Each example is mirrored across Rust, R, Python, and Julia where that comparison
|
|
|
97
98
|
|
|
98
99
|
## Development notes
|
|
99
100
|
|
|
100
|
-
Repository metadata on GitHub is synced from `Cargo.toml` by
|
|
101
|
+
Repository metadata on GitHub is synced from `Cargo.toml` by [.github/workflows/repo-metadata.yml](.github/workflows/repo-metadata.yml). Preflight with `task repo-metadata`; the workflow needs a valid **`REPO_ADMIN_TOKEN`** secret (see [CONTRIBUTING.md](CONTRIBUTING.md)).
|
|
101
102
|
|
|
102
|
-
|
|
103
|
+
Local checks mirror CI in layers: **`task preflight`** before push (lint, compile graph, `cargo audit`, metadata dry-run), **`task ci`** before a PR ([AGENTS.md](AGENTS.md) for hook details). Install `cargo-audit` once: `cargo install cargo-audit`.
|