glmm 0.1.0__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.
- glmm-0.1.0/.github/workflows/ci.yml +160 -0
- glmm-0.1.0/.github/workflows/publish-r-universe.yml +61 -0
- glmm-0.1.0/.github/workflows/release.yml +234 -0
- glmm-0.1.0/.gitignore +22 -0
- glmm-0.1.0/CHANGELOG.md +191 -0
- glmm-0.1.0/Cargo.lock +1623 -0
- glmm-0.1.0/Cargo.toml +122 -0
- glmm-0.1.0/LICENSE +674 -0
- glmm-0.1.0/PKG-INFO +119 -0
- glmm-0.1.0/README.md +137 -0
- glmm-0.1.0/documentation/TUTORIAL-PYTHON.md +191 -0
- glmm-0.1.0/documentation/TUTORIAL-R.md +217 -0
- glmm-0.1.0/documentation/TUTORIAL-RUST.md +181 -0
- glmm-0.1.0/documentation/algorithms-glmm.md +399 -0
- glmm-0.1.0/documentation/algorithms-lmm.md +304 -0
- glmm-0.1.0/documentation/algorithms.md +241 -0
- glmm-0.1.0/documentation/supported_families.md +53 -0
- glmm-0.1.0/faststats/__init__.py +1 -0
- glmm-0.1.0/faststats/glmm/__init__.py +8 -0
- glmm-0.1.0/glmm/__init__.py +408 -0
- glmm-0.1.0/glmm-python/Cargo.toml +19 -0
- glmm-0.1.0/glmm-python/src/convert.rs +134 -0
- glmm-0.1.0/glmm-python/src/lib.rs +84 -0
- glmm-0.1.0/glmm-python/src/orchestrate.rs +348 -0
- glmm-0.1.0/pyproject.toml +51 -0
- glmm-0.1.0/python/LICENSE +674 -0
- glmm-0.1.0/python/README.md +96 -0
- glmm-0.1.0/python/faststats/__init__.py +1 -0
- glmm-0.1.0/python/faststats/glmm/__init__.py +8 -0
- glmm-0.1.0/python/glmm/__init__.py +408 -0
- glmm-0.1.0/python/pyproject.toml +51 -0
- glmm-0.1.0/python/tests/test_api.py +74 -0
- glmm-0.1.0/python/tests/test_factor_levels.py +92 -0
- glmm-0.1.0/python/tests/test_fit_wiring.py +86 -0
- glmm-0.1.0/python/tests/test_packaging.py +11 -0
- glmm-0.1.0/python/tests/test_summary.py +97 -0
- glmm-0.1.0/python/tests/test_validation.py +213 -0
- glmm-0.1.0/r/.Rbuildignore +6 -0
- glmm-0.1.0/r/.gitignore +11 -0
- glmm-0.1.0/r/DESCRIPTION +31 -0
- glmm-0.1.0/r/NAMESPACE +30 -0
- glmm-0.1.0/r/R/extendr-wrappers.R +25 -0
- glmm-0.1.0/r/R/fastglmm-methods.R +392 -0
- glmm-0.1.0/r/R/fastglmm-package.R +3 -0
- glmm-0.1.0/r/R/fastglmm.R +484 -0
- glmm-0.1.0/r/README.md +79 -0
- glmm-0.1.0/r/cleanup +1 -0
- glmm-0.1.0/r/cleanup.win +1 -0
- glmm-0.1.0/r/configure +3 -0
- glmm-0.1.0/r/configure.win +2 -0
- glmm-0.1.0/r/man/VarCorr.Rd +32 -0
- glmm-0.1.0/r/man/confint.fastglmm.Rd +28 -0
- glmm-0.1.0/r/man/fastglmm-package.Rd +22 -0
- glmm-0.1.0/r/man/fastglmm.Rd +135 -0
- glmm-0.1.0/r/man/fastglmm_fit.Rd +17 -0
- glmm-0.1.0/r/man/fixef.Rd +24 -0
- glmm-0.1.0/r/man/formula.fastglmm.Rd +25 -0
- glmm-0.1.0/r/man/isSingular.Rd +21 -0
- glmm-0.1.0/r/man/ranef.Rd +21 -0
- glmm-0.1.0/r/man/sigma.fastglmm.Rd +20 -0
- glmm-0.1.0/r/man/summary.fastglmm.Rd +24 -0
- glmm-0.1.0/r/src/.gitignore +8 -0
- glmm-0.1.0/r/src/Makevars.in +52 -0
- glmm-0.1.0/r/src/Makevars.win.in +51 -0
- glmm-0.1.0/r/src/entrypoint.c +10 -0
- glmm-0.1.0/r/src/fastglmm-win.def +2 -0
- glmm-0.1.0/r/tests/testthat/helper-benchmark.R +34 -0
- glmm-0.1.0/r/tests/testthat/test-acceptance-benchmark.R +84 -0
- glmm-0.1.0/r/tests/testthat/test-errors.R +123 -0
- glmm-0.1.0/r/tests/testthat/test-marshalling.R +96 -0
- glmm-0.1.0/r/tests/testthat/test-methods.R +140 -0
- glmm-0.1.0/r/tests/testthat.R +4 -0
- glmm-0.1.0/r/tools/config.R +113 -0
- glmm-0.1.0/r/tools/cran-tarball.sh +90 -0
- glmm-0.1.0/r/tools/msrv.R +117 -0
- glmm-0.1.0/src/consts.rs +444 -0
- glmm-0.1.0/src/family.rs +367 -0
- glmm-0.1.0/src/fit/common.rs +482 -0
- glmm-0.1.0/src/fit/common_tests.rs +1203 -0
- glmm-0.1.0/src/fit/glm.rs +386 -0
- glmm-0.1.0/src/fit/glm_tests.rs +888 -0
- glmm-0.1.0/src/fit/glmm.rs +472 -0
- glmm-0.1.0/src/fit/glmm_tests.rs +2394 -0
- glmm-0.1.0/src/fit/lmm.rs +198 -0
- glmm-0.1.0/src/fit/lmm_tests.rs +1449 -0
- glmm-0.1.0/src/fit/loop_advanced_seam.rs +431 -0
- glmm-0.1.0/src/fit/mod.rs +682 -0
- glmm-0.1.0/src/fit/ols.rs +130 -0
- glmm-0.1.0/src/fit/ols_tests.rs +124 -0
- glmm-0.1.0/src/formula/error.rs +113 -0
- glmm-0.1.0/src/formula/materialize.rs +748 -0
- glmm-0.1.0/src/formula/mod.rs +31 -0
- glmm-0.1.0/src/formula/parse.rs +448 -0
- glmm-0.1.0/src/glm.rs +1013 -0
- glmm-0.1.0/src/glmm/agq.rs +631 -0
- glmm-0.1.0/src/glmm/deviance.rs +552 -0
- glmm-0.1.0/src/glmm/mod.rs +949 -0
- glmm-0.1.0/src/glmm/pirls.rs +1829 -0
- glmm-0.1.0/src/glmm/se.rs +641 -0
- glmm-0.1.0/src/glmm/tests.rs +4594 -0
- glmm-0.1.0/src/glmm/workspace.rs +1021 -0
- glmm-0.1.0/src/ids.rs +117 -0
- glmm-0.1.0/src/lib.rs +72 -0
- glmm-0.1.0/src/linalg.rs +29 -0
- glmm-0.1.0/src/lme.rs +3130 -0
- glmm-0.1.0/src/lmm.rs +4754 -0
- glmm-0.1.0/src/loop_advanced.rs +107 -0
- glmm-0.1.0/src/ols.rs +1182 -0
- glmm-0.1.0/src/simd_transcendental.rs +950 -0
- glmm-0.1.0/src/sparse/glmm.rs +1299 -0
- glmm-0.1.0/src/sparse/mod.rs +1931 -0
- glmm-0.1.0/src/sparse/tests.rs +4544 -0
- glmm-0.1.0/src/spec.rs +274 -0
- glmm-0.1.0/src/start.rs +27 -0
- glmm-0.1.0/src/test_support.rs +277 -0
- glmm-0.1.0/tests/contrasts_oracle.rs +101 -0
- glmm-0.1.0/tests/fixtures/contrasts_fixtures.rs +69 -0
- glmm-0.1.0/tests/fixtures/gen_glmm_hessian_vcov.R +40 -0
- glmm-0.1.0/tests/fixtures/glmm_hessian_vcov.json +114 -0
- glmm-0.1.0/tests/formula_fit.rs +675 -0
- glmm-0.1.0/tests/gen_contrasts_fixtures.R +82 -0
- glmm-0.1.0/tests/parse_suite.rs +416 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
# Single-crate mirror of the MCPower engine's `rust` job, plus the three gates a
|
|
4
|
+
# standalone publishable crate needs: an MSRV floor check, a wasm compile (the
|
|
5
|
+
# crate ships a cfg(wasm32) faer split), and a publish dry-run so `main` is always
|
|
6
|
+
# in a releasable state.
|
|
7
|
+
on:
|
|
8
|
+
push:
|
|
9
|
+
branches: [main, dev]
|
|
10
|
+
pull_request:
|
|
11
|
+
branches: [main]
|
|
12
|
+
# Let release.yml reuse these exact gates before publishing (single source of truth).
|
|
13
|
+
workflow_call:
|
|
14
|
+
|
|
15
|
+
# Newer pushes supersede in-flight runs on the same ref.
|
|
16
|
+
concurrency:
|
|
17
|
+
group: ci-${{ github.ref }}
|
|
18
|
+
cancel-in-progress: true
|
|
19
|
+
|
|
20
|
+
env:
|
|
21
|
+
CARGO_TERM_COLOR: always
|
|
22
|
+
|
|
23
|
+
jobs:
|
|
24
|
+
# ── fmt + clippy + the regex-free-kernel guard, on stable ─────────────────
|
|
25
|
+
# Split out from the #[test] suite so the four feature-variant test runs can
|
|
26
|
+
# go wide in a matrix (below) instead of recompiling serially behind clippy.
|
|
27
|
+
lint:
|
|
28
|
+
name: lint
|
|
29
|
+
runs-on: ubuntu-latest
|
|
30
|
+
timeout-minutes: 15
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@v4
|
|
33
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
34
|
+
with:
|
|
35
|
+
components: rustfmt, clippy
|
|
36
|
+
- uses: Swatinem/rust-cache@v2
|
|
37
|
+
- name: fmt
|
|
38
|
+
run: cargo fmt --all -- --check
|
|
39
|
+
- name: clippy
|
|
40
|
+
# The crate's workspace lints set clippy::all = deny, so a plain clippy
|
|
41
|
+
# run is the correctness gate — no extra `-- -D warnings` needed.
|
|
42
|
+
run: cargo clippy --all-targets
|
|
43
|
+
- name: guard — the default-off kernel links no regex
|
|
44
|
+
# `formula` is on by default (semver-covered surface), so the kernel's
|
|
45
|
+
# regex-free property is no longer structural — this assertion IS the
|
|
46
|
+
# guarantee. See GLMM.md's dependency-direction decision.
|
|
47
|
+
run: |
|
|
48
|
+
if cargo tree -e normal -p glmm --no-default-features | grep -q regex; then
|
|
49
|
+
echo "::error::default-off kernel pulls in regex — the formula feature is leaking"
|
|
50
|
+
cargo tree -e normal -p glmm --no-default-features | grep -B5 regex
|
|
51
|
+
exit 1
|
|
52
|
+
fi
|
|
53
|
+
echo "Kernel is regex-free with default features off."
|
|
54
|
+
|
|
55
|
+
# ── the full #[test] suite across all four feature configs, in parallel ───
|
|
56
|
+
test:
|
|
57
|
+
name: test
|
|
58
|
+
runs-on: ubuntu-latest
|
|
59
|
+
timeout-minutes: 15
|
|
60
|
+
env:
|
|
61
|
+
# The dhat bounded-allocation gates assert exact alloc counts; faer/rayon
|
|
62
|
+
# multi-thread jitter perturbs them, so pin the pool to one thread.
|
|
63
|
+
RAYON_NUM_THREADS: 1
|
|
64
|
+
strategy:
|
|
65
|
+
# A failing feature config must not cancel the others — we want every
|
|
66
|
+
# leg's result on one push, not just the first to break.
|
|
67
|
+
fail-fast: false
|
|
68
|
+
matrix:
|
|
69
|
+
# Each entry is one recompile-and-test on its own runner:
|
|
70
|
+
# default — the semver-covered surface.
|
|
71
|
+
# loop_advanced — unstable scratch-explicit surface, off by default;
|
|
72
|
+
# must still compile+pass so the hot-path consumer
|
|
73
|
+
# never breaks silently.
|
|
74
|
+
# parallel — rayon in-fit parallelism (AGQ cluster loop), off by
|
|
75
|
+
# default; includes the bit-identity test pinning it
|
|
76
|
+
# to the serial path's exact output.
|
|
77
|
+
# no-default — the parse-once/fit-many formula-free kernel; the
|
|
78
|
+
# formula tests are #![cfg(feature = "formula")] and
|
|
79
|
+
# skip themselves.
|
|
80
|
+
include:
|
|
81
|
+
- { name: default, flags: "" }
|
|
82
|
+
- { name: loop_advanced, flags: "--features loop_advanced" }
|
|
83
|
+
- { name: parallel, flags: "--features parallel" }
|
|
84
|
+
- { name: no-default, flags: "--no-default-features" }
|
|
85
|
+
steps:
|
|
86
|
+
- uses: actions/checkout@v4
|
|
87
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
88
|
+
- uses: Swatinem/rust-cache@v2
|
|
89
|
+
- name: test (${{ matrix.name }})
|
|
90
|
+
run: cargo test ${{ matrix.flags }}
|
|
91
|
+
|
|
92
|
+
# ── MSRV floor: the bobyqa dep pins rust-version = 1.85 ───────────────────
|
|
93
|
+
msrv:
|
|
94
|
+
name: msrv (1.85)
|
|
95
|
+
runs-on: ubuntu-latest
|
|
96
|
+
timeout-minutes: 15
|
|
97
|
+
steps:
|
|
98
|
+
- uses: actions/checkout@v4
|
|
99
|
+
- uses: dtolnay/rust-toolchain@1.85
|
|
100
|
+
- uses: Swatinem/rust-cache@v2
|
|
101
|
+
- run: cargo check --all-targets
|
|
102
|
+
|
|
103
|
+
# ── wasm: keep the cfg(wasm32) faer config honest ─────────────────────────
|
|
104
|
+
wasm:
|
|
105
|
+
name: wasm
|
|
106
|
+
runs-on: ubuntu-latest
|
|
107
|
+
timeout-minutes: 15
|
|
108
|
+
steps:
|
|
109
|
+
- uses: actions/checkout@v4
|
|
110
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
111
|
+
with:
|
|
112
|
+
targets: wasm32-unknown-unknown
|
|
113
|
+
- uses: Swatinem/rust-cache@v2
|
|
114
|
+
- run: cargo check --target wasm32-unknown-unknown
|
|
115
|
+
- name: check (parallel feature, must be inert on wasm)
|
|
116
|
+
# rayon is target-dep-excluded on wasm32, so the feature must compile to
|
|
117
|
+
# a no-op here, never pull rayon in or break the build.
|
|
118
|
+
run: cargo check --target wasm32-unknown-unknown --features parallel
|
|
119
|
+
|
|
120
|
+
# ── package: gate the publishable state on every push ─────────────────────
|
|
121
|
+
package:
|
|
122
|
+
name: publish dry-run
|
|
123
|
+
runs-on: ubuntu-latest
|
|
124
|
+
timeout-minutes: 15
|
|
125
|
+
steps:
|
|
126
|
+
- uses: actions/checkout@v4
|
|
127
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
128
|
+
- uses: Swatinem/rust-cache@v2
|
|
129
|
+
- run: cargo publish --dry-run
|
|
130
|
+
|
|
131
|
+
# ── python: build the glmm wheel, install it, test + lint (spec §7) ───────
|
|
132
|
+
python:
|
|
133
|
+
name: python (glmm wheel)
|
|
134
|
+
runs-on: ubuntu-latest
|
|
135
|
+
timeout-minutes: 20
|
|
136
|
+
steps:
|
|
137
|
+
- uses: actions/checkout@v4
|
|
138
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
139
|
+
- uses: Swatinem/rust-cache@v2
|
|
140
|
+
- uses: actions/setup-python@v5
|
|
141
|
+
with:
|
|
142
|
+
python-version: "3.12"
|
|
143
|
+
- name: Build wheel (abi3, Linux x86_64)
|
|
144
|
+
uses: PyO3/maturin-action@v1
|
|
145
|
+
with:
|
|
146
|
+
working-directory: python
|
|
147
|
+
command: build
|
|
148
|
+
args: --release --out dist
|
|
149
|
+
- name: Install the built wheel (non-editable, catches packaging mistakes)
|
|
150
|
+
run: pip install --find-links python/dist glmm
|
|
151
|
+
- name: Install test + lint deps
|
|
152
|
+
run: pip install pytest ruff
|
|
153
|
+
- name: pytest against the installed wheel, run outside the source tree
|
|
154
|
+
run: |
|
|
155
|
+
cd "$RUNNER_TEMP"
|
|
156
|
+
python -m pytest "$GITHUB_WORKSPACE/python/tests" -v
|
|
157
|
+
- name: ruff check
|
|
158
|
+
run: ruff check python/
|
|
159
|
+
- name: ruff format --check
|
|
160
|
+
run: ruff format --check python/
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
name: Publish (r-universe)
|
|
2
|
+
|
|
3
|
+
# Publishes fastglmm to r-universe. r-universe builds straight from a git ref
|
|
4
|
+
# via `R CMD build`, which strips out-of-package path deps (r/ depends on
|
|
5
|
+
# ../glmm-r, which depends on ../ — the glmm kernel), so it cannot build the
|
|
6
|
+
# repo tree directly. Instead this workflow runs cran-tarball.sh --vendor to
|
|
7
|
+
# stage the self-contained, offline-vendored package and force-pushes that
|
|
8
|
+
# staged tree as the *root* of an orphan `r-universe` branch. r-universe's
|
|
9
|
+
# registry (a separate repo the user controls) points at that branch; it
|
|
10
|
+
# notices the ref change and rebuilds. main never holds the vendored tree —
|
|
11
|
+
# the branch is a generated artifact (docs/GLMM/plans/2026-07-16-r-repo-publishing-spec.md).
|
|
12
|
+
on:
|
|
13
|
+
push:
|
|
14
|
+
tags: ["r-*"]
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
verify:
|
|
18
|
+
uses: ./.github/workflows/ci.yml
|
|
19
|
+
|
|
20
|
+
version-check:
|
|
21
|
+
name: Verify tag matches r/DESCRIPTION
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
needs: [verify]
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v4
|
|
26
|
+
- name: Check tag matches DESCRIPTION version
|
|
27
|
+
shell: bash
|
|
28
|
+
run: |
|
|
29
|
+
TAG="${GITHUB_REF#refs/tags/r-}"
|
|
30
|
+
VERSION=$(grep -m1 '^Version: ' r/DESCRIPTION | cut -d' ' -f2)
|
|
31
|
+
if [ "$VERSION" != "$TAG" ]; then
|
|
32
|
+
echo "::error::r/DESCRIPTION has Version: $VERSION, tag is r-$TAG"
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
echo "r/DESCRIPTION matches tag r-$TAG."
|
|
36
|
+
|
|
37
|
+
publish-r-universe:
|
|
38
|
+
name: Stage + push r-universe branch
|
|
39
|
+
needs: [version-check]
|
|
40
|
+
runs-on: ubuntu-latest
|
|
41
|
+
timeout-minutes: 20
|
|
42
|
+
permissions:
|
|
43
|
+
contents: write # force-push the generated r-universe branch with GITHUB_TOKEN
|
|
44
|
+
steps:
|
|
45
|
+
- uses: actions/checkout@v4
|
|
46
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
47
|
+
- uses: Swatinem/rust-cache@v2
|
|
48
|
+
- uses: r-lib/actions/setup-r@v2
|
|
49
|
+
- name: Stage self-contained, vendored package
|
|
50
|
+
run: r/tools/cran-tarball.sh --vendor
|
|
51
|
+
- name: Force-push staged tree as the r-universe branch root
|
|
52
|
+
working-directory: target/cran-stage/fastglmm
|
|
53
|
+
run: |
|
|
54
|
+
git init -q
|
|
55
|
+
git config user.name "github-actions[bot]"
|
|
56
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
57
|
+
git add -A
|
|
58
|
+
git commit -q -m "r-universe: fastglmm ${GITHUB_REF#refs/tags/r-}"
|
|
59
|
+
git push --force "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" HEAD:r-universe
|
|
60
|
+
env:
|
|
61
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
# One tag, one release: `v*` ships the crate to crates.io AND the Python package
|
|
4
|
+
# to PyPI from the same run. The Python package is a thin wrapper over the crate
|
|
5
|
+
# with no independent version or changelog, so it has no separate cadence.
|
|
6
|
+
#
|
|
7
|
+
# Both indexes use Trusted Publishing (OIDC) — no stored tokens.
|
|
8
|
+
#
|
|
9
|
+
# PREREQUISITES:
|
|
10
|
+
# - crates.io: crate exists and has a Trusted Publisher pointing at THIS repo +
|
|
11
|
+
# release.yml + the `release` environment. (No pending-publisher flow there,
|
|
12
|
+
# so 0.0.1 was published manually; this covers 0.0.2+.)
|
|
13
|
+
# - PyPI: a pending publisher for project `glmm` bound to this repo +
|
|
14
|
+
# release.yml + the same `release` environment (shared with crates.io —
|
|
15
|
+
# protection rules on it gate both uploads).
|
|
16
|
+
on:
|
|
17
|
+
push:
|
|
18
|
+
tags: ["v*"]
|
|
19
|
+
|
|
20
|
+
env:
|
|
21
|
+
CARGO_TERM_COLOR: always
|
|
22
|
+
|
|
23
|
+
jobs:
|
|
24
|
+
# Run the full CI suite (fmt + clippy + test + msrv + wasm + publish dry-run)
|
|
25
|
+
# against the tagged commit. Everything downstream is gated on this — a tag that
|
|
26
|
+
# fails any gate never reaches an index.
|
|
27
|
+
verify:
|
|
28
|
+
uses: ./.github/workflows/ci.yml
|
|
29
|
+
|
|
30
|
+
# The three versions are lockstep-coupled but otherwise enforced only by a
|
|
31
|
+
# comment in pyproject.toml. This makes drift a release-time failure.
|
|
32
|
+
version-check:
|
|
33
|
+
name: Verify tag matches all three versions
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/checkout@v4
|
|
37
|
+
- name: Check 3-way version lockstep
|
|
38
|
+
shell: bash
|
|
39
|
+
run: |
|
|
40
|
+
TAG="${GITHUB_REF#refs/tags/v}"
|
|
41
|
+
fail=0
|
|
42
|
+
check() { # $1 = found version, $2 = file it came from
|
|
43
|
+
if [ "$1" != "$TAG" ]; then
|
|
44
|
+
echo "::error::$2 has version '$1', tag is '$TAG'"; fail=1
|
|
45
|
+
fi
|
|
46
|
+
}
|
|
47
|
+
CRATE=$(grep -m1 '^version = ' Cargo.toml | cut -d'"' -f2)
|
|
48
|
+
PYCRATE=$(grep -m1 '^version = ' glmm-python/Cargo.toml | cut -d'"' -f2)
|
|
49
|
+
PYPROJ=$(grep -m1 '^version = ' python/pyproject.toml | cut -d'"' -f2)
|
|
50
|
+
check "$CRATE" Cargo.toml
|
|
51
|
+
check "$PYCRATE" glmm-python/Cargo.toml
|
|
52
|
+
check "$PYPROJ" python/pyproject.toml
|
|
53
|
+
[ "$fail" = 0 ] || exit 1
|
|
54
|
+
echo "All three at $TAG."
|
|
55
|
+
|
|
56
|
+
build_wheels:
|
|
57
|
+
name: Build & test wheel (${{ matrix.target }} on ${{ matrix.os }})
|
|
58
|
+
needs: [verify, version-check]
|
|
59
|
+
runs-on: ${{ matrix.os }}
|
|
60
|
+
strategy:
|
|
61
|
+
fail-fast: false
|
|
62
|
+
matrix:
|
|
63
|
+
# One abi3 wheel per platform (abi3-py310 → no per-Python matrix), built
|
|
64
|
+
# AND tested on its own hardware so no wheel ships untested. macOS is
|
|
65
|
+
# Apple Silicon only: the Intel macos-13 runner was retired Dec 2025 —
|
|
66
|
+
# Intel Macs fall back to the sdist.
|
|
67
|
+
# Linux is also built+tested by ci.yml's python job (via verify) —
|
|
68
|
+
# deliberate: that job gates every push, this one produces the release
|
|
69
|
+
# artifact with the manylinux tag. Don't deduplicate.
|
|
70
|
+
include:
|
|
71
|
+
- { os: ubuntu-latest, target: x86_64, manylinux: '2_28' }
|
|
72
|
+
- { os: macos-latest, target: aarch64 }
|
|
73
|
+
- { os: windows-latest, target: x64 }
|
|
74
|
+
steps:
|
|
75
|
+
- uses: actions/checkout@v4
|
|
76
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
77
|
+
- uses: actions/setup-python@v5
|
|
78
|
+
with:
|
|
79
|
+
python-version: '3.12'
|
|
80
|
+
# maturin-action, not cibuildwheel: pyproject's manifest-path reaches out to
|
|
81
|
+
# ../glmm-python, which depends on ... cibuildwheel mounts
|
|
82
|
+
# only the package dir, so those paths would not resolve; this mounts the
|
|
83
|
+
# whole workspace.
|
|
84
|
+
- name: Build wheel
|
|
85
|
+
uses: PyO3/maturin-action@v1
|
|
86
|
+
with:
|
|
87
|
+
target: ${{ matrix.target }}
|
|
88
|
+
# manylinux applies to the Linux entry only; 'auto' is a no-op on the
|
|
89
|
+
# macOS/Windows entries that omit it.
|
|
90
|
+
manylinux: ${{ matrix.manylinux || 'auto' }}
|
|
91
|
+
working-directory: python
|
|
92
|
+
args: --release --out dist
|
|
93
|
+
- name: Verify native extension + license are in the wheel
|
|
94
|
+
shell: bash
|
|
95
|
+
run: |
|
|
96
|
+
python - <<'PY'
|
|
97
|
+
import glob, sys, zipfile
|
|
98
|
+
wheels = glob.glob("python/dist/*.whl")
|
|
99
|
+
assert wheels, "no wheel built"
|
|
100
|
+
for whl in wheels:
|
|
101
|
+
names = zipfile.ZipFile(whl).namelist()
|
|
102
|
+
if not any("_native" in n for n in names):
|
|
103
|
+
sys.exit(f"::error::native extension (_native) missing from {whl}")
|
|
104
|
+
lic = [n for n in names if n.endswith("dist-info/licenses/LICENSE")]
|
|
105
|
+
if not lic:
|
|
106
|
+
sys.exit(f"::error::LICENSE missing from {whl}")
|
|
107
|
+
body = zipfile.ZipFile(whl).read(lic[0]).decode()
|
|
108
|
+
# python/LICENSE must stay a real copy, never a symlink to ../LICENSE:
|
|
109
|
+
# Git for Windows does not materialize symlinks without core.symlinks,
|
|
110
|
+
# which actions/checkout does not set, so the runner would embed the
|
|
111
|
+
# 10-byte string "../LICENSE" as the GPL text. This catches that.
|
|
112
|
+
if "GNU GENERAL PUBLIC LICENSE" not in body:
|
|
113
|
+
sys.exit(f"::error::LICENSE in {whl} is not the GPL text")
|
|
114
|
+
print(f" ok: {whl}")
|
|
115
|
+
PY
|
|
116
|
+
- name: Install the built wheel and run the suite
|
|
117
|
+
shell: bash
|
|
118
|
+
run: |
|
|
119
|
+
python -m pip install --upgrade pip
|
|
120
|
+
python -m pip install python/dist/*.whl
|
|
121
|
+
python -m pip install pytest
|
|
122
|
+
# Scratch dir + absolute tests path: `import glmm` must resolve to the
|
|
123
|
+
# installed wheel, never python/glmm/ in the source tree.
|
|
124
|
+
cd "$RUNNER_TEMP"
|
|
125
|
+
python -m pytest "$GITHUB_WORKSPACE/python/tests" -v
|
|
126
|
+
- uses: actions/upload-artifact@v4
|
|
127
|
+
with:
|
|
128
|
+
name: wheels-${{ matrix.os }}-${{ matrix.target }}
|
|
129
|
+
path: python/dist/*.whl
|
|
130
|
+
|
|
131
|
+
build_sdist:
|
|
132
|
+
name: Build & test sdist
|
|
133
|
+
needs: [verify, version-check]
|
|
134
|
+
runs-on: ubuntu-latest
|
|
135
|
+
steps:
|
|
136
|
+
- uses: actions/checkout@v4
|
|
137
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
138
|
+
- uses: actions/setup-python@v5
|
|
139
|
+
with:
|
|
140
|
+
python-version: '3.12'
|
|
141
|
+
# Plain maturin, not maturin-action: an sdist needs no manylinux container,
|
|
142
|
+
# so the action's container machinery buys nothing here. (The build only
|
|
143
|
+
# archives sources; the Rust toolchain above is for the install step below.)
|
|
144
|
+
- name: Build sdist
|
|
145
|
+
working-directory: python
|
|
146
|
+
run: |
|
|
147
|
+
python -m pip install "maturin>=1.9,<2.0"
|
|
148
|
+
maturin sdist --out dist
|
|
149
|
+
# The sdist is the only install path for Intel Macs and Linux aarch64, so it
|
|
150
|
+
# gets the same gate as the wheels: install it (a real source build — hence
|
|
151
|
+
# the Rust toolchain above) and run the suite.
|
|
152
|
+
- name: Install from the sdist and run the suite
|
|
153
|
+
shell: bash
|
|
154
|
+
run: |
|
|
155
|
+
python -m pip install python/dist/*.tar.gz
|
|
156
|
+
python -m pip install pytest
|
|
157
|
+
cd "$RUNNER_TEMP"
|
|
158
|
+
python -m pytest "$GITHUB_WORKSPACE/python/tests" -v
|
|
159
|
+
- uses: actions/upload-artifact@v4
|
|
160
|
+
with:
|
|
161
|
+
name: sdist
|
|
162
|
+
path: python/dist/*.tar.gz
|
|
163
|
+
|
|
164
|
+
publish-pypi:
|
|
165
|
+
name: Publish to PyPI
|
|
166
|
+
needs: [build_wheels, build_sdist]
|
|
167
|
+
runs-on: ubuntu-latest
|
|
168
|
+
environment: release # must match the PyPI trusted-publisher config
|
|
169
|
+
permissions:
|
|
170
|
+
id-token: write # mints the OIDC token PyPI verifies
|
|
171
|
+
steps:
|
|
172
|
+
- uses: actions/download-artifact@v4
|
|
173
|
+
with:
|
|
174
|
+
path: dist/
|
|
175
|
+
- name: Flatten artifacts
|
|
176
|
+
run: |
|
|
177
|
+
mkdir -p dist-flat
|
|
178
|
+
find dist/ \( -name '*.whl' -o -name '*.tar.gz' \) -exec cp {} dist-flat/ \;
|
|
179
|
+
ls -la dist-flat/
|
|
180
|
+
- name: Publish to PyPI
|
|
181
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
182
|
+
with:
|
|
183
|
+
packages-dir: dist-flat/
|
|
184
|
+
# No skip-existing, unlike the crates.io guard below: on the real index
|
|
185
|
+
# "file already exists" means the version was not bumped, and that must
|
|
186
|
+
# fail loudly.
|
|
187
|
+
|
|
188
|
+
# Ordered last, after PyPI: both uploads are irreversible, and this way a failing
|
|
189
|
+
# wheel never leaves a crate release stranded without its Python counterpart. If
|
|
190
|
+
# this job fails after PyPI succeeded, re-run THIS JOB alone — a full workflow
|
|
191
|
+
# re-run dies in publish-pypi on the already-uploaded files.
|
|
192
|
+
publish-crates:
|
|
193
|
+
name: cargo publish
|
|
194
|
+
needs: [publish-pypi]
|
|
195
|
+
runs-on: ubuntu-latest
|
|
196
|
+
timeout-minutes: 20
|
|
197
|
+
# Naming an environment lets you attach required-reviewer / branch protection
|
|
198
|
+
# to releases. It MUST match the environment set in the crates.io Trusted
|
|
199
|
+
# Publisher config (or remove it in both places).
|
|
200
|
+
environment: release
|
|
201
|
+
permissions:
|
|
202
|
+
# Required: lets the job request the OIDC identity token crates.io verifies.
|
|
203
|
+
id-token: write
|
|
204
|
+
steps:
|
|
205
|
+
- uses: actions/checkout@v4
|
|
206
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
207
|
+
- uses: Swatinem/rust-cache@v2
|
|
208
|
+
# cargo publish has no --skip-existing and hard-fails on an existing version,
|
|
209
|
+
# so re-running a tag after a transient failure would error instead of
|
|
210
|
+
# converging. This guard makes it a no-op instead.
|
|
211
|
+
- name: Skip if this version is already on crates.io
|
|
212
|
+
id: guard
|
|
213
|
+
shell: bash
|
|
214
|
+
run: |
|
|
215
|
+
TAG="${GITHUB_REF#refs/tags/v}"
|
|
216
|
+
# Sparse-index path for a 4-char crate name: /gl/mm/glmm. One JSON object
|
|
217
|
+
# per line, one per published version. `curl -f` so a 404 (crate absent)
|
|
218
|
+
# yields empty rather than an HTML body.
|
|
219
|
+
PUBLISHED=$(curl -sf https://index.crates.io/gl/mm/glmm | jq -r '.vers' || true)
|
|
220
|
+
if grep -qx "$TAG" <<<"$PUBLISHED"; then
|
|
221
|
+
echo "glmm $TAG already on crates.io — skipping publish."
|
|
222
|
+
echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
223
|
+
else
|
|
224
|
+
echo "skip=false" >> "$GITHUB_OUTPUT"
|
|
225
|
+
fi
|
|
226
|
+
- name: Authenticate to crates.io (Trusted Publishing)
|
|
227
|
+
if: steps.guard.outputs.skip == 'false'
|
|
228
|
+
id: auth
|
|
229
|
+
uses: rust-lang/crates-io-auth-action@v1
|
|
230
|
+
- name: Publish
|
|
231
|
+
if: steps.guard.outputs.skip == 'false'
|
|
232
|
+
run: cargo publish
|
|
233
|
+
env:
|
|
234
|
+
CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
|
glmm-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/target
|
|
2
|
+
/Cargo.lock
|
|
3
|
+
dhat-heap.json
|
|
4
|
+
python/venv/
|
|
5
|
+
__pycache__/
|
|
6
|
+
*.egg-info/
|
|
7
|
+
python/build/
|
|
8
|
+
python/.pytest_cache/
|
|
9
|
+
python/glmm/_native*.so
|
|
10
|
+
python/glmm/_native*.pyd
|
|
11
|
+
|
|
12
|
+
# parity: fixed-seed simulated inputs (regenerated by run.sh --prep; the freeze is
|
|
13
|
+
# the committed seed + prep script, not the bytes) and per-engine result JSONs
|
|
14
|
+
# (regenerated every run). Committed inputs: data_empirical/*.csv (lme4/nlme). Durable
|
|
15
|
+
# summaries live in parity/reports/ and parity/weights/ (not under results/).
|
|
16
|
+
# The flat *.csv fixtures under data_simulated/ are small and committed (cargo
|
|
17
|
+
# test's include_str! depends on them, incl. in CI); only the 429MB grid/
|
|
18
|
+
# campaign dataset is gitignored.
|
|
19
|
+
/parity/data_simulated/grid/
|
|
20
|
+
/parity/weights/data_simulated/
|
|
21
|
+
/parity/results/
|
|
22
|
+
/parity/weights/results/
|
glmm-0.1.0/CHANGELOG.md
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the `glmm` crate are recorded here. Format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
|
+
|
|
6
|
+
The Python package (`glmm` on PyPI) is versioned in lockstep with the crate and
|
|
7
|
+
shares these entries; Python-specific notes are called out where they differ.
|
|
8
|
+
|
|
9
|
+
## [Unreleased]
|
|
10
|
+
|
|
11
|
+
## [0.1.0] — 2026-07-16
|
|
12
|
+
|
|
13
|
+
First release of the Python package (`glmm` on PyPI), and the first crate
|
|
14
|
+
release since `0.0.2`. The breaking changes below are real breaks against the
|
|
15
|
+
published `0.0.2` — `ModelSpec` is now structure-only and the `mcpower` feature
|
|
16
|
+
is renamed `loop_advanced`. `0.0.3` was never published, so it is not a
|
|
17
|
+
migration source.
|
|
18
|
+
|
|
19
|
+
All four estimators are wired into the stable `fit` dispatch: OLS; GLM
|
|
20
|
+
(Gaussian, binomial logit/probit, Poisson, Gamma, negative binomial); LMM
|
|
21
|
+
(closed-form single-intercept + BOBYQA general); GLMM (dense and sparse-Z, all
|
|
22
|
+
families including NB), with AGQ (nAGQ > 1) for single scalar random effects.
|
|
23
|
+
Validated against R/lme4 and Julia/MixedModels.jl across a 23-rung dataset
|
|
24
|
+
parity manifest plus a 15-rung prior-weights harness.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **A factor's level order is no longer silently discarded.** `glmm::formula`
|
|
29
|
+
sorted every factor's levels lexicographically, so the treatment-contrast base
|
|
30
|
+
was whichever label sorted first, regardless of what the caller asked for. A
|
|
31
|
+
deliberately ordered categorical — `pd.Categorical(x, categories=["low",
|
|
32
|
+
"med", "high"])` — was refactored to base `"high"`, returning a different
|
|
33
|
+
coefficient for a different question with nothing in the output to reveal it.
|
|
34
|
+
`Column::Factor` now takes `{ levels, codes }`, so the caller states the order
|
|
35
|
+
and level 0 is the base. Python passes a `Categorical`'s
|
|
36
|
+
`categories`/`codes` through; a plain string column has no declared order and
|
|
37
|
+
is sorted by `Column::factor_from_labels` — the same lexicographic default as
|
|
38
|
+
R's `factor()`, now a default rather than an imposition.
|
|
39
|
+
- Python: a categorical of non-strings (`pd.Categorical([1, 2, 3])`) was
|
|
40
|
+
classified numeric and fit as one continuous slope instead of expanding to
|
|
41
|
+
dummies. Column classification now checks the dtype before sniffing values.
|
|
42
|
+
- Python: `summary()` printed `group 0` instead of the grouping's name, and its
|
|
43
|
+
per-term rows carried no labels — `Lowered::re_groups` was never carried
|
|
44
|
+
across the PyO3 shim. It now is, and `summary()` prints e.g. `Subject:` with
|
|
45
|
+
`(Intercept)`/`Days` rows.
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
|
|
49
|
+
- `glmm::formula` — the R-style formula frontend is now part of the crate,
|
|
50
|
+
behind the `formula` feature (on by default). `lower("y ~ x + (1|g)", &table,
|
|
51
|
+
family)` builds the kernel's inputs from a formula string and a data table.
|
|
52
|
+
Previously an unpublished companion crate, so it was unreachable for anyone
|
|
53
|
+
installing from crates.io.
|
|
54
|
+
- `default-features = false` gives the formula-free kernel, which links no
|
|
55
|
+
`regex` — the configuration for parse-once/fit-many hot paths.
|
|
56
|
+
- **`Fit::vcov`** — the full `p×p` fixed-effect covariance `Cov(β̂)`, on every
|
|
57
|
+
path. `Fit::se` is its diagonal and cannot answer anything about two
|
|
58
|
+
coefficients jointly, so a contrast, a confidence interval, or anything of
|
|
59
|
+
`vcov()`/`confint`/`glht`/`emmeans`'s shape needed off-diagonals that were
|
|
60
|
+
being computed and thrown away (GLMM) or never formed (OLS/GLM/LMM). It is
|
|
61
|
+
finite exactly where `se` is. Also on the Python `Fit`, as a `(p, p)` array.
|
|
62
|
+
- Python `Fit` gained `n_eval` (optimizer evaluation count), `deviance` (the
|
|
63
|
+
minimized criterion — **not** comparable across models, see the docs), and
|
|
64
|
+
`re_groups`. All three were already on the Rust `Fit`; none crossed the shim.
|
|
65
|
+
|
|
66
|
+
### Changed
|
|
67
|
+
|
|
68
|
+
- **Python: `theta=` is renamed `init_theta=`.** One call had two unrelated
|
|
69
|
+
parameters named `theta`: the negative-binomial shape seed and, inside
|
|
70
|
+
`warm_start={"theta": …}`, the random-effect Cholesky vector. The seed takes
|
|
71
|
+
the name R already uses for it (`MASS::glm.nb(init.theta=)`);
|
|
72
|
+
`warm_start["theta"]` is unchanged, matching lme4's `start=list(theta=)`.
|
|
73
|
+
- **Python: `targets=` is removed.** It exposed `FitOptions::target_indices`, a
|
|
74
|
+
performance knob for MCPower's hot path that leaves non-target SEs `NaN`. That
|
|
75
|
+
hot path drives the Rust surface directly, where the option is unchanged; no
|
|
76
|
+
Python caller wants `summary()` printing `NA` for standard errors it could
|
|
77
|
+
have computed.
|
|
78
|
+
- Python: the native call returns a dict keyed by field name rather than a
|
|
79
|
+
positional tuple. Internal, but it is why `re_groups`/`n_eval`/`deviance`
|
|
80
|
+
could go missing unnoticed.
|
|
81
|
+
|
|
82
|
+
### LMM cold start
|
|
83
|
+
|
|
84
|
+
#### Changed
|
|
85
|
+
|
|
86
|
+
- **LMM cold starts now use the unit-diagonal blind seed** (diagonal θ at 1,
|
|
87
|
+
off-diagonal vech entries at 0 — the lme4/MixedModels convention), on both
|
|
88
|
+
the dense (`fit_lmm`) and sparse (`fit_mle_sparse`) Gaussian paths. The
|
|
89
|
+
former start set *every* component to 1; on wide-slope designs (q ≥ 4 with
|
|
90
|
+
correlated slopes) that start funneled BOBYQA into a second-best local
|
|
91
|
+
optimum on 8 of 9 adjudicated grid cells (deviance gaps +0.23 to +57.4 vs
|
|
92
|
+
the best-known optima, now frozen as goldens under `parity/goldens/optima/`).
|
|
93
|
+
With the new seed the fitted optimum matches or beats MixedModels on all 9.
|
|
94
|
+
Intercept-only and uncorrelated-slope models have no off-diagonal
|
|
95
|
+
components and are bit-identical. Full-grid effect vs MixedModels on the
|
|
96
|
+
gaussian slope stratum: worse-than-MM cells drop 8 → 2 — the two
|
|
97
|
+
remaining are *new* coin-flips where the old start happened to hold the
|
|
98
|
+
best-known basin (`lmm_q6_g300p5_bal_base` +0.008,
|
|
99
|
+
`lmm_q8_g3000p5_bal_lowsnr` +2.03; goldens frozen for both). It also fixes
|
|
100
|
+
the dense-vs-sparse basin disagreement behind the `noz_sparse_grid_agrees`
|
|
101
|
+
cell-20 failure. Eval counts on affected wide-slope fits move both ways
|
|
102
|
+
(grid-wide gaussian-slope total −10%). The sparse non-Gaussian GLMM joint
|
|
103
|
+
seed already used this shape; the Gaussian paths now match it.
|
|
104
|
+
|
|
105
|
+
### Prior weights
|
|
106
|
+
|
|
107
|
+
#### Added
|
|
108
|
+
|
|
109
|
+
- **`FitOptions::weights`** — per-row prior (case) weights, lme4's `weights=`.
|
|
110
|
+
An aggregated binomial (y = success proportion, weight = trial count) now
|
|
111
|
+
fits directly — lme4's `cbind(s, m−s)` objective, which shares its argmin
|
|
112
|
+
(and so β/SE/varcomp) with the expanded-Bernoulli fit — letting the
|
|
113
|
+
`sim_sparse_binomial` parity rung fit its 240 aggregated rows instead of the
|
|
114
|
+
3,059-row Bernoulli expansion. Parity holds at unchanged tolerances; the
|
|
115
|
+
per-solve O(n·width²) cost collapses accordingly.
|
|
116
|
+
|
|
117
|
+
#### Changed
|
|
118
|
+
|
|
119
|
+
- `FitOptions.weights` now supported on all paths (was: sparse binomial GLMM
|
|
120
|
+
only); nAGQ>1 with weights rejected.
|
|
121
|
+
|
|
122
|
+
### Two-stage GLMM optimizer
|
|
123
|
+
|
|
124
|
+
#### Changed
|
|
125
|
+
|
|
126
|
+
- **GLMM fits now use a two-stage optimizer** (lme4's structure, Bates et al. 2015
|
|
127
|
+
§3): a fast θ-only search profiles the fixed effects β out per PIRLS iteration,
|
|
128
|
+
then a short joint (θ, β) polish on the exact Laplace objective warm-started from
|
|
129
|
+
it. The converged (θ̂, β̂) and all standard errors are unchanged up to optimizer
|
|
130
|
+
tolerance — the parity goldens hold at their existing tolerances — but the outer
|
|
131
|
+
evaluation count drops materially (roughly 2× fewer BOBYQA evaluations on the
|
|
132
|
+
grouseticks 3-crossed Poisson fixture). The prior single-stage joint solve remains
|
|
133
|
+
available as an internal A/B toggle. `Fit::n_eval` now includes stage-1
|
|
134
|
+
evaluations, so eval counts are not directly comparable to versions before this
|
|
135
|
+
change.
|
|
136
|
+
|
|
137
|
+
#### Added
|
|
138
|
+
|
|
139
|
+
- **PIRLS step-halving.** The inner penalized-IRLS loop now backtracks (halves the
|
|
140
|
+
step, up to 10 times) when a full Fisher-scoring step raises the penalized
|
|
141
|
+
deviance, hardening convergence on ill-scaled joint (u, β) steps; an exhausted
|
|
142
|
+
backtrack surfaces as the existing non-converged/NaN failure state.
|
|
143
|
+
|
|
144
|
+
### M3.5 — warm-start entry-split
|
|
145
|
+
|
|
146
|
+
The fit surface now separates model *structure*, optimizer *warm-start state*, and
|
|
147
|
+
method *knobs* into three distinct places (`docs/GLMM/api.md`, Layers A–C). The
|
|
148
|
+
stable `fit`/`fit_grouped` signatures are unchanged; the breakage is in the shapes
|
|
149
|
+
they consume.
|
|
150
|
+
|
|
151
|
+
#### Changed (breaking)
|
|
152
|
+
|
|
153
|
+
- **`ModelSpec` is structure-only.** Removed the method knobs `wald_se` and `nagq`
|
|
154
|
+
and every magnitude payload — a `ModelSpec` can no longer carry a start estimate.
|
|
155
|
+
- `ReStructure` and `Grouping` lost `tau_squared` and now hold
|
|
156
|
+
`slopes: Vec<ColumnId>` (the `SlopeTerm` struct, which bundled a column with its
|
|
157
|
+
variance/correlation magnitudes, is deleted along with the `re_correlation_*`
|
|
158
|
+
helpers).
|
|
159
|
+
- `Family::Gamma` lost its `dispersion: Option<f64>` payload;
|
|
160
|
+
`Family::NegativeBinomial` lost its `theta: Option<f64>` payload.
|
|
161
|
+
- **`FitOptions` gained the relocated knobs:** `wald_se`, `nagq`, and `dispersion`
|
|
162
|
+
(the Gamma fix-vs-estimate directive). All are defaulted — construct with
|
|
163
|
+
`..FitOptions::default()` (Wald SE `Hessian`, `nagq` 1 = Laplace, `dispersion`
|
|
164
|
+
`None` = estimate φ post-fit). `FitOptions` now implements `Default`.
|
|
165
|
+
- **The stable `fit`/`fit_grouped` cold-start the optimizer** — they no longer derive
|
|
166
|
+
a warm start from spec magnitudes; the kernels use their `THETA0` blind start. The
|
|
167
|
+
converged MLE is unchanged up to optimizer tolerance (start-independent), so the
|
|
168
|
+
oracle goldens stay green at their existing tolerances.
|
|
169
|
+
- **Cargo feature `mcpower` renamed to `loop_advanced`.** Capability-named rather
|
|
170
|
+
than consumer-named; still off by default, still the unstable scratch-explicit
|
|
171
|
+
loop-tier surface with no semver guarantees. The `cluster_theta_truth` re-export is
|
|
172
|
+
removed (truth-start magnitudes no longer live in `ModelSpec`).
|
|
173
|
+
|
|
174
|
+
#### Added
|
|
175
|
+
|
|
176
|
+
- **`StartValues { beta, theta }`** — the warm-start primitive (api.md Layer B): raw
|
|
177
|
+
optimizer state (`beta` = fixed-effect start, `theta` = RE Cholesky parameters),
|
|
178
|
+
not high-level variances. Exported `pub` only behind the `loop_advanced` feature;
|
|
179
|
+
the stable tier never takes it. Carries no `phi`/`nb_theta`: Gamma φ is profiled and
|
|
180
|
+
the GLMM neg-binomial θ search is a global bracket, so neither warm-starts anything
|
|
181
|
+
reachable through the loop surface.
|
|
182
|
+
|
|
183
|
+
#### Migration — MCPower pin-bump action
|
|
184
|
+
|
|
185
|
+
MCPower consumes a pinned published `glmm`, so this rename is not a live break. When
|
|
186
|
+
MCPower next bumps its pinned `glmm`:
|
|
187
|
+
|
|
188
|
+
- switch its feature selection `mcpower` → `loop_advanced`;
|
|
189
|
+
- build any spec-derived start as a raw `StartValues.theta` (column-major vech of the
|
|
190
|
+
RE Cholesky parameters) instead of relying on the removed `cluster_theta_truth` /
|
|
191
|
+
`ModelSpec` magnitude fields.
|