Describe the bug
SaturationTemperature.calcSaturationTemperature() remains approximately 10x slower in NeqSim 3.20.0 than in 3.1.5 for repeated UMR-PRU dew-point tuning, despite the partial performance repair in #3271 / commit af6d5be4.
This is a follow-up to #3269. That issue correctly identified the full-range saturation scan introduced by #2219 / commit ec18449b4. The v3.19 fix stops after finding the uppermost crossing, but every call still starts at 1200 K and descends in 10 K increments. For natural-gas dew points near 290 K this still requires about 90 coarse TPflash calls before bisection.
The cost is amplified when SaturationTemperature is called repeatedly by an outer root finder. A representative 24-component UMR-PRU tuning workload calls it approximately 22 times per request.
A direct parent/commit A/B test attributes the original regression to ec18449b4:
| Revision |
Median per saturation call |
TP flashes |
Saturation temperature |
25f0d281 (parent) |
0.287 s |
24 |
17.913278198 C |
ec18449b4 |
4.649 s |
139 |
17.913280106 C |
That is a 16.2x slowdown with a numerical difference of only about 1.9e-6 C for this case.
Application-level warm timings with identical current caller code and identical input are:
| jneqsim version |
Warm request time |
Relative to 3.1.5 |
| 3.1.5 |
3.724 s |
1.0x |
| 3.18.0 |
56.136 s |
15.1x |
| 3.20.0 |
38.144 s |
10.2x |
Thus #3271 improved 3.18 performance, but did not restore the pre-3.12 behavior for near-boundary iterative workloads.
To Reproduce
Run the following with jneqsim==3.1.5 and jneqsim==3.20.0. Use the same Python caller and run at least twice in one process to separate JVM/JIT startup from steady-state time.
import time
from jneqsim import neqsim
composition = {
"CO2": 0.00645,
"nitrogen": 0.00966,
"methane": 0.949,
"ethane": 0.0258,
"propane": 0.00352,
"i-butane": 0.00152,
"n-butane": 0.000881,
"i-pentane": 0.00079,
"n-pentane": 0.000395,
"2-m-C5": 0.000401,
"3-m-C5": 0.000122,
"n-hexane": 0.000198,
"n-heptane": 0.000133,
"c-hexane": 0.000798,
"benzene": 1.65e-5,
"n-octane": 3.39e-5,
"c-C7": 0.000487,
"toluene": 3.62e-5,
"n-nonane": 2.57e-5,
"c-C8": 5.81e-5,
"m-Xylene": 1.7e-5,
"nC10": 1.16e-5,
"nC11": 0.0,
"nC12": 0.0,
}
fluid = neqsim.thermo.system.SystemUMRPRUMCEos(280.0, 52.1)
for name, amount in composition.items():
fluid.addComponent(name, amount)
fluid.setMixingRule("HV", "UNIFAC_UMRPRU")
for run_number in (1, 2):
test_fluid = fluid.clone()
simulation = neqsim.pvtsimulation.simulation.SaturationTemperature(test_fluid)
started = time.perf_counter()
simulation.run()
print(run_number, time.perf_counter() - started, test_fluid.getTemperature("C"))
The production-style amplification can be reproduced by evaluating the saturation temperature repeatedly while bisecting a heavy-component scaling factor between 0.5 and 1.5 with relative tolerance 1e-5.
Expected behavior
Retain the uppermost-crossing correctness introduced by #2219 without paying a near-global scan cost on every call. Repeated calls initialized near the previous saturation boundary should approach the performance of 3.1.5.
A possible direction is an adaptive/local bracket around the current state (or previous converged boundary), with expansion and a global-scan fallback when no safe bracket is found. Flash-count regression tests should cover both ordinary near-boundary fluids and retrograde/multiple-crossing fluids.
Actual behavior
In 3.20.0, every saturation-temperature calculation begins at 1200 K and scans downward in fixed 10 K steps. A boundary near 291 K therefore incurs approximately 90 coarse multiphase TPflash evaluations, followed by bisection and a final flash. An outer optimizer multiplies this cost across every objective evaluation.
The current tests constrain the result and require fewer than 115 flashes, but that ceiling still permits the observed 10x application regression. There is no near-boundary continuation/performance regression test.
Environment:
- OS: Windows 11
- Java version: Eclipse Temurin OpenJDK 25.0.4 LTS
- NeqSim version:
v3.1.5, v3.18.0, v3.20.0, plus source A/B at 25f0d281 and ec18449b4
- Python neqsim version: not applicable; reproduced with
jneqsim directly
- jneqsim versions:
3.1.5, 3.18.0, 3.20.0
Additional context
- First causal commit:
ec18449b470cc728cc8367f6ef1d8364afadd9af (fix TPflash error (#2219)), first released in v3.12.0.
- Partial repair:
af6d5be4e368e393460caea1d91ac377aae89f2c (fix(pvt): stop saturation scan at upper boundary (#3271)), first released in v3.19.0.
- The TP-flash correctness changes and uppermost-crossing behavior should be preserved. This report concerns the remaining search complexity and repeated-call performance.
- Numerical output changes between releases were observed at application level, but they are not attributed to this performance bug; this issue is scoped to runtime and flash count.
Describe the bug
SaturationTemperature.calcSaturationTemperature()remains approximately 10x slower in NeqSim 3.20.0 than in 3.1.5 for repeated UMR-PRU dew-point tuning, despite the partial performance repair in #3271 / commitaf6d5be4.This is a follow-up to #3269. That issue correctly identified the full-range saturation scan introduced by #2219 / commit
ec18449b4. The v3.19 fix stops after finding the uppermost crossing, but every call still starts at1200 Kand descends in10 Kincrements. For natural-gas dew points near 290 K this still requires about 90 coarseTPflashcalls before bisection.The cost is amplified when
SaturationTemperatureis called repeatedly by an outer root finder. A representative 24-component UMR-PRU tuning workload calls it approximately 22 times per request.A direct parent/commit A/B test attributes the original regression to
ec18449b4:25f0d281(parent)ec18449b4That is a 16.2x slowdown with a numerical difference of only about
1.9e-6 Cfor this case.Application-level warm timings with identical current caller code and identical input are:
Thus #3271 improved 3.18 performance, but did not restore the pre-3.12 behavior for near-boundary iterative workloads.
To Reproduce
Run the following with
jneqsim==3.1.5andjneqsim==3.20.0. Use the same Python caller and run at least twice in one process to separate JVM/JIT startup from steady-state time.The production-style amplification can be reproduced by evaluating the saturation temperature repeatedly while bisecting a heavy-component scaling factor between
0.5and1.5with relative tolerance1e-5.Expected behavior
Retain the uppermost-crossing correctness introduced by #2219 without paying a near-global scan cost on every call. Repeated calls initialized near the previous saturation boundary should approach the performance of 3.1.5.
A possible direction is an adaptive/local bracket around the current state (or previous converged boundary), with expansion and a global-scan fallback when no safe bracket is found. Flash-count regression tests should cover both ordinary near-boundary fluids and retrograde/multiple-crossing fluids.
Actual behavior
In 3.20.0, every saturation-temperature calculation begins at
1200 Kand scans downward in fixed10 Ksteps. A boundary near 291 K therefore incurs approximately 90 coarse multiphaseTPflashevaluations, followed by bisection and a final flash. An outer optimizer multiplies this cost across every objective evaluation.The current tests constrain the result and require fewer than 115 flashes, but that ceiling still permits the observed 10x application regression. There is no near-boundary continuation/performance regression test.
Environment:
v3.1.5,v3.18.0,v3.20.0, plus source A/B at25f0d281andec18449b4jneqsimdirectly3.1.5,3.18.0,3.20.0Additional context
ec18449b470cc728cc8367f6ef1d8364afadd9af(fix TPflash error (#2219)), first released in v3.12.0.af6d5be4e368e393460caea1d91ac377aae89f2c(fix(pvt): stop saturation scan at upper boundary (#3271)), first released in v3.19.0.