pathsim-chem 0.2.4__tar.gz → 0.2.6__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.
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/PKG-INFO +1 -1
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/pyproject.toml +4 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/_version.py +3 -3
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/tritium/glc.py +248 -107
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem.egg-info/PKG-INFO +1 -1
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem.egg-info/SOURCES.txt +3 -0
- pathsim_chem-0.2.6/src/pathsim_chem.egg-info/scm_file_list.json +91 -0
- pathsim_chem-0.2.6/src/pathsim_chem.egg-info/scm_version.json +8 -0
- pathsim_chem-0.2.6/tests/tritium/test_glc.py +252 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/.github/workflows/pypi_deployment.yml +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/.github/workflows/tests.yml +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/.gitignore +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/LICENSE +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/README.md +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/.readthedocs.yaml +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/Makefile +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/make.bat +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/requirements.txt +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/source/_autosummary/pathsim_chem.rst +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/source/_static/custom.css +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/source/api.rst +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/source/conf.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/source/examples/cstr_reaction.ipynb +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/source/examples/equation_of_state.ipynb +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/source/examples/flash_distillation.ipynb +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/source/examples/heat_exchanger.ipynb +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/source/examples/multicomponent_flash.ipynb +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/source/examples/point_kinetics.ipynb +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/source/examples/process_flowsheet.ipynb +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/source/examples/valve_characteristics.ipynb +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/source/examples/vapor_pressure_curves.ipynb +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/source/examples/vle_calculation.ipynb +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/source/index.rst +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/source/logos/chem_logo.png +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/source/logos/pathsim_icon.png +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/source/logos/pathsim_logo.png +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/source/modules/tritium.rst +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/source/pathsim_docs.mplstyle +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/setup.cfg +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/__init__.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/neutronics/__init__.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/neutronics/point_kinetics.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/process/__init__.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/process/cstr.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/process/distillation.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/process/flash_drum.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/process/heat_exchanger.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/process/heater.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/process/mixer.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/process/multicomponent_flash.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/process/pfr.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/process/valve.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/thermodynamics/__init__.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/thermodynamics/activity_coefficients.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/thermodynamics/averages.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/thermodynamics/corrections.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/thermodynamics/correlations.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/thermodynamics/enthalpy.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/thermodynamics/equations_of_state.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/thermodynamics/fugacity_coefficients.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/thermodynamics/reactions.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/tritium/README.md +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/tritium/__init__.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/tritium/bubbler.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/tritium/ionisation_chamber.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/tritium/residencetime.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/tritium/splitter.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/tritium/tcap.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem.egg-info/dependency_links.txt +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem.egg-info/requires.txt +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem.egg-info/top_level.txt +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/neutronics/__init__.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/neutronics/test_point_kinetics.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/process/__init__.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/process/test_cstr.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/process/test_distillation.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/process/test_flash_drum.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/process/test_heat_exchanger.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/process/test_heater.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/process/test_mixer.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/process/test_multicomponent_flash.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/process/test_pfr.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/process/test_valve.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/thermodynamics/__init__.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/thermodynamics/test_activity_coefficients.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/thermodynamics/test_averages.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/thermodynamics/test_corrections.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/thermodynamics/test_correlations.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/thermodynamics/test_enthalpy.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/thermodynamics/test_equations_of_state.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/thermodynamics/test_fugacity_coefficients.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/thermodynamics/test_reactions.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/tritium/__init__.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/tritium/test_bubbler.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/tritium/test_ionisation_chamber.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/tritium/test_residencetime.py +0 -0
- {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/tritium/test_splitter.py +0 -0
|
@@ -29,6 +29,10 @@ dependencies = [
|
|
|
29
29
|
"pathsim>0.13",
|
|
30
30
|
"numpy>=1.15",
|
|
31
31
|
"scipy>=1.2",
|
|
32
|
+
# If a dependency ships native code that can't be installed in Pyodide,
|
|
33
|
+
# tag it with `; sys_platform != 'emscripten'` so a normal pip install
|
|
34
|
+
# still pulls it eagerly but micropip in the browser skips it. Pair
|
|
35
|
+
# this with a defensive re-export in `pathsim_chem/__init__.py`.
|
|
32
36
|
]
|
|
33
37
|
|
|
34
38
|
[project.optional-dependencies]
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.2.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 2,
|
|
21
|
+
__version__ = version = '0.2.6'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 6)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g27b677c30'
|
|
@@ -3,15 +3,17 @@ Bubble column gas-liquid contactor model solver.
|
|
|
3
3
|
|
|
4
4
|
This module solves the coupled, non-linear, second-order ordinary differential
|
|
5
5
|
equations that describe tritium transport in a counter-current bubble column,
|
|
6
|
-
based on the model by C. Malara (1995).
|
|
6
|
+
based on the model by C. Malara (1995). The boundary value problem is solved
|
|
7
|
+
with the native :class:`~pathsim.blocks.BVP1D` block, which warm-starts the
|
|
8
|
+
mesh between evaluations and skips the re-solve when the boundary data is
|
|
9
|
+
unchanged.
|
|
7
10
|
"""
|
|
8
11
|
|
|
9
12
|
import numpy as np
|
|
10
|
-
from scipy.integrate import solve_bvp
|
|
11
13
|
from scipy.optimize import root_scalar
|
|
12
14
|
import scipy.constants as const
|
|
13
|
-
|
|
14
|
-
from pathsim.
|
|
15
|
+
|
|
16
|
+
from pathsim.blocks import BVP1D
|
|
15
17
|
|
|
16
18
|
# --- Physical Constants ---
|
|
17
19
|
g = const.g # m/s^2, Gravitational acceleration
|
|
@@ -162,71 +164,79 @@ def _calculate_dimensionless_groups(params, phys_props):
|
|
|
162
164
|
}
|
|
163
165
|
|
|
164
166
|
|
|
165
|
-
def
|
|
166
|
-
"""
|
|
167
|
-
Set up and solve the Boundary Value Problem for tritium extraction.
|
|
167
|
+
def _ode_system(xi, S, dim):
|
|
168
|
+
"""First-order ODE system (4 equations) of the dimensionless GLC model.
|
|
168
169
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
170
|
+
The state is ``S = [x_T, dx_T/d(xi), y_T2, dy_T2/d(xi)]``. The signature
|
|
171
|
+
matches what the native :class:`~pathsim.blocks.BVP1D` block expects, with
|
|
172
|
+
the spatial mesh ``xi`` of shape ``(m,)`` and the state ``S`` of shape
|
|
173
|
+
``(4, m)``.
|
|
172
174
|
|
|
173
175
|
Args:
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
elements (int): Number of elements for the initial mesh.
|
|
176
|
+
xi (numpy.ndarray): Dimensionless axial coordinate, shape ``(m,)``.
|
|
177
|
+
S (numpy.ndarray): State, shape ``(4, m)``.
|
|
178
|
+
dim (dict): Dimensionless groups (Bo_l, phi_l, Bo_g, phi_g, psi, nu).
|
|
178
179
|
|
|
179
180
|
Returns:
|
|
180
|
-
|
|
181
|
+
numpy.ndarray: Derivatives ``dS/d(xi)``, shape ``(4, m)``.
|
|
181
182
|
"""
|
|
182
183
|
Bo_l, phi_l, Bo_g, phi_g, psi, nu = (
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
184
|
+
dim["Bo_l"],
|
|
185
|
+
dim["phi_l"],
|
|
186
|
+
dim["Bo_g"],
|
|
187
|
+
dim["phi_g"],
|
|
188
|
+
dim["psi"],
|
|
189
|
+
dim["nu"],
|
|
189
190
|
)
|
|
190
191
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
term1 = (1 + 2 * psi / Bo_g) * dy_T2_dxi
|
|
203
|
-
term2 = phi_g * theta
|
|
204
|
-
dS3_dxi = (Bo_g / (1 - psi * xi)) * (term1 - term2) # d^2(y_T2)/d(xi)^2
|
|
205
|
-
|
|
206
|
-
return np.vstack((dS0_dxi, dS1_dxi, dS2_dxi, dS3_dxi))
|
|
207
|
-
|
|
208
|
-
def boundary_conditions(Sa, Sb):
|
|
209
|
-
"""Defines the boundary conditions at xi=0 (Sa) and xi=1 (Sb)."""
|
|
210
|
-
if BCs == "C-C": # Closed-Closed
|
|
211
|
-
res1 = Sa[1] # dx_T/d(xi) = 0 at xi=0
|
|
212
|
-
res2 = Sb[0] - (1 - (1 / Bo_l) * Sb[1]) # x_T(1) = 1 - ...
|
|
213
|
-
res3 = Sa[2] - y_T2_in - (1 / Bo_g) * Sa[3] # y_T2(0) = y_T2_in + ...
|
|
214
|
-
res4 = Sb[3] # dy_T2/d(xi) = 0 at xi=1
|
|
215
|
-
elif BCs == "O-C": # Open-Closed
|
|
216
|
-
res1 = Sa[1] # dx_T/d(xi) = 0 at xi=0
|
|
217
|
-
res2 = Sb[0] - 1.0 # x_T(1) = 1
|
|
218
|
-
res3 = Sa[2] - y_T2_in # y_T2(0) = y_T2_in
|
|
219
|
-
res4 = Sb[3] # dy_T2/d(xi) = 0 at xi=1
|
|
220
|
-
return np.array([res1, res2, res3, res4])
|
|
221
|
-
|
|
222
|
-
xi = np.linspace(0, 1, elements + 1)
|
|
223
|
-
y_guess = np.zeros((4, xi.size))
|
|
224
|
-
return solve_bvp(
|
|
225
|
-
ode_system, boundary_conditions, xi, y_guess, tol=1e-5, max_nodes=10000
|
|
226
|
-
)
|
|
192
|
+
x_T, dx_T_dxi, y_T2, dy_T2_dxi = S
|
|
193
|
+
theta = x_T - np.sqrt(np.maximum(0, (1 - psi * xi) * y_T2 / nu))
|
|
194
|
+
|
|
195
|
+
dS0_dxi = dx_T_dxi # d(x_T)/d(xi)
|
|
196
|
+
dS1_dxi = Bo_l * (phi_l * theta - dx_T_dxi) # d^2(x_T)/d(xi)^2
|
|
197
|
+
dS2_dxi = dy_T2_dxi # d(y_T2)/d(xi)
|
|
198
|
+
term1 = (1 + 2 * psi / Bo_g) * dy_T2_dxi
|
|
199
|
+
term2 = phi_g * theta
|
|
200
|
+
dS3_dxi = (Bo_g / (1 - psi * xi)) * (term1 - term2) # d^2(y_T2)/d(xi)^2
|
|
201
|
+
|
|
202
|
+
return np.vstack((dS0_dxi, dS1_dxi, dS2_dxi, dS3_dxi))
|
|
227
203
|
|
|
228
204
|
|
|
229
|
-
def
|
|
205
|
+
def _boundary_conditions(Sa, Sb, dim, y_T2_in, BCs):
|
|
206
|
+
"""Boundary-condition residuals at xi=0 (Sa) and xi=1 (Sb).
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
Sa (numpy.ndarray): State at xi=0 (liquid outlet).
|
|
210
|
+
Sb (numpy.ndarray): State at xi=1 (liquid inlet).
|
|
211
|
+
dim (dict): Dimensionless groups (uses Bo_l, Bo_g).
|
|
212
|
+
y_T2_in (float): Inlet mole fraction of T2 in the gas phase.
|
|
213
|
+
BCs (str): Boundary-condition type, ``"C-C"`` or ``"O-C"``.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
numpy.ndarray: The four boundary-condition residuals.
|
|
217
|
+
|
|
218
|
+
Raises:
|
|
219
|
+
ValueError: If ``BCs`` is not a recognised type.
|
|
220
|
+
"""
|
|
221
|
+
Bo_l, Bo_g = dim["Bo_l"], dim["Bo_g"]
|
|
222
|
+
|
|
223
|
+
if BCs == "C-C": # Closed-Closed
|
|
224
|
+
res1 = Sa[1] # dx_T/d(xi) = 0 at xi=0
|
|
225
|
+
res2 = Sb[0] - (1 - (1 / Bo_l) * Sb[1]) # x_T(1) = 1 - ...
|
|
226
|
+
res3 = Sa[2] - y_T2_in - (1 / Bo_g) * Sa[3] # y_T2(0) = y_T2_in + ...
|
|
227
|
+
res4 = Sb[3] # dy_T2/d(xi) = 0 at xi=1
|
|
228
|
+
elif BCs == "O-C": # Open-Closed
|
|
229
|
+
res1 = Sa[1] # dx_T/d(xi) = 0 at xi=0
|
|
230
|
+
res2 = Sb[0] - 1.0 # x_T(1) = 1
|
|
231
|
+
res3 = Sa[2] - y_T2_in # y_T2(0) = y_T2_in
|
|
232
|
+
res4 = Sb[3] # dy_T2/d(xi) = 0 at xi=1
|
|
233
|
+
else:
|
|
234
|
+
raise ValueError(f"Unknown boundary condition type: {BCs!r}")
|
|
235
|
+
|
|
236
|
+
return np.array([res1, res2, res3, res4])
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _process_results(S_ends, params, phys_props, dim_params):
|
|
230
240
|
"""
|
|
231
241
|
Process the BVP solution to produce dimensional results.
|
|
232
242
|
|
|
@@ -235,26 +245,25 @@ def _process_results(solution, params, phys_props, dim_params):
|
|
|
235
245
|
mass balance check, and aggregates all results.
|
|
236
246
|
|
|
237
247
|
Args:
|
|
238
|
-
|
|
248
|
+
S_ends (numpy.ndarray): Solution sampled at the domain endpoints,
|
|
249
|
+
shape ``(4, 2)`` with column 0 at xi=0 (liquid outlet) and column 1
|
|
250
|
+
at xi=1 (liquid inlet).
|
|
239
251
|
params (dict): The original input parameters.
|
|
240
252
|
phys_props (dict): The calculated physical properties.
|
|
241
253
|
dim_params (dict): The calculated dimensionless groups.
|
|
242
254
|
|
|
243
255
|
Returns:
|
|
244
|
-
|
|
256
|
+
dict: The dimensional results dictionary.
|
|
245
257
|
"""
|
|
246
258
|
|
|
247
259
|
# Unpack parameters
|
|
248
260
|
c_T_in, P_in, T = params["c_T_in"], params["P_in"], params["T"]
|
|
249
261
|
y_T2_in = params["y_T2_in"]
|
|
250
262
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
# Dimensionless results
|
|
255
|
-
x_T_outlet_dimless = solution.y[0, 0]
|
|
263
|
+
# Dimensionless results (xi=0 -> column 0, xi=1 -> column 1)
|
|
264
|
+
x_T_outlet_dimless = S_ends[0, 0]
|
|
256
265
|
Q_l, Q_g = phys_props["Q_l"], phys_props["Q_g"]
|
|
257
|
-
y_T2_out =
|
|
266
|
+
y_T2_out = S_ends[2, 1]
|
|
258
267
|
efficiency = 1 - x_T_outlet_dimless
|
|
259
268
|
|
|
260
269
|
# Dimensional results
|
|
@@ -297,13 +306,16 @@ def _process_results(solution, params, phys_props, dim_params):
|
|
|
297
306
|
results.update(phys_props)
|
|
298
307
|
results.update(dim_params)
|
|
299
308
|
|
|
300
|
-
return results
|
|
309
|
+
return results
|
|
301
310
|
|
|
302
311
|
|
|
303
312
|
def solve(params):
|
|
304
313
|
"""
|
|
305
314
|
Main solver function for the bubble column model.
|
|
306
315
|
|
|
316
|
+
Builds the physical properties and dimensionless groups, then solves the
|
|
317
|
+
boundary value problem with the native :class:`~pathsim.blocks.BVP1D` block.
|
|
318
|
+
|
|
307
319
|
Args:
|
|
308
320
|
params (dict): A dictionary of all input parameters for the model,
|
|
309
321
|
including operational conditions and geometry.
|
|
@@ -311,8 +323,12 @@ def solve(params):
|
|
|
311
323
|
Returns:
|
|
312
324
|
list: A list containing:
|
|
313
325
|
- dict: A dictionary containing the simulation results.
|
|
314
|
-
-
|
|
315
|
-
|
|
326
|
+
- pathsim.blocks.BVP1D: The BVP block, exposing the refined mesh
|
|
327
|
+
(``.x``) and the sampled solution (``.solution()``).
|
|
328
|
+
|
|
329
|
+
Raises:
|
|
330
|
+
ValueError: If the calculated gas outlet pressure is non-positive.
|
|
331
|
+
RuntimeError: If the BVP solver fails to converge.
|
|
316
332
|
"""
|
|
317
333
|
# Adjust inlet gas concentration to avoid numerical instability at zero
|
|
318
334
|
y_T2_in = max(params["y_T2_in"], 1e-20)
|
|
@@ -333,16 +349,31 @@ def solve(params):
|
|
|
333
349
|
# 2. Calculate dimensionless groups for the ODE system
|
|
334
350
|
dim_params = _calculate_dimensionless_groups(params, phys_props)
|
|
335
351
|
|
|
336
|
-
# 3. Solve the boundary value problem
|
|
337
|
-
|
|
352
|
+
# 3. Solve the boundary value problem with the native BVP1D block, sampling
|
|
353
|
+
# the two domain endpoints (xi=0 and xi=1)
|
|
354
|
+
bvp = BVP1D(
|
|
355
|
+
fun=lambda x, y, p, u: _ode_system(x, y, dim_params),
|
|
356
|
+
bc=lambda ya, yb, p, u: _boundary_conditions(
|
|
357
|
+
ya, yb, dim_params, y_T2_in, params["BCs"]
|
|
358
|
+
),
|
|
359
|
+
n=4,
|
|
360
|
+
domain=(0.0, 1.0),
|
|
361
|
+
n_nodes=params["elements"] + 1,
|
|
362
|
+
x_eval=np.array([0.0, 1.0]),
|
|
363
|
+
tol=1e-5,
|
|
364
|
+
max_nodes=10000,
|
|
365
|
+
)
|
|
366
|
+
bvp.update(0.0)
|
|
367
|
+
if not bvp.success:
|
|
368
|
+
raise RuntimeError("BVP solver failed to converge.")
|
|
338
369
|
|
|
339
370
|
# 4. Process and return the results in a dimensional format
|
|
340
|
-
|
|
371
|
+
results = _process_results(bvp.solution(), params, phys_props, dim_params)
|
|
341
372
|
|
|
342
|
-
return [results,
|
|
373
|
+
return [results, bvp]
|
|
343
374
|
|
|
344
375
|
|
|
345
|
-
class GLC(
|
|
376
|
+
class GLC(BVP1D):
|
|
346
377
|
r"""Counter-current bubble column gas-liquid contactor (GLC) for tritium extraction.
|
|
347
378
|
|
|
348
379
|
Solves the coupled, non-linear, second-order boundary value problem that
|
|
@@ -352,10 +383,18 @@ class GLC(pathsim.blocks.Function):
|
|
|
352
383
|
transfer via Sieverts' law, and hydrostatic pressure variation along
|
|
353
384
|
the column.
|
|
354
385
|
|
|
355
|
-
The block is
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
386
|
+
The block is a specialisation of the native :class:`~pathsim.blocks.BVP1D`
|
|
387
|
+
block: the constructor seeds the parent with the Malara right-hand side and
|
|
388
|
+
boundary conditions, and the four block inputs supply the per-evaluation
|
|
389
|
+
boundary data. The hydrodynamic correlations and dimensionless groups are
|
|
390
|
+
computed from the current input inside the collocation callbacks. As for any
|
|
391
|
+
``BVP1D``, the solve is warm-started from the previous mesh and skipped
|
|
392
|
+
entirely when the input is unchanged. After each solve the dimensionless
|
|
393
|
+
endpoint solution is post-processed into the dimensional output ports.
|
|
394
|
+
|
|
395
|
+
Use :meth:`results` to retrieve the full dimensional result dictionary,
|
|
396
|
+
which additionally contains partial pressures, physical properties and the
|
|
397
|
+
dimensionless groups.
|
|
359
398
|
|
|
360
399
|
Reference: https://doi.org/10.13182/FST95-A30485
|
|
361
400
|
|
|
@@ -393,6 +432,11 @@ class GLC(pathsim.blocks.Function):
|
|
|
393
432
|
Gravitational acceleration [m/s²]. Default: ``scipy.constants.g``.
|
|
394
433
|
initial_nb_of_elements : int, optional
|
|
395
434
|
Number of mesh elements for the initial BVP grid. Default: 20.
|
|
435
|
+
tol : float, optional
|
|
436
|
+
Solver tolerance for the BVP. Default: 1e-5.
|
|
437
|
+
max_nodes : int, optional
|
|
438
|
+
Maximum number of mesh nodes allowed during BVP refinement. Default:
|
|
439
|
+
10000.
|
|
396
440
|
"""
|
|
397
441
|
|
|
398
442
|
input_port_labels = {
|
|
@@ -421,7 +465,11 @@ class GLC(pathsim.blocks.Function):
|
|
|
421
465
|
BCs,
|
|
422
466
|
g=const.g,
|
|
423
467
|
initial_nb_of_elements=20,
|
|
468
|
+
tol=1e-5,
|
|
469
|
+
max_nodes=10000,
|
|
424
470
|
):
|
|
471
|
+
#fixed operating point and geometry; the four block inputs supply the
|
|
472
|
+
#per-evaluation boundary data (c_T_in, flow_l, y_T2_inlet, flow_g)
|
|
425
473
|
self.params = {
|
|
426
474
|
"P_in": P_in,
|
|
427
475
|
"L": L,
|
|
@@ -431,33 +479,126 @@ class GLC(pathsim.blocks.Function):
|
|
|
431
479
|
"elements": initial_nb_of_elements,
|
|
432
480
|
"BCs": BCs,
|
|
433
481
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
return (
|
|
455
|
-
c_T_out,
|
|
456
|
-
y_T2_out,
|
|
457
|
-
eff,
|
|
458
|
-
P_total_outlet,
|
|
459
|
-
Q_l,
|
|
460
|
-
Q_g_out,
|
|
461
|
-
n_T_out_liquid,
|
|
462
|
-
n_T_out_gas,
|
|
482
|
+
self.BCs = BCs
|
|
483
|
+
|
|
484
|
+
#cache of (physical properties, dimensionless groups) keyed on the input
|
|
485
|
+
#so the hydrodynamic correlations run once per operating point instead of
|
|
486
|
+
#on every collocation mesh point
|
|
487
|
+
self._u_cache = None
|
|
488
|
+
self._phys = None
|
|
489
|
+
self._dim = None
|
|
490
|
+
|
|
491
|
+
#seed the native BVP block with the Malara model physics; sample the two
|
|
492
|
+
#domain endpoints so the dimensionless outlet/inlet states are available
|
|
493
|
+
super().__init__(
|
|
494
|
+
fun=self._ode,
|
|
495
|
+
bc=self._bc,
|
|
496
|
+
n=4,
|
|
497
|
+
domain=(0.0, 1.0),
|
|
498
|
+
n_nodes=initial_nb_of_elements + 1,
|
|
499
|
+
x_eval=np.array([0.0, 1.0]),
|
|
500
|
+
tol=tol,
|
|
501
|
+
max_nodes=max_nodes,
|
|
463
502
|
)
|
|
503
|
+
|
|
504
|
+
def _physics(self, u):
|
|
505
|
+
"""Physical properties and dimensionless groups for the input `u`.
|
|
506
|
+
|
|
507
|
+
Cached on `u` so the hydrodynamic correlations (and the gas hold-up
|
|
508
|
+
root solve) run once per operating point rather than on every mesh
|
|
509
|
+
point of the collocation solve.
|
|
510
|
+
"""
|
|
511
|
+
if self._u_cache is not None and np.array_equal(u, self._u_cache):
|
|
512
|
+
return self._phys, self._dim
|
|
513
|
+
|
|
514
|
+
c_T_in, flow_l, y_T2_inlet, flow_g = u
|
|
515
|
+
p = dict(self.params)
|
|
516
|
+
p["c_T_in"] = c_T_in
|
|
517
|
+
p["flow_l"] = flow_l
|
|
518
|
+
p["flow_g"] = flow_g
|
|
519
|
+
p["y_T2_in"] = y_T2_inlet
|
|
520
|
+
|
|
521
|
+
phys = _calculate_properties(p)
|
|
522
|
+
|
|
523
|
+
#guard against a non-physical (non-positive) gas outlet pressure
|
|
524
|
+
P_out = p["P_in"] - phys["rho_l"] * (1 - phys["epsilon_g"]) * g * p["L"]
|
|
525
|
+
if P_out <= 0:
|
|
526
|
+
raise ValueError(
|
|
527
|
+
f"Calculated gas outlet pressure is non-positive ({P_out:.2e} Pa). "
|
|
528
|
+
"Check input parameters P_in, L, etc."
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
dim = _calculate_dimensionless_groups(p, phys)
|
|
532
|
+
|
|
533
|
+
self._u_cache = np.array(u, dtype=float)
|
|
534
|
+
self._phys, self._dim = phys, dim
|
|
535
|
+
return phys, dim
|
|
536
|
+
|
|
537
|
+
def _ode(self, xi, S, p, u):
|
|
538
|
+
"""Malara right-hand side, with dimensionless groups derived from `u`."""
|
|
539
|
+
_, dim = self._physics(u)
|
|
540
|
+
return _ode_system(xi, S, dim)
|
|
541
|
+
|
|
542
|
+
def _bc(self, Sa, Sb, p, u):
|
|
543
|
+
"""Malara boundary conditions, with groups derived from `u`."""
|
|
544
|
+
_, dim = self._physics(u)
|
|
545
|
+
y_T2_in = max(u[2], 1e-20)
|
|
546
|
+
return _boundary_conditions(Sa, Sb, dim, y_T2_in, self.BCs)
|
|
547
|
+
|
|
548
|
+
def update(self, t):
|
|
549
|
+
"""Solve the BVP and expose the dimensional results at the output ports.
|
|
550
|
+
|
|
551
|
+
Delegates the solve to :class:`~pathsim.blocks.BVP1D` (warm-started, and
|
|
552
|
+
skipped when the input is unchanged), then post-processes the
|
|
553
|
+
dimensionless endpoint solution into the eight dimensional output ports.
|
|
554
|
+
|
|
555
|
+
Parameters
|
|
556
|
+
----------
|
|
557
|
+
t : float
|
|
558
|
+
evaluation time
|
|
559
|
+
|
|
560
|
+
Raises
|
|
561
|
+
------
|
|
562
|
+
RuntimeError
|
|
563
|
+
if the BVP solve did not converge
|
|
564
|
+
"""
|
|
565
|
+
super().update(t)
|
|
566
|
+
if not self.success:
|
|
567
|
+
raise RuntimeError("BVP solver failed to converge.")
|
|
568
|
+
|
|
569
|
+
res = self.results()
|
|
570
|
+
self.outputs.update_from_array(np.array([
|
|
571
|
+
res["c_T_outlet [mol/m^3]"],
|
|
572
|
+
res["y_T2_outlet_gas"],
|
|
573
|
+
res["extraction_efficiency [fraction]"],
|
|
574
|
+
res["total_gas_P_outlet [Pa]"],
|
|
575
|
+
res["liquid_vol_flow [m^3/s]"],
|
|
576
|
+
res["gas_vol_flow_outlet [m^3/s]"],
|
|
577
|
+
res["tritium_out_liquid [mol/s]"],
|
|
578
|
+
res["tritium_out_gas [mol/s]"],
|
|
579
|
+
]))
|
|
580
|
+
|
|
581
|
+
def results(self):
|
|
582
|
+
"""Post-process the current BVP solution into dimensional results.
|
|
583
|
+
|
|
584
|
+
Returns
|
|
585
|
+
-------
|
|
586
|
+
dict, None
|
|
587
|
+
Dimensional results (outlet concentration, extraction efficiency,
|
|
588
|
+
outlet pressure, volumetric flows, tritium molar flows and the mass
|
|
589
|
+
balance) for the most recent input, or ``None`` if no successful
|
|
590
|
+
solve has happened yet.
|
|
591
|
+
"""
|
|
592
|
+
if not self.success:
|
|
593
|
+
return None
|
|
594
|
+
|
|
595
|
+
u = self.inputs.to_array()
|
|
596
|
+
c_T_in, flow_l, y_T2_inlet, flow_g = u
|
|
597
|
+
p = dict(self.params)
|
|
598
|
+
p["c_T_in"] = c_T_in
|
|
599
|
+
p["flow_l"] = flow_l
|
|
600
|
+
p["flow_g"] = flow_g
|
|
601
|
+
p["y_T2_in"] = y_T2_inlet
|
|
602
|
+
|
|
603
|
+
phys, dim = self._physics(u)
|
|
604
|
+
return _process_results(self.solution(), p, phys, dim)
|
|
@@ -34,6 +34,8 @@ src/pathsim_chem.egg-info/PKG-INFO
|
|
|
34
34
|
src/pathsim_chem.egg-info/SOURCES.txt
|
|
35
35
|
src/pathsim_chem.egg-info/dependency_links.txt
|
|
36
36
|
src/pathsim_chem.egg-info/requires.txt
|
|
37
|
+
src/pathsim_chem.egg-info/scm_file_list.json
|
|
38
|
+
src/pathsim_chem.egg-info/scm_version.json
|
|
37
39
|
src/pathsim_chem.egg-info/top_level.txt
|
|
38
40
|
src/pathsim_chem/neutronics/__init__.py
|
|
39
41
|
src/pathsim_chem/neutronics/point_kinetics.py
|
|
@@ -87,6 +89,7 @@ tests/thermodynamics/test_fugacity_coefficients.py
|
|
|
87
89
|
tests/thermodynamics/test_reactions.py
|
|
88
90
|
tests/tritium/__init__.py
|
|
89
91
|
tests/tritium/test_bubbler.py
|
|
92
|
+
tests/tritium/test_glc.py
|
|
90
93
|
tests/tritium/test_ionisation_chamber.py
|
|
91
94
|
tests/tritium/test_residencetime.py
|
|
92
95
|
tests/tritium/test_splitter.py
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
{
|
|
2
|
+
"files": [
|
|
3
|
+
"README.md",
|
|
4
|
+
"LICENSE",
|
|
5
|
+
"pyproject.toml",
|
|
6
|
+
".gitignore",
|
|
7
|
+
"docs/Makefile",
|
|
8
|
+
"docs/requirements.txt",
|
|
9
|
+
"docs/make.bat",
|
|
10
|
+
"docs/.readthedocs.yaml",
|
|
11
|
+
"docs/source/index.rst",
|
|
12
|
+
"docs/source/api.rst",
|
|
13
|
+
"docs/source/pathsim_docs.mplstyle",
|
|
14
|
+
"docs/source/conf.py",
|
|
15
|
+
"docs/source/_autosummary/pathsim_chem.rst",
|
|
16
|
+
"docs/source/_static/custom.css",
|
|
17
|
+
"docs/source/modules/tritium.rst",
|
|
18
|
+
"docs/source/logos/pathsim_logo.png",
|
|
19
|
+
"docs/source/logos/pathsim_icon.png",
|
|
20
|
+
"docs/source/logos/chem_logo.png",
|
|
21
|
+
"docs/source/examples/vle_calculation.ipynb",
|
|
22
|
+
"docs/source/examples/point_kinetics.ipynb",
|
|
23
|
+
"docs/source/examples/multicomponent_flash.ipynb",
|
|
24
|
+
"docs/source/examples/valve_characteristics.ipynb",
|
|
25
|
+
"docs/source/examples/vapor_pressure_curves.ipynb",
|
|
26
|
+
"docs/source/examples/cstr_reaction.ipynb",
|
|
27
|
+
"docs/source/examples/equation_of_state.ipynb",
|
|
28
|
+
"docs/source/examples/flash_distillation.ipynb",
|
|
29
|
+
"docs/source/examples/process_flowsheet.ipynb",
|
|
30
|
+
"docs/source/examples/heat_exchanger.ipynb",
|
|
31
|
+
"src/pathsim_chem/__init__.py",
|
|
32
|
+
"src/pathsim_chem/tritium/bubbler.py",
|
|
33
|
+
"src/pathsim_chem/tritium/README.md",
|
|
34
|
+
"src/pathsim_chem/tritium/__init__.py",
|
|
35
|
+
"src/pathsim_chem/tritium/ionisation_chamber.py",
|
|
36
|
+
"src/pathsim_chem/tritium/residencetime.py",
|
|
37
|
+
"src/pathsim_chem/tritium/tcap.py",
|
|
38
|
+
"src/pathsim_chem/tritium/glc.py",
|
|
39
|
+
"src/pathsim_chem/tritium/splitter.py",
|
|
40
|
+
"src/pathsim_chem/thermodynamics/averages.py",
|
|
41
|
+
"src/pathsim_chem/thermodynamics/__init__.py",
|
|
42
|
+
"src/pathsim_chem/thermodynamics/corrections.py",
|
|
43
|
+
"src/pathsim_chem/thermodynamics/enthalpy.py",
|
|
44
|
+
"src/pathsim_chem/thermodynamics/reactions.py",
|
|
45
|
+
"src/pathsim_chem/thermodynamics/correlations.py",
|
|
46
|
+
"src/pathsim_chem/thermodynamics/fugacity_coefficients.py",
|
|
47
|
+
"src/pathsim_chem/thermodynamics/equations_of_state.py",
|
|
48
|
+
"src/pathsim_chem/thermodynamics/activity_coefficients.py",
|
|
49
|
+
"src/pathsim_chem/process/distillation.py",
|
|
50
|
+
"src/pathsim_chem/process/mixer.py",
|
|
51
|
+
"src/pathsim_chem/process/__init__.py",
|
|
52
|
+
"src/pathsim_chem/process/multicomponent_flash.py",
|
|
53
|
+
"src/pathsim_chem/process/valve.py",
|
|
54
|
+
"src/pathsim_chem/process/pfr.py",
|
|
55
|
+
"src/pathsim_chem/process/flash_drum.py",
|
|
56
|
+
"src/pathsim_chem/process/heater.py",
|
|
57
|
+
"src/pathsim_chem/process/heat_exchanger.py",
|
|
58
|
+
"src/pathsim_chem/process/cstr.py",
|
|
59
|
+
"src/pathsim_chem/neutronics/point_kinetics.py",
|
|
60
|
+
"src/pathsim_chem/neutronics/__init__.py",
|
|
61
|
+
"tests/tritium/__init__.py",
|
|
62
|
+
"tests/tritium/test_glc.py",
|
|
63
|
+
"tests/tritium/test_splitter.py",
|
|
64
|
+
"tests/tritium/test_bubbler.py",
|
|
65
|
+
"tests/tritium/test_residencetime.py",
|
|
66
|
+
"tests/tritium/test_ionisation_chamber.py",
|
|
67
|
+
"tests/thermodynamics/__init__.py",
|
|
68
|
+
"tests/thermodynamics/test_corrections.py",
|
|
69
|
+
"tests/thermodynamics/test_equations_of_state.py",
|
|
70
|
+
"tests/thermodynamics/test_reactions.py",
|
|
71
|
+
"tests/thermodynamics/test_activity_coefficients.py",
|
|
72
|
+
"tests/thermodynamics/test_averages.py",
|
|
73
|
+
"tests/thermodynamics/test_correlations.py",
|
|
74
|
+
"tests/thermodynamics/test_enthalpy.py",
|
|
75
|
+
"tests/thermodynamics/test_fugacity_coefficients.py",
|
|
76
|
+
"tests/process/test_pfr.py",
|
|
77
|
+
"tests/process/__init__.py",
|
|
78
|
+
"tests/process/test_valve.py",
|
|
79
|
+
"tests/process/test_multicomponent_flash.py",
|
|
80
|
+
"tests/process/test_heater.py",
|
|
81
|
+
"tests/process/test_distillation.py",
|
|
82
|
+
"tests/process/test_flash_drum.py",
|
|
83
|
+
"tests/process/test_cstr.py",
|
|
84
|
+
"tests/process/test_mixer.py",
|
|
85
|
+
"tests/process/test_heat_exchanger.py",
|
|
86
|
+
"tests/neutronics/test_point_kinetics.py",
|
|
87
|
+
"tests/neutronics/__init__.py",
|
|
88
|
+
".github/workflows/pypi_deployment.yml",
|
|
89
|
+
".github/workflows/tests.yml"
|
|
90
|
+
]
|
|
91
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
########################################################################################
|
|
2
|
+
##
|
|
3
|
+
## TESTS FOR
|
|
4
|
+
## 'tritium.glc.py'
|
|
5
|
+
##
|
|
6
|
+
########################################################################################
|
|
7
|
+
|
|
8
|
+
# IMPORTS ==============================================================================
|
|
9
|
+
|
|
10
|
+
import unittest
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
from scipy.integrate import solve_bvp
|
|
14
|
+
|
|
15
|
+
from pathsim_chem.tritium import GLC
|
|
16
|
+
from pathsim_chem.tritium import glc
|
|
17
|
+
|
|
18
|
+
from pathsim import Simulation, Connection
|
|
19
|
+
from pathsim.blocks import BVP1D, Constant, Scope
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# HELPERS =============================================================================
|
|
23
|
+
|
|
24
|
+
#fixed operating point and geometry used across the tests
|
|
25
|
+
_BASE = dict(P_in=2e5, L=1.0, D=0.1, T=623.0, g=9.81)
|
|
26
|
+
|
|
27
|
+
#per-evaluation boundary data (c_T_in, flow_l, y_T2_inlet, flow_g)
|
|
28
|
+
_INPUT = (1e-3, 1.0, 0.0, 1e-4)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _reference(BCs):
|
|
32
|
+
"""Independent gold-standard solve of the Malara model with scipy.solve_bvp.
|
|
33
|
+
|
|
34
|
+
Returns the dimensionless outlet liquid concentration ``x_T(0)`` and the
|
|
35
|
+
dimensionless outlet gas fraction ``y_T2(1)``.
|
|
36
|
+
"""
|
|
37
|
+
c_T_in, flow_l, y_T2_inlet, flow_g = _INPUT
|
|
38
|
+
p = dict(_BASE)
|
|
39
|
+
p.update(elements=20, BCs=BCs, c_T_in=c_T_in, flow_l=flow_l,
|
|
40
|
+
flow_g=flow_g, y_T2_in=y_T2_inlet)
|
|
41
|
+
|
|
42
|
+
phys = glc._calculate_properties(p)
|
|
43
|
+
dim = glc._calculate_dimensionless_groups(p, phys)
|
|
44
|
+
y2in = max(p["y_T2_in"], 1e-20)
|
|
45
|
+
|
|
46
|
+
Bo_l, phi_l, Bo_g, phi_g, psi, nu = (
|
|
47
|
+
dim["Bo_l"], dim["phi_l"], dim["Bo_g"], dim["phi_g"], dim["psi"], dim["nu"]
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def ode(xi, S):
|
|
51
|
+
x_T, dx, y_T2, dy = S
|
|
52
|
+
th = x_T - np.sqrt(np.maximum(0, (1 - psi * xi) * y_T2 / nu))
|
|
53
|
+
return np.vstack((
|
|
54
|
+
dx,
|
|
55
|
+
Bo_l * (phi_l * th - dx),
|
|
56
|
+
dy,
|
|
57
|
+
(Bo_g / (1 - psi * xi)) * ((1 + 2 * psi / Bo_g) * dy - phi_g * th),
|
|
58
|
+
))
|
|
59
|
+
|
|
60
|
+
def bc(Sa, Sb):
|
|
61
|
+
if BCs == "C-C":
|
|
62
|
+
return np.array([Sa[1], Sb[0] - (1 - (1 / Bo_l) * Sb[1]),
|
|
63
|
+
Sa[2] - y2in - (1 / Bo_g) * Sa[3], Sb[3]])
|
|
64
|
+
return np.array([Sa[1], Sb[0] - 1.0, Sa[2] - y2in, Sb[3]])
|
|
65
|
+
|
|
66
|
+
xi = np.linspace(0, 1, 21)
|
|
67
|
+
sol = solve_bvp(ode, bc, xi, np.zeros((4, 21)), tol=1e-5, max_nodes=10000)
|
|
68
|
+
return sol.y[0, 0], sol.y[2, -1]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# TESTS ===============================================================================
|
|
72
|
+
|
|
73
|
+
class TestGLC(unittest.TestCase):
|
|
74
|
+
"""
|
|
75
|
+
Test the implementation of the 'GLC' bubble-column gas-liquid contactor
|
|
76
|
+
block from the fusion toolbox. The block inherits from the native `BVP1D`
|
|
77
|
+
block and seeds it with the Malara (1995) tritium-extraction model.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def test_init(self):
|
|
81
|
+
"""The block is a BVP1D subclass seeded with the GLC problem dimensions."""
|
|
82
|
+
blk = GLC(BCs="C-C", **_BASE)
|
|
83
|
+
self.assertIsInstance(blk, BVP1D)
|
|
84
|
+
self.assertEqual(blk.n, 4)
|
|
85
|
+
self.assertEqual(blk.domain, (0.0, 1.0))
|
|
86
|
+
#four named input ports supply the boundary data
|
|
87
|
+
self.assertEqual(len(blk.inputs), 4)
|
|
88
|
+
for label in ("c_T_in", "flow_l", "y_T2_inlet", "flow_g"):
|
|
89
|
+
self.assertIn(label, blk.input_port_labels)
|
|
90
|
+
#eight named dimensional output ports
|
|
91
|
+
for label in ("c_T_out", "y_T2_out", "eff", "P_out", "Q_l", "Q_g_out",
|
|
92
|
+
"n_T_out_liquid", "n_T_out_gas"):
|
|
93
|
+
self.assertIn(label, blk.output_port_labels)
|
|
94
|
+
#no successful solve yet, so no dimensional results
|
|
95
|
+
self.assertIsNone(blk.results())
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_solve_matches_reference_cc(self):
|
|
99
|
+
"""The C-C solve matches an independent scipy.solve_bvp reference."""
|
|
100
|
+
blk = GLC(BCs="C-C", **_BASE)
|
|
101
|
+
blk.inputs.update_from_array(np.array(_INPUT))
|
|
102
|
+
blk.update(0.0)
|
|
103
|
+
|
|
104
|
+
self.assertTrue(blk.success)
|
|
105
|
+
x_T_ref, y_T2_ref = _reference("C-C")
|
|
106
|
+
res = blk.results()
|
|
107
|
+
self.assertAlmostEqual(res["c_T_outlet [mol/m^3]"],
|
|
108
|
+
x_T_ref * _INPUT[0], places=12)
|
|
109
|
+
self.assertAlmostEqual(res["y_T2_outlet_gas"], y_T2_ref, places=12)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_solve_matches_reference_oc(self):
|
|
113
|
+
"""The O-C solve matches an independent scipy.solve_bvp reference."""
|
|
114
|
+
blk = GLC(BCs="O-C", **_BASE)
|
|
115
|
+
blk.inputs.update_from_array(np.array(_INPUT))
|
|
116
|
+
blk.update(0.0)
|
|
117
|
+
|
|
118
|
+
self.assertTrue(blk.success)
|
|
119
|
+
x_T_ref, y_T2_ref = _reference("O-C")
|
|
120
|
+
res = blk.results()
|
|
121
|
+
self.assertAlmostEqual(res["c_T_outlet [mol/m^3]"],
|
|
122
|
+
x_T_ref * _INPUT[0], places=12)
|
|
123
|
+
self.assertAlmostEqual(res["y_T2_outlet_gas"], y_T2_ref, places=12)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_results_physical(self):
|
|
127
|
+
"""The dimensional results are physically sensible and mass conserving."""
|
|
128
|
+
blk = GLC(BCs="C-C", **_BASE)
|
|
129
|
+
blk.inputs.update_from_array(np.array(_INPUT))
|
|
130
|
+
blk.update(0.0)
|
|
131
|
+
res = blk.results()
|
|
132
|
+
|
|
133
|
+
#extraction efficiency in [0, 1] and outlet below inlet concentration
|
|
134
|
+
self.assertGreaterEqual(res["extraction_efficiency [fraction]"], 0.0)
|
|
135
|
+
self.assertLessEqual(res["extraction_efficiency [fraction]"], 1.0)
|
|
136
|
+
self.assertLess(res["c_T_outlet [mol/m^3]"], _INPUT[0])
|
|
137
|
+
|
|
138
|
+
#hydrostatic head drops the gas pressure below the inlet pressure
|
|
139
|
+
self.assertLess(res["total_gas_P_outlet [Pa]"], _BASE["P_in"])
|
|
140
|
+
|
|
141
|
+
#closed tritium mass balance
|
|
142
|
+
self.assertAlmostEqual(res["Total tritium in [mol/s]"],
|
|
143
|
+
res["Total tritium out [mol/s]"], places=12)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_output_ports(self):
|
|
147
|
+
"""The eight dimensional output ports match the results dictionary."""
|
|
148
|
+
blk = GLC(BCs="C-C", **_BASE)
|
|
149
|
+
blk.inputs.update_from_array(np.array(_INPUT))
|
|
150
|
+
blk.update(0.0)
|
|
151
|
+
|
|
152
|
+
res = blk.results()
|
|
153
|
+
for label, key in (
|
|
154
|
+
("c_T_out", "c_T_outlet [mol/m^3]"),
|
|
155
|
+
("y_T2_out", "y_T2_outlet_gas"),
|
|
156
|
+
("eff", "extraction_efficiency [fraction]"),
|
|
157
|
+
("P_out", "total_gas_P_outlet [Pa]"),
|
|
158
|
+
("Q_l", "liquid_vol_flow [m^3/s]"),
|
|
159
|
+
("Q_g_out", "gas_vol_flow_outlet [m^3/s]"),
|
|
160
|
+
("n_T_out_liquid", "tritium_out_liquid [mol/s]"),
|
|
161
|
+
("n_T_out_gas", "tritium_out_gas [mol/s]"),
|
|
162
|
+
):
|
|
163
|
+
idx = blk.output_port_labels[label]
|
|
164
|
+
self.assertAlmostEqual(blk.outputs[idx], res[key], places=12)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_unknown_bc_raises(self):
|
|
168
|
+
"""An unknown boundary-condition type is rejected at solve time."""
|
|
169
|
+
blk = GLC(BCs="bogus", **_BASE)
|
|
170
|
+
blk.inputs.update_from_array(np.array(_INPUT))
|
|
171
|
+
with self.assertRaises(ValueError):
|
|
172
|
+
blk.update(0.0)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_input_unchanged_skips_resolve(self):
|
|
176
|
+
"""A re-evaluation with unchanged input does not re-solve the BVP."""
|
|
177
|
+
import pathsim.blocks.bvp as bvpmod
|
|
178
|
+
|
|
179
|
+
blk = GLC(BCs="C-C", **_BASE)
|
|
180
|
+
blk.inputs.update_from_array(np.array(_INPUT))
|
|
181
|
+
|
|
182
|
+
calls = [0]
|
|
183
|
+
orig = bvpmod.solve_bvp
|
|
184
|
+
def _counting(*a, **k):
|
|
185
|
+
calls[0] += 1
|
|
186
|
+
return orig(*a, **k)
|
|
187
|
+
bvpmod.solve_bvp = _counting
|
|
188
|
+
try:
|
|
189
|
+
blk.update(0.0) #first solve
|
|
190
|
+
blk.update(0.0) #unchanged input -> skipped
|
|
191
|
+
self.assertEqual(calls[0], 1)
|
|
192
|
+
|
|
193
|
+
blk.inputs[3] = 2e-4 #flow_g
|
|
194
|
+
blk.update(0.0) #changed input -> solve again
|
|
195
|
+
self.assertEqual(calls[0], 2)
|
|
196
|
+
finally:
|
|
197
|
+
bvpmod.solve_bvp = orig
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def test_solve_function(self):
|
|
201
|
+
"""The standalone solve() helper returns results and the BVP block."""
|
|
202
|
+
p = dict(_BASE)
|
|
203
|
+
p.update(elements=20, BCs="C-C", c_T_in=_INPUT[0], flow_l=_INPUT[1],
|
|
204
|
+
y_T2_in=_INPUT[2], flow_g=_INPUT[3])
|
|
205
|
+
results, bvp = glc.solve(p)
|
|
206
|
+
|
|
207
|
+
self.assertIsInstance(bvp, BVP1D)
|
|
208
|
+
self.assertTrue(bvp.success)
|
|
209
|
+
x_T_ref, _ = _reference("C-C")
|
|
210
|
+
self.assertAlmostEqual(results["c_T_outlet [mol/m^3]"],
|
|
211
|
+
x_T_ref * _INPUT[0], places=12)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def test_non_physical_pressure_raises(self):
|
|
215
|
+
"""A column tall enough to drive the outlet pressure non-positive fails."""
|
|
216
|
+
#very tall column at low inlet pressure -> hydrostatic head exceeds P_in
|
|
217
|
+
blk = GLC(P_in=1e4, L=10.0, D=0.1, T=623.0, BCs="C-C")
|
|
218
|
+
blk.inputs.update_from_array(np.array(_INPUT))
|
|
219
|
+
with self.assertRaises(ValueError):
|
|
220
|
+
blk.update(0.0)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def test_simulation(self):
|
|
224
|
+
"""The block runs inside a Simulation driven by constant sources."""
|
|
225
|
+
srcs = [Constant(v) for v in _INPUT]
|
|
226
|
+
block = GLC(BCs="C-C", **_BASE)
|
|
227
|
+
sco = Scope()
|
|
228
|
+
|
|
229
|
+
sim = Simulation(
|
|
230
|
+
blocks=[*srcs, block, sco],
|
|
231
|
+
connections=(
|
|
232
|
+
[Connection(srcs[i], block[i]) for i in range(4)]
|
|
233
|
+
#connect the named efficiency output port, as a downstream
|
|
234
|
+
#consumer would
|
|
235
|
+
+ [Connection(block["eff"], sco[0])]
|
|
236
|
+
),
|
|
237
|
+
log=False,
|
|
238
|
+
)
|
|
239
|
+
sim.run(0.05)
|
|
240
|
+
|
|
241
|
+
self.assertTrue(block.success)
|
|
242
|
+
res = block.results()
|
|
243
|
+
np.testing.assert_allclose(
|
|
244
|
+
block.outputs[block.output_port_labels["eff"]],
|
|
245
|
+
res["extraction_efficiency [fraction]"], atol=1e-12
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
# RUN TESTS LOCALLY ====================================================================
|
|
250
|
+
|
|
251
|
+
if __name__ == '__main__':
|
|
252
|
+
unittest.main(verbosity=2)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/thermodynamics/activity_coefficients.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/thermodynamics/equations_of_state.py
RENAMED
|
File without changes
|
{pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/thermodynamics/fugacity_coefficients.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/thermodynamics/test_activity_coefficients.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/thermodynamics/test_fugacity_coefficients.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|