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.
Files changed (190) hide show
  1. {lme_python-0.1.7 → lme_python-0.1.8}/.github/workflows/ci.yml +26 -4
  2. {lme_python-0.1.7 → lme_python-0.1.8}/.github/workflows/python-release.yml +3 -2
  3. {lme_python-0.1.7 → lme_python-0.1.8}/.github/workflows/repo-metadata.yml +11 -1
  4. lme_python-0.1.8/AGENTS.md +173 -0
  5. {lme_python-0.1.7 → lme_python-0.1.8}/CHANGELOG.md +15 -0
  6. {lme_python-0.1.7 → lme_python-0.1.8}/CONTRIBUTING.md +59 -5
  7. {lme_python-0.1.7 → lme_python-0.1.8}/Cargo.lock +5 -5
  8. {lme_python-0.1.7 → lme_python-0.1.8}/Cargo.toml +9 -2
  9. {lme_python-0.1.7 → lme_python-0.1.8}/GUIDE.md +57 -3
  10. {lme_python-0.1.7 → lme_python-0.1.8}/PKG-INFO +1 -1
  11. {lme_python-0.1.7 → lme_python-0.1.8}/README.md +6 -5
  12. lme_python-0.1.8/RELEASING.md +255 -0
  13. {lme_python-0.1.7 → lme_python-0.1.8}/REPO_COMPLETION_BY_AREA.md +14 -14
  14. lme_python-0.1.8/Taskfile.yml +108 -0
  15. lme_python-0.1.8/lefthook.yml +40 -0
  16. lme_python-0.1.8/mise.toml +16 -0
  17. {lme_python-0.1.7 → lme_python-0.1.8}/pyproject.toml +13 -0
  18. {lme_python-0.1.7 → lme_python-0.1.8}/python/Cargo.lock +20 -52
  19. {lme_python-0.1.7 → lme_python-0.1.8}/python/Cargo.toml +2 -2
  20. {lme_python-0.1.7 → lme_python-0.1.8}/python/PYTHON_GUIDE.md +39 -12
  21. {lme_python-0.1.7 → lme_python-0.1.8}/python/examples/glmer_cbpp.py +5 -4
  22. {lme_python-0.1.7 → lme_python-0.1.8}/python/examples/glmer_grouseticks.py +15 -14
  23. {lme_python-0.1.7 → lme_python-0.1.8}/python/examples/lm_basics.py +2 -2
  24. {lme_python-0.1.7 → lme_python-0.1.8}/python/examples/lmer_sleepstudy.py +9 -7
  25. {lme_python-0.1.7 → lme_python-0.1.8}/python/examples/model_comparison.py +22 -20
  26. {lme_python-0.1.7 → lme_python-0.1.8}/python/examples/plotting_demo/compare_plots.py +22 -14
  27. {lme_python-0.1.7 → lme_python-0.1.8}/python/examples/plotting_demo/find_rscript.py +1 -2
  28. {lme_python-0.1.7 → lme_python-0.1.8}/python/examples/plotting_demo/numeric_overlay.py +1 -3
  29. {lme_python-0.1.7 → lme_python-0.1.8}/python/examples/plotting_demo/plot_demo.py +9 -3
  30. {lme_python-0.1.7 → lme_python-0.1.8}/python/examples/plotting_demo/run_all.py +10 -3
  31. {lme_python-0.1.7 → lme_python-0.1.8}/python/examples/verification_project/parity.py +17 -21
  32. lme_python-0.1.8/python/lme_python.pyi +150 -0
  33. {lme_python-0.1.7 → lme_python-0.1.8}/python/requirements-ci.txt +5 -5
  34. {lme_python-0.1.7 → lme_python-0.1.8}/python/src/lib.rs +612 -77
  35. lme_python-0.1.8/python/tests/test_api_parity.py +138 -0
  36. {lme_python-0.1.7 → lme_python-0.1.8}/python/tests/test_basic.py +97 -66
  37. lme_python-0.1.8/scripts/ci/README.md +27 -0
  38. lme_python-0.1.8/scripts/ci/lme_ci.py +404 -0
  39. lme_python-0.1.8/scripts/ci/run.sh +30 -0
  40. lme_python-0.1.8/scripts/local_ci.ps1 +4 -0
  41. lme_python-0.1.8/scripts/local_ci.sh +4 -0
  42. {lme_python-0.1.7 → lme_python-0.1.8}/scripts/sync_github_repo_metadata.py +35 -4
  43. {lme_python-0.1.7 → lme_python-0.1.8}/src/anova.rs +6 -169
  44. lme_python-0.1.8/src/contrast.rs +266 -0
  45. {lme_python-0.1.7 → lme_python-0.1.8}/src/ddf.rs +11 -5
  46. {lme_python-0.1.7 → lme_python-0.1.8}/src/glmm_math.rs +27 -7
  47. {lme_python-0.1.7 → lme_python-0.1.8}/src/kenward_roger.rs +11 -1
  48. lme_python-0.1.8/src/kr_modcomp.rs +327 -0
  49. lme_python-0.1.8/src/kr_vcov_adj.rs +296 -0
  50. {lme_python-0.1.7 → lme_python-0.1.8}/src/lib.rs +31 -2
  51. {lme_python-0.1.7 → lme_python-0.1.8}/src/model_matrix.rs +1 -2
  52. lme_python-0.1.8/src/nlmm/fit.rs +528 -0
  53. lme_python-0.1.8/src/nlmm/formula.rs +124 -0
  54. lme_python-0.1.8/src/nlmm/mod.rs +37 -0
  55. lme_python-0.1.8/src/nlmm/sslogis.rs +37 -0
  56. {lme_python-0.1.7 → lme_python-0.1.8}/src/optimizer.rs +7 -2
  57. {lme_python-0.1.7 → lme_python-0.1.8}/tests/data/golden_parity_manifest.json +28 -2
  58. lme_python-0.1.8/tests/data/orange.csv +36 -0
  59. lme_python-0.1.8/tests/data/orange_nlmer.json +15 -0
  60. {lme_python-0.1.7 → lme_python-0.1.8}/tests/data/pastes_cask_reml.json +5 -1
  61. lme_python-0.1.8/tests/test_contrast.rs +83 -0
  62. {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_failure_modes.rs +14 -1
  63. lme_python-0.1.8/tests/test_glmm_weighted.rs +64 -0
  64. lme_python-0.1.8/tests/test_kr_modcomp_pastes.rs +51 -0
  65. lme_python-0.1.8/tests/test_nlmm_orange.rs +54 -0
  66. lme_python-0.1.7/RELEASING.md +0 -208
  67. lme_python-0.1.7/scripts/local_ci.ps1 +0 -85
  68. lme_python-0.1.7/scripts/local_ci.sh +0 -86
  69. {lme_python-0.1.7 → lme_python-0.1.8}/.github/workflows/audit.yml +0 -0
  70. {lme_python-0.1.7 → lme_python-0.1.8}/.github/workflows/benchmarks.yml +0 -0
  71. {lme_python-0.1.7 → lme_python-0.1.8}/.github/workflows/crate-publish-dry-run.yml +0 -0
  72. {lme_python-0.1.7 → lme_python-0.1.8}/.github/workflows/fuzz-smoke.yml +0 -0
  73. {lme_python-0.1.7 → lme_python-0.1.8}/.gitignore +0 -0
  74. {lme_python-0.1.7 → lme_python-0.1.8}/BENCHMARKS.md +0 -0
  75. {lme_python-0.1.7 → lme_python-0.1.8}/LICENSE +0 -0
  76. {lme_python-0.1.7 → lme_python-0.1.8}/benches/bench_load_production.rs +0 -0
  77. {lme_python-0.1.7 → lme_python-0.1.8}/benches/bench_math.rs +0 -0
  78. {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/COMPARISONS.md +0 -0
  79. {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/categorical_anova.R +0 -0
  80. {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/categorical_anova.jl +0 -0
  81. {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/categorical_anova.py +0 -0
  82. {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/categorical_anova.rs +0 -0
  83. {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/comparison_chart.png +0 -0
  84. {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/glmm_cbpp.R +0 -0
  85. {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/glmm_cbpp.jl +0 -0
  86. {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/glmm_cbpp.py +0 -0
  87. {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/glmm_cbpp.rs +0 -0
  88. {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/glmm_gaussian.R +0 -0
  89. {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/glmm_gaussian.jl +0 -0
  90. {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/glmm_gaussian.py +0 -0
  91. {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/glmm_gaussian.rs +0 -0
  92. {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/glmm_grouseticks.R +0 -0
  93. {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/glmm_grouseticks.jl +0 -0
  94. {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/glmm_grouseticks.py +0 -0
  95. {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/glmm_grouseticks.rs +0 -0
  96. {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/lmer_weighted.R +0 -0
  97. {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/lmer_weighted.jl +0 -0
  98. {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/lmer_weighted.py +0 -0
  99. {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/lmer_weighted.rs +0 -0
  100. {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/lmm_dyestuff.R +0 -0
  101. {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/lmm_dyestuff.jl +0 -0
  102. {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/lmm_dyestuff.py +0 -0
  103. {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/lmm_dyestuff.rs +0 -0
  104. {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/lmm_pastes.R +0 -0
  105. {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/lmm_pastes.jl +0 -0
  106. {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/lmm_pastes.py +0 -0
  107. {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/lmm_pastes.rs +0 -0
  108. {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/lmm_penicillin.R +0 -0
  109. {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/lmm_penicillin.jl +0 -0
  110. {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/lmm_penicillin.py +0 -0
  111. {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/lmm_penicillin.rs +0 -0
  112. {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/sleepstudy.R +0 -0
  113. {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/sleepstudy.jl +0 -0
  114. {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/sleepstudy.py +0 -0
  115. {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/sleepstudy.rs +0 -0
  116. {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/sleepstudy_ml.R +0 -0
  117. {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/sleepstudy_ml.jl +0 -0
  118. {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/sleepstudy_ml.py +0 -0
  119. {lme_python-0.1.7 → lme_python-0.1.8}/comparisons/sleepstudy_ml.rs +0 -0
  120. {lme_python-0.1.7 → lme_python-0.1.8}/docs/benchmarks/app.js +0 -0
  121. {lme_python-0.1.7 → lme_python-0.1.8}/docs/benchmarks/index.html +0 -0
  122. {lme_python-0.1.7 → lme_python-0.1.8}/docs/benchmarks/styles.css +0 -0
  123. {lme_python-0.1.7 → lme_python-0.1.8}/docs/index.html +0 -0
  124. {lme_python-0.1.7 → lme_python-0.1.8}/python/examples/plotting_demo/.gitignore +0 -0
  125. {lme_python-0.1.7 → lme_python-0.1.8}/python/examples/plotting_demo/README.md +0 -0
  126. {lme_python-0.1.7 → lme_python-0.1.8}/python/examples/plotting_demo/figure_specs.py +0 -0
  127. {lme_python-0.1.7 → lme_python-0.1.8}/python/examples/plotting_demo/paths.py +0 -0
  128. {lme_python-0.1.7 → lme_python-0.1.8}/python/examples/plotting_demo/plot_r.R +0 -0
  129. {lme_python-0.1.7 → lme_python-0.1.8}/python/examples/verification_project/README.md +0 -0
  130. {lme_python-0.1.7 → lme_python-0.1.8}/python/examples/verification_project/conftest.py +0 -0
  131. {lme_python-0.1.7 → lme_python-0.1.8}/python/examples/verification_project/paths.py +0 -0
  132. {lme_python-0.1.7 → lme_python-0.1.8}/python/examples/verification_project/run.py +0 -0
  133. {lme_python-0.1.7 → lme_python-0.1.8}/python/examples/verification_project/test_parity.py +0 -0
  134. {lme_python-0.1.7 → lme_python-0.1.8}/scripts/ast_explorations/test_fiasto.rs +0 -0
  135. {lme_python-0.1.7 → lme_python-0.1.8}/scripts/ast_explorations/test_fiasto_offset.rs +0 -0
  136. {lme_python-0.1.7 → lme_python-0.1.8}/scripts/build_benchmark_site.py +0 -0
  137. {lme_python-0.1.7 → lme_python-0.1.8}/scripts/dump_dyestuff.R +0 -0
  138. {lme_python-0.1.7 → lme_python-0.1.8}/scripts/dump_pastes.R +0 -0
  139. {lme_python-0.1.7 → lme_python-0.1.8}/scripts/make_penicillin_csv.py +0 -0
  140. {lme_python-0.1.7 → lme_python-0.1.8}/scripts/plot_comparisons.py +0 -0
  141. {lme_python-0.1.7 → lme_python-0.1.8}/scripts/run_cross_language_benchmarks.py +0 -0
  142. {lme_python-0.1.7 → lme_python-0.1.8}/src/anova_contrasts.rs +0 -0
  143. {lme_python-0.1.7 → lme_python-0.1.8}/src/family.rs +0 -0
  144. {lme_python-0.1.7 → lme_python-0.1.8}/src/formula.rs +0 -0
  145. {lme_python-0.1.7 → lme_python-0.1.8}/src/math.rs +0 -0
  146. {lme_python-0.1.7 → lme_python-0.1.8}/src/robust.rs +0 -0
  147. {lme_python-0.1.7 → lme_python-0.1.8}/src/satterthwaite.rs +0 -0
  148. {lme_python-0.1.7 → lme_python-0.1.8}/tests/categorical_anova_test.rs +0 -0
  149. {lme_python-0.1.7 → lme_python-0.1.8}/tests/data/cbpp_binary.csv +0 -0
  150. {lme_python-0.1.7 → lme_python-0.1.8}/tests/data/dyestuff.csv +0 -0
  151. {lme_python-0.1.7 → lme_python-0.1.8}/tests/data/glmm_binomial.json +0 -0
  152. {lme_python-0.1.7 → lme_python-0.1.8}/tests/data/glmm_poisson.json +0 -0
  153. {lme_python-0.1.7 → lme_python-0.1.8}/tests/data/grouseticks.csv +0 -0
  154. {lme_python-0.1.7 → lme_python-0.1.8}/tests/data/intercept_only.json +0 -0
  155. {lme_python-0.1.7 → lme_python-0.1.8}/tests/data/mock_crossed.json +0 -0
  156. {lme_python-0.1.7 → lme_python-0.1.8}/tests/data/mock_ml.json +0 -0
  157. {lme_python-0.1.7 → lme_python-0.1.8}/tests/data/pastes.csv +0 -0
  158. {lme_python-0.1.7 → lme_python-0.1.8}/tests/data/penicillin.csv +0 -0
  159. {lme_python-0.1.7 → lme_python-0.1.8}/tests/data/penicillin.json +0 -0
  160. {lme_python-0.1.7 → lme_python-0.1.8}/tests/data/random_slopes.json +0 -0
  161. {lme_python-0.1.7 → lme_python-0.1.8}/tests/data/sleepstudy.csv +0 -0
  162. {lme_python-0.1.7 → lme_python-0.1.8}/tests/generate_mock_data.py +0 -0
  163. {lme_python-0.1.7 → lme_python-0.1.8}/tests/generate_test_data.R +0 -0
  164. {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_anova.rs +0 -0
  165. {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_anova_types.rs +0 -0
  166. {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_conditional_real.rs +0 -0
  167. {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_confint_simulate.rs +0 -0
  168. {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_coverage_edge_cases.rs +0 -0
  169. {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_coverage_gaps.rs +0 -0
  170. {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_crossed_mock.rs +0 -0
  171. {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_e2e_lmer.rs +0 -0
  172. {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_edge_cases.rs +0 -0
  173. {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_errors.rs +0 -0
  174. {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_formula.rs +0 -0
  175. {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_formula_stress.rs +0 -0
  176. {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_gaps.rs +0 -0
  177. {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_glmm.rs +0 -0
  178. {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_golden_parity.rs +0 -0
  179. {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_intercept_only.rs +0 -0
  180. {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_kenward_roger.rs +0 -0
  181. {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_ml_optimization.rs +0 -0
  182. {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_no_intercept.rs +0 -0
  183. {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_numerical_parity.rs +0 -0
  184. {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_predict.rs +0 -0
  185. {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_production_load.rs +0 -0
  186. {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_random_slopes.rs +0 -0
  187. {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_robust.rs +0 -0
  188. {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_sandwich_math.R +0 -0
  189. {lme_python-0.1.7 → lme_python-0.1.8}/tests/test_satterthwaite.rs +0 -0
  190. {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: Install OpenBLAS
86
- run: brew install openblas
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 or GitHub App token with repository Administration: write."
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
- To run the same **core** checks as [GitHub Actions CI](.github/workflows/ci.yml) locally (`--locked` build/test, Python 3.11 `maturin develop` + `pytest tests/`, `fmt`, `clippy`, `check --all-targets`, doctests, `doc`):
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
- ./scripts/local_ci.sh
68
+ task ci
69
+ # or
70
+ python3 scripts/ci/lme_ci.py ci
34
71
  ```
35
72
 
36
73
  ```powershell
37
- .\scripts\local_ci.ps1
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 same flow runs in CI on every push/PR: a fresh `python/.venv` is created with Python **3.11**, 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)). The [`scripts/local_ci.sh`](scripts/local_ci.sh) / [`scripts/local_ci.ps1`](scripts/local_ci.ps1) helpers temporarily back up existing `python/.venv` and repo-root `.venv`, run the Python 3.11 flow, then restore them.
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.7"
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.37"
2951
+ version = "0.23.41"
2952
2952
  source = "registry+https://github.com/rust-lang/crates.io-index"
2953
- checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
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.10"
2975
+ version = "0.103.13"
2976
2976
  source = "registry+https://github.com/rust-lang/crates.io-index"
2977
- checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef"
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.7"
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
- [target.'cfg(not(target_arch = "x86_64"))'.dependencies]
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.7"
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 pool marginal dfs with the same `get_Fstat_ddf()` rule (full `pbkrtest::KRmodcomp` per term is not implemented)
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 user-defined contrast matrices are not implemented.
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lme_python
3
- Version: 0.1.7
3
+ Version: 0.1.8
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Python :: Implementation :: PyPy
@@ -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
- - `glmer()` for binomial, poisson, gaussian, and gamma mixed models
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 not covered.
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 Rust crate exposes a broader surface than the Python bindings. The Python package is useful, but it is not yet a full mirror of the Rust API.
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 the workflow in [.github/workflows/repo-metadata.yml](.github/workflows/repo-metadata.yml). If you change the package description, homepage, keywords, or categories, the GitHub About box will be updated on the next metadata sync run.
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
- To run the same checks as CI locally (format, clippy, build, tests, docs), use [`scripts/local_ci.sh`](scripts/local_ci.sh) or [`scripts/local_ci.ps1`](scripts/local_ci.ps1) (see [CHANGELOG.md](CHANGELOG.md) 0.1.6).
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`.