Skip to main content
Glama
README.md
# water-chem-calc

Offline textbook water chemistry calculations with explicit units, visible working,
and stated limits. Use the same 20 formulas through the `wcc` CLI, Python, a local
browser page, or MCP. No API key, account, cloud calculation, or internet connection
is required after installation.

**For learning and estimates only.** This tool does not select equipment, recommend
chemical dosing, supply operating targets, or control a treatment plant.

![Local calculator with a worked pure-water example](docs/calculator.png)

## Install

Python 3.11 or newer is required. There are no native chemistry engines or external
services to install.

```bash
git clone https://github.com/GuoHaoyu1688/water-chem-calc.git
cd water-chem-calc
python3 -m venv .venv
. .venv/bin/activate
pip install -e '.[dev]'
```

For an existing environment: `pip install -r requirements.txt`. On Windows, use
`.venv\Scripts\activate` instead of the POSIX activation command.

## Quick start

```bash
wcc list --json
wcc calc ion_exchange.cycle_throughput --resin-volume 5m3 --capacity 800mol/m3 --ion-load 0.5mmol/L --json
wcc serve
```

The calculation produces approximately **8,000 m³** from `5 × 800 ÷ 0.5`.
The `mol` notation in this legacy example means **moles of charge equivalents**;
the result includes a warning. Prefer `--capacity 800eq/m3 --ion-load 0.5meq/L`.
For calcium, 1 mmol/L of Ca²⁺ is 2 meq/L, not 1 meq/L.

Open `http://127.0.0.1:8769` for the browser calculator. All assets are local. Each
formula has its own form and shows assumptions before submission and working,
references, and the disclaimer after calculation. The server must remain running;
this is an offline local application, not a standalone `file://` HTML calculator.

## Formula coverage

| Category | Formula IDs | Scope |
| --- | --- | --- |
| Units | `units.concentration`, `units.conductivity`, `units.hardness` | mg/L, mmol/L, meq/L, CaCO3 basis, ppm/ppb with density, conductance and hardness units |
| Conductivity | `conductivity.pure_water`, `conductivity.temperature_compensation`, `conductivity.ions` | 0–100 °C pure-water table, configurable linear compensation, dilute 25 °C ionic sum |
| Ammonia | `ammonia.from_concentration`, `ammonia.from_conductivity`, `ammonia.cation_exchange` | 25 °C ammonia-water equilibrium and a separate ideal NaCl → HCl exchange illustration |
| Ion exchange | `ion_exchange.cycle_throughput`, `ion_exchange.resin_volume`, `ion_exchange.regenerant_mass`, `ion_exchange.specific_consumption`, `ion_exchange.bed_geometry` | User-supplied working capacity, active regenerant mass, superficial velocity and geometry |
| Oxygen | `oxygen.saturation` | Air-equilibrated freshwater, 0–40 °C and 0.5–1.1 atm absolute |
| Saturation | `scaling.indices` | Simplified LSI, RSI and saturation pH; no corrosion-rate prediction |
| Balances | `balance.cycles`, `balance.cooling`, `balance.boiler`, `balance.ions` | Conserved-tracer ratio, cooling-water/boiler mass balances and signed ionic charge error |

Every result includes original inputs, normalized values, the expression,
substitution steps, values with units, assumptions, application bounds, references,
warnings and the educational-use disclaimer. Full input specifications are available
from `wcc explain FORMULA --json` and [the formula catalogue](docs/formulas.md).

## Units and examples

Quote quantities containing spaces. Compact forms such as `5m3`, `800eq/m3`,
`25degC`, and `10uS/cm` work; `µS/cm`, `μS/cm`, `°C`, kelvin and Fahrenheit are also
accepted where dimensionally appropriate. A dimensional quantity requires units.
A bare number is only valid for dimensionless inputs such as pH, charge and cycles.
Arithmetic expressions are not accepted as quantity values.

```bash
wcc calc conductivity.pure_water --temperature '25 degC' --json
wcc calc ammonia.from_concentration --concentration '1 mg/L' --json
wcc calc conductivity.ions --ions '{"Ca":"0.1 mmol/L","Cl":"0.2 mmol/L"}' --json
wcc calc units.concentration --value '40.078 mg/L' --molar-mass '40.078 g/mol' --charge 2 --target 'meq/L' --json
wcc calc balance.cooling --evaporation '100 m3/h' --cycles 5 --drift '2 m3/h' --json
wcc calc oxygen.saturation --temperature '25 degC' --pressure '1 atm' --json
```

Examples above use fictional quantities. Ammonia mass is reported **as NH3**, not
as nitrogen. Supported ion keys are `H`, `OH`, `NH4`, `Na`, `K`, `Ca`, `Mg`, `Cl`,
`SO4`, `HCO3`, and `NO3`. Ionic concentrations accept mol/L or mg/L; they are already
speciated inputs, not a general acid-base speciation model. Hardness units `dH`,
`fH`, `mmol/L`, `meq/L`, and `mg/L as CaCO3` are reporting conventions.

## CLI and data

All commands are noninteractive. `--json` writes one standard JSON envelope to
stdout; errors use `{"ok":false,"error":{"code":"...","message":"..."}}`.
Exit codes: 0 success, 1 runtime/storage failure, 2 invalid inputs or model range,
3 missing file/formula; an interrupted long-running server exits 130.

Global options can appear before or after the command: `--json`, `--quiet`,
`--version`, `--data-dir PATH`, `--config config.toml`. Configuration supports
`data_dir` and `timezone`; default timezone is `Asia/Shanghai`. Explicit `--data-dir`
overrides `WCC_DATA_DIR`, then config, then `./data`.

