8000 Ship the grower report, batch sheet and tank grouping in the package (#79) by rafatahmed · Pull Request #83 · 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

- Ship the grower outputs in the package (#79). They were example code only (#73), so an installed FlahaX gave growers JSON. New `flahax batch` command: one formula (packaged, or your targets) and one water (`ro` or a measured analysis, closed at its pH) to `report.html`, `batch-sheet.html` and `summary.json`, with the coupled pH plan (your acids from certificate values, plus the packaged bases). In Python, `summarize_batch`, `render_report` and `render_batch_sheet` do the same from any `plan_fertilizer_workflow` result; when a reagent is selected the sheet weighs the refitted recipe that counts its nutrients. `group_tanks` is the sourced default concentrate grouping: the Yara page 65 compatibility table (read with page 63) as three composition rules (calcium with phosphate, calcium with sulfate, magnesium with phosphate), which reproduce its twelve incompatible pairs exactly; micronutrients and chelates go to tank A with calcium and stay out of phosphate concentrates, and tank A is never acidified (owner rule). The tomato examples use it, so micronutrients no longer show as 'Not in table'; their acid text now reflects #78 (nitric needs review, phosphoric misses P). `default_compatibility_rules` gives `plan_stocks` the same rules as records. Stock solubility is still not checked: the outputs say so.
- Positive saturation indices of phases that do not form at feed conditions no longer reject pH and delivery plans (#78). FlahaX and live PHREEQC 3.8.6 agree on every sign at the coupled target states, so the indices are right; the question was which phases form. Hydroxylapatite (precursor pathway, magnesium-inhibited), goethite (forms slowly from ferrihydrite), anhydrous MnHPO4 (unmeasured constant, log K -25.4 from 1976 program data; not reported to form at 25 C or in dilute feeds) and vivianite (Fe(II) phosphate of reducing environments; the model holds Fe(II) fixed) now make a plan `requires_review`, each with its reason and sources; feeds containing iron(II) carry an advisory that the iron oxidizes and precipitates as Fe(III) phosphate. Brushite, monetite, gypsum, struvite, calcite, ferrihydrite and every other phase, and any allocated solid, still reject. One policy (`saturation_policy.py`) now serves the coupled pH plan, acid selection, equilibrium delivery and plan validation. In the 26-formula RO-water audit at pH 5.8, 21 formulas now get a usable pH plan (12 also meet every target), against none before; the 5 still rejected have monetite or brushite supersaturated. Owner decision and cited research summary on #78.
- Plan the recipe and its pH reagent together (#74, #76, #77) and add pH-up reagents (#75). The grower-path audit found that 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 at all (#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). The new `ph_adjustment` workflow stage (`plan_coupled_ph`) fits the recipe without acids as nutrient salts, solves the acid or base dose to the target pH on the same equilibrium model, counts the reagent's nutrients as supply and refits until recipe and dose agree (bracketed, so recipes that switch products between nearby doses still settle). `reagent_dose` is the shared acid/base solve; `plan_acid_options` now uses it with bit-identical results. In the same audit every applicable candidate (62) now gets a computed dose at exactly the target pH (re-adding it and recalculating from scratch reproduces pH 5.8), and the five pH-3 feeds now mix at 4.5-4.9 and receive a base. All 62 are still rejected by the unchanged precipitation gate (hydroxylapatite and MnHPO4 supersaturated in every case), which #78 addresses next; each rejection names the phases and keeps the dose. The catalogue gains Potassium Hydroxide (45% solution) and Potassium Bicarbonate, and Potassium Carbonate is linked to its sourced theoretical analysis (K 56.581 -> 56.580316 %, owner-approved), all three `role: ph_reagent` and never selected as nutrient salts; sources are Sigma-Aldrich 417661, 237205 and 209619 (#75). `catalogue_ph_reagents()` returns ready reagent records for them. The catalogue now has 32 products; the mixed_target_ph_nitric PHREEQC fixture was recaptured with the pinned PHREEQC 3.8.6 because it contains potassium carbonate (nitric dose -7.8e-6 relative).
- Record the 0.5.0 publication. The published 0.5.0 manual and changelog still carry the release-candidate wording ('not yet published', 'Unreleased'); they are corrected here, and the publishing procedure now has an explicit finalize step before tagging (#71).
Expand Down
113 changes: 113 additions & 0 deletions docs/site/guide/grower-outputs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Batch report, batch sheet and concentrate tanks

The installed package turns one formula and one water into three files: an HTML
report, a printable A4 batch sheet and a JSON summary. Everything is pure Python
and offline: the HTML has inline styles and charts, and no script, font or network
access. These are planning documents, not dosing instructions: measure EC and pH
after mixing.

## From the command line

```bash
flahax batch --formula "Tomato (Howard Resh)" --water ro --acid HNO3:0.60:1.37 --output tomato-batch
```

`flahax batch --list-formulas` lists the packaged formulas. `--crop` accepts text
that matches exactly one formula name and lists the matches otherwise. Use
`--targets my-targets.json` for your own mg/L targets (N_NO3 and N_NH4 as N).

| Option | Meaning | Default |
|---|---|---|
| `--water` | `ro` (no ions) or a water-analysis JSON file (below) | required |
| `--volume` | final batch litres | 1000 |
| `--ph` | target feed pH | 5.8 |
| `--ratio` | final litres per litre of concentrate | 100 |
| `--acid` | `FORMULA:MASS_FRACTION:DENSITY[:NAME]` from your certificate, for example `HNO3:0.60:1.37`; repeat for more | none: acids are never assumed |
| `--max-reagent-litres` | largest pH reagent volume per batch | volume / 200 |
| `--products` | JSON array of your own product records | the packaged catalogue |
| `--rules` | JSON array of extra `compatibilityRule` records | none |
| `--output` | a new directory; existing paths are never overwritten | required |

The packaged bases (potassium hydroxide 45 %, potassium bicarbonate, potassium
carbonate) are always evaluated, so a feed that mixes below the target pH gets a
base. The exit code is 0 for a ready or review plan, 3 for a blocked plan and 1
for an input error.

### Water analysis file

```json
{"id": "station-104", "measured_ph": 7.8,
"mg_per_litre": {"Ca": 244, "Mg": 100.8, "K": 44.2, "Na": 680, "Cl": 1280.4,
"S": 241.43, "N_NO3": 3.474, "HCO3": 196.9}}
```

Elements are mg/L of the element (S as S, P as P, N_NO3 and N_NH4 as N);
`HCO3` is bicarbonate in mg/L as HCO3. Iron is taken as Fe(III), the form in
aerated water. The analysis is closed at its measured pH on chloride
(`balance_water_analysis`), and an analysis more than 10 % out of charge balance
is refused. One litre of water is taken as one kilogram.

## What the files say

The plan status is one of:

| Status | Meaning |
|---|---|
| `ready` | Every target is met and the pH plan is accepted. |
| `requires_review` | Usable after review: a target is missed with its reason, or a review-class phase is supersaturated (see [saturation indices](water-to-delivery.md#saturation-indices-and-acceptance)). |
| `blocked` | Not usable as planned: a phase that forms at feed conditions is supersaturated, no reagent reaches the pH, or the chemistry could not be calculated. The reason is given. |

When a pH reagent is selected, the recipe to weigh is the refitted one that
counts the reagent's nutrients. The batch sheet lists what to weigh per tank,
the concentrate strength at the injection ratio, the reagent volume, the
warnings to read before mixing and blank lines for measured EC and pH.

## Concentrate tanks

The default grouping (`group_tanks`) follows Yara, *Fertilizantes para
fertirrigación*, page 65 mixing compatibility table, read with page 63. The
table's twelve "No" pairs are exactly three rules, applied to every product from
its elemental analysis:

| Keep apart | Why |
|---|---|
| calcium and phosphate | calcium phosphate precipitates in a concentrate (pp. 63, 65) |
| calcium and sulfate | gypsum precipitates in a concentrate (pp. 63, 65) |
| magnesium and phosphate | magnesium phosphate precipitates in a concentrate (p. 65) |

FlahaX rule (owner decision, #79): micronutrients and chelates go to **tank A**
with the calcium products, because their doses are too small to precipitate
there; they are kept out of phosphate concentrates, where manganese and iron
phosphates can form (#78). **Tank A is never acidified** (Yara p. 63: do not
acidify tanks with microelement chelates): dose acid on its own channel or into
another tank. pH reagents always have their own channel.

The table states compatibility, not solubility. Check that every concentrate
dissolves at its strength and temperature; `plan_stocks` does this with your
sourced solubility limits, and `default_compatibility_rules(products)` gives it
the same rules as records. Your own `compatibilityRule` records (`--rules`, or
`rules=` in Python) separate further products and override the micronutrient
rule.

## From Python

```python
from flahax import (VolumeLitres, catalogue_ph_reagents, load_library, plan_fertilizer_workflow,
render_batch_sheet, render_report, summarize_batch)

library = load_library()
products = library['salts']
targets = next(f['targets'] for f in library['formulas'] if f['name'] == 'Tomato (Howard Resh)')
nitric = dict(id='my-nitric', name='Nitric acid 60%', source='supplier certificate', revision='1',
recordedAt='2026-10-22T00:00:00Z', chemicalFormula='HNO3', massFraction=.60, densityKgPerL=1.37)
workflow = plan_fertilizer_workflow(
products, targets, {}, 5.8, final_volume=VolumeLitres(1000),
equilibrium=dict(water_totals={}, water_mass_kg=1000, water_analysis_id='RO', complete_analysis=True,
carbon_boundary='closed', temperature_c=25, phases=[]),
ph_adjustment=dict(products=[nitric] + catalogue_ph_reagents(), maximum_reagent_volume_litres=5))
summary = summarize_batch(workflow, products, title='Tomato', water_label='RO water')
open('report.html', 'w', encoding='utf-8').write(render_report(summary))
open('batch-sheet.html', 'w', encoding='utf-8').write(render_batch_sheet(summary))
```

`summary` is plain JSON-ready data with the same numbers as the two pages.
2 changes: 2 additions & 0 deletions docs/site/guide/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,8 @@ Each command also accepts its documented JSON object on stdin without
or phosphoric acid. EC is bounded manufacturer-anchor screening, not a
general concentrated-stock model. Equipment sizing gives flow/volume
requirements, not a manufacturer model or actuator commands.
For a grower report and printable batch sheet from one formula and one water,
use `flahax batch` (see [batch report, sheet and tanks](grower-outputs.md)).
See [site inputs and schemas](site-planning.md), [source-sheet review](source-sheet-review.md)
and [current development status](https://github.com/rafatahmed/Flahax/blob/main/docs/development-status.md).

Expand Down
1 change: 1 addition & 0 deletions docs/site/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ Install and first steps <guide/usage>
:hidden:

For growers and agronomists <growers>
Batch report, sheet and tanks <guide/grower-outputs>
```

```{toctree}
Expand Down
16 changes: 9 additions & 7 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,17 @@ python -m examples.tomato_report --output build/tomato-report
| Command | What it shows |
|---|---|
| `tomato_simple` | The shipped Howard Resh tomato formula in RO water: products and doses, a 1000 L batch, nutrient fit, the formula's ion balance, product identities and an EC estimate. The unacidified mix is pH 6.9, outside the 5.3-5.8 feed range, and the example says so. |
| `tomato_advanced` | A published brackish well (Shomar et al. 2024, station 104) with Dutch open-system tomato targets (De Kreij, Voogt and Baas). Steps: balance the laboratory analysis at its measured pH; choose the largest blend with desalinated water that keeps Na and Cl within 3 mmol/L (9% well water); formulate on it, showing why phosphoric acid is withdrawn as a P source (it drives the feed to pH 3.8); speciate the mix; evaluate nitric and phosphoric acid (both refused, with FlahaX's reasons) and compute the nitric dose to pH 5.5, which also closes the nitrate gap; estimate EC; group concentrates into tanks; size injection; and run three growth-stage formulas, each as its own batch. |
| `tomato_advanced` | A published brackish well (Shomar et al. 2024, station 104) with Dutch open-system tomato targets (De Kreij, Voogt and Baas). Steps: balance the laboratory analysis at its measured pH; choose the largest blend with desalinated water that keeps Na and Cl within 3 mmol/L (9% well water); formulate on it, showing why phosphoric acid is withdrawn as a P source (it drives the feed to pH 3.8); speciate the mix; evaluate nitric and phosphoric acid (nitric needs review of two review-class phases, phosphoric is refused for its P, with FlahaX's reasons) and compute the nitric dose to pH 5.5, which also closes the nitrate gap; estimate EC; group concentrates into tanks; size injection; and run three growth-stage formulas, each as its own batch. |
| `tomato_report` | `tomato-report.html`: both examples with charts and tables. `batch-sheet-simple.html` and `batch-sheet-advanced.html`: printable one-page grower sheets (water, what to weigh per tank, concentrate g/L, acid step, and measure-after-mixing lines). Offline HTML; print to PDF from the browser. |

Concentrate tanks are grouped with the **cited Yara compatibility table**
(`04_Fertilizer.pdf`, p. 65, read with p. 63) by `examples/tomato_tanks.py`; it
states compatibility, not solubility, and products it does not list are marked
"Not in table", never guessed. Example acid grades, the site figures for
injection sizing and the 1:100 ratio are illustrative: replace them with your
own certificates and site data, and measure EC and pH after mixing.
Concentrate tanks are grouped by the packaged default, `flahax.group_tanks`: the
cited Yara compatibility table (`04_Fertilizer.pdf`, p. 65, read with p. 63) as
three composition rules, with micronutrients and chelates in tank A beside the
calcium products and tank A never acidified. It states compatibility, not
solubility. Example acid grades, the site figures for injection sizing and the
1:100 ratio are illustrative: replace them with your own certificates and site
data, and measure EC and pH after mixing. For your own formula and water, the
installed command `flahax batch` writes the same kind of report and batch sheet.

From the repository root in PowerShell:

Expand Down
21 changes: 15 additions & 6 deletions examples/tomato_advanced.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,11 @@

Run from the repository root: python -m examples.tomato_advanced
"""
from flahax import (VolumeLitres, balance_water_analysis, calculate_ec_from_totals, load_library,
from flahax import (VolumeLitres, balance_water_analysis, group_tanks, calculate_ec_from_totals, load_library,
plan_acid_options, plan_catalogue_nitric_target, plan_fertilizer_workflow, recommend,
size_injection_requirements)
from flahax.product_conversion import MOLAR_MASS

from .tomato_tanks import group_tanks
from flahax.tank_grouping import ANALOGUE

# Shomar et al. (2024), Groundwater, doi:10.1111/gwat.13411, supplementary Table S1, station 104
# (Qatar). Measured pH 7.80; mg/L; CO3-2 is total inorganic carbon expressed as C.
Expand Down Expand Up @@ -73,6 +72,16 @@ def largest_blend(balanced):
return 0


def tank_table(salts, products):
"""The packaged default grouping (flahax.group_tanks) in the shape this report prints."""
grouping = group_tanks(salts, products)
return dict(source=grouping.source, separations=grouping.separations,
tanks={t.name: dict(products=[dict(name=p.name, analogue=ANALOGUE.get(p.name, 'by composition'),
gramsPerLitre=p.grams_per_litre, basis=p.basis)
for p in t.products], notes=list(t.notes))
for t in grouping.tanks})


def run():
library = load_library()
salts = library['salts']
Expand Down Expand Up @@ -128,7 +137,7 @@ def plan(products):
for label, state in (('mixed', mixed), ('acidified', nitric.target))}

# 6. Concentrate tanks from the cited compatibility table, then injection sizing.
tanks = group_tanks(recipe['salts'])
tanks = tank_table(recipe['salts'], salts)
channels = {name: dict(final_litres_per_stock_litre=INJECTION_RATIO, available_stock_litres=50)
for name in tanks['tanks']}
sizing = size_injection_requirements(channels=channels, **SITE)
Expand Down Expand Up @@ -176,7 +185,8 @@ def plan(products):
'Targets: De Kreij, Voogt and Baas (1999) tomato, open system; target pH 5.5 from the same table.',
'Phosphoric acid is withdrawn as a P source because, on this water, it drives the feed below the target pH.',
'Acid grades are EXAMPLES (60% HNO3 1.37 kg/L; 75% H3PO4 1.58 kg/L); use your certificate.',
'Tank grouping uses the cited Yara page 65 compatibility table; stock solubility is not verified.',
'Tank grouping is the packaged default (flahax.group_tanks): Yara page 63/65 rules, micronutrients and '
'chelates in tank A, which is never acidified. Stock solubility is not verified.',
'Site figures for injection sizing are illustrative.',
'Positive saturation indices mean a precipitation risk, not a predicted rate; measure after mixing.'])

Expand Down Expand Up @@ -214,7 +224,6 @@ def main():
print('\nConcentrate tanks (1:%d):' % r['injection_ratio'])
for name, tank in r['tanks']['tanks'].items():
print(f" Tank {name}: " + ', '.join(p['name'] for p in tank['products']))
print(' Not in the table:', ', '.join(r['tanks']['not_covered']))
print('\nStages on the same water:')
for s in r['stages']:
print(f" {s['stage']:<50} {'met' if s['feasible'] else 'missed'}; worst {s['worst_miss_percent']:.1f}%")
Expand Down
Loading
Loading
0