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.
Files changed (97) hide show
  1. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/PKG-INFO +1 -1
  2. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/pyproject.toml +4 -0
  3. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/_version.py +3 -3
  4. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/tritium/glc.py +248 -107
  5. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem.egg-info/PKG-INFO +1 -1
  6. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem.egg-info/SOURCES.txt +3 -0
  7. pathsim_chem-0.2.6/src/pathsim_chem.egg-info/scm_file_list.json +91 -0
  8. pathsim_chem-0.2.6/src/pathsim_chem.egg-info/scm_version.json +8 -0
  9. pathsim_chem-0.2.6/tests/tritium/test_glc.py +252 -0
  10. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/.github/workflows/pypi_deployment.yml +0 -0
  11. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/.github/workflows/tests.yml +0 -0
  12. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/.gitignore +0 -0
  13. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/LICENSE +0 -0
  14. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/README.md +0 -0
  15. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/.readthedocs.yaml +0 -0
  16. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/Makefile +0 -0
  17. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/make.bat +0 -0
  18. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/requirements.txt +0 -0
  19. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/source/_autosummary/pathsim_chem.rst +0 -0
  20. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/source/_static/custom.css +0 -0
  21. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/source/api.rst +0 -0
  22. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/source/conf.py +0 -0
  23. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/source/examples/cstr_reaction.ipynb +0 -0
  24. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/source/examples/equation_of_state.ipynb +0 -0
  25. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/source/examples/flash_distillation.ipynb +0 -0
  26. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/source/examples/heat_exchanger.ipynb +0 -0
  27. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/source/examples/multicomponent_flash.ipynb +0 -0
  28. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/source/examples/point_kinetics.ipynb +0 -0
  29. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/source/examples/process_flowsheet.ipynb +0 -0
  30. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/source/examples/valve_characteristics.ipynb +0 -0
  31. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/source/examples/vapor_pressure_curves.ipynb +0 -0
  32. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/source/examples/vle_calculation.ipynb +0 -0
  33. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/source/index.rst +0 -0
  34. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/source/logos/chem_logo.png +0 -0
  35. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/source/logos/pathsim_icon.png +0 -0
  36. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/source/logos/pathsim_logo.png +0 -0
  37. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/source/modules/tritium.rst +0 -0
  38. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/docs/source/pathsim_docs.mplstyle +0 -0
  39. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/setup.cfg +0 -0
  40. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/__init__.py +0 -0
  41. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/neutronics/__init__.py +0 -0
  42. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/neutronics/point_kinetics.py +0 -0
  43. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/process/__init__.py +0 -0
  44. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/process/cstr.py +0 -0
  45. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/process/distillation.py +0 -0
  46. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/process/flash_drum.py +0 -0
  47. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/process/heat_exchanger.py +0 -0
  48. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/process/heater.py +0 -0
  49. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/process/mixer.py +0 -0
  50. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/process/multicomponent_flash.py +0 -0
  51. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/process/pfr.py +0 -0
  52. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/process/valve.py +0 -0
  53. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/thermodynamics/__init__.py +0 -0
  54. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/thermodynamics/activity_coefficients.py +0 -0
  55. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/thermodynamics/averages.py +0 -0
  56. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/thermodynamics/corrections.py +0 -0
  57. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/thermodynamics/correlations.py +0 -0
  58. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/thermodynamics/enthalpy.py +0 -0
  59. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/thermodynamics/equations_of_state.py +0 -0
  60. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/thermodynamics/fugacity_coefficients.py +0 -0
  61. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/thermodynamics/reactions.py +0 -0
  62. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/tritium/README.md +0 -0
  63. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/tritium/__init__.py +0 -0
  64. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/tritium/bubbler.py +0 -0
  65. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/tritium/ionisation_chamber.py +0 -0
  66. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/tritium/residencetime.py +0 -0
  67. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/tritium/splitter.py +0 -0
  68. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem/tritium/tcap.py +0 -0
  69. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem.egg-info/dependency_links.txt +0 -0
  70. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem.egg-info/requires.txt +0 -0
  71. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/src/pathsim_chem.egg-info/top_level.txt +0 -0
  72. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/neutronics/__init__.py +0 -0
  73. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/neutronics/test_point_kinetics.py +0 -0
  74. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/process/__init__.py +0 -0
  75. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/process/test_cstr.py +0 -0
  76. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/process/test_distillation.py +0 -0
  77. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/process/test_flash_drum.py +0 -0
  78. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/process/test_heat_exchanger.py +0 -0
  79. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/process/test_heater.py +0 -0
  80. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/process/test_mixer.py +0 -0
  81. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/process/test_multicomponent_flash.py +0 -0
  82. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/process/test_pfr.py +0 -0
  83. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/process/test_valve.py +0 -0
  84. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/thermodynamics/__init__.py +0 -0
  85. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/thermodynamics/test_activity_coefficients.py +0 -0
  86. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/thermodynamics/test_averages.py +0 -0
  87. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/thermodynamics/test_corrections.py +0 -0
  88. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/thermodynamics/test_correlations.py +0 -0
  89. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/thermodynamics/test_enthalpy.py +0 -0
  90. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/thermodynamics/test_equations_of_state.py +0 -0
  91. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/thermodynamics/test_fugacity_coefficients.py +0 -0
  92. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/thermodynamics/test_reactions.py +0 -0
  93. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/tritium/__init__.py +0 -0
  94. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/tritium/test_bubbler.py +0 -0
  95. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/tritium/test_ionisation_chamber.py +0 -0
  96. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/tritium/test_residencetime.py +0 -0
  97. {pathsim_chem-0.2.4 → pathsim_chem-0.2.6}/tests/tritium/test_splitter.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pathsim-chem
