NeqSim can reproduce common pressure-volume-temperature (PVT) laboratory experiments with an equation-of-state fluid. Use measured composition and characterized heavy fractions as the starting point, then compare simulated and measured values at the same temperature and pressure schedule.
Units and naming
The simulation classes do not all use the same setter signatures. Apply these conventions explicitly:
| Quantity | Convention |
|---|---|
| EOS constructor temperature | K |
| EOS constructor pressure | bara |
addTBPfraction and addPlusFraction molar mass |
kg/mol |
| TBP/plus-fraction density | Specific gravity (numerically g/cm³); kg/m³ inputs are auto-detected and converted |
BasePVTsimulation.setTemperature(value, unit) |
Use "C" or "K" explicitly |
| Pressure arrays | bara |
SeparatorTest.setSeparatorConditions temperature array |
K |
ViscositySim.setTemperaturesAndPressures temperature array |
K |
ViscositySim viscosity outputs |
Pa·s; multiply by 1000 for cP |
ConstantMassExpansion.getOilDensity() |
Oil-phase density in kg/m³; NaN if no oil phase exists |
ConstantMassExpansion.getOilViscosity() |
Oil-phase dynamic viscosity in Pa·s; NaN if no oil phase exists |
For CCE oil-property comparisons, use getOilDensity() and getOilViscosity().
Both select the named oil phase above and below the bubble point, preserve the
constant-composition path, and share the pressure-array index. The legacy
getDensity() reports phase-zero density, which is normally gas density in a
gas-oil state. The legacy getViscosity() reports single-phase viscosity and NaN
in multiphase states. Their existing behavior is preserved. The named oil
accessors reuse the physical-property initialization already performed at each
pressure and require no additional flash calculations. Regression evidence:
ConstantMassExpansionOilPropertiesTest.
addTBPfraction and addPlusFraction preserve + in a pseudo-component label when
they append the internal _PC suffix, for example C20+_PC. However, the default Pedersen
plus-fraction characterization parses a terminal carbon number from that label. Use a numeric
label such as C20, not C20+, before calling characterisePlusFraction().
Supported experiments
| Experiment | Class | Main results |
|---|---|---|
| Constant mass expansion (CCE/CME) | ConstantMassExpansion |
Relative volume, liquid volume as percent of saturation volume, gas Z-factor, Y-function |
| Constant volume depletion (CVD) | ConstantVolumeDepletion |
Saturation pressure, relative volume, liquid volume as percent of saturation volume, cumulative mole-percent depletion |
| Differential liberation (DL) | DifferentialLiberation |
Saturation pressure, Bo, Bg, Rs, oil density, gas Z-factor |
| Legacy separator test | SeparatorTest |
Per-stage GOR and oil formation-volume factor arrays |
| Swelling test | SwellingTest |
Pressure and relative-oil-volume arrays |
| Pressure/temperature viscosity grid | ViscositySim |
Gas, oil, and aqueous viscosity arrays in Pa·s |
SwellingTest does not calculate minimum miscibility pressure (MMP). Determine MMP with a
dedicated slim-tube or multi-contact workflow; do not infer it from the swelling-factor
curve alone.
Repeated saturation-temperature tuning
SaturationTemperature defaults to a descending 30–1200 K search on a 10 K grid.
This selects the uppermost resolved two-phase-to-single-phase crossing, including
retrograde and disconnected phase regions, independently of the initial temperature.
The result is refined to a bracket width of 1e-5 K. Regions narrower than the coarse
grid can be missed; this is not a complete phase-envelope calculation.
For repeated dew-point fitting, explicitly supply bounds that contain the desired upper boundary throughout the pressure and composition range being fitted:
SaturationTemperature simulation = new SaturationTemperature(fluid);
simulation.setTemperatureSearchBounds(260.0, 320.0); // K; establish these for your fluid
simulation.run();
double saturationTemperatureK = simulation.getSaturationTemperature();
This scans the same 10 K grid inside the supplied interval, including both endpoints, and refines its uppermost crossing with full multiphase TP flashes. It re-evaluates the current fluid on every call, so composition and pressure changes do not reuse stale phase classifications. The bounds remain fixed between calls. The 24-component UMR-PRU regression for issue #3795 covers heavy-component factors from 0.5 to 1.5 at 52.1 bara and limits each bounded call to 30 flashes.
If the upper endpoint is multiphase or the interval has no resolved crossing, the calculation falls back to the global search. If that search finds no boundary, the legacy result is 1200 K; it must not be interpreted as a converged saturation point. The caller’s multiphase-check setting is restored even when a flash throws.
Bounds are a caller-supplied physical constraint, not an automatic global guarantee.
A local crossing and a single-phase upper endpoint cannot rule out a separate
two-phase region above the bounds. Establish a suitable interval using global scans
or phase-envelope calculations across the tuning range. Use
clearTemperatureSearchBounds() to return to the global search when the interval
is no longer justified. Existing callers retain the global behavior; they must opt in
to bounded searching to obtain the reduced flash count.
Executable constant-mass-expansion example
This complete Java example uses SI molar masses for every characterized fraction. The pressure schedule and returned arrays share the same index.
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import neqsim.pvtsimulation.simulation.ConstantMassExpansion;
import neqsim.thermo.system.SystemInterface;
import neqsim.thermo.system.SystemSrkEos;
public final class ConstantMassExpansionExample {
private static final Logger logger =
LogManager.getLogger(ConstantMassExpansionExample.class);
private ConstantMassExpansionExample() {}
public static void main(String[] args) {
SystemInterface fluid = new SystemSrkEos(370.65, 350.0);
fluid.addComponent("nitrogen", 0.39);
fluid.addComponent("CO2", 0.30);
fluid.addComponent("methane", 40.20);
fluid.addComponent("ethane", 7.61);
fluid.addComponent("propane", 7.95);
fluid.addComponent("i-butane", 1.19);
fluid.addComponent("n-butane", 4.08);
fluid.addComponent("i-pentane", 1.39);
fluid.addComponent("n-pentane", 2.15);
fluid.addComponent("n-hexane", 2.79);
fluid.addTBPfraction("C7", 4.28, 95.0 / 1000.0, 0.729);
fluid.addTBPfraction("C8", 4.31, 106.0 / 1000.0, 0.749);
fluid.addTBPfraction("C9", 3.08, 121.0 / 1000.0, 0.770);
fluid.addTBPfraction("C10", 2.47, 135.0 / 1000.0, 0.786);
fluid.addTBPfraction("C11", 1.91, 148.0 / 1000.0, 0.792);
fluid.addTBPfraction("C12", 1.69, 161.0 / 1000.0, 0.804);
fluid.addTBPfraction("C13", 1.59, 175.0 / 1000.0, 0.819);
fluid.addTBPfraction("C14", 1.22, 196.0 / 1000.0, 0.833);
fluid.addTBPfraction("C15", 1.25, 206.0 / 1000.0, 0.836);
fluid.addTBPfraction("C16", 1.00, 225.0 / 1000.0, 0.843);
fluid.addTBPfraction("C17", 0.99, 236.0 / 1000.0, 0.840);
fluid.addTBPfraction("C18", 0.92, 245.0 / 1000.0, 0.846);
fluid.addTBPfraction("C19", 0.60, 265.0 / 1000.0, 0.857);
fluid.addPlusFraction("C20", 6.64, 453.0 / 1000.0, 0.918);
fluid.getCharacterization().getLumpingModel().setNumberOfPseudoComponents(12);
fluid.getCharacterization().characterisePlusFraction();
fluid.setMixingRule("classic");
double[] pressuresBara = {
351.4, 323.2, 301.5, 275.9, 250.1, 226.1, 205.9, 197.3,
189.3, 183.3, 165.0, 131.2, 108.3, 85.3, 55.6
};
ConstantMassExpansion cce = new ConstantMassExpansion(fluid);
cce.setTemperature(97.5, "C");
cce.setPressures(pressuresBara);
cce.runCalc();
double[] relativeVolume = cce.getRelativeVolume();
double[] liquidRelativeVolume = cce.getLiquidRelativeVolume();
double[] gasZ = cce.getZgas();
double[] yFunction = cce.getYfactor();
if (relativeVolume.length != pressuresBara.length
|| liquidRelativeVolume.length != pressuresBara.length
|| gasZ.length != pressuresBara.length
|| yFunction.length != pressuresBara.length
|| !Double.isFinite(relativeVolume[0])
|| !Double.isFinite(liquidRelativeVolume[12])
|| !Double.isFinite(gasZ[12])
|| !Double.isFinite(yFunction[12])
|| !Double.isFinite(cce.getSaturationPressure())
|| cce.getSaturationPressure() <= 0.0) {
throw new IllegalStateException("Constant-mass-expansion result is invalid");
}
for (int i = 0; i < pressuresBara.length; i++) {
logger.info("{} bara: Vrel={}, Vliq={} %, Zgas={}, Y={}", pressuresBara[i],
relativeVolume[i], liquidRelativeVolume[i], gasZ[i], yFunction[i]);
}
logger.info("Saturation pressure: {} bara", cce.getSaturationPressure());
}
}
The focused documentation regression compiles and executes this exact marked Java 8 fence, while its retained numerical regression checks representative values and array lengths. Treat calculated values as model results, not laboratory measurements.
Current API map
Constant volume depletion
Configure ConstantVolumeDepletion with setTemperature, setPressures, and runCalc.
Read getSaturationPressure, getRelativeVolume, getLiquidRelativeVolume,
getCummulativeMolePercDepleted, getZmix, and getZgas. The spelling
getCummulativeMolePercDepleted is the current compatibility API.
getLiquidRelativeVolume returns percent of saturation volume, not a unitless fraction.
The legacy setTemperaturesAndPressures(temperaturesK, pressuresBara) also sets the
pressure schedule used by runCalc, replacing any earlier schedule. runCalc remains
isothermal: use setTemperature for its common temperature; the temperature array
is used by runTuning. Regression evidence:
ConstantVolumeDepletionTest.
Differential liberation
Configure DifferentialLiberation with setTemperature, setPressures, and runCalc.
Read getSaturationPressure, getBo, getBg, getRs, getOilDensity, getZgas, and
getRelGasGravity. Result arrays align with the pressure schedule.
Separator tests
The legacy SeparatorTest accepts all stages in one call:
setSeparatorConditions(temperaturesK, pressuresBara). After runCalc, read the per-stage
arrays from getGOR and getBofactor. For named stages, reservoir-condition handling,
stock-tank results, and cumulative GOR, prefer MultiStageSeparatorTest; see the
PVT simulation overview.
Swelling tests
Set the injection fluid with setInjectionGas, supply cumulative injected-gas mole
percentages with setCummulativeMolePercentGasInjected, and call runCalc. Read the
pressure and relative-oil-volume arrays from getPressures and getRelativeOilVolume.
The percentages are relative to the original oil moles: an addition of 20 mol% means 0.2 mol of injection fluid per initial mole of oil, not 20% of the final mixture. Supply a nonempty, finite, nonnegative, nondecreasing schedule. Repeated amounts repeat a stage without adding gas; the first amount may be nonzero.
Set the experiment temperature with setTemperature(value, "C") or
setTemperature(value, "K"); the one-argument setter always uses K, including after
an earlier Celsius setting. Explicit nonfinite or nonpositive absolute temperatures
are rejected by runCalc; an explicit NaN is not treated as an omitted setting.
If no temperature is set, the constructor oil’s temperature is used. The unmodified reference oil and
every swollen mixture are evaluated at that same temperature. At zero addition
the swelling factor is exactly one. Previously, the reference could use the
construction temperature while later stages used another temperature, and the
unit supplied to the two-argument setter was ignored.
The ratio uses density-corrected volumes, including configured volume translation. Previously, the uncorrected cubic-EOS volume was used even when liquid-density volume shifts were enabled.
Each run starts from the oil snapshot captured by the constructor. To use a
different initial oil or EOS parameter set, construct a new SwellingTest after
configuring that fluid. The original oil and injection-gas template remain
unchanged; getThermoSystem returns the final swollen mixture. Rerunning the same
experiment does not inject the gas twice.
This API follows bubble points only. It does not follow swelling through a
critical transition onto a dew branch, calculate MMP, or generate the full CCE
dropout curve at each addition. Failed bubble-point solves throw
IllegalStateException with the failing stage and original cause; result arrays
remain NaN unless every stage succeeds. Do not interpret a failed solve as a
physical swelling factor. Follow the remaining EOR work in
the qualification matrix.
SwellingTestTest checks temperature units, cumulative component/mass balances,
new injection components under SRK and PR, input preservation, repeat runs and
explicit failures. Its binary SRK numerical benchmark is independently generated
by devtools/generate_srk_swelling_reference.py; it is not experimental oil data.
Viscosity grids
ViscositySim.setTemperaturesAndPressures(temperaturesK, pressuresBara) defines paired
states. After runCalc, use getGasViscosity, getOilViscosity, and
getAqueousViscosity. These arrays are in Pa·s.
For compatibility, these numerical arrays contain zero where the corresponding
phase is absent. Use getGasPhaseAvailable, getOilPhaseAvailable and
getAqueousPhaseAvailable to distinguish absence from a computed value at each
paired state. The Boolean arrays are captured from the actual flash within
runCalc; a viscosity value alone does not establish phase presence. The
PVT study adapter and report retain these masks in
the canonical dataset and encode absent-phase viscosities as JSON null.
Calibration workflow
Calibrate a fluid only against traceable measurements and keep a separate validation set. A practical sequence is:
- Verify composition, component names, molar-mass units, densities, and test conditions.
- Characterize the plus fraction and freeze the chosen lumping scheme.
- Compare untuned saturation pressure and volumetric trends with measurements.
- Adjust only physically justified characterization or interaction parameters.
- Re-run every experiment and report residuals, parameter bounds, and validation results.
There is no universal one-call tuning recipe for all PVT experiments. Regression choices depend on the fluid, available measurements, and intended prediction range.