8000 Plan the recipe and its pH reagent together; add pH-up reagents (#74-#77) by rafatahmed · Pull Request #81 · rafatahmed/Flahax · GitHub
[go: up one dir, main page]

Skip to content

Plan the recipe and its pH reagent together; add pH-up reagents (#74-#77) - #81

Merged
rafatahmed merged 3 commits into
mainfrom
fix/coupled-ph-formulation
Oct 22, 2026
Merged

rafatahmed merged 3 commits into
mainfrom
fix/coupled-ph-formulation

Conversation

@rafatahmed
Copy link
Copy Markdown
Owner

Closes #74. Closes #75. Closes #76. Closes #77.

Problem (grower-path audit)

In RO water at target pH 5.8, none of the 26 shipped formulas got a usable pH plan:

Fix

ph_adjustment workflow stage / plan_coupled_ph(). The recipe and its pH reagent are planned together:

  1. Fit the recipe without pH reagents. Acids are not nutrient salts in this mode (recommend() uses phosphoric acid as a P source without regard to pH, driving feeds to pH 3 #76).
  2. Speciate the mix. If it is below the target pH it needs a base; above, an acid. Products in the other direction are reported as not_applicable.
  3. Solve the reagent dose to the target pH on the same equilibrium model.
  4. Count the reagent's nutrients as supply (N from nitric, P from phosphoric, K from the bases) and refit.
    • This repeats until recipe and dose agree, using a bracketed search on F(d) − d.
    • Plain repetition oscillated for phosphoric acid, and the recipe can switch products between nearby doses (one feed jumped between pH 6.87 and 7.90). In that case the nearer side is taken and its nutrients are rescored at the dose that recipe needs.
  5. Every candidate keeps its dose, contribution, recipe and target state, even when it is rejected. A missed target still gets a plan (A small target miss blocks acid planning even when the acid would close the gap #77).

reagent_dose(). One acid/base solve:

  • an acid adds its anion;
  • KOH adds K⁺ with implicit hydroxide;
  • KHCO₃ and K₂CO₃ add K⁺ with inorganic carbon (closed carbon boundary).

plan_acid_options now uses it. Results are bit-identical: I compared 16 formulas × 3 acids.

Catalogue (#75, owner decisions, sources on #75):

Product Source K Basis
Potassium Hydroxide (45% solution), new Sigma-Aldrich 417661; 1.456 kg/L 31.3594 % grade_typical
Potassium Bicarbonate, new Sigma-Aldrich 237205 39.053724 % identity_theoretical
Potassium Carbonate, now sourced Sigma-Aldrich 209619 56.580316 % (was 56.581, unsourced) identity_theoretical
  • All three are role: ph_reagent and are never selected by recommend().
  • catalogue_ph_reagents() returns ready reagent records for them.
  • The catalogue now has 32 products.
  • Acids stay user-supplied, with assay and density from the supplier certificate.

Evidence (same audit after the change)

  • Doses: all 62 applicable candidates get a dose at exactly pH 5.8. As an independent check, re-adding the dose to the recipe and recalculating pH from scratch reproduces 5.8 (tested).
  • pH-3 feeds: the five formulas that were at pH 3 now mix at 4.5–4.9 and receive a base (KOH, KHCO₃ or K₂CO₃).
  • Nutrient fit: equal or better for 20 of 26 formulas. The six that are "worse" were the pH-3 recipes: their 0.03–0.09% fits depended on phosphoric acid and were unusable. With potassium phosphates and a potassium base they miss nitrate by 3–9%, explained by unmetTargets.
  • Still rejected: every candidate is still rejected by the unchanged precipitation gate. Hydroxylapatite and MnHPO₄ are supersaturated in all 62. That is Positive SI of slow-forming phases (hydroxylapatite, MnHPO4) blocks every acid plan #78, next. Each rejection names the phases and keeps the dose.

Also

  • The mixed_target_ph_nitric PHREEQC fixture was recaptured with pinned PHREEQC 3.8.6, because it contains potassium carbonate (nitric dose −7.8e-6 relative). tools/phreeqc_evidence.py gains --case and now writes LF JSON.
  • The competitor harness excludes pH reagents, so Horticalc is not handed KOH as a nutrient.
  • Docs: a "Recipe and pH reagent together" section, the stage table, catalogue counts, and the CHANGELOG.

Known

The coupled plan calls recommend() several times per reagent: about 1–3 s per candidate, about 7 s per formula with five reagents. That's acceptable for a batch plan; it can be optimised later.

Tests

  • 391 tests pass with live PHREEQC (tests/test_ph_adjustment.py, 8 new).
  • Bases: each base reaches the target exactly.
  • Wrong direction: an acid on an acidic feed, or a base on an alkaline one, is refused.
  • Catalogue: the three bases are sourced, and recommend() never selects them.
  • Alkaline feed (Tomato):
    • phosphoric acid meets every target with its P counted;
    • nitric acid's N–Ca conflict is explained (±1.8%).
  • Acidic feed (Chilli): gets a base, and phosphoric acid is not used as a P source.
  • Unbalanced formula (Pepper): still gets a pH plan.
  • Rejections: each one names its phases and keeps the dose.

…75, #76, #77)

Grower-path audit: none of the 26 shipped formulas in RO water got a
usable pH plan. The recipe met N and P exactly and the acid then broke
those targets (#74); a feed below the target pH had no correction (#75);
phosphoric acid chosen as a P source drove five formulas to pH 3.0-3.4
(#76); and a 1% target miss blocked acid planning (#77).

- ph_adjustment workflow stage and plan_coupled_ph(): fit the recipe
  without acids as nutrient salts, solve the acid or base dose to the
  target pH on the same equilibrium model, count the reagent's nutrients
  as supply and refit until recipe and dose agree. A bracketed search on
  F(d) - d settles both slow oscillations and recipes that switch products
  between nearby doses; candidates keep their dose when rejected.
- reagent_dose(): one acid/base solve (acid anion or K+ with implicit
  hydroxide, KHCO3 and K2CO3 with closed inorganic carbon).
  plan_acid_options uses it with bit-identical results (16 formulas x 3
  acids compared).
- Catalogue: Potassium Hydroxide (45% solution) and Potassium Bicarbonate
  added; Potassium Carbonate linked to its sourced theoretical analysis
  (K 56.581 -> 56.580316 %, owner-approved). All three role ph_reagent,
  never selected by recommend(). Sigma-Aldrich 417661, 237205, 209619.
  catalogue_ph_reagents() returns their reagent records. 32 products.
- The mixed_target_ph_nitric PHREEQC fixture is recaptured with pinned
  PHREEQC 3.8.6 (it contains potassium carbonate; nitric dose -7.8e-6
  relative). tools/phreeqc_evidence.py gains --case and writes LF JSON.

Audit after the change: all 62 applicable candidates get a dose at
exactly pH 5.8 (re-adding it and recalculating reproduces the target),
the pH-3 feeds now mix at 4.5-4.9 and receive a base; every candidate is
still rejected by the unchanged precipitation gate (hydroxylapatite and
MnHPO4), which #78 addresses next.
The previous commit's message described this change, but it was not
included. write_text on Windows produced CRLF in expected.json, which is
stored byte-exact; the recaptured fixture was normalised by hand.
…d count

verify_distribution hard-coded 30 products, so the 32-product catalogue
failed CI on every Python version. The packaged count is now compared
with src/flahax/data/library.json.
@read-the-docs-community
Copy link
Copy Markdown

@rafatahmed
rafatahmed merged commit d3a0456 into main Oct 22, 2026
6 checks passed
@rafatahmed
rafatahmed deleted the fix/coupled-ph-formulation branch October 22, 2026 08:57
@rafatahmed rafatahmed self-assigned this Oct 23, 2026
rafatahmed added a commit that referenced this pull request Nov 7, 2026
Plan the recipe and its pH reagent together; add pH-up reagents (#74-#77)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
0