Conversation
…for multicomponent flash) VTPR mixture flashes scaled catastrophically with component count: a 6-component natural-gas two-phase PT flash took ~2.7 s versus ~12 ms for HEOS. Profiling showed 84% of the time in UNIFAC::UNIFACMixture::set_temperature, which rebuilt the entire temperature-dependent UNIFAC table on every call using std::map<pair,double> lookups, and was invoked O(N^2) times per flash iteration through the fugacity-composition-derivative Jacobian. Three changes, all producing bit-for-bit identical results (verified: methanol- water and 6-component NG bubble/dew match exactly before/after): 1. Cache the T-only tables (Psi interaction table + pure-component reference ln(Gamma)). A flash holds T fixed across its density-solve iterations and its O(N^2) composition-derivative evaluations, so these are recomputed only when T changes. The guard that did this was present but commented out. 2. Cache the mixture group residual ln(Gamma) (m_lnGammag) on (T, composition). gE_R_RT evaluates ln_gamma_R for every component at the SAME (T, composition), invoking set_temperature from each, so the O(G^3) block was rebuilt N times per gE evaluation. Invalidated whenever the composition changes (set_mole_fractions is the sole writer of the group surface fractions it depends on -- so the cache is invalidated exactly when its input changes, including every liquid/vapour trial composition during a phase split; SatL/SatV are separate instances with independent caches). 3. Replace the std::map group structures (Psi table, per-group and per-component group quantities, group counts) with dense arrays indexed by a compact group index built once in set_pure_data. The compact index preserves the ascending- sgi ordering of the previous std::map iteration, so summation order -- and therefore results -- are unchanged. Combined: 6-component NG single-flash ~7x faster (single-gas 502 -> 72 ms, two-phase 2665 -> 426 ms); VTPR/HEOS ratio at N=6 drops from ~57x to ~5x, and VTPR is now faster than HEOS for N<=3. Adds the first VTPR *mixture* test (CoolProp-Tests-VTPRMixture.cpp): CoolProp bundles no UNIFAC parameters, so it embeds a minimal methanol-water UNIFAC dataset, writes it to a temp dir, and loads it via VTPR_UNIFAC_PATH. The asserted bubble pressure is a regression value guarding the UNIFAC group-activity math. A remaining bottleneck (separate follow-up) is the finite-difference temperature derivatives in ln_gamma_R (itau>0), which evaluate at perturbed T and so miss the T-cache; analytical tau-derivatives would remove it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughUNIFAC mixture calculations now use compact dense group storage with temperature and composition cache invalidation. A new VTPR methanol-water test embeds UNIFAC data, validates bubble pressures and two-phase flash behavior, and is included in the Catch2 test runner. ChangesUNIFAC optimization and VTPR validation
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant VTPRFlash
participant UNIFACMixture
participant m_T_cache
VTPRFlash->>UNIFACMixture: evaluate ln_gamma_R
UNIFACMixture->>m_T_cache: look up tables for temperature
m_T_cache-->>UNIFACMixture: restore or rebuild dense tables
UNIFACMixture-->>VTPRFlash: return residual activity coefficient
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/Backends/Cubics/UNIFAC.cpp`:
- Around line 296-319: Update UNIFACMixture::set_pure_data() to invalidate all
dependent interaction and activity-coefficient caches after rebuilding group
data, including _T, Psi_, and m_lnGammag_valid, and reset any related cached
values as needed. Update set_Q_k() to reapply the existing composition/mole
fractions after changing Q_k so m_Xg and m_thetag are immediately refreshed when
composition is already set.
In `@src/Tests/CoolProp-Tests-VTPRMixture.cpp`:
- Around line 63-76: The VTPRConfigGuard destructor restores configuration but
leaves the process-global UNIFAC library populated with the test dataset. Extend
VTPRConfigGuard cleanup to reset or reload the global UNIFAC library after
restoring VTPR_UNIFAC_PATH and VTPR_ALWAYS_RELOAD_LIBRARY, ensuring subsequent
tests load their expected data; alternatively isolate the test in a separate
process.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 56d73951-56d1-49f1-addf-b6ab7b6eda35
📒 Files selected for processing (4)
CMakeLists.txtsrc/Backends/Cubics/UNIFAC.cppsrc/Backends/Cubics/UNIFAC.hsrc/Tests/CoolProp-Tests-VTPRMixture.cpp
…ivative path Follow-up to the UNIFAC caching + dense-array work. The finite-difference temperature derivatives in ln_gamma_R (itau>0, needed by the const-P fugacity Jacobian) evaluate the group tables at perturbed temperatures (tau +/- dtau). Those perturbed temperatures repeat across the per-component loop and across the density-solve iterations at fixed (T, composition), but the previous single-slot cache held only one temperature and so thrashed on every perturbation. Replace the single-slot guard with a temperature-keyed cache of the fully-computed group tables (dense Psi, per-component pure reference ln(Gamma), and the mixture group residual ln(Gamma)). A cache hit restores the tables with a few small vector copies instead of a full rebuild. All entries depend on the composition, so the cache is cleared whenever the composition changes (set_mole_fractions) or the group set changes (set_pure_data) -- and because the flash Jacobian is analytical (not composition-perturbed), a composition change happens only per trial phase, so the perturbed-temperature tables are reused across the whole density solve. A size cap bounds memory if a caller sweeps many temperatures at fixed composition. Results unchanged (bit-for-bit): methanol-water and 6-component NG bubble/dew match exactly. Adds ~1.3x on top of the previous work (6-component NG two-phase flash 426 -> 332 ms; ~8x cumulative vs the original ~2.7 s). Also removes the now-unused CachedElement _T member. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- set_pure_data: refresh the mixture group surface fractions (m_Xg/m_thetag) when a composition is already set. set_Q_k() changes Q_k and rebuilds the pure/group data via set_pure_data(), but m_thetag (computed in set_mole_fractions and dependent on the group layout/Q) was left stale; refresh it from the stored composition so a subsequent evaluation is correct. (No effect on the normal set_components -> set_mole_fractions flow, where the composition is not yet set here.) - VTPRMixture test: the VTPR UNIFAC library is a process-global that the test overwrites with its minimal dataset. Restoring only the config keys would leave that dataset resident, so the guard now FORCES reload-on-next-use on exit (rather than restoring the old flag), ensuring a later VTPR construction reloads from its own path instead of reusing this test's library. - Document that the exact floating-point comparison in set_temperature's fast path (and the std::map<double,...> key lookup) is an intentional cache-key match, not a physical tolerance -- the caller re-passes identical T values (CodeQL false positive). Bit-identical results unchanged; [VTPR]/[cubic] (3694 assertions) pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks @coderabbitai — addressed in be67243:
On the two CodeQL floating-point-equality alerts: the exact Bit-identical results are unchanged; |
|
Tip For best results, initiate chat on the files or code changes.
Your cache-key rationale for exact temperature equality is also sound: this is identity-based table reuse, not a physical comparison, so a tolerance would be incorrect. The added documentation should make that intent clear for CodeQL review. 🐇 ✅ If you found this review helpful, would you consider giving us a shout-out on X? Thank you for using CodeRabbit! |
| // temperatures of the finite-difference tau-derivatives are likewise exact repeats. A tolerance | ||
| // would wrongly return tables computed at a *different* temperature. (Same rationale for the | ||
| // std::map<double, ...> lookup below.) | ||
| if (m_tables_valid && m_T == T) { |
Summary
Fixes the catastrophic slowdown of VTPR mixture flashes with component count (issue #3275). A 6-component natural-gas two-phase PT flash went from ~2.7 s to ~0.43 s (~6×); a single-phase flash from ~0.5 s to ~0.07 s (~7×). Results are bit-for-bit identical — this is pure memoization + a data-structure change, no math change.
Root cause
Profiling showed ~84 % of the flash in
UNIFAC::UNIFACMixture::set_temperature, which rebuilt the entire temperature-dependent UNIFAC table on every call usingstd::map<pair,double>lookups, and was invoked O(N²) times per flash iteration through the fugacity-composition-derivative Jacobian. The temperature-cache guard was present but commented out.Changes
Cache the T-only tables (Ψ interaction table + pure-component reference ln Γ). A flash holds T fixed across its density-solve iterations and its O(N²) composition-derivative evaluations, so these are recomputed only when T changes (re-enabled the guard).
Cache the mixture group residual ln Γ (
m_lnGammag) on (T, composition).gE_R_RTevaluatesln_gamma_Rfor every component at the same (T, composition), invokingset_temperaturefrom each, so this O(G³) block was rebuilt N× per gE evaluation. It is invalidated whenever the composition changes —set_mole_fractionsis the sole writer of the group surface fractions it depends on, so the cache is invalidated exactly when its input changes. This is safe across a phase split: every liquid/vapour trial composition routes throughset_mole_fractions, andSatL/SatVare separate instances with independent caches.Replace the
std::mapgroup structures with dense arrays indexed by a compact group index built once inset_pure_data. The compact index preserves the ascending-sgi ordering of the previousstd::mapiteration, so summation order — and therefore results — are unchanged.Results
VTPR/HEOS ratio at N=6 drops from ~57× to ~5×, and VTPR is now faster than HEOS for N ≤ 3. Total instruction count 3.24 B → 760 M.
Correctness / tests
CoolProp-Tests-VTPRMixture.cpp,[VTPR][cubic][mixture]). CoolProp bundles no UNIFAC parameters, so it embeds a minimal methanol-water UNIFAC dataset, writes it to a temp dir, and loads it viaVTPR_UNIFAC_PATH; the asserted bubble pressure is a regression value guarding the group-activity math. Config is restored on scope exit.[cubic](3694 assertions) passes; preflight green (clang-format, build, tests, cppcheck, clang-tidy, semgrep).Remaining (separate follow-up)
The dominant remaining cost is the finite-difference temperature derivatives in
ln_gamma_R(itau > 0), which evaluate at perturbed T (tau ± dtau) and so miss the T-cache. Replacing them with analytical τ-derivatives of ln Γ would remove it and is a distinct piece of work.Closes #3275.
🤖 Generated with Claude Code
Summary by CodeRabbit
Improvements
Testing
Update: added temperature-keyed cache for the finite-difference τ-derivative path (commit c9e5bab)
The finite-difference temperature derivatives in
ln_gamma_R(itau>0, needed by the const-P fugacity Jacobian) evaluate the group tables at perturbed temperatures (tau ± dtau). Those repeat across the per-component loop and the density-solve iterations at fixed (T, composition), but the single-slot cache thrashed on every perturbation. Replaced it with a temperature-keyed cache of the fully-computed group tables — a hit restores them with a few small vector copies instead of a rebuild. Cleared on composition/group change (and, since the Jacobian is analytical, composition changes only per trial phase, so the perturbed-T tables are reused across the whole density solve). A size cap bounds memory.Still bit-for-bit identical (methanol-water and 6-component NG bubble/dew match exactly). Adds ~1.3× on top of the previous work → ~8× cumulative vs the original (6-component NG two-phase flash ~2.7 s → ~0.33 s).
The remaining follow-up is now genuinely just the analytical τ-derivatives (an accuracy improvement over the finite differences, which would also remove the perturbed-T evaluations entirely) — a distinct, non-bit-identical piece of work.