efta 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- efta-1.0.0/CHANGELOG.md +22 -0
- efta-1.0.0/LICENSE +21 -0
- efta-1.0.0/MANIFEST.in +6 -0
- efta-1.0.0/PKG-INFO +404 -0
- efta-1.0.0/README.md +367 -0
- efta-1.0.0/efta/__init__.py +223 -0
- efta-1.0.0/efta/balance.py +431 -0
- efta-1.0.0/efta/errors.py +169 -0
- efta-1.0.0/efta/mixture.py +271 -0
- efta-1.0.0/efta/model/__init__.py +34 -0
- efta-1.0.0/efta/model/distribution.py +387 -0
- efta-1.0.0/efta/model/fitting.py +1278 -0
- efta-1.0.0/efta/model/freaction.py +789 -0
- efta-1.0.0/efta/model/ga.py +314 -0
- efta-1.0.0/efta/model/mass_action.py +204 -0
- efta-1.0.0/efta/model/suggest.py +536 -0
- efta-1.0.0/efta/periodic_table.py +412 -0
- efta-1.0.0/efta/plotting.py +719 -0
- efta-1.0.0/efta/py.typed +0 -0
- efta-1.0.0/efta/reaction.py +725 -0
- efta-1.0.0/efta/reactions.py +922 -0
- efta-1.0.0/efta/solution.py +1119 -0
- efta-1.0.0/efta/solventextraction/__init__.py +107 -0
- efta-1.0.0/efta/solventextraction/multistage.py +1245 -0
- efta-1.0.0/efta/solventextraction/sx.py +583 -0
- efta-1.0.0/efta/solventextraction/units.py +194 -0
- efta-1.0.0/efta/solver/__init__.py +78 -0
- efta-1.0.0/efta/solver/_shared.py +692 -0
- efta-1.0.0/efta/solver/dispatch.py +464 -0
- efta-1.0.0/efta/solver/find.py +255 -0
- efta-1.0.0/efta/solver/method_a.py +149 -0
- efta-1.0.0/efta/solver/method_b.py +203 -0
- efta-1.0.0/efta/solver/method_de.py +175 -0
- efta-1.0.0/efta/solver/method_l.py +257 -0
- efta-1.0.0/efta/species.py +838 -0
- efta-1.0.0/efta/styling.py +505 -0
- efta-1.0.0/efta/system.py +550 -0
- efta-1.0.0/efta.egg-info/PKG-INFO +404 -0
- efta-1.0.0/efta.egg-info/SOURCES.txt +42 -0
- efta-1.0.0/efta.egg-info/dependency_links.txt +1 -0
- efta-1.0.0/efta.egg-info/requires.txt +12 -0
- efta-1.0.0/efta.egg-info/top_level.txt +1 -0
- efta-1.0.0/pyproject.toml +68 -0
- efta-1.0.0/setup.cfg +4 -0
efta-1.0.0/CHANGELOG.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to efta are documented here.
|
|
4
|
+
|
|
5
|
+
## [1.0.0] — 2026-04-06
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Initial PyPI release.
|
|
9
|
+
- `reaction` and `reactions` classes for defining and solving coupled chemical equilibria.
|
|
10
|
+
- Four numerical solver methods: Method L (Newton in log-space), Method A (extent-of-reaction), Method B (fsolve variants), Method DE (differential evolution).
|
|
11
|
+
- `solution` and `mixture` classes for managing equilibrium results.
|
|
12
|
+
- Solvent extraction module (`efta.solventextraction`) with single-stage `sx` and multistage topologies (`countercurrent`, `crosscurrent`, `strip_countercurrent`, `strip_crosscurrent`).
|
|
13
|
+
- Activity coefficient support via `reaction.set_gamma()` and `reactions.set_gamma()`.
|
|
14
|
+
- Precipitation/Ksp reactions (`ksp=True`).
|
|
15
|
+
- Parameter fitting (`model`, `analyze`, `montecarlo`) with bootstrap uncertainty.
|
|
16
|
+
- `freaction` / `freactions` for parameterised reactions with `$(xN)` placeholders.
|
|
17
|
+
- Full periodic table (`periodic_table`) with IUPAC 2021 atomic masses.
|
|
18
|
+
- Plotting utilities: concentration sweeps, speciation fraction diagrams, stage profiles.
|
|
19
|
+
- `PlotStyle` singleton (`efta.style`) for consistent plot customisation.
|
|
20
|
+
- Styling helpers: `coloring()`, `randomize_color()`, built-in palettes including `'colorblind'`, `'ocean'`, `'earth'`.
|
|
21
|
+
- Complete error hierarchy (`EftaError`, `ConvergenceError`, `ConvergenceWarning`, …).
|
|
22
|
+
- `py.typed` marker — efta is fully typed.
|
efta-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 efta contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
efta-1.0.0/MANIFEST.in
ADDED
efta-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: efta
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Equilibrium Formulation and Thermodynamic Analysis — a Python library for solving chemical equilibrium problems
|
|
5
|
+
Author: efta contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Source Code, https://github.com/arsyadmdz/efta
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/arsyadmdz/efta/issues
|
|
9
|
+
Project-URL: Documentation, https://efta.readthedocs.io
|
|
10
|
+
Keywords: chemistry,equilibrium,thermodynamics,speciation,solvent extraction,chemical engineering,hydrometallurgy
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Intended Audience :: Education
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Chemistry
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Operating System :: OS Independent
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: numpy>=1.24
|
|
27
|
+
Requires-Dist: scipy>=1.10
|
|
28
|
+
Requires-Dist: matplotlib>=3.6
|
|
29
|
+
Provides-Extra: fitting
|
|
30
|
+
Requires-Dist: scipy>=1.10; extra == "fitting"
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
34
|
+
Requires-Dist: ruff; extra == "dev"
|
|
35
|
+
Requires-Dist: mypy; extra == "dev"
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
<p align="center">
|
|
39
|
+
<img src="https://raw.githubusercontent.com/arsyadmdz/efta/main/docs/_static/logo.png" width="200" alt="efta logo">
|
|
40
|
+
</p>
|
|
41
|
+
|
|
42
|
+
<h1 align="center">efta — Equilibrium Formulation and Thermodynamic Analysis</h1>
|
|
43
|
+
|
|
44
|
+
<p align="center">
|
|
45
|
+
<a href="https://pypi.org/project/efta/"><img src="https://badge.fury.io/py/efta.svg" alt="PyPI version"></a>
|
|
46
|
+
<a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/python-3.10+-blue.svg" alt="Python 3.10+"></a>
|
|
47
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
|
|
48
|
+
<a href="https://efta.readthedocs.io"><img src="https://readthedocs.org/projects/efta/badge/?version=latest" alt="Documentation"></a>
|
|
49
|
+
</p>
|
|
50
|
+
|
|
51
|
+
**efta** is a Python library for solving chemical equilibrium problems — from simple acid-base dissociation to complex multi-phase solvent extraction systems.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Features
|
|
56
|
+
|
|
57
|
+
- **Flexible reaction input** — define reactions as strings (`'Fe[3+] + 3OH[-] = Fe(OH)3(s)'`), dicts, or coefficient–name pairs
|
|
58
|
+
- **Multi-reaction systems** — solve coupled equilibria simultaneously using four built-in numerical methods
|
|
59
|
+
- **Speciation and precipitation** — handles both aqueous speciation (Ka, Kb, Kf) and solubility products (Ksp)
|
|
60
|
+
- **Solvent extraction** — single-stage and multistage counter-current / cross-current extraction circuits
|
|
61
|
+
- **Activity coefficients** — plug in Davies, Debye-Hückel, or any custom gamma function
|
|
62
|
+
- **Parameter fitting** — fit equilibrium constants to measured concentration data, with Monte Carlo uncertainty analysis
|
|
63
|
+
- **Plotting** — concentration profiles, speciation fraction diagrams, and extraction stage profiles via matplotlib
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Installation
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pip install efta
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Requires Python 3.10+ and NumPy, SciPy, matplotlib (installed automatically).
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Quick Start
|
|
78
|
+
|
|
79
|
+
### Acid-Base Equilibrium
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from efta import reaction, reactions
|
|
83
|
+
|
|
84
|
+
# Define individual equilibrium reactions with their equilibrium constants
|
|
85
|
+
acetic_acid = reaction('CH3COOH = CH3COO[-] + H[+]', 1.8e-5) # Ka of acetic acid
|
|
86
|
+
water_autoion = reaction('H2O = H[+] + OH[-]', 1e-14) # Kw of water
|
|
87
|
+
|
|
88
|
+
# Combine reactions into a system and solve
|
|
89
|
+
sys = reactions(acetic_acid, water_autoion)
|
|
90
|
+
|
|
91
|
+
# Provide initial concentrations (mol/L) for all species
|
|
92
|
+
c_eq = sys.equilibrium({
|
|
93
|
+
'CH3COOH': 0.1, # 0.1 M acetic acid
|
|
94
|
+
'CH3COO[-]': 0.0,
|
|
95
|
+
'H[+]': 1e-7, # neutral pH starting guess
|
|
96
|
+
'OH[-]': 1e-7,
|
|
97
|
+
'H2O': 1.0,
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
import numpy as np
|
|
101
|
+
print(f"pH = {-np.log10(c_eq['H[+]']):.2f}") # → pH ≈ 2.87
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Precipitation / Solubility
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from efta import reaction, reactions
|
|
108
|
+
|
|
109
|
+
# ksp=True tells efta this is a solubility product reaction
|
|
110
|
+
calcite = reaction('CaCO3(s) = Ca[2+] + CO3[2-]', 3.36e-9, ksp=True)
|
|
111
|
+
|
|
112
|
+
sys = reactions(calcite)
|
|
113
|
+
c_eq = sys.equilibrium({'CaCO3(s)': 1.0, 'Ca[2+]': 0.0, 'CO3[2-]': 0.0})
|
|
114
|
+
|
|
115
|
+
print(f"[Ca²⁺] = {c_eq['Ca[2+]']:.2e} mol/L")
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Concentration Sweeps and Plotting
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
import numpy as np
|
|
122
|
+
|
|
123
|
+
# Sweep initial [CH3COOH] from 1e-4 to 1.0 M on a log scale
|
|
124
|
+
fig, ax = sys.plot(
|
|
125
|
+
{'CH3COOH': [1e-4, 1.0], 'H[+]': 1e-7, 'OH[-]': 1e-7, 'H2O': 1.0},
|
|
126
|
+
sweep='CH3COOH',
|
|
127
|
+
logx=True,
|
|
128
|
+
logy=True,
|
|
129
|
+
n_points=60,
|
|
130
|
+
)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Species Notation
|
|
136
|
+
|
|
137
|
+
efta uses a compact notation for chemical species:
|
|
138
|
+
|
|
139
|
+
| Notation | Meaning | Example |
|
|
140
|
+
|---|---|---|
|
|
141
|
+
| `Fe[3+]` | Fe with charge 3+ | ferric iron |
|
|
142
|
+
| `OH[-]` | hydroxide | |
|
|
143
|
+
| `Fe(OH)3(s)` | solid phase | ferric hydroxide precipitate |
|
|
144
|
+
| `H2A2(org)` | organic phase | di-2-ethylhexylphosphoric acid |
|
|
145
|
+
| `e[-]` | electron | for redox reactions |
|
|
146
|
+
| `$(1/3)` | fractional coefficient | `$(1/3)Fe[3+]` |
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Reaction Construction
|
|
151
|
+
|
|
152
|
+
Four equivalent ways to define the same reaction:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from efta import reaction
|
|
156
|
+
|
|
157
|
+
# 1. String (most readable)
|
|
158
|
+
r = reaction('Fe[3+] + 3OH[-] = Fe(OH)3(s)', 1e3)
|
|
159
|
+
|
|
160
|
+
# 2. Stoichiometry dict (negative = reactant, positive = product)
|
|
161
|
+
r = reaction({'Fe[3+]': -1, 'OH[-]': -3, 'Fe(OH)3(s)': 1}, 1e3)
|
|
162
|
+
|
|
163
|
+
# 3. Separate reactant and product dicts
|
|
164
|
+
r = reaction({'Fe[3+]': 1, 'OH[-]': 3}, {'Fe(OH)3(s)': 1}, 1e3)
|
|
165
|
+
|
|
166
|
+
# 4. (coefficient, name) pairs — last argument is K
|
|
167
|
+
r = reaction((-1, 'Fe[3+]'), (-3, 'OH[-]'), (1, 'Fe(OH)3(s)'), 1e3)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Combining Reactions
|
|
171
|
+
|
|
172
|
+
Reactions can be added and scaled. K values update automatically:
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
# Adding two reactions combines their stoichiometries; K values multiply
|
|
176
|
+
r_combined = r1 + r2
|
|
177
|
+
|
|
178
|
+
# Scaling multiplies all coefficients; K is raised to that power
|
|
179
|
+
r_half = r1 / 2 # divide all coefficients by 2 → K becomes √K
|
|
180
|
+
r_rev = r1 * -1 # reverse the reaction → K becomes 1/K
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## The `reactions` System
|
|
186
|
+
|
|
187
|
+
A `reactions` object holds multiple coupled reactions and provides the solver interface:
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
from efta import reaction, reactions
|
|
191
|
+
|
|
192
|
+
sys = reactions(r1, r2, r3)
|
|
193
|
+
|
|
194
|
+
# --- Solve for equilibrium concentrations ---
|
|
195
|
+
c_eq = sys.equilibrium({'Fe[3+]': 0.01, 'OH[-]': 1e-7, ...})
|
|
196
|
+
|
|
197
|
+
# --- Inspect which species are in the system ---
|
|
198
|
+
print(sys.species) # frozenset of all species names
|
|
199
|
+
print(sys.aqueous_species) # aqueous species only
|
|
200
|
+
print(sys.organic_species) # organic-phase species only
|
|
201
|
+
|
|
202
|
+
# --- Plot a concentration sweep ---
|
|
203
|
+
fig, ax = sys.plot({'Fe[3+]': [1e-5, 0.1], ...}, sweep='Fe[3+]', logx=True)
|
|
204
|
+
|
|
205
|
+
# --- Inverse solve: find initial [X] that gives a target equilibrium ---
|
|
206
|
+
c_target = sys.find(
|
|
207
|
+
unknown='NaOH',
|
|
208
|
+
c0={'NaOH': 0.0, 'H[+]': 1e-7, ...},
|
|
209
|
+
target={'H[+]': 1e-8}, # target pH 8
|
|
210
|
+
)
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## The `solution` Class
|
|
216
|
+
|
|
217
|
+
A `solution` pairs a concentration dict with a volume, and provides convenient access methods:
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
from efta import solution
|
|
221
|
+
|
|
222
|
+
sol = solution({'H[+]': 1e-4, 'OH[-]': 1e-10, 'H2O': 1.0}, volume=0.5)
|
|
223
|
+
|
|
224
|
+
sol['H[+]'] # 1e-4 — species concentration in mol/L
|
|
225
|
+
sol.pH # 4.0 — convenience property
|
|
226
|
+
sol.ionic_strength # mol/L
|
|
227
|
+
sol.moles('H[+]') # mol = concentration × volume
|
|
228
|
+
sol.mass('H2O') # g = moles × molar mass
|
|
229
|
+
|
|
230
|
+
sol.aqueous # dict of aqueous-phase species only
|
|
231
|
+
sol.organic # dict of organic-phase species only
|
|
232
|
+
sol.solid # dict of solid-phase species only
|
|
233
|
+
|
|
234
|
+
sol_2L = sol(2.0) # clone with 2 L volume — moles are preserved
|
|
235
|
+
mixed = sol1 + sol2 # mix two solutions (moles add, volumes add)
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Creating a `solution` directly from `reactions`
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
sol = sys.solution({'CH3COOH': 0.1, 'H2O': 1.0}, volume=1.0)
|
|
242
|
+
# Returns a solution object at equilibrium
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## Activity Coefficients (Non-Ideal Systems)
|
|
248
|
+
|
|
249
|
+
Register a gamma function for any species in a reaction. The solver calls it at each iteration and adjusts K accordingly:
|
|
250
|
+
|
|
251
|
+
```python
|
|
252
|
+
import math
|
|
253
|
+
|
|
254
|
+
# Davies equation activity coefficient (depends on ionic strength I)
|
|
255
|
+
def davies(I):
|
|
256
|
+
sqI = math.sqrt(I)
|
|
257
|
+
return 10 ** (-0.509 * 3**2 * (sqI / (1 + sqI) - 0.3 * I))
|
|
258
|
+
|
|
259
|
+
# 'I' is a special token — efta computes ionic strength and passes it to davies()
|
|
260
|
+
rxn.set_gamma('Fe[3+]', (davies, 'I'))
|
|
261
|
+
|
|
262
|
+
# Gamma depending on another species' concentration
|
|
263
|
+
rxn.set_gamma('Fe[3+]', (lambda c_cl: 1 - 0.1 * c_cl, 'Cl[-]'))
|
|
264
|
+
|
|
265
|
+
# Constant gamma (no dependencies)
|
|
266
|
+
rxn.set_gamma('Fe[3+]', (lambda: 0.5,))
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## Solvent Extraction
|
|
272
|
+
|
|
273
|
+
efta includes a full solvent extraction module for modelling liquid–liquid extraction processes.
|
|
274
|
+
|
|
275
|
+
```python
|
|
276
|
+
from efta import reaction, solution
|
|
277
|
+
from efta.solventextraction import (
|
|
278
|
+
sx, countercurrent, distribution_coef, separation_factor, splitter
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
# Extraction reaction: metal transfers from aqueous to organic phase
|
|
282
|
+
rxn = reaction('LaCl[2+] + 3H2A2(org) = LaClA2(HA)4(org) + 2H[+]', 10.6)
|
|
283
|
+
|
|
284
|
+
feed = solution({'LaCl[2+]': 0.003, 'H[+]': 0.3}, volume=1.0) # aqueous feed
|
|
285
|
+
org = solution({'H2A2(org)': 0.25}, volume=1.0) # organic phase
|
|
286
|
+
|
|
287
|
+
# Single-stage extraction
|
|
288
|
+
stage = sx(rxn, feed, org)
|
|
289
|
+
stage.run()
|
|
290
|
+
extract, raffinate = stage.outlets[0], stage.outlets[1]
|
|
291
|
+
|
|
292
|
+
D = stage.distribution_coef('La') # D = [La]_org / [La]_aq
|
|
293
|
+
beta = stage.separation_factor('La', 'Ce') # β = D(La) / D(Ce)
|
|
294
|
+
|
|
295
|
+
# 5-stage counter-current extraction circuit
|
|
296
|
+
ms = countercurrent(rxn, stages=5, feed=feed, organic=org)
|
|
297
|
+
ms.run() # solve all stages at equilibrium
|
|
298
|
+
ms.run(efficiency=0.85) # with stage efficiency < 1
|
|
299
|
+
|
|
300
|
+
raffinate = ms.outlets[5] # aqueous exit after stage 5
|
|
301
|
+
extract = ms.outlets[1] # organic exit after stage 1
|
|
302
|
+
|
|
303
|
+
# Plot concentration profile across stages
|
|
304
|
+
ms.plot(['La', 'Ce'], phase='aq')
|
|
305
|
+
|
|
306
|
+
# Reflux design with a flow splitter
|
|
307
|
+
split = splitter(1, 2) # splits flow: 1/3 reflux, 2/3 forward
|
|
308
|
+
reflux, forward = split(extract)
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Available multistage topologies:
|
|
312
|
+
|
|
313
|
+
| Function | Description |
|
|
314
|
+
|---|---|
|
|
315
|
+
| `countercurrent` | Aqueous feeds stage 1→n, organic feeds stage n→1 |
|
|
316
|
+
| `crosscurrent` | Aqueous feeds stage 1→n, fresh organic at every stage |
|
|
317
|
+
| `strip_countercurrent` | Organic 1→n, aqueous n→1 (stripping mode) |
|
|
318
|
+
| `strip_crosscurrent` | Organic 1→n, fresh aqueous at every stage |
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
## Parameter Fitting
|
|
323
|
+
|
|
324
|
+
Fit unknown equilibrium constants to experimental data:
|
|
325
|
+
|
|
326
|
+
```python
|
|
327
|
+
from efta import freaction, freactions, model, analyze
|
|
328
|
+
|
|
329
|
+
# $(x1) is a free parameter — efta will optimise it
|
|
330
|
+
r_fit = freaction('Fe[3+] + 3OH[-] = Fe(OH)3(s)', '$(x1)', ksp=True)
|
|
331
|
+
|
|
332
|
+
# Experimental data: list of {species: measured_concentration} dicts
|
|
333
|
+
data = [
|
|
334
|
+
{'Fe[3+]': 1e-5, 'OH[-]': 1e-3},
|
|
335
|
+
{'Fe[3+]': 2e-5, 'OH[-]': 5e-4},
|
|
336
|
+
# ...
|
|
337
|
+
]
|
|
338
|
+
|
|
339
|
+
best_fit = model(r_fit, data)
|
|
340
|
+
print(f"Best log K = {best_fit.logK:.2f}")
|
|
341
|
+
|
|
342
|
+
# Bootstrap uncertainty analysis
|
|
343
|
+
result = analyze(r_fit, data, n_bootstrap=200)
|
|
344
|
+
print(f"log K = {result.logK_mean:.2f} ± {result.logK_std:.2f}")
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
## Module Reference
|
|
350
|
+
|
|
351
|
+
| Module | Description |
|
|
352
|
+
|---|---|
|
|
353
|
+
| `efta.reaction` | `reaction` class — single equilibrium reaction |
|
|
354
|
+
| `efta.reactions` | `reactions` class — coupled system and solver |
|
|
355
|
+
| `efta.species` | Species name parsing: `species()`, `formula()`, `charge()`, `components()` |
|
|
356
|
+
| `efta.solution` | `solution` class — composition + volume |
|
|
357
|
+
| `efta.mixture` | `mixture` class — ordered collection of solutions |
|
|
358
|
+
| `efta.balance` | Cluster detection and conservation-law analysis |
|
|
359
|
+
| `efta.system` | System assembly, activity coefficients, extent↔concentration |
|
|
360
|
+
| `efta.solver` | Numerical solvers (Method L, A, B, DE) |
|
|
361
|
+
| `efta.model` | Parameter fitting, Monte Carlo analysis |
|
|
362
|
+
| `efta.plotting` | Concentration plots, speciation diagrams, `style` singleton |
|
|
363
|
+
| `efta.styling` | Runtime palette and font-size helpers |
|
|
364
|
+
| `efta.periodic_table` | Atomic masses and element lookup |
|
|
365
|
+
| `efta.solventextraction.sx` | Single-stage extraction |
|
|
366
|
+
| `efta.solventextraction.multistage` | Multistage extraction circuits |
|
|
367
|
+
| `efta.solventextraction.units` | `splitter` flow-splitter unit |
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## Error Handling
|
|
372
|
+
|
|
373
|
+
All efta exceptions inherit from `EftaError`, so you can catch the entire family with a single clause:
|
|
374
|
+
|
|
375
|
+
```python
|
|
376
|
+
from efta import EftaError, ConvergenceError, ConvergenceWarning
|
|
377
|
+
import warnings
|
|
378
|
+
|
|
379
|
+
# Turn convergence warnings into hard errors (useful during debugging)
|
|
380
|
+
warnings.filterwarnings('error', category=ConvergenceWarning)
|
|
381
|
+
|
|
382
|
+
try:
|
|
383
|
+
c_eq = sys.equilibrium(c0)
|
|
384
|
+
except ConvergenceError as e:
|
|
385
|
+
print(f"Solver failed — best residual: {e.residual:.2e}")
|
|
386
|
+
except EftaError as e:
|
|
387
|
+
print(f"efta error: {e}")
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
| Exception | Raised when |
|
|
391
|
+
|---|---|
|
|
392
|
+
| `SpeciesError` | Species name cannot be parsed |
|
|
393
|
+
| `ReactionError` | Reaction is malformed (bad K, empty side, …) |
|
|
394
|
+
| `BalanceError` | Automatic balancing fails |
|
|
395
|
+
| `InputError` | Invalid argument passed to a function |
|
|
396
|
+
| `ConcentrationError` | Negative or missing initial concentration |
|
|
397
|
+
| `ConvergenceError` | All solver methods fail to converge |
|
|
398
|
+
| `ConvergenceWarning` | Solver returns a result but residual exceeds tolerance |
|
|
399
|
+
|
|
400
|
+
---
|
|
401
|
+
|
|
402
|
+
## License
|
|
403
|
+
|
|
404
|
+
MIT — see [LICENSE](LICENSE) for details.
|