Skip to content

Commit 4d89f26

Browse files
authored
Merge pull request #29 from pathsim/cleanup/glc-remove-solve
Clean up GLC module: drop solve(), slim results, honest mass balance
2 parents 27b677c + 2224fb8 commit 4d89f26

2 files changed

Lines changed: 30 additions & 108 deletions

File tree

src/pathsim_chem/tritium/glc.py

Lines changed: 18 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -272,106 +272,34 @@ def _process_results(S_ends, params, phys_props, dim_params):
272272
P_T2_out = y_T2_out * P_out
273273
P_T2_in = y_T2_in * P_in
274274

275-
# Mass balance check
276-
n_T_in_liquid = c_T_in * Q_l # mol/s
277-
n_T_out_liquid = c_T_out * Q_l # mol/s
278-
n_T2_in_gas = P_T2_in * Q_g / (R * T) # mol/s
279-
n_T_in_gas = n_T2_in_gas * 2 # mol/s
275+
# Tritium molar flows from the converged solution (mol/s). Each T2 molecule
276+
# carries two tritium atoms.
277+
n_T_in_liquid = c_T_in * Q_l
278+
n_T_in_gas = 2 * P_T2_in * Q_g / (R * T)
279+
n_T_out_liquid = c_T_out * Q_l
280280
Q_g_out = Q_g * (P_in / P_out) # m3/s
281-
n_T2_out_gas = P_T2_out * Q_g_out / (R * T) # mol/s
282-
n_T_out_gas = n_T2_out_gas * 2 # mol/s
281+
n_T_out_gas = 2 * P_T2_out * Q_g_out / (R * T)
283282

284-
# Adjust for any mass balance error
285-
mass_balance_error = (n_T_in_liquid + n_T_in_gas) - (n_T_out_liquid + n_T_out_gas)
286-
n_T_out_gas += mass_balance_error * efficiency
287-
n_T_out_liquid += mass_balance_error * (1 - efficiency)
283+
n_T_in = n_T_in_liquid + n_T_in_gas
284+
n_T_out = n_T_out_liquid + n_T_out_gas
288285

289-
results = {
290-
"Total tritium in [mol/s]": n_T_in_liquid + n_T_in_gas,
291-
"Total tritium out [mol/s]": n_T_out_liquid + n_T_out_gas,
292-
"tritium_out_liquid [mol/s]": n_T_out_liquid,
293-
"tritium_out_gas [mol/s]": n_T_out_gas,
294-
"extraction_efficiency [fraction]": efficiency,
286+
# Mass-balance residual reported as a diagnostic. For closed-closed (C-C)
287+
# boundary conditions it is at numerical-noise level; for open-closed (O-C)
288+
# it reflects the dispersive flux neglected by the open inlet condition.
289+
return {
295290
"c_T_outlet [mol/m^3]": c_T_out,
296-
"P_T2_inlet_gas [Pa]": P_T2_in,
297-
"P_T2_outlet_gas [Pa]": P_T2_out,
298291
"y_T2_outlet_gas": y_T2_out,
299-
"total_gas_P_inlet [Pa]": P_in,
292+
"extraction_efficiency [fraction]": efficiency,
300293
"total_gas_P_outlet [Pa]": P_out,
301294
"liquid_vol_flow [m^3/s]": Q_l,
302295
"gas_vol_flow_outlet [m^3/s]": Q_g_out,
296+
"tritium_out_liquid [mol/s]": n_T_out_liquid,
297+
"tritium_out_gas [mol/s]": n_T_out_gas,
298+
"Total tritium in [mol/s]": n_T_in,
299+
"Total tritium out [mol/s]": n_T_out,
300+
"mass_balance_residual [mol/s]": n_T_in - n_T_out,
303301
}
304302

305-
# Add all calculated parameters to the results dictionary
306-
results.update(phys_props)
307-
results.update(dim_params)
308-
309-
return results
310-
311-
312-
def solve(params):
313-
"""
314-
Main solver function for the bubble column model.
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-
319-
Args:
320-
params (dict): A dictionary of all input parameters for the model,
321-
including operational conditions and geometry.
322-
323-
Returns:
324-
list: A list containing:
325-
- dict: A dictionary containing the simulation results.
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.
332-
"""
333-
# Adjust inlet gas concentration to avoid numerical instability at zero
334-
y_T2_in = max(params["y_T2_in"], 1e-20)
335-
336-
# 1. Calculate physical, hydrodynamic, and mass transfer properties
337-
phys_props = _calculate_properties(params)
338-
339-
# Pre-solver check for non-physical outlet pressure
340-
P_out = params["P_in"] - (
341-
phys_props["rho_l"] * (1 - phys_props["epsilon_g"]) * g * params["L"]
342-
)
343-
if P_out <= 0:
344-
raise ValueError(
345-
f"Calculated gas outlet pressure is non-positive ({P_out:.2e} Pa). "
346-
"Check input parameters P_in, L, etc."
347-
)
348-
349-
# 2. Calculate dimensionless groups for the ODE system
350-
dim_params = _calculate_dimensionless_groups(params, phys_props)
351-
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.")
369-
370-
# 4. Process and return the results in a dimensional format
371-
results = _process_results(bvp.solution(), params, phys_props, dim_params)
372-
373-
return [results, bvp]
374-
375303

376304
class GLC(BVP1D):
377305
r"""Counter-current bubble column gas-liquid contactor (GLC) for tritium extraction.

tests/tritium/test_glc.py

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -122,9 +122,16 @@ def test_solve_matches_reference_oc(self):
122122
x_T_ref * _INPUT[0], places=12)
123123
self.assertAlmostEqual(res["y_T2_outlet_gas"], y_T2_ref, places=12)
124124

125+
#the open inlet condition neglects the dispersive flux, so O-C does not
126+
#close the mass balance exactly; the residual is small but non-trivial
127+
#(a few per mille) and is reported honestly rather than hidden
128+
rel = abs(res["mass_balance_residual [mol/s]"]) / res["Total tritium in [mol/s]"]
129+
self.assertGreater(rel, 1e-6)
130+
self.assertLess(rel, 1e-2)
131+
125132

126133
def test_results_physical(self):
127-
"""The dimensional results are physically sensible and mass conserving."""
134+
"""The dimensional results are physically sensible."""
128135
blk = GLC(BCs="C-C", **_BASE)
129136
blk.inputs.update_from_array(np.array(_INPUT))
130137
blk.update(0.0)
@@ -138,9 +145,10 @@ def test_results_physical(self):
138145
#hydrostatic head drops the gas pressure below the inlet pressure
139146
self.assertLess(res["total_gas_P_outlet [Pa]"], _BASE["P_in"])
140147

141-
#closed tritium mass balance
142-
self.assertAlmostEqual(res["Total tritium in [mol/s]"],
143-
res["Total tritium out [mol/s]"], places=12)
148+
#closed-closed boundary conditions conserve tritium to numerical noise;
149+
#the residual is reported, not redistributed back into the outputs
150+
rel = abs(res["mass_balance_residual [mol/s]"]) / res["Total tritium in [mol/s]"]
151+
self.assertLess(rel, 1e-6)
144152

145153

146154
def test_output_ports(self):
@@ -197,20 +205,6 @@ def _counting(*a, **k):
197205
bvpmod.solve_bvp = orig
198206

199207

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-
214208
def test_non_physical_pressure_raises(self):
215209
"""A column tall enough to drive the outlet pressure non-positive fails."""
216210
#very tall column at low inlet pressure -> hydrostatic head exceeds P_in

0 commit comments

Comments
 (0)