3
- Version: 0.2.4
3
+ Version: 0.2.6
4
4
  Summary: Chemical Engineering Blocks for PathSim
5
5
  Author: PathSim Contributors
6
6
  License: MIT
@@ -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.4'
22
- __version_tuple__ = version_tuple = (0, 2, 4)
21
+ __version__ = version = '0.2.6'
22
+ __version_tuple__ = version_tuple = (0, 2, 6)
23
23
 
24
- __commit_id__ = commit_id = 'gd3cc4c4a2'
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
- import pathsim
14
- from pathsim.utils.register import Register
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 _solve_bvp_system(dim_params, y_T2_in, BCs, elements):
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
- This function defines the system of ordinary differential equations (ODEs)
170
- and the corresponding boundary conditions, then solves the BVP using
171
- `scipy.integrate.solve_bvp`.
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
- dim_params (dict): Dictionary of dimensionless groups.
175
- y_T2_in (float): Inlet mole fraction of T2 in the gas phase.
176
- BCs (str): String specifying the boundary conditions (e.g., "C-C").
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
- scipy.integrate.OdeSolution: The solution object from `solve_bvp`.
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
- dim_params["Bo_l"],
184
- dim_params["phi_l"],
185
- dim_params["Bo_g"],
186
- dim_params["phi_g"],
187
- dim_params["psi"],
188
- dim_params["nu"],
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
- def ode_system(xi, S):
192
- """
193
- Defines the system of 4 first-order ODEs.
194
- S = [x_T, dx_T/d(xi), y_T2, dy_T2/d(xi)]
195
- """
196
- x_T, dx_T_dxi, y_T2, dy_T2_dxi = S
197
- theta = x_T - np.sqrt(np.maximum(0, (1 - psi * xi) * y_T2 / nu))
198
-
199
- dS0_dxi = dx_T_dxi # d(x_T)/d(xi)
200
- dS1_dxi = Bo_l * (phi_l * theta - dx_T_dxi) # d^2(x_T)/d(xi)^2
201
- dS2_dxi = dy_T2_dxi # d(y_T2)/d(xi)
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 _process_results(solution, params, phys_props, dim_params):
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
- solution (scipy.integrate.OdeSolution): The BVP solution object.
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
- list: A list containing the results dictionary and the solution object.
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
- if not solution.success:
252
- raise RuntimeError("BVP solver failed to converge.")
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 = solution.y[2, -1]
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, solution
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
- - scipy.integrate.OdeSolution: The raw solution object from the
315
- BVP solver.
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
- solution = _solve_bvp_system(dim_params, y_T2_in, params["BCs"], params["elements"])
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
- [results, solution] = _process_results(solution, params, phys_props, dim_params)
371
+ results = _process_results(bvp.solution(), params, phys_props, dim_params)
341
372
 
342
- return [results, solution]
373
+ return [results, bvp]
343
374
 
344
375
 
345
- class GLC(pathsim.blocks.Function):
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 intended for steady-state tritium extraction calculations
356
- in fusion blanket systems. At each evaluation it computes
357
- temperature-dependent fluid properties, dimensionless groups, and solves
358
- the BVP using ``scipy.integrate.solve_bvp``.
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
- super().__init__(func=self.func)
435
-
436
- def func(self, c_T_in, flow_l, y_T2_inlet, flow_g):
437
- new_params = self.params.copy()
438
- new_params["c_T_in"] = c_T_in
439
- new_params["flow_l"] = flow_l
440
- new_params["y_T2_in"] = y_T2_inlet
441
- new_params["flow_g"] = flow_g
442
-
443
- res, _ = solve(new_params)
444
-
445
- c_T_out = res["c_T_outlet [mol/m^3]"]
446
- y_T2_out = res["y_T2_outlet_gas"]
447
- eff = res["extraction_efficiency [fraction]"]
448
- P_total_outlet = res["total_gas_P_outlet [Pa]"]
449
- Q_l = res["liquid_vol_flow [m^3/s]"]
450
- Q_g_out = res["gas_vol_flow_outlet [m^3/s]"]
451
- n_T_out_liquid = res["tritium_out_liquid [mol/s]"]
452
- n_T_out_gas = res["tritium_out_gas [mol/s]"]
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pathsim-chem
3
- Version: 0.2.4
3
+ Version: 0.2.6
4
4
  Summary: Chemical Engineering Blocks for PathSim
5
5
  Author: PathSim Contributors
6
6
  License: MIT
@@ -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,8 @@
1
+ {
2
+ "tag": "0.2.6",
3
+ "distance": 0,
4
+ "node": "g27b677c3026ba13de35f6534a5a64797a23e046b",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-06-25"
8
+ }
@@ -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