From 6636bbc28d3ce902e08f2a05b69d900726c31da0 Mon Sep 17 00:00:00 2001 From: Abel Heinsbroek Date: Thu, 20 Aug 2026 14:30:03 +0200 Subject: [PATCH 1/2] update docs, document adding/modifying/querying solutions --- docs/getting-started/running-an-analysis.md | 55 ++++++++++-- docs/guide/solutions.md | 96 +++++++++++++++++---- 2 files changed, 123 insertions(+), 28 deletions(-) diff --git a/docs/getting-started/running-an-analysis.md b/docs/getting-started/running-an-analysis.md index 6114a69..fd7b1a8 100644 --- a/docs/getting-started/running-an-analysis.md +++ b/docs/getting-started/running-an-analysis.md @@ -4,31 +4,68 @@ icon: lucide/play # Running an analysis -After [installing](installation.md) PhreeqPython, an analysis is: create an engine, add a solution, then query or change it. Nothing has to be listed in `SELECTED_OUTPUT` first. +After [installing](installation.md) PhreeqPython, an analysis is: create an engine, add a solution, query it, and remove it when you no longer need it. Nothing has to be listed in `SELECTED_OUTPUT` first. -???+ info "You can run this example" +???+ info "You can run these examples" - Click **Run** (or Ctrl+Enter). The first run loads Pyodide and can take a few seconds. + Click **Run** (or Ctrl+Enter), or **Run all** to execute every editor in order. The first run loads Pyodide and can take a few seconds. Editors on this page **share a session**, so later cells reuse `pp` and `solution`. -```pyodide session="analysis" height="14-22" install="../../wheels/phreeqpython-1.6.2-py3-none-any.whl" +## Create a PhreeqPython instance + +Import the package and construct a [`PhreeqPython`](../reference/phreeqpython.md#phreeqpython.PhreeqPython) object. That object is the engine: it loads a thermodynamic database (default `vitens.dat`) and keeps the numbered PHREEQC entities in memory — [solutions](../reference/solution.md#solution.Solution), [gases](../reference/gas.md#gas.Gas), and [equilibrium phases](../reference/equilibriumphase.md#equilibriumphase.EquilibriumPhase). + +```pyodide session="analysis" height="6-10" install="../../wheels/phreeqpython-1.6.2-py3-none-any.whl" from phreeqpython import PhreeqPython pp = PhreeqPython() +print('solutions in the engine', pp.get_solution_list()) +``` + +Every [`Solution`](../reference/solution.md#solution.Solution) (and gas or phase) you create is stored on this instance. Mixing, copying, and reacting always go through that same engine. + +!!! warning "Do not mix solutions from different instances" + + A `Solution` is a handle to a number *inside one engine*. `a + b` (and any other interaction) only works when both solutions belong to the same `PhreeqPython` instance. Creating a second instance starts a second, independent PHREEQC engine; solutions from one cannot be mixed, copied, or reacted with solutions from the other. + +Set `pp.ip.debug = True` (or [`PhreeqPython(debug=True)`](../reference/phreeqpython.md#phreeqpython.PhreeqPython.__init__)) to print the PHREEQC snippets before they are sent. + +## Add a solution + +[`add_solution`](../reference/phreeqpython.md#phreeqpython.PhreeqPython.add_solution) creates water in the engine from PHREEQC `SOLUTION` keywords (pH, units, element totals, …). PhreeqPython assigns a number automatically: the first solution is `0`, then `1`, `2`, and so on. The Python object is a handle to that numbered solution ([`solution.number`](../reference/solution.md#solution.Solution.number)). + +```pyodide session="analysis" height="12-20" install="../../wheels/phreeqpython-1.6.2-py3-none-any.whl" solution = pp.add_solution({ 'pH': 7, 'units': 'mmol/kgw', 'Na': 1, 'Cl': 1, }) -solution.add('KCl', 1) +print(solution) +print('number', solution.number) +print('solutions in the engine', pp.get_solution_list()) +``` + +Copies and mixtures also get a new number. `add_solution_simple` is for a known chemical composition (for example 1 mmol NaOH); see [Solutions](../guide/solutions.md). +## Query a solution + +After each calculation you can read [`Solution`](../reference/solution.md#solution.Solution) properties directly; [`pH`](../reference/solution.md#solution.Solution.pH), [`total`](../reference/solution.md#solution.Solution.total), speciation, saturation indices, etc. + +```pyodide session="analysis" height="8-14" install="../../wheels/phreeqpython-1.6.2-py3-none-any.whl" print('pH', solution.pH) -print('K mmol', solution.total('K')) +print('Na mmol', solution.total('Na')) print('Cl mmol', solution.total('Cl')) +print('SC', round(solution.sc, 1), 'uS/cm') ``` -That is the whole loop: `add_solution` creates water in the engine, `add` reacts KCl into it, and `pH` / `total` read properties back. +## Forget a solution + +PHREEQC keeps every numbered solution in the engine until you delete it. `add_solution`, [`copy()`](../reference/solution.md#solution.Solution.copy), and mixing (`+` / `*`) each allocate a new number, so a long-running model — especially a loop that copies solutions, or a kinetic rate function that makes a temporary copy — can accumulate unused solutions and grow in memory. -Set `pp.ip.debug = True` (or `PhreeqPython(debug=True)`) to print the PHREEQC snippets before they are sent. +Call [`forget()`](../reference/solution.md#solution.Solution.forget) when you are done with a solution. That sends a PHREEQC `DELETE` for its number. Use [`get_solution_list()`](../reference/phreeqpython.md#phreeqpython.PhreeqPython.get_solution_list) to see which numbers are still in the engine. -Next: the [Solutions](../guide/solutions.md) guide covers creating, querying, and changing solutions in more detail. The [Examples](../examples/index.md) tab has worked calculations you can run in the browser. +```pyodide session="analysis" height="6-10" install="../../wheels/phreeqpython-1.6.2-py3-none-any.whl" +print('before', pp.get_solution_list()) +solution.forget() +print('after', pp.get_solution_list()) +``` \ No newline at end of file diff --git a/docs/guide/solutions.md b/docs/guide/solutions.md index b0783c6..646ef40 100644 --- a/docs/guide/solutions.md +++ b/docs/guide/solutions.md @@ -4,24 +4,28 @@ icon: lucide/droplets # Solutions -A `Solution` is an aqueous composition that stays in the PHREEQC engine. You create it on a `PhreeqPython` instance, then query or change it in place. +A [`Solution`](../reference/solution.md#solution.Solution) is an aqueous composition that stays in the PHREEQC engine. You create it on a [`PhreeqPython`](../reference/phreeqpython.md#phreeqpython.PhreeqPython) instance, then query or change it in place. + +There are two ways to define the composition: a complete PHREEQC `SOLUTION` block, or a known chemical recipe. Both assign a solution number automatically. ???+ info "You can run these examples" Click **Run** (or Ctrl+Enter), or **Run all** to execute every editor in order. The first run loads Pyodide and can take a few seconds. Editors on this page **share a session**. -## Creating a solution +## Adding a complete solution + +Use [`add_solution`](../reference/phreeqpython.md#phreeqpython.PhreeqPython.add_solution) when you have an analytical composition, or you need PHREEQC `SOLUTION` options: pH, pe, units, density, charge balance, redox, or equilibrium with a phase. + +The dictionary is written as identifier lines in a [`SOLUTION`](https://water.usgs.gov/water-resources/software/PHREEQC/documentation/phreeqc3-html/phreeqc3-48.htm) data block. Keys and values are the same identifiers and data items as in the [PHREEQC manual](https://pubs.usgs.gov/tm/06/a43/): `temp`, `pH`, `pe`, `units`, `density`, `redox`, element names, `Alkalinity`, and so on. Values may be numbers or strings when PHREEQC needs extra tokens (`as HCO3`, `charge`, a phase name). -`add_solution_simple` takes salts as a reaction. `add_solution` uses PHREEQC `SOLUTION` keywords (pH, units, element totals, charge balance, …). +PhreeqPython assigns the solution number; do not put a number in the dictionary. -```pyodide session="solutions" height="14-22" install="../../wheels/phreeqpython-1.6.2-py3-none-any.whl" +```pyodide session="solutions" height="16-26" install="../../wheels/phreeqpython-1.6.2-py3-none-any.whl" from phreeqpython import PhreeqPython pp = PhreeqPython() -solution = pp.add_solution_simple({'CaCl2': 1.0, 'NaHCO3': 2.0}, temperature=15) - -seawater = pp.add_solution({ +solution = pp.add_solution({ 'units': 'ppm', 'pH': 8.22, 'temp': 25.0, @@ -33,14 +37,68 @@ seawater = pp.add_solution({ 'Alkalinity': '141.682 as HCO3', 'S(6)': 2712.0, }) -print('pH', round(solution.pH, 2), 'SC', round(solution.sc, 2)) +print(solution) +print('pH', round(solution.pH, 2), 'SC', round(solution.sc, 1)) +``` + +That call sends: + +``` +SOLUTION 0 + units ppm + pH 8.22 + temp 25.0 + Ca 412.3 + Mg 1291.8 + Na 10768.0 + K 399.1 + Cl 19353.0 + Alkalinity 141.682 as HCO3 + S(6) 2712.0 +SAVE SOLUTION 0 +END +``` + +Typical `SOLUTION` lines you can pass through the dictionary: + +- `units`: `mmol/kgw`, `ppm`, `mg/L`, … +- `pH` / `pe`: a number, `charge`, or equilibrium with a phase (`'pH': '8 Gibbsite'`) +- element totals: `'Ca': 1`, `'S(6)': 2712.0`, `'Al': '0.2 mg/kgw'` +- charge balance: `'Cl': 'charge'` +- phase equilibrium on an element: `'Al': '1e3 Gibbsite'` + +See the [SOLUTION keyword](https://water.usgs.gov/water-resources/software/PHREEQC/documentation/phreeqc3-html/phreeqc3-48.htm) in the PHREEQC Version 3 manual for the full identifier list and syntax. + +## Adding a simple solution + +Use [`add_solution_simple`](../reference/phreeqpython.md#phreeqpython.PhreeqPython.add_solution_simple) when the solution has a known chemical composition, for example a solution of 1 mmol NaOH. The dictionary keys are chemical formulas; the values are amounts. Optional arguments: `temperature` (default 25 °C) and `units` (default `mmol`; also `mol`, `mg`, or `ug`). + +PhreeqPython creates an empty `SOLUTION` at that temperature, then adds the formulas in a PHREEQC [`REACTION`](https://water.usgs.gov/water-resources/software/PHREEQC/documentation/phreeqc3-html/phreeqc3-40.htm) step. Formula names follow PHREEQC (case-sensitive), as described in the [PHREEQC manual](https://pubs.usgs.gov/tm/06/a43/). + +```pyodide session="solutions" height="8-14" install="../../wheels/phreeqpython-1.6.2-py3-none-any.whl" +naoh = pp.add_solution_simple({'NaOH': 1}) +print(naoh) +print('pH', round(naoh.pH, 2)) +print('Na mmol', round(naoh.total('Na'), 3)) +``` + +That call sends: + +``` +SOLUTION 1 +-temp 25 +REACTION 1 +NaOH 1.0 +1 mmol +SAVE SOLUTION 1 +END ``` -Pass `database='phreeqc.dat'` (or another bundled `.dat`) to `PhreeqPython(...)` when you need a different thermodynamic database. +Amounts are converted to mmol before the `REACTION`. Pass `units='mg'` (or another unit) when the dictionary is not already in mmol. ## Querying properties -After each calculation you can read bulk properties, totals, speciation, and saturation indices — no `SELECTED_OUTPUT` block. +After each calculation you can read bulk properties, totals, speciation, and saturation indices, with no `SELECTED_OUTPUT` block. ```pyodide session="solutions" height="10-16" install="../../wheels/phreeqpython-1.6.2-py3-none-any.whl" print('pH', round(solution.pH, 2)) @@ -52,20 +110,20 @@ print('SI Calcite', round(solution.si('Calcite'), 2)) print('HCO3- mmol', round(solution.species['HCO3-'], 4)) ``` -Useful accessors: +Useful accessors ([Solution](../reference/solution.md#solution.Solution)): -- `pH`, `pe`, `sc`, `temperature`, `mass`, `volume`, `density`, `I` -- `total(species)` / `total_element(element)` — amounts, default mmol -- `species`, `elements`, `phases` — dictionaries -- `si(phase)` / `sr(phase)` — saturation index and ratio +- [`pH`](../reference/solution.md#solution.Solution.pH), [`pe`](../reference/solution.md#solution.Solution.pe), [`sc`](../reference/solution.md#solution.Solution.sc), [`temperature`](../reference/solution.md#solution.Solution.temperature), [`mass`](../reference/solution.md#solution.Solution.mass), [`volume`](../reference/solution.md#solution.Solution.volume), [`density`](../reference/solution.md#solution.Solution.density), [`I`](../reference/solution.md#solution.Solution.I) +- [`total(species)`](../reference/solution.md#solution.Solution.total) / [`total_element(element)`](../reference/solution.md#solution.Solution.total_element): amounts, default mmol +- [`species`](../reference/solution.md#solution.Solution.species), [`elements`](../reference/solution.md#solution.Solution.elements), [`phases`](../reference/solution.md#solution.Solution.phases): dictionaries +- [`si(phase)`](../reference/solution.md#solution.Solution.si) / [`sr(phase)`](../reference/solution.md#solution.Solution.sr): saturation index and ratio ## Changing a solution -`add`, `remove`, `change`, `change_ph`, `change_temperature`, `saturate`, and `desaturate` update the same numbered solution in the engine. +[`add`](../reference/solution.md#solution.Solution.add), [`remove`](../reference/solution.md#solution.Solution.remove), [`change`](../reference/solution.md#solution.Solution.change), [`change_ph`](../reference/solution.md#solution.Solution.change_ph), [`change_temperature`](../reference/solution.md#solution.Solution.change_temperature), [`saturate`](../reference/solution.md#solution.Solution.saturate), and [`desaturate`](../reference/solution.md#solution.Solution.desaturate) update the same numbered solution in the engine. !!! warning "Changes are additive" - These methods change `solution` **in place**. If you run this cell again, another 1 mmol of NaOH is added. To start over, rerun **Creating a solution**. + These methods change `solution` **in place**. If you run this cell again, another 1 mmol of NaOH is added. To start over, rerun **Adding a complete solution**. ```pyodide session="solutions" height="10-16" install="../../wheels/phreeqpython-1.6.2-py3-none-any.whl" solution.add('NaOH', 1, 'mmol') @@ -81,7 +139,7 @@ print('T', solution.temperature) ## Mixing and copies -Scale and add solutions with `*` and `+`. `copy()` makes an independent duplicate; `forget()` removes a solution from the engine. +Scale and add solutions with [`*`](../reference/solution.md#solution.Solution.__mul__) and [`+`](../reference/solution.md#solution.Solution.__add__). [`copy()`](../reference/solution.md#solution.Solution.copy) makes an independent duplicate; [`forget()`](../reference/solution.md#solution.Solution.forget) removes a solution from the engine. ```pyodide session="solutions" height="10-16" install="../../wheels/phreeqpython-1.6.2-py3-none-any.whl" a = pp.add_solution_simple({'NaCl': 1}) @@ -94,4 +152,4 @@ print('copy SC', round(duplicate.sc, 1)) duplicate.forget() ``` -See the [API reference for Solution](../reference/solution.md) for every method, and the [Examples](../examples/index.md) for full calculations. +See the [API reference for Solution](../reference/solution.md) for every method, and the [Examples](../examples/index.md) for more examples of how to use PhreeqPython. From d4d607c32198b1f67a0c5961989446f44a51dc30 Mon Sep 17 00:00:00 2001 From: Abel Heinsbroek Date: Thu, 20 Aug 2026 14:44:40 +0200 Subject: [PATCH 2/2] update docs, fix weird bug relating F to Ni? --- docs/examples/ca-f-fluorite.md | 2 +- phreeqpython/utility.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/examples/ca-f-fluorite.md b/docs/examples/ca-f-fluorite.md index e840ab4..1621971 100644 --- a/docs/examples/ca-f-fluorite.md +++ b/docs/examples/ca-f-fluorite.md @@ -4,7 +4,7 @@ icon: lucide/beaker # Ca–F equilibrium with fluorite -The relation between fluoride and calcium in water, after [Appelo's fluorite example](http://hydrochemistry.eu/exmpls/ca_f.html). Calcite and fluorite stay at equilibrium while albite dissolves. +The relation between fluoride and calcium in water, after [Appelo's fluorite example](http://hydrochemistry.eu/exmpls/ca_f.html). ???+ info "You can run these examples" diff --git a/phreeqpython/utility.py b/phreeqpython/utility.py index 7baa3d7..0bfef95 100644 --- a/phreeqpython/utility.py +++ b/phreeqpython/utility.py @@ -1,8 +1,7 @@ from periodictable import formula as chemform def convert_units(formula, amount, from_units='mol', to_units='mmol'): - if formula == 'F' : - formula = 'Ni' + if from_units == to_units: return amount