8000 Make recommend() 2.8-4x faster in pure Python with identical recipes by rafatahmed · Pull Request #60 · rafatahmed/Flahax · GitHub
[go: up one dir, main page]

Skip to content

Make recommend() 2.8-4x faster in pure Python with identical recipes - #60

Merged
rafatahmed merged 2 commits into
mainfrom
fix/recommend-performance
Oct 20, 2026
Merged

rafatahmed merged 2 commits into
mainfrom
fix/recommend-performance

Conversation

@rafatahmed
Copy link
Copy Markdown
Owner

Fixes #59 within the owner's constraint: pure Python, zero runtime dependencies.

Root cause (measured)

NNLS was 97% of recommend() time: about 44 solves per call (simplification trials, ceiling refits, miss diagnosis). Each solve rebuilt a Householder least-squares factorisation over every row, including one ridge row per product, about 8-13 times, and recomputed the dual weights over the full matrix every iteration.

Changes (all pure Python)

  1. Warm-start NNLS in simplification trials. Each trial starts from the current solution without the removed product. Lawson-Hanson from a feasible start reaches the same optimum. Ceiling refits keep the cold start: warm-starting them was measured to reach the limit along a different weighting path (valid, but not the same trade-off, and slower), so it is not used there.
  2. Gram-matrix least squares. Cholesky on lazily cached column inner products, refined against the original columns only when a pivot shows weak conditioning. It falls back to Householder below a pivot floor, so rank decisions on ill-conditioned problems (the Investigate NNLS objective discrepancy for near-dependent columns #36 near-dependent case) are unchanged.
  3. Dual weights from the Gram cache over the passive columns.
  4. Diagnosis shares drop-one fits across missed targets instead of repeating them.

Evidence (52 benchmark cases, best of three alternating runs, same machine)

Configuration main This PR Speed-up
Default (ammonium limit) 247.8 ms 89.1 ms 2.8x
Cl ceiling 100 mg/L 297.3 ms 94.8 ms 3.1x
No ceilings 146.1 ms 36.4 ms 4.0x

Recipes are identical: across 156 recipe runs (3 configurations x 52 cases), no worse fit and no product-set change; the largest final-concentration change is 3e-11 mg/L. 357 tests pass (1 skipped), including the #36 near-dependent and scale tests.

Note for #40

The earlier 7.3 ms Horticalc figure is one compiled SciPy NNLS fit; FlahaX's recommend() is a pipeline (fit, simplify, recover, ceilings, diagnosis). #40 will time single fit against single fit and pipeline against pipeline.

…trix (#59)

Profiling recommend() over the 52 benchmark cases showed NNLS at 97% of
the time: about 44 solves per call, each rebuilding a Householder
factorisation over every row (including one ridge row per product)
about 8-13 times.

- _simplify warm-starts each trial refit from the current solution
  without the removed product. Lawson-Hanson from a feasible start
  reaches the same optimum in fewer steps. Ceiling refits keep the cold
  start: warm-starting them reached the limit along a different weight
  path (valid, but not the same trade-off).
- Passive-set least squares use Cholesky on a lazily cached Gram matrix
  with two refinement steps (corrected semi-normal equations), falling
  back to Householder when a pivot shows poor conditioning, so rank
  decisions on ill-conditioned problems are unchanged.

Mean recommend() time falls from 239 to about 136 ms (default ammonium
limit) and from 247 to about 77 ms with no ceilings. All 52 recipes are
unchanged (largest final concentration change 3e-11 mg/L, identical
product sets).
#59)

Dual weights A^T(b - Ax) now come from cached Gram entries over the
passive columns instead of a full matrix pass each iteration; Gram
solutions are refined only when a Cholesky pivot shows weak
conditioning; the unmet-target diagnosis computes each drop-one fit
once instead of once per missed target; the unused _matvec is removed.

Best of three alternating runs over the 52 benchmark cases against
main: 248 -> 89 ms (default), 297 -> 95 ms (Cl ceiling), 146 -> 36 ms
(no ceilings). Recipes are identical (largest final change 3e-11 mg/L).
@read-the-docs-community
Copy link
Copy Markdown

Documentation build overview

📚 Flahax | 🛠️ Build #35278755 | 📁 Comparing 0a4c558 against latest (2e9d1d1)

  🔍 Preview build  

1 file changed
± changelog.html

@rafatahmed
rafatahmed merged commit 50751e4 into main Oct 20, 2026
6 checks passed
@rafatahmed
rafatahmed deleted the fix/recommend-performance branch October 20, 2026 19:07
@rafatahmed rafatahmed self-assigned this Oct 23, 2026
rafatahmed added a commit that referenced this pull request Nov 7, 2026
Make recommend() 2.8-4x faster in pure Python with identical recipes
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

recommend() is about 30x slower than a comparable NNLS formulation solver

1 participant

0