| Command | Purpose |
| --- | --- |
| `wcc list [--category NAME]` | List registered formula metadata |
| `wcc explain FORMULA` | Formula inputs, assumptions, ranges and references |
| `wcc calc FORMULA --INPUT VALUE ...` | Calculate and save local history |
| `wcc calc FORMULA --inputs '{...}'` | Supply a JSON input object; individual flags override keys |
| `wcc history` | List saved CLI calculations |
| `wcc export --format json\|csv --out PATH` | Export history; replaces the specified output file |
| `wcc import PATH` | Validate and merge a version-1 snapshot, skipping duplicate IDs |
| `wcc backup` | Create a dated SQLite ZIP under `data/backups/` |
| `wcc clear-history --yes` | Delete local history after making a backup |
| `wcc serve [--host 127.0.0.1] [--port 8769]` | Run the loopback-only browser calculator |
| `wcc mcp` | Start read-only MCP over stdio |

CLI calculations save to `data/calculations.sqlite`. Identical formula/input
objects share an ID and retain their first timestamp; different unit spellings
can have separate records. Python, browser and MCP calculations do **not** save
history. Snapshot JSON is `{schema_version:1, records:[...]}`; CSV has `id` and
`record_json` columns and a UTF-8 BOM. Imports recompute every result and reject
modified, incompatible or malformed records before writing anything. Backups
contain a consistent SQLite snapshot; see [AGENTS.md](AGENTS.md) for restoration.

## Python and MCP

```python
from water_chem_calc import ion_exchange, conductivity, calculate

result = ion_exchange.cycle_throughput("5m3", "800eq/m3", "0.5meq/L")
print(result["result"]["throughput"])
print(conductivity.pure_water("25degC")["steps"])
result = calculate("balance.ions", {"ions": {"Ca": "1mmol/L", "Cl": "2mmol/L"}})
```

Every formula is also importable from its category module. MCP uses the official
Python SDK and exposes `list_formulas(category?)`, `calculate(formula, inputs)`,
and `explain_formula(formula)`, all sharing the core engine. Example client config:

```json
{
  "mcpServers": {
    "water-chem-calc": {
      "command": "/workspace/apps/water-chem-calc/.venv/bin/python",
      "args": ["-m", "water_chem_calc.mcp_server"]
    }
  }
}
```

Replace the executable with the actual installation path. Tool schemas are in
[docs/mcp-tools.json](docs/mcp-tools.json); a headless-agent guide is in [AGENTS.md](AGENTS.md).

## References, limitations and extension

Sources are public textbook/literature values, including NIST, USGS, DOE, WRC and
peer-reviewed pure-water measurements. [Methods and reference notes](docs/methods.md)
explain the approximations and independent checks. Calculations never fetch sources.

- The pure-water table gives about 0.0551 µS/cm at 25 °C. Rounded tabular constants
  and interpolation are for estimates, not conductivity-meter calibration.
- Ammonia inversion is unique only on the supported branch above the model's
  pure-water baseline. A shallow minimum near neutral water makes lower readings
  ambiguous. Other salts invalidate ammonia-only inversion.
- Oxygen data exclude hot deaerators, salinity and high pressure. Linear conductivity
  compensation does not replace pure-water or ammonia temperature models.
- LSI/RSI are simplified carbonate indices. Passing the implementation's screening
  bounds does not validate a real sample or establish steam-cycle applicability.
- Resin working capacities and regeneration levels must be supplied by the user.
  Geometry, throughput and mass arithmetic do not constitute equipment design.
- No uncertainty propagation, real-water activity correction, dynamic process model,
  laboratory certification, or operating-control advice is supplied.

Register new formulas under `src/water_chem_calc/formulas/*.yaml`, implement them
in `core/calculations.py`, add a dispatcher entry and an importable category wrapper,
and add at least two independent hand checks plus invalid-input examples. The CLI
catalogue, input flags and browser forms read the YAML automatically. Constants are
bundled under `src/water_chem_calc/data/constants.yaml`; runtime `data/` remains
private and ignored. YAML never executes Python expressions or loads arbitrary plugins.

## FAQ

**Why does ppm require density?** ppm means mg/kg. It equals mg/L only when the
solution density is 1 kg/L; the default assumption is visible in the result.

**Why is 1 mmol/L calcium different from 1 meq/L?** Calcium is divalent; its charge
amount is twice its amount of ions. Conductivity tables already contain molar
conductivities, so charge is not multiplied a second time there.

**Why is a below-baseline ammonia reading rejected?** Replacing highly mobile H+
with NH4+ initially lowers conductivity by a small amount, creating two possible
ammonia concentrations for a narrow conductivity interval.

**Can I enter boiler oxygen at 150 °C?** No. That is outside this freshwater model.
The tool rejects extrapolation rather than inventing high-temperature validity.

## Verification and roadmap

```bash
ruff check .
ruff format --check .
pytest -q --cov=water_chem_calc
python scripts/rebuild_oxygen_table.py
```

Tests include two hand-checked examples per formula, unit errors, model boundaries,
water autoionization, ambiguous inversion, ionic charge closure, infeasible balances,
transactional import/export, backups, CLI, MCP and HTTP. CI targets Python 3.11/3.12
on Ubuntu. [Verification record](docs/verification.md) separates local checks from CI.

Future work: validated uncertainty reporting, additional ionic species with primary
references, and carefully bounded activity models. MIT license; see [LICENSE](LICENSE).