Repository navigation
Solve binding ceilings as constraints; test NNLS optimality per column (#68) - #69
Merged
Merged
Conversation
…lumn (#68) recommend() with the default ammonium limit was 2-5x slower than without it, even where the limit did not bind, and its recipes depended on the solver's starting point. Measured against SciPy on every fit: - NNLS stopped early. Its optimality test used one tolerance scaled by the whole matrix (max|A| |b|); a heavily weighted ceiling row inflated it for every column, hiding real descent directions (a zero product with a positive dual weight of 1.9e-6; doses off by up to 4.2e-3 g/L). Each column's dual weight is now compared with the rounding error of its own terms. This keeps the scale invariance #36 needed. - A binding ceiling was a 1024-weight penalty. That problem is so flat in double precision that doses are fixed only to ~2e-3 g/L, for SciPy too. The ceiling is now a constraint: a weight-8 row aimed at aim - shift, where the shift (the scaled Lagrange multiplier) is found by a bracketed secant/Illinois search; the excess is non-increasing in the shift and affine while NNLS keeps its active set. The matrix is the same across the search, so one Gram cache is shared. Every ceiling fit now matches SciPy to 1.4e-10 g/L and warm and cold starts agree to 7e-9 g/L. Over the 52 benchmark recipes recommend() is 34% faster with the default ammonium limit (4.20 -> 2.78 s) and unchanged with a 3 mmol/L Cl ceiling (3.77 -> 3.87 s); Benchmark v1's formulation workload drops from about 30 to 16 ms. Default pass counts are unchanged (34/52, 571/636); with the Cl ceiling 41 -> 42 recipes and 598 -> 599 targets; no limit violations. Benchmark v1 scorecards: NNLS 7/7, chemistry 21/21.
rafatahmed
added a commit
that referenced
this pull request
Oct 20, 2026
Hosted run 39515183621 on main at df0d30b (Benchmark v1, success; normal CI run 39515081885, success). Every correctness gate passes: 7/7 NNLS, 21/21 stored chemistry, 17/17 timing workloads; complete recipes 34/52. The unmerged 4dffdf8 snapshot predates #69 and is removed; its capture notes record why.
This was referenced Oct 20, 2026
rafatahmed
added a commit
that referenced
this pull request
Nov 7, 2026
Solve binding ceilings as constraints; test NNLS optimality per column (#68)
rafatahmed
added a commit
that referenced
this pull request
Nov 7, 2026
Hosted run 39515183621 on main at df0d30b (Benchmark v1, success; normal CI run 39515081885, success). Every correctness gate passes: 7/7 NNLS, 21/21 stored chemistry, 17/17 timing workloads; complete recipes 34/52. The unmerged 4dffdf8 snapshot predates #69 and is removed; its capture notes record why.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #68.
Problem
With the default ammonium limit (#54),
recommend()was 2–5× slower than without it, even where the limit did not bind. On the Benchmark v1 formulation workload it was 31.8 ms against 14.5 ms before #54 (same machine). Investigating it turned up a correctness issue as well: recipes under a binding ceiling depended on the solver's starting point.Root causes (each measured against SciPy NNLS on every fit)
10·max(m,n)·ε·max|A|·‖b‖. A heavily weighted ceiling row inflated it for every column, including columns that row does not touch. On the 585 ceiling fits of the benchmark, FlahaX left products at zero whose dual weight was still positive (up to 1.9e-6). Doses were up to 4.2e-3 g/L away from the optimum.Fix (pure Python)
cross[j] − Σ G[j][k]·x[k]is compared with the rounding error of its own terms,10·max(m,n)·ε·(Σ|a_rj·b_r| + Σ|G[j][k]|·x[k]). This keeps Investigate NNLS objective discrepancy for near-dependent columns #36's scale invariance (tested at scales 1e-8, 1, 1e8). The bound comes out of the same loop as the weights.aim − shift. The shift is the scaled Lagrange multiplier, and the aim is the limit less 1 ppm.limitViolations.nnls(..., gram=)shares one Gram cache.Evidence
main(4dffdf8)Tests
tests/test_ceiling_constraint.py:mainit returns x₂ = 0 instead of 1);main);recommend()docstring now describe ceilings as constraints. The API reference was regenerated.