Fix drying gas mass-rate balance#108
Conversation
Mazhar331
left a comment
There was a problem hiding this comment.
Maintainer review
The physics of this change is right. Consolidating the molar→mass conversion into a single point in unit_model is the correct shape of fix for #21: Liquid_1.name_species is the full species list, so mw aligns column-wise with dry_rate, the non-volatile columns are zero and therefore unaffected, mw is g/mol (consistent with mw_avg_gas / 1000 at Drying_Model.py:253), and the /1000 yields kg/mol. After the change transfer_gas is [kg m^-3 s^-1] / [-] / [kg m^-3] = [1/s], dimensionally consistent with the convection term, which is exactly what #21 asks for. Removing the local dry_rate_mass block from material_balance correctly avoids double-converting the #20 saturation fix.
What I cannot sign off on is the test coverage. The three new tests do not detect the bug this PR fixes. I verified this by mutation, not by reading:
1. Deleting the fix leaves the suite green. I removed self.dry_rate = self._drying_rate_mass_basis(self.dry_rate) (Drying_Model.py:274) — the entire fix — and ran pytest tests/test_drying_model.py: 3 passed. Both material_balance tests are fed dry_rate directly as a parameter, and _drying_rate_mass_basis is exercised in isolation, so nothing asserts that unit_model actually applies the conversion before the balances consume it.
2. The gas-species test passes when fed molar rates. I reverted the dry_rate fixture in test_material_balance_uses_mass_drying_rate_for_gas_species to the pre-fix molar values ([[2.0, 0.0, 4.0], [3.0, 0.0, 5.0]]) — the precise input shape bug #21 describes — and the test still passed. It cannot fail on a units error, because expected_dygas_dt re-derives the production expression from the same dry_rate input. The test does not set dryer.Liquid_1 at all, which is itself the tell: material_balance no longer touches MW, so a test living there cannot assert anything about mass basis.
Details and fixes are in the inline comments.
Blocking
The unit_model conversion has no regression coverage
Inline at PharmaPy/Drying_Model.py:274. The PR's stated regression is unguarded: a future refactor can drop the conversion line and CI stays green. Add a test that drives unit_model (or asserts on the dry_rate handed to the balances, e.g. by monkeypatching material_balance/energy_balance to capture it) and pins the mass-basis value.
test_material_balance_uses_mass_drying_rate_for_gas_species is tautological
Inline at tests/test_drying_model.py:120. expected_dygas_dt duplicates the implementation under test, so it asserts an identity rather than a behavior, and the name overstates what it checks. Replace the recomputation with hand-computed literals, as test_material_balance_uses_mass_drying_rate_for_saturation correctly does with mass_rate = np.array([0.22, 0.284]).
Nonblocking
energy_balance behavior changes with no test
Because energy_balance receives self.dry_rate, both sensible_heat (Drying_Model.py:368) and drying_terms (Drying_Model.py:397) are now scaled by mw/1000 — roughly 0.018 for water, 0.046 for the third species in the fixtures. dTg_dt and dTcond_dt therefore change by more than an order of magnitude. This is the correct direction (it is #48's root cause, as the PR body says), and I agree it should not close #48, but no test in the repository calls energy_balance or unit_model, so this is a large, entirely uncovered numerical change riding along in a PR titled for the gas balance. A unit_model-level test that asserts on the returned model_eqns would cover this and the blocking item above together. Note this also interacts with the * 2 latent-heat factor tracked in #24.
Mixed line endings introduced
Inline at PharmaPy/Drying_Model.py:203. Drying_Model.py is a CRLF file (697 CRLF lines); this PR writes LF-terminated lines into the hunks it touches, taking LF-only lines from 16 to 27. About 16 of the 33 +/- lines in the diff are pure end-of-line churn — def get_drying_rate(...) and return dry_rates show as modified while being byte-identical in content. git diff --ignore-cr-at-eol reduces the file's diff to 9 insertions / 6 deletions, which is the real change. There is no .gitattributes; either add one with *.py text eol=lf and normalize deliberately in a separate commit, or keep this diff CRLF-consistent so review and git blame stay clean.
self.dry_rate units are no longer documented
Inline at PharmaPy/Drying_Model.py:220. The removed comment in material_balance was the only place stating the basis of the rate. get_drying_rate remains molar while self.dry_rate is now mass — a real trap for the next maintainer. A one-line unit annotation on _drying_rate_mass_basis and at the unit_model assignment costs nothing.
Questions
None.
Tests run
All in the repository's pharmapy conda environment, at head 4029b17.
pytest tests/test_drying_model.py -q -rs— 3 passed, 0 skipped.pytest tests/ -q -rs— 43 passed, 0 skipped, 2 warnings (both pre-existing:numpy.matlibPendingDeprecationWarningfromDrying_Model.py:8, and a zero-mass/moles/volRuntimeWarningfromPhases.py:203).pytest tests/ --collect-only -m ""— 43 collected, identical to the default run, so no marker group is being silently deselected. The only registered marker isassimulo, and Assimulo is installed here.- Mutation check A: deleted
Drying_Model.py:274in a scratch worktree →3 passed(should have failed). - Mutation check B: molar
dry_ratefixture in the gas-species test →1 passed(should have failed). - Verified
dry_rate/get_drying_ratehave no consumers outsideDrying_Model.py, soself.dry_rateswitching basis leaks into no reporting or plotting path. - Cleaned the
__pycache__directories my test runs created; note they are not covered by.gitignorein this repo, which is pre-existing and out of scope here.
Linked issue
Closes #21 is accurate. The gas transfer term at Drying_Model.py:329 now adds a mass source to a mass-fraction state, which is the core ask, and the two independent verifications on the issue both name MW / 1000 as the fix. The coordination notes on #20/#48/#24 match what the code actually does.
Merge readiness
Checks on the reviewed head 4029b17 are green: Core tests SUCCESS, Assimulo integration tests SUCCESS. mergeStateStatus is CLEAN, the head is level with master (0 commits behind), so PR-head and merge-result checks coincide here. master has no branch protection and GitHub reports no reviewDecision gate.
This is a COMMENT review because I am the PR author; GitHub rejects APPROVE and REQUEST_CHANGES from the author, so this account cannot provide a formal approval if one is wanted. No approval gate is currently reported by GitHub.
I would not merge this until the blocking issues above are addressed. Both are test-only changes and neither requires touching Drying_Model.py logic, which I believe is correct as written.
| self.dry_rate = self.get_drying_rate(x_liq, temp_sol, y_gas, | ||
| self.pres_gas) | ||
|
|
||
| self.dry_rate = self._drying_rate_mass_basis(self.dry_rate) |
There was a problem hiding this comment.
Blocking — this line is the entire fix, and nothing tests it.
I deleted exactly this line in a scratch worktree and ran pytest tests/test_drying_model.py: 3 passed. Both material_balance tests receive dry_rate as a parameter, and _drying_rate_mass_basis is tested in isolation, so no test asserts that unit_model converts before material_balance and energy_balance consume the rate. A future refactor can drop this line and CI stays green — the regression test does not guard the regression.
Concrete fix: add a test that drives unit_model and pins the mass-basis rate actually passed downstream. Capturing it is cheap, e.g.
captured = {}
monkeypatch.setattr(dryer, 'material_balance',
lambda *a, **k: captured.setdefault('dry_rate', a[8]) and [np.zeros(2)]*3)or assert directly on the returned model_eqns, which would also cover the untested energy_balance change (see the energy-balance-uncovered finding in the review body).
Separately, please annotate the basis here — after this line self.dry_rate is [kg m^-3 s^-1], while get_drying_rate still returns [mol m^-3 s^-1].
There was a problem hiding this comment.
Addressed in a2c80e1: added a unit_model test that captures the dry_rate passed into both balances and pins the mass-basis values, plus unit comments at the conversion site.
There was a problem hiding this comment.
Still addressed at current head. The unit_model handoff test continues to verify that the converted mass-basis dry_rate reaches both balance methods; 70b5ba7 only adds the requested unit/doc clarity around that path.
| return dry_rates | ||
|
|
||
| def _drying_rate_mass_basis(self, dry_rate): | ||
| mw = np.asarray(self.Liquid_1.mw) / 1000 # [kg/mol] |
There was a problem hiding this comment.
Nonblocking — the comment this PR removed from material_balance was the only place documenting the rate's basis. Now get_drying_rate returns molar and self.dry_rate holds mass, with nothing saying so at either site.
A docstring or one-line annotation is enough:
def _drying_rate_mass_basis(self, dry_rate):
"""Convert molar drying rates [mol m^-3 s^-1] to mass rates [kg m^-3 s^-1]."""
mw = np.asarray(self.Liquid_1.mw) / 1000 # [kg/mol]
return dry_rate * mwFor what it is worth I checked the alignment and it is correct: self.name_species = self.Liquid_1.name_species (Drying_Model.py:129), so mw is indexed over the full species set and broadcasts column-wise against dry_rate; the non-volatile/supercritical columns are zero in get_drying_rate, so scaling them is a no-op.
There was a problem hiding this comment.
Addressed in a2c80e1: _drying_rate_mass_basis now has a unit docstring, and unit_model annotates the molar output from get_drying_rate and the mass-basis rate consumed by the balances.
There was a problem hiding this comment.
Addressed at current head. get_drying_rate is now documented as returning molar rates [mol/m**3/s], unit_model documents the single conversion point, and material_balance documents the mass-basis contract [kg/m**3/s].
| return y_equil | ||
|
|
||
| def get_drying_rate(self, x_liq, temp_cond, y_gas, p_gas): | ||
| def get_drying_rate(self, x_liq, temp_cond, y_gas, p_gas): |
There was a problem hiding this comment.
Nonblocking — this line is byte-identical in content to master; it appears in the diff only because its line ending changed from CRLF to LF. Drying_Model.py is a CRLF file (697 CRLF lines), and this PR writes LF lines into the hunks it touches, taking LF-only lines from 16 to 27.
About 16 of the diff's 33 +/- lines are pure EOL churn — return dry_rates below has the same problem. git diff --ignore-cr-at-eol collapses this file to 9 insertions / 6 deletions, which is the real change.
There is no .gitattributes in the repo. Either normalize deliberately (add *.py text eol=lf and convert in a separate commit) or keep this diff CRLF-consistent, so review and git blame stay readable.
There was a problem hiding this comment.
Addressed in a2c80e1: restored Drying_Model.py to CRLF-consistent line endings. The remaining model diff is visible with git diff --ignore-cr-at-eol.
There was a problem hiding this comment.
Still addressed at current head. Drying_Model.py remains CRLF in the worktree/index, and the CRLF-aware staged whitespace check passed.
|
Addressed the review feedback in commit Changes made:
Tests run:
CI on head
Not intentionally declined: none. Remaining follow-up is the already documented #48/#24 energy-specific validation, outside this PR's #21 scope. |
Verification of
|
| return dry_rate * mw | ||
|
|
||
| def unit_model(self, time, states, sw=None): | ||
| ''' |
There was a problem hiding this comment.
Addressed in beb53e4: the adjacent unit_model docstring now consistently uses triple-double quotes.
There was a problem hiding this comment.
Still addressed at current head. The unit_model docstring consistently uses triple-double quotes.
|
Addressed the latest review feedback in commit Changes made:
Tests run:
No feedback was declined. CI is currently running on this head. GitHub still reports |
Review-resolution check — @bernalde's requested changesRead-only verification of head @bernalde comment-by-comment
Earlier maintainer (self) review — still holding at headThe two blocking items from the
The prior in-PR verification comment recorded the mutation checks (dropping the conversion line and feeding molar fixtures now both fail, where they previously passed). CI evidenceOn head Remaining items
Next actionNo further review round is warranted on technical grounds — the changes since the review are confined to docstrings, a units comment, and line endings, with no behavior change outside the commented lines. The one required human action is on @bernalde's side: GitHub still reports Read-only assessment; no files, checks, or review state were modified. |
|
Addressed the latest unit/comment feedback in Changes made:
Validation:
No review comments were declined. Remaining follow-up boundary: #48/#24 still need energy-balance-specific validation before those issues can close. The formal PR review decision still shows |
…-basis Test drying energy rate basis
Summary
unit_modelafterget_drying_rate.Closes #21.
Refs #17, #20, #24, #48, #103.
Unit Contract
get_drying_ratereturns component volumetric drying rates on a molar basis[mol/m**3/s].unit_modelconverts that rate once using molecular weights[kg/mol]; after_drying_rate_mass_basis,self.dry_rateis[kg/m**3/s].material_balanceconsumes mass-basis drying rates[kg/m**3/s]; saturation, gas mass fractions, liquid mass fractions, porosity, and gas holdup terms are dimensionless[-].dsat_dt,dygas_dt, anddxliq_dtin[1/s].energy_balancenow receives the same mass-basisself.dry_rate, which is the shared conversion needed by High: [Drying] Energy terms multiply molar drying rates by mass-basis heat quantities #48, but High: [Drying] Energy terms multiply molar drying rates by mass-basis heat quantities #48 still needs energy-specific validation before it can close.Coordination Notes
dry_rate_massconversion insidematerial_balance. This PR intentionally removes that local conversion and replaces it with one shared conversion point inunit_model, so the Critical: [Drying] Liquid saturation balance mixes molar drying rate with mass density #20 saturation fix is preserved without a double MW/1000 conversion.get_drying_rateremains the molar volumetric-rate calculator (mol m^-3 s^-1), matchingk_y * a_V * delta_y. This PR converts its output once before the model balances consume it.energy_balance. Becauseenergy_balancereceives the convertedself.dry_rate, this PR covers the shared MW/1000 conversion needed by High: [Drying] Energy terms multiply molar drying rates by mass-basis heat quantities #48. It does not close High: [Drying] Energy terms multiply molar drying rates by mass-basis heat quantities #48 because that issue still needs energy-specific validation.* 2factor in the same energy expression. High: [Drying] Energy terms multiply molar drying rates by mass-basis heat quantities #48/High: [Drying] Condensed-phase energy balance doubles latent heat #24 should be resolved with a clear follow-up diff, likely by removing the* 2and adding an isolated energy-balance regression.get_drying_rate,unit_model, andmaterial_balance; final energy-balance unit validation remains pending High: [Drying] Energy terms multiply molar drying rates by mass-basis heat quantities #48/High: [Drying] Condensed-phase energy balance doubles latent heat #24.Tests
conda run -n pharmapy python -m pytest tests/test_drying_model.py -qconda run -n pharmapy python -m pytest tests/ -m "not assimulo" -qconda run -n pharmapy python -m pytest tests/ -qgit -c core.whitespace=blank-at-eol,blank-at-eof,space-before-tab,cr-at-eol diff --cached --checkBranch Hygiene
masterorigin/master