You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Status: Verified bug against the current master source in this fork.
Severity: High.
Area labels: area:drying.
Tackle batch: P2 - shared thermo, phase, and basis correctness.
The source review confirmed that the cited code path and failure mode are still present. The original audit details are preserved below for reproduction notes and suggested fixes.
Original audit details
Severity: High · Part of #67 (round-2 audit batch)
All three defects live in PharmaPy/Drying_Model.py in the same drying subsystem and are exercised on the same code paths (material_balance RHS + solve_unit initialization). The gas-velocity (243-244) and gas-species-source (334) defects both corrupt the gas-phase balance and would be fixed and validated together in a single drying-correctness pass; the single-node init omission (472) is the same solve_unit setup that feeds that balance. Bundling avoids three overlapping PRs touching the same file and lets one regression test cover the coupled transient. The Wilke vapor-viscosity defect is intentionally kept separate (different file, general thermo property).
This issue bundles 3 defects that share one fix/PR.
1. (High) Darcy gas velocity discards relative permeability k_ra and divides by mean saturation (wrong gas flow at both saturation limits)
Location:PharmaPy/Drying_Model.py:243-244 (k_ra computed but unused at 239-241; correct form commented at 242)
Lines 239-241 compute the two-phase gas relative permeability k_ra = (1-sat_red)^2*(1-sat_red^1.4) but it is never used. The active line 243-244 computes vel_gas = self.dPg_dz/(alphavisc_gasrho_sol*(1-porosity))/np.mean(satur), i.e. single-phase Darcy velocity (= k_permdPg_dz/visc, since k_perm=1/alpha/rho_sol/(1-porosity) at line 520) divided by mean saturation. This (a) drops the k_ra throttle entirely and (b) substitutes a 1/mean(S) factor that runs backwards at the limits: at full saturation (S~1) gas cannot flow but the code returns full single-phase velocity, and as S decreases the velocity grows instead of approaching the finite k_ra->1 limit. The physically correct, intended form (k_permk_ra*dPg_dz/visc_gas) is the commented line 242.
Trigger: Any normal Drying.solve_unit run: unit_model is the integrator RHS, so line 243-244 executes on every CVode step; vel_gas (u_gas) feeds gas convection (line 332), the mass-transfer denominator (line 334), and the energy-convection term (line 387).
Wrong result: Gas superficial velocity is wrong in magnitude and trend throughout the transient (full single-phase speed even when the cake is liquid-saturated; growing as the cake dries instead of bounded by k_ra). This corrupts gas convection, mass-transfer, and energy-convection terms, distorting the entire moisture and temperature profile. Note: amplification is bounded (mean(satur) floors near the irreducible saturation ~0.155, giving ~6.5x, not the 1/eps blow-up originally claimed), but the physics is qualitatively wrong.
Suggested fix: Restore the relative-permeability form by uncommenting line 242: vel_gas = self.k_perm * k_ra * self.dPg_dz / visc_gas, and remove the /np.mean(satur) factor on line 243-244. Ensure gas velocity -> 0 as S -> 1 and stays finite as S -> 0.
2. (High) Gas-phase species evaporation source over-divided by an extra (1-satur) factor
Location:PharmaPy/Drying_Model.py:334
epsilon_gas (line 324) is already porosity*(1-satur), the gas volume fraction. The correct accumulation coefficient for a mass-fraction source is eps_grho_g, so the source should be N_i/(porosity(1-satur)rho_gas). But line 334 codes transfer_gas = dry_rate.T/epsilon_gas/dens_gas/(1-satur), dividing additionally by (1-satur), i.e. by porosity(1-satur)^2rho_gas. The companion total_mass_correction (line 336) already correctly carries the single 1/(1-satur) from d(eps_g)/dt, and the energy balance (line 362, denom_gas = cvg_mixepsilon_gas*rho_gas) uses exactly one (1-satur) for the same accumulation coefficient, directly contradicting line 334.
Trigger: Any normal Drying.solve_unit run. Saturation starts near 1 for a fresh wet cake, so (1-satur) is small (~0.05-0.2), making the spurious factor inflate the evaporative source ~5x-20x in the early high-saturation regime that dominates the transient.
Wrong result: The gas-phase y_i source (transfer_gas) is too large by 1/(1-satur), so the predicted gas-composition rise from evaporation is grossly overstated and the gas-phase species balance does not match the liquid loss; the whole coupled drying transient is biased.
3. (High) Uniform/single-node initialization omits temp_cond from the initial state vector
Location:PharmaPy/Drying_Model.py:470-475 (else branch; cf. correct if branch at 463-468)
When x_liq_init.ndim==1 and len(satur_init)==1, line 472 builds states_tuple = (satur_init, y_gas_init, x_liq_init, temp_gas_init), omitting temp_cond_init, giving a per-node width of 2+num_comp+num_volatiles. The parallel branch (lines 463-468) correctly stacks all five state groups (S, y_gas, x_liq, Tg, Tcond). unit_model later does states.reshape(-1, 3 + num_comp + self.num_volatiles) (line 225), which expects the temp_cond column.
Trigger: Drying.solve_unit with a Cake whose saturation is a single-element array (single representative node) and a uniform (1-D mass_frac) liquid composition, which hits the else branch at line 470.
Wrong result: model(0, states_prev.ravel()) at line 575 raises 'cannot reshape array of size N into shape (...)' (crash before the integrator starts) in ~87% of realistic dims; in the ~13% where divisibility coincidentally holds, state columns are silently misaligned (temp parsed from x_liq), corrupting every balance.
Suggested fix: Include temp_cond_init in the tuple to match the other branch: states_tuple = (satur_init, y_gas_init, x_liq_init, temp_gas_init, temp_cond_init).
Related existing issues (referenced, not modified):#20, #21, #24, #28, #37, #42, #48 (existing drying balance/property issues in the same module; reference only). #21 specifically shares line 334 (mol-vs-mass) but is a distinct mechanism from the extra (1-satur) factor here.
Found by a multi-agent source audit (round 2); confirmed by re-reading the source and cross-checked against all 80 existing open issues. Each defect was verified by an independent code-truth skeptic and a novelty skeptic.
Maintainer triage (June 29, 2026)
mastersource in this fork.The source review confirmed that the cited code path and failure mode are still present. The original audit details are preserved below for reproduction notes and suggested fixes.
Original audit details
Severity: High · Part of #67 (round-2 audit batch)
All three defects live in PharmaPy/Drying_Model.py in the same drying subsystem and are exercised on the same code paths (material_balance RHS + solve_unit initialization). The gas-velocity (243-244) and gas-species-source (334) defects both corrupt the gas-phase balance and would be fixed and validated together in a single drying-correctness pass; the single-node init omission (472) is the same solve_unit setup that feeds that balance. Bundling avoids three overlapping PRs touching the same file and lets one regression test cover the coupled transient. The Wilke vapor-viscosity defect is intentionally kept separate (different file, general thermo property).
This issue bundles 3 defects that share one fix/PR.
1. (High) Darcy gas velocity discards relative permeability k_ra and divides by mean saturation (wrong gas flow at both saturation limits)
Location:
PharmaPy/Drying_Model.py:243-244 (k_ra computed but unused at 239-241; correct form commented at 242)Lines 239-241 compute the two-phase gas relative permeability k_ra = (1-sat_red)^2*(1-sat_red^1.4) but it is never used. The active line 243-244 computes vel_gas = self.dPg_dz/(alphavisc_gasrho_sol*(1-porosity))/np.mean(satur), i.e. single-phase Darcy velocity (= k_permdPg_dz/visc, since k_perm=1/alpha/rho_sol/(1-porosity) at line 520) divided by mean saturation. This (a) drops the k_ra throttle entirely and (b) substitutes a 1/mean(S) factor that runs backwards at the limits: at full saturation (S~1) gas cannot flow but the code returns full single-phase velocity, and as S decreases the velocity grows instead of approaching the finite k_ra->1 limit. The physically correct, intended form (k_permk_ra*dPg_dz/visc_gas) is the commented line 242.
2. (High) Gas-phase species evaporation source over-divided by an extra (1-satur) factor
Location:
PharmaPy/Drying_Model.py:334epsilon_gas (line 324) is already porosity*(1-satur), the gas volume fraction. The correct accumulation coefficient for a mass-fraction source is eps_grho_g, so the source should be N_i/(porosity(1-satur)rho_gas). But line 334 codes transfer_gas = dry_rate.T/epsilon_gas/dens_gas/(1-satur), dividing additionally by (1-satur), i.e. by porosity(1-satur)^2rho_gas. The companion total_mass_correction (line 336) already correctly carries the single 1/(1-satur) from d(eps_g)/dt, and the energy balance (line 362, denom_gas = cvg_mixepsilon_gas*rho_gas) uses exactly one (1-satur) for the same accumulation coefficient, directly contradicting line 334.
3. (High) Uniform/single-node initialization omits temp_cond from the initial state vector
Location:
PharmaPy/Drying_Model.py:470-475 (else branch; cf. correct if branch at 463-468)When x_liq_init.ndim==1 and len(satur_init)==1, line 472 builds states_tuple = (satur_init, y_gas_init, x_liq_init, temp_gas_init), omitting temp_cond_init, giving a per-node width of 2+num_comp+num_volatiles. The parallel branch (lines 463-468) correctly stacks all five state groups (S, y_gas, x_liq, Tg, Tcond). unit_model later does states.reshape(-1, 3 + num_comp + self.num_volatiles) (line 225), which expects the temp_cond column.
Related existing issues (referenced, not modified): #20, #21, #24, #28, #37, #42, #48 (existing drying balance/property issues in the same module; reference only). #21 specifically shares line 334 (mol-vs-mass) but is a distinct mechanism from the extra (1-satur) factor here.
Found by a multi-agent source audit (round 2); confirmed by re-reading the source and cross-checked against all 80 existing open issues. Each defect was verified by an independent code-truth skeptic and a novelty skeptic.