8000 Benchmark the authors' Irriblend-DSW optimiser, now MIT-licensed (#40) by rafatahmed · Pull Request #89 · rafatahmed/Flahax · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased

- Benchmark the authors' Irriblend-DSW optimiser in the competitor comparison (#40). Its authors added the MIT licence on 2026-10-20 (commit a046d5c), so the formulation track now imports their unmodified `irriblend_dsw_funs.py` (SHA-256 checked) and calls `ferti_eco_min`; through the same calls it reproduces all 97 water blends of their two published case-study outputs exactly. The independent LP after their method stays beside it, and every engine is also scored on macronutrients only, because Irriblend-DSW models no micronutrients. New snapshot 2026-10-22-competitors-1ade5b6: on the benchmark's fixed inputs (equal prices, non-binding EC cap) the optimiser returns 30 of 52 recipes, meets N, P, K and S in every one, and overshoots Mg or Ca as its lower-bound constraints allow; the comparison page explains each result. All other accuracy figures are unchanged from the previous snapshot.
- Rewrite the growers page as a task path (#80): enter the water analysis (with unit conversions, measured zeros and detection limits), choose the formula, run `flahax batch`, read the batch sheet; then a worked tomato example on the station 104 well blended with RO water (8% passes the 3 mmol/L Na and Cl limits, 9% is blocked), what to measure after mixing and when to call an agronomist. A test reruns the example and checks the page's numbers. `flahax batch --water-share PERCENT` blends the analysed water with ion-free water: nutrients and limits scale the laboratory values and the chemistry dilutes the measured-pH record (#84). Formulation and pH stages that need review now say why (incidental nutrients without a declared maximum; the chosen reagent's review findings and missed targets) instead of an empty reason.
- Fix `recommend()` raising 'NNLS did not reach optimality' when the source water alone exceeds one declared maximum while another is binding (#86), for example 9% of the station 104 well with Na and Cl maxima of 3 mmol/L. A ceiling the water already reaches cannot be met, so its Lagrange search grew the shift without bound (#68) until NNLS failed. Products carrying that ion are now left out of the fit directly, the infinite-multiplier limit the search was approaching, and the excess is reported in `limitViolations` as before. Over 312 formula, water-share and ceiling cases the recipes are identical wherever the old search succeeded, and the failing cases now equal the fit without chloride products.
- Concentration limits and nutrient targets now always use the laboratory water analysis; the charge-balanced water is used for chemistry only (#84). `balance_water_analysis` closes an analysis' charge on chloride or sodium, a modelling step, but its totals were passed on as the water: the nitric delivery stage rebuilt its nutrient rows and limits from them, `flahax batch` (#79) formulated on them, and the tomato example chose its blend on them. On Shomar et al. (2024) station 104 the model has 1158.5 mg/L chloride against 1280.4 measured, so the example reported 9% well water as within the 3 mmol/L chloride limit; on the laboratory value 9% is 115.2 mg/L and fails, and 8% (102.4 mg/L) is the largest passing share (boundary about 8.3%). Balancing on sodium raises sodium from 680 to 759 mg/L, so the error could also reject a passing water. Now `BalancedWaterAnalysis` keeps `analytical_totals` (the analysis as entered, defensively copied) beside the model, with its provenance, and `diluted(fraction)` blends it with ion-free water, scaling both and recomputing the blend's chemistry. The workflow accepts such a record as `equilibrium['water_balance']`, rejects contradictory water inputs, passes `analytical_water_totals` to `plan_equilibrium_delivery` (rows and limits on the analysis, dose on the chemistry), and reports laboratory and modelled water in `water_basis`. When the balancing alone would change a limit decision the plan is `requires_review` with the reason; a laboratory-basis exceedance always blocks, and a review never downgrades a block. The coupled pH planner's no-reagent-needed candidate now gets the same limit and saturation checks as dosed candidates (it skipped both), and the workflow rechecks the selected plan's limits. Balancing itself is now explicit: an analysis that already closes at its measured pH (within 0.0001% of its total charge) is used unchanged and needs no balance ion, so chloride-free waters no longer fail on the default chloride; `CO3-2` = 0 marks measured carbon-free water (omitted carbon is still `missing_carbon`); measured zeros and `below_detection` components are held fixed; the closure is `reconciled` only within a stated `balance_ion_uncertainty_percent` (else `reconciliation_out_of_bounds`), and without one it is `assumed` and the chemistry, and every plan built on it, asks for review. No other ion is tried automatically (`fixed_balance_ion`). `flahax batch` takes nutrients straight from the analysis file, accepts measured zeros, detection limits, a balance ion and its uncertainty, and gains `--limit SYMBOL=MG_PER_L`; undeclared limits are never assumed. The report shows laboratory, modelled and adjusted water, each limit with its decision basis, and the batch sheet the limits on the laboratory basis. The tomato example now blends 8% well water and every dependent number is recomputed; the publication example keeps its 1:3 blend, and only its printed blend chloride changes (290 -> 320 mg/L, the laboratory value). The grower-outputs page now gives the 5% imbalance screen (it said 10%).
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
49acd9ac121c1a2fdb1919fb28bff9e846ae849839d33e382f566f763ffea66f README.txt
f5b43f8084b3608c9b170670e3a18e3fd23e3d279749e185c499129607d75485 chemistry.json
6eb72b40bbe6ec591545f3bc7444727476d599a5142d5d6410bcfcc1ac2814ee ec.json
5dec105a3f53e7220bc401fbe5bc497687f989248c83e3ab9b2943d8bc3a10d4 environment.json
9c022688b8f198564706d8094af0b00dcd6a48491854a2f30c0e6a345cac6f97 formulation.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
FlahaX competitor comparison (#40)

Measured commit: 1ade5b666f5381b5fedcba2c1fce84fea784f261
Python 3.13.15 on Windows-11-10.0.26200-SP0

formulation.json FlahaX vs Horticalc v0.6.7 vs Irriblend-DSW (a046d5c) and an LP after its method
chemistry.json FlahaX vs live PHREEQC (PhreeqPython 1.6.2) on the golden fixtures
ec.json EC25 against IUPAC KCl primary standards, and catalogue agreement
environment.json interpreter, platform and the full package list of the run

Protocol: tools/competitors/README.md in the repository at the measured commit
(benchmarks/competitors/README.md before #64).
Timings are best of three on one machine; compare them only within this snapshot.
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
{
"base_sha256": "ab0a8f7c7375e1bd997990f4bc3a9af497516f10e57f4aaa3cefb55dccac5ae7",
"cases": [
{
"fixture": "products/ch_micro",
"replay_max_difference": 4.957811938766099e-12,
"flahax_passes": true,
"failed": [],
"checks": 112,
"phreeqpython_ms": 2.53800000064075,
"flahax_ms": 4.98650000008638
},
{
"fixture": "products/citrate",
"replay_max_difference": 4.796163466380676e-13,
"flahax_passes": true,
"failed": [],
"checks": 35,
"phreeqpython_ms": 1.175699999294011,
"flahax_ms": 0.6472000004578149
},
{
"fixture": "products/cu_edta",
"replay_max_difference": 5.051958851254312e-12,
"flahax_passes": true,
"failed": [],
"checks": 42,
"phreeqpython_ms": 1.7917000004672445,
"flahax_ms": 0.966800000242074
},
{
"fixture": "products/fe_dtpa",
"replay_max_difference": 8.572698106945609e-12,
"flahax_passes": true,
"failed": [],
"checks": 38,
"phreeqpython_ms": 1.2597999993886333,
"flahax_ms": 0.9644999990996439
},
{
"fixture": "products/fe_eddha",
"replay_max_difference": 4.710898338089464e-12,
"flahax_passes": true,
"failed": [],
"checks": 38,
"phreeqpython_ms": 1.2256000009074342,
"flahax_ms": 1.060600001437706
},
{
"fixture": "products/fe_edta",
"replay_max_difference": 4.192202140984591e-12,
"flahax_passes": true,
"failed": [],
"checks": 44,
"phreeqpython_ms": 1.7363000006298535,
"flahax_ms": 1.0812999989866512
},
{
"fixture": "products/mixed_products",
"replay_max_difference": 4.9684700798025005e-12,
"flahax_passes": true,
"failed": [],
"checks": 243,
"phreeqpython_ms": 3.9770999992470024,
"flahax_ms": 63.331299999845214
},
{
"fixture": "products/mixed_target_ph_nitric",
"replay_max_difference": 4.973799150320701e-12,
"flahax_passes": true,
"failed": [],
"checks": 275,
"phreeqpython_ms": 5.967299999610987,
"flahax_ms": 65.43210000018007
},
{
"fixture": "products/mn_edta",
"replay_max_difference": 4.654054919228656e-12,
"flahax_passes": true,
"failed": [],
"checks": 38,
"phreeqpython_ms": 1.619599999685306,
"flahax_ms": 1.110799999878509
},
{
"fixture": "products/zn_edta",
"replay_max_difference": 4.600764214046649e-12,
"flahax_passes": true,
"failed": [],
"checks": 42,
"phreeqpython_ms": 1.5081000001373468,
"flahax_ms": 1.0767000003397698
},
{
"fixture": "phases/calcite",
"replay_max_difference": 4.809486142676178e-13,
"flahax_passes": true,
"failed": [],
"checks": 24,
"phreeqpython_ms": 1.8359999994572718,
"flahax_ms": 1.4621999998780666
},
{
"fixture": "phases/calcium_phosphate",
"replay_max_difference": 4.46753745109163e-12,
"flahax_passes": true,
"failed": [],
"checks": 31,
"phreeqpython_ms": 1.5514999995502876,
"flahax_ms": 1.332699999693432
},
{
"fixture": "phases/ferrous_phases",
"replay_max_difference": 4.74997818855627e-12,
"flahax_passes": true,
"failed": [],
"checks": 39,
"phreeqpython_ms": 2.185499999541207,
"flahax_ms": 1.6439999999420252
},
{
"fixture": "phases/gypsum",
"replay_max_difference": 1.874503544358685e-09,
"flahax_passes": true,
"failed": [],
"checks": 24,
"phreeqpython_ms": 1.6620999995211605,
"flahax_ms": 1.176800000393996
},
{
"fixture": "phases/magnesium_salts",
"replay_max_difference": 4.618527782440651e-13,
"flahax_passes": true,
"failed": [],
"checks": 24,
"phreeqpython_ms": 1.6737000005377922,
"flahax_ms": 0.6296000010479474
},
{
"fixture": "phases/mixed_product_phases",
"replay_max_difference": 4.973799150320701e-12,
"flahax_passes": true,
"failed": [],
"checks": 225,
"phreeqpython_ms": 14.801999999690452,
"flahax_ms": 66.86640000043553
},
{
"fixture": "phases/struvite",
"replay_max_difference": 4.32720526077901e-12,
"flahax_passes": true,
"failed": [],
"checks": 28,
"phreeqpython_ms": 2.024199999141274,
"flahax_ms": 2.244100000098115
},
{
"fixture": "phases/trace_carbonates",
"replay_max_difference": 4.916955731459893e-12,
"flahax_passes": true,
"failed": [],
"checks": 65,
"phreeqpython_ms": 3.4868000002461486,
"flahax_ms": 4.26809999953548
},
{
"fixture": "phases/trace_hydroxides",
"replay_max_difference": 4.977351863999502e-12,
"flahax_passes": true,
"failed": [],
"checks": 65,
"phreeqpython_ms": 3.331199999593082,
"flahax_ms": 3.3172000003105495
},
{
"fixture": "phases/trace_phosphates",
"replay_max_difference": 4.884981308350689e-12,
"flahax_passes": true,
"failed": [],
"checks": 78,
"phreeqpython_ms": 4.512100000283681,
"flahax_ms": 5.594100000962499
},
{
"fixture": "phases/zero_stock",
"replay_max_difference": 2.9238833576528123e-12,
"flahax_passes": true,
"failed": [],
"checks": 24,
"phreeqpython_ms": 1.6790999998193001,
"flahax_ms": 0.6696000000374625
}
],
"merged_sha256": "cbd2685777a10e31a21f7ea48ab934a49e1b45804cccc7b59f40c1c5162a70cd"
}
Loading
Loading
0