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
10 changes: 10 additions & 0 deletions ext/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
## External Dependencies

### IPhreeqc

The contents of the `iphreeqc-3.8.6-17100` folder are extracted from the IPhreeqc module ("Linux (any processor)")
downloaded from `https://www.usgs.gov/software/phreeqc-version-3`.

Specfically, at the time of writing (10/31/2025), the file
`https://water.usgs.gov/water-resources/software/PHREEQC/iphreeqc-3.8.6-17100.tar.gz`
(md5sum: `91d8485139b423d5f0382e9215d9aea1`)
6 changes: 5 additions & 1 deletion src/bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,11 @@ PYBIND11_MODULE(_bindings, m) {
.def("get_selected_output_column_count", &IPhreeqcWrapper::get_selected_output_column_count)
.def("get_value", &IPhreeqcWrapper::get_value)
.def("get_component_count", &IPhreeqcWrapper::get_component_count)
.def("get_component", &IPhreeqcWrapper::get_component);
.def("get_component", &IPhreeqcWrapper::get_component)
.def("set_dump_string_on", &IPhreeqcWrapper::set_dump_string_on)
.def("get_dump_string", &IPhreeqcWrapper::get_dump_string)
.def("set_log_string_on", &IPhreeqcWrapper::set_log_string_on)
.def("get_log_string", &IPhreeqcWrapper::get_log_string);


#ifdef VERSION_INFO
Expand Down
16 changes: 16 additions & 0 deletions src/iphreeqc_wrapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,22 @@ class IPhreeqcWrapper {
return GetComponent(id, i);
}

std::string get_dump_string() {
return GetDumpString(id);
}

int set_dump_string_on(int i) {
return SetDumpStringOn(id, i);
}

std::string get_log_string() {
return GetLogString(id);
}

int set_log_string_on(int i) {
return SetLogStringOn(id, i);
}

private:
int id;
};
Expand Down
5 changes: 2 additions & 3 deletions src/pyphreeqc/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from .interface import IPhreeqc, Var
from .core import Phreeqc

__all__ = [
"IPhreeqc",
"Var"
"Phreeqc"
]
94 changes: 94 additions & 0 deletions src/pyphreeqc/core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
from typing import Any
from pathlib import Path
from pyphreeqc._bindings import PyIPhreeqc
from pyphreeqc.var import Var
from pyphreeqc.solution import Solution


class Phreeqc:
def __init__(self, database: str = "phreeqc.dat", database_directory: Path | None = None):
self._ext = PyIPhreeqc()

if database_directory is None:
database_directory = Path(__file__).parent / "database"
self._ext.load_database(str(database_directory / database))

self._solutions: list[Solution] = []

# TODO: Is VAR the common denominator for most operations?
# Here we create one and modify it in operations instead of having
# the caller create new VARs per operation.
self._var: Var = Var()

self.output = PhreeqcOutput(self)

def __len__(self):
return len(self._solutions)

def __getattr__(self, item) -> None:
"""Delegate attribute access to the underlying PyIPhreeqc instance."""
if hasattr(self._ext, item):
return getattr(self._ext, item)
raise AttributeError(f"Phreeqc has no attribute '{item}'")

def add_solution(self, solution_dict: dict) -> None:
solution = Solution(solution_dict)
index = len(self)
self.run_string(f"""
SOLUTION {index}
{solution}
SAVE SOLUTION {index}
END
""")
self._solutions.append(solution)

def remove_solution(self, index: int) -> Solution:
self.run_string(f"""
DELETE
-solution {index}
""")
return self._solutions.pop(index)


class PhreeqcOutput:
def __init__(self, phreeqc: Phreeqc):
self._phreeqc = phreeqc

def __getitem__(self, item) -> Any:
if not isinstance(item, tuple):
item = (item,)
while len(item) < 2:
item += (slice(None),)

row_idx, col_idx = item

if isinstance(row_idx, slice):
row_indices = range(*row_idx.indices(self.shape[0]))
elif isinstance(row_idx, int):
row_indices = [row_idx]
else:
raise TypeError("Row index must be int or slice")

if isinstance(col_idx, slice):
col_indices = range(*col_idx.indices(self.shape[1]))
elif isinstance(col_idx, int):
col_indices = [col_idx]
else:
raise TypeError("Column index must be int or slice")

result = []
for row in row_indices:
row_values = []
for col in col_indices:
self._phreeqc._ext.get_value(row, col, self._phreeqc._var._var.var)
row_values.append(self._phreeqc._var.value)
result.append(
row_values if len(col_indices) > 1 else row_values[0])

if len(row_indices) == 1:
return result[0]
return result

@property
def shape(self) -> tuple[int, int]:
return self._phreeqc.get_selected_output_row_count(), self._phreeqc.get_selected_output_column_count()
111 changes: 0 additions & 111 deletions src/pyphreeqc/interface.py

This file was deleted.

6 changes: 6 additions & 0 deletions src/pyphreeqc/solution.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class Solution(dict):
# A solution is nothing but a dict of str: Any mapping
pass

def __str__(self):
return "\n".join(f"{k} {v}" for k, v in self.items())
49 changes: 49 additions & 0 deletions src/pyphreeqc/var.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from typing import Any
from pyphreeqc._bindings import PyVar, PY_VAR_TYPE, PY_VRESULT


class Var:
def __init__(self, value: Any | None = None):
self._var = PyVar()
self._var.var.type = PY_VAR_TYPE.TT_EMPTY
self.value = value # will invoke setter

@property
def value(self) -> Any:
match self._var.var.type:
case PY_VAR_TYPE.TT_EMPTY:
return None
case PY_VAR_TYPE.TT_ERROR:
return self._var.var.vresult
case PY_VAR_TYPE.TT_LONG:
return self._var.var.lVal
case PY_VAR_TYPE.TT_DOUBLE:
return self._var.var.dVal
case PY_VAR_TYPE.TT_STRING:
return self._var.var.sVal
case _:
raise RuntimeError("Unknown type")

@value.setter
def value(self, value) -> None:
# If we were previously holding a string, we need to free it by
# creating a new PyVar
if self._var.var.type == PY_VAR_TYPE.TT_STRING:
self._var = PyVar()

if isinstance(value, PY_VRESULT):
self._var.var.type = PY_VAR_TYPE.TT_ERROR
self._var.var.vresult = value
elif isinstance(value, int):
self._var.var.type = PY_VAR_TYPE.TT_LONG
self._var.var.lVal = value
elif isinstance(value, float):
self._var.var.type = PY_VAR_TYPE.TT_DOUBLE
self._var.var.dVal = value
elif isinstance(value, str):
self._var.var.type = PY_VAR_TYPE.TT_STRING
self._var.var.sVal = value
elif value is None:
self._var.var.type = PY_VAR_TYPE.TT_EMPTY
else:
raise RuntimeError("Unknown type")
Loading
Loading