History
v0.1 stabilization and release assessment
This pass stabilizes the existing estimators. It adds no model families, backend interfaces, formula handling, or broader R compatibility. The core remains PHP-free; the extension converts ordinary arrays and returns complete, synchronous fits. The contracts, IDE stubs in the repository, and offline reference tests define the supported v0.1 surface.
What the release gate establishes
PHP=/path/to/php PHP_CONFIG=/matching/php-config ./scripts/check checks formatting, Clippy, all Rust tests, an optimized extension build/load, reflected native signatures/defaults, stubs, nine PHP suites, and every example. The validation record captures the exact source/fixture/binary hashes, executed commands and local software versions. No R, NumPy, SciPy or statsmodels is needed for statistical tests or application fitting. If Python is installed, the gate also checks provenance hashes. A macOS/Linux GitHub workflow runs the same command; a configured workflow is not evidence that those remote platforms have passed.
This workstation passed 49 Rust tests and nine PHP suites on Apple Silicon, Rust 1.99 and Homebrew PHP 8.5.10. Seven independent R/Python audit scripts were also rerun successfully, including the logistic-mixed integrated-likelihood checks. The additional published-data mode verifies all four Gaussian datasets against lme4, nlme, statsmodels and the current extension. There are 19 additional deterministic base R stress fits, checked in both Rust and PHP, alongside the original fixtures.
| Estimator | Supported structure | Offline reference properties | Independent development checks |
|---|---|---|---|
| OLS | Full-rank explicit design; positive prior weights; offsets | Coefficients, covariance/SE, t/p/CI, fitted/residuals, RSS, R², ML LL; intercept/no-intercept and zero-variance conventions | R lm and statsmodels OLS; weighted/offset R fixtures |
| Logistic | Binary or integer-trial grouped binomial; logit; offsets | Coefficients, inverse-information covariance, normal Wald inference, probability/response residuals, normalized LL, deviance/null deviance/AIC | R glm and statsmodels; weighted/offset R audit |
| Probit | Same binomial input with probit link | Same properties; expected-information covariance | R glm and statsmodels; weighted/offset R audit |
| Poisson | Integer counts; log link; positive prior weights; offsets | Coefficients, inverse-information covariance, Wald inference, means/residuals, normalized LL, deviance/null deviance/AIC | R glm and statsmodels; weighted/offset R audit |
| Fast Gaussian mixed | One grouping factor/random intercept, ML | Fixed inference, covariance, variances, conditional effects, fixed/conditional fits and residuals, LL/AIC | lme4, nlme and statsmodels on synthetic/published data |
| General Gaussian mixed | ML/REML; correlated/independent slopes; multiple crossed/nested terms; weights/offsets | Fixed covariance/inference, term covariance/effects, all fitted/residual vectors, LL/AIC/BIC, singular flags; four R optimizer references | lme4; sleepstudy statsmodels ML/REML; two native optimizers |
| Logistic mixed | One Gaussian random intercept; binary/grouped trials; offsets; Laplace/2–61 AGQ | All fixed inference, group modes/variance, probabilities/residuals, normalized LL/AIC/BIC; 28 same-setting lme4 fits | SciPy independent likelihood/QUADPACK integration, independently optimized cbpp ML, statsmodels GLM boundary reduction |
The reference manifest indexes software versions actually recorded in fixtures, generators and their hashes, input hashes, model/options sources, compared quantities and tolerance policies. It does not invent missing historical package versions or represent old outputs as regenerated. Fixture values and test sources remain authoritative. Older fixed-model fixtures record R’s version; generator code records family, formula, seed and controls.
Correctness fixes and compatibility notes
- Single-level logistic/probit now canonicalize accepted integer successes to
round(y*trials)/trials, matching the mixed fitter. Count tolerance is 1e-8. Tiny encoded values cannot create fictitious events; all-failure/all-success data still fail. Response residuals use canonical proportions. - Every binomial interface now enforces the previously mixed-only limit of 1,000,000 trials per row, including original rows subsequently dropped. This is a deliberate validation tightening: larger-count binomial likelihood arithmetic has not been independently verified.
- OLS rejects nonzero residuals whose squared norm/variance underflows, rather than returning RSS=0 and an infinite perfect-fit likelihood. Exact zero residuals retain the documented infinity/undefined-inference convention. Overflowed total/weighted fitted variation now fails explicitly instead of leaking a spurious undefined R²; this was reproduced with weighted outcomes around 10^155.
- GLMs preserve their ordinary zero-coefficient start. For extreme offsets (absolute logit/Poisson offset >20, probit >8) or Poisson counts >10^8, an explicit ones column enables a finite marginal intercept fallback. Poisson intercept/exposure arithmetic uses log-sum-exp. Tests include constant offsets ±1,000. This does not establish arbitrary extreme heterogeneous-offset support.
- Poisson log masses for counts at least 16 use deviance plus Stirling error, avoiding cancellation between large factorial/log-mean terms. Counts of 10^12 are checked against R; deviance keeps its existing expm1 calculation.
- Binomial final LL/deviance/AIC receive explicit range checks. Only rounding-level negative deviance is rounded to zero; a larger violation of saturated likelihood fails.
- Rust errors now carry
ErrorKind; PHP retains theExceptionclass and adds stable codes 1001–1008. Rust.0still exposes the message; direct tuple construction changes toStatsError::new(kind, message). - Successful result arrays gain diagnostics; existing top-level keys and PHP signatures/defaults remain. Strict consumers must allow the additional key. Missing-row warnings are still emitted once and also recorded in diagnostics. The two internal PHP-boundary
expectcalls were removed. ext-php-rs’s handler catches Rust unwinding panics, but allocation failure/process abort is not recoverable; no memory-safety guarantee is inferred from ordinary unit tests.
Stress tests and numerical limits
tests/fixtures/stress-r.json records each design, response, weight, offset, reference specification, R version and tolerance. It tests near collinearity, columns in different units, outcomes scaled by 10^-140 and 10^140, tiny positive residual variance, weighted/no-intercept estimation, rare outcomes, ±1,000 constant offsets, sparse Poisson outcomes and 10^12 counts. Known ±1,000 binary predictors with balanced non-extreme observations remain valid (R saturation warnings are recorded), and a no-intercept Poisson model tests known means down to exp(-500).
Absolute tolerances are expressed in response units: for scaled OLS estimates, SE/CI/fits/residuals they multiply response_scale, and for covariance its square. Dimensionless statistics and log likelihood are not rescaled. This avoids requiring relative accuracy in covariance entries whose mathematical value is zero. The nearly collinear fixture uses a disclosed 1e-6 relative tolerance; this does not relax the original, well-conditioned reference suite.
Existing tests retain exact rank deficiency, complete/quasi separation, nonfinite and invalid inputs, zero/invalid weights, exhausted budgets, legitimate variance boundaries, correlated slopes, nested/crossed and four-level Gaussian designs, ML/REML, predictor-unit invariance, and quadrature tail-weight stability. The new dense-limit test rejects q=513 before factorization.
Limitations remain:
Numerical inputs become f64, with a 53-bit significand. Integer-valued inputs beyond 2^53 can lose exactness during PHP-to-Rust conversion; arbitrary-precision count/design arithmetic is not supported. The stress checks do not establish accuracy for every representable count or input magnitude.
Full rank at floating-point precision does not imply well-conditioned inference. Separation detection is a direction certificate, not a complete linear-programming proof. Finite uncertain estimates can be valid; difficult problems may fail rank, line-search, arithmetic or iteration checks.
General Gaussian computation is dense: q≤512 and n(q+p+1)≤50,000,000, excluding additional temporary matrices and PHP arrays. These are guards, not a memory reservation or sparse-solver capability. No new general-mixed performance claim is inferred from the fast specialization.
General Gaussian convergence uses native optimizer termination/bounds and finite factors/outputs. A final gradient/Hessian convergence audit of variance parameters is not implemented; diagnostics do not fabricate one. Multi-start global optimality is not established. Use reference comparisons and start/optimizer sensitivity for difficult fits.
Gaussian inference uses plug-in GLS covariance and normal Wald tests, not Satterthwaite/Kenward–Roger corrections. REML criteria depend on the fixed design and should not compare models with different fixed-effect columns.
Logistic mixed inference uses joint observed information of the chosen approximate likelihood. Its score gate and positive Hessian are numerical checks, not approximation-accuracy proofs. Tau≤20, scaled beta bounds±100, one grouping term, integer trials and max61 AGQ remain explicit limits.
In the rare-event fixture, Laplace estimates group SD≈7.30 while 61 nodes give ≈3.36. Agreement with lme4 at one node does not establish accuracy. The integration notebook retains the independent error checks and explains grouped-binomial likelihood constants.
Missing values use listwise deletion; this is a data policy, not an assumption that missingness is ignorable or that deletion produces unbiased inference.
PHP contract gate
stubs/php-stats.php is for IDE/static-analysis indexing, not runtime inclusion. Generate it with the extension loaded:
php -n -d "extension=$STATS_EXTENSION" scripts/php-stubs.php
php -n -d "extension=$STATS_EXTENSION" scripts/php-stubs.php --checkThe generator reads actual native argument types/names/defaults via Reflection. tests/fixtures/php-api.json freezes those signatures and expected result keys; tests/stability.php fits every estimator and checks result keys and diagnostic aliases. Updating the API fixture requires an explicit --write-api, which should only follow compatibility review. Structured PHPDoc documents matrix vectors, group types, random terms and per-function return shapes. ext-php-rs also offers its official cargo-php stub workflow; our maintained declarations additionally include result-array shapes.
Performance preservation
The saved pre-audit binary was compared with the stabilized binary using the same inputs, runtime, five complete timed fits after warm-up, and single-thread environment. Every numerical comparison passed. Timings include PHP input conversion and full result materialization; JSON serialization is outside timed fitting. Whole-process RSS includes runtime/startup/serialization. There is one local measurement batch, so small percentage differences in millisecond fits are not evidence of regressions or speed improvements. No matrix backend or Gaussian square-root path was replaced.
Targeted investigation caught a 100,000-row Poisson regression during this pass: an unconditional marginal-intercept start took 61 iterations instead of six, with negligible estimate differences but a roughly 16-fold slowdown. It was restricted to extreme inputs where the zero start is unsafe. The final report also records iteration/evaluation counts. This preserves the existing ordinary iteration path and avoids treating a theoretically helpful start as an optimization.
from pathlib import Path
import json
import pandas as pd
from IPython.display import display
ROOT=next(p for p in [Path.cwd(),*Path.cwd().parents] if (p/'Cargo.toml').exists())
import sys
sys.path.insert(0,str(ROOT/'docs/figures'))
from bridge import render_figure
r=json.loads((ROOT/'docs/benchmarks/v01-preservation-results.json').read_text())
rows=[]
for c in r['cases']:
for label in ['baseline','v01']:
v=c['implementations'][label]
rows.append(dict(model=c['model'],observations=c['n'],implementation=label,
median_full_fit_seconds=v['median_fit_seconds'],
minimum_full_fit_seconds=v['min_fit_seconds'],
peak_process_MiB=v['peak_rss_bytes']/2**20,
numerical_agreement=c['numerical_agreement']))
t=pd.DataFrame(rows);display(t)
render_figure('preservation')
print('Measured binary hashes:',r['baseline_sha256'],r['extension_sha256'])| model | observations | implementation | median_full_fit_seconds | minimum_full_fit_seconds | peak_process_MiB | numerical_agreement | |
|---|---|---|---|---|---|---|---|
| 0 | ols | 100000 | baseline | 0.011617 | 0.011058 | 90.109375 | True |
| 1 | ols | 100000 | v01 | 0.011166 | 0.010576 | 90.078125 | True |
| 2 | logistic | 100000 | baseline | 0.049437 | 0.048514 | 98.546875 | True |
| 3 | logistic | 100000 | v01 | 0.050164 | 0.049780 | 103.765625 | True |
| 4 | probit | 100000 | baseline | 0.197393 | 0.196846 | 98.515625 | True |
| 5 | probit | 100000 | v01 | 0.198086 | 0.196919 | 100.109375 | True |
| 6 | poisson | 100000 | baseline | 0.038398 | 0.038262 | 92.359375 | True |
| 7 | poisson | 100000 | v01 | 0.032667 | 0.031978 | 104.359375 | True |
| 8 | lmm | 100000 | baseline | 0.014693 | 0.014335 | 105.453125 | True |
| 9 | lmm | 100000 | v01 | 0.014728 | 0.014269 | 105.562500 | True |
| 10 | lmm_fit | 1000 | baseline | 0.000952 | 0.000941 | 28.734375 | True |
| 11 | lmm_fit | 1000 | v01 | 0.000971 | 0.000951 | 28.703125 | True |
| 12 | logistic_mixed | 100000 | baseline | 0.770679 | 0.767073 | 125.109375 | True |
| 13 | logistic_mixed | 100000 | v01 | 0.773718 | 0.770516 | 122.718750 | True |
figures/v01-preservation.svg
Measured binary hashes: 97d30d44ec33fe9a1e3a579f6c938ffe055531aad1fd88bf573988a1715b45a2 1248e3e4e909b15f6df24f2fe3f2b58768c4c36858702bf8d3fae89ec37fa620
The full report records hashes, versions, samples, all errors, timings and memory. Reproducing a before/after comparison requires retaining the old compiled extension and using tests/benchmarks/run-v01.py --baseline /path/to/old/library. Historical R/Python benchmark reports remain historical measurements with their original hashes; they are not relabeled as this binary’s performance. Current R/Python correctness audits are separate from performance preservation.
Release readiness and remaining gates
The native functions, offline statistical suite and examples work on the tested macOS/PHP combination. This is a release candidate, not a claim of universal production readiness and not a published release.
Before distribution:
- Run the new CI on Linux and macOS and record results; exercise each PHP/SAPI, architecture and ZTS/NTS combination advertised as supported. PHP 8.3+ is the intended consumer range, but this pass executed PHP 8.5.10 CLI only.
- Preserve the root MIT LICENSE and the separate notices/provenance for bundled reference data and third-party assets when preparing distribution packages.
- Review diagnostics, input tightening and the retained numerical limits against application requirements. Confirm FPM headers/build ABI and restart workers after installation; a CLI load does not verify web-server deployment.
- Choose release metadata/distribution packaging and publish through the project’s release workflow. This pass does not create a tag, commit, push or package release.
No full R/lme4 compatibility promise is made. New families, inference methods, sparse general mixed computation and broader GLMM designs are outside this pass.
Review follow-up
A subsequent source review found obsolete exclusions in the contracts page and three PHP validation paths that emitted uncoded Exception instances. Those paths now use the same typed 1001 code for out-of-range binomial outcomes, invalid Poisson counts and invalid group IDs. The group parser is shared rather than duplicated. Regression tests check codes for logistic/probit, both simple mixed calls and general Gaussian mixed grouping, including invalid values on rows that would otherwise be dropped. Statistical algorithms and public signatures are unchanged.
The first GitHub CI run failed during macOS binding generation with incompatible Clang ARM builtin headers; Linux was cancelled by matrix fail-fast. The workflow now aligns Homebrew compiler, libclang and resource headers, and lets both platforms finish independently. This configuration change passes the full local gate with the aligned LLVM toolchain: 49 Rust tests, nine PHP suites and all seven examples. It still needs a successful remote run and is not Linux numerical verification. The original local validation record remains a dated snapshot.
An optional constrained first-order/curvature audit for general Gaussian variance parameters remains future work. Optimizer termination and verified stationarity are distinct; the current contract does not claim the latter. No strict stationarity option, prediction-only interface or new model family is introduced in this follow-up.
From phpstats/phit@76b22f0.