Skip to content
Merged

Docs #61

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
2 changes: 1 addition & 1 deletion docs/examples/ca-f-fluorite.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
55 changes: 46 additions & 9 deletions docs/getting-started/running-an-analysis.md
Original file line number Diff line number Diff line change
Expand Up @@ -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())
```
96 changes: 77 additions & 19 deletions docs/guide/solutions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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))
Expand All @@ -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')
Expand All @@ -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})
Expand All @@ -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.
3 changes: 1 addition & 2 deletions phreeqpython/utility.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down
Loading