Complete Technical Documentation
Version 3.0.0
Generated: January 2026
NeqSim (Non-Equilibrium Simulator) is a comprehensive Java library for thermodynamic, physical property, and process simulation. This documentation covers all major packages and provides detailed guides for developing applications.
import neqsim.thermo.system.SystemSrkEos;
import neqsim.thermodynamicoperations.ThermodynamicOperations;
// Create a natural gas fluid
SystemInterface gas = new SystemSrkEos(298.15, 50.0);
gas.addComponent("methane", 0.90);
gas.addComponent("ethane", 0.05);
gas.addComponent("propane", 0.03);
gas.addComponent("CO2", 0.02);
gas.setMixingRule("classic");
// Perform flash calculation
ThermodynamicOperations ops = new ThermodynamicOperations(gas);
ops.TPflash();
// Get properties
System.out.println("Density: " + gas.getDensity("kg/m3") + " kg/m³");
System.out.println("Compressibility: " + gas.getZ());
| Package | Documentation | Description |
|---|---|---|
neqsim.thermo |
thermo/ | Thermodynamic systems, phases, components, equations of state, mixing rules, fluid characterization |
neqsim.thermodynamicoperations |
thermodynamicoperations/ | Flash calculations, phase envelopes, saturation operations |
neqsim.physicalproperties |
physical_properties/ | Transport properties: viscosity, thermal conductivity, diffusivity, interfacial tension |
| Package | Documentation | Description |
|---|---|---|
neqsim.process |
process/ | Process equipment, unit operations, controllers, process systems, safety systems |
neqsim.fluidmechanics |
fluidmechanics/ | Pipeline flow, pressure drop, two-phase flow, flow nodes |
| Package | Documentation | Description |
|---|---|---|
neqsim.pvtsimulation |
pvtsimulation/ | PVT experiments: CME, CVD, DL, separator tests, swelling tests |
neqsim.blackoil |
blackoil/ | Black oil model, PVT tables, Rs, Bo, Bg correlations |
| Package | Documentation | Description |
|---|---|---|
neqsim.pvtsimulation.flowassurance |
pvtsimulation/flowassurance/ | Asphaltene stability, De Boer screening, CPA-based onset calculations |
| Package | Documentation | Description |
|---|---|---|
neqsim.chemicalreactions |
chemicalreactions/ | Chemical equilibrium, reaction kinetics |
| Package | Documentation | Description |
|---|---|---|
neqsim.standards |
standards/ | ISO 6976, ISO 6578, ISO 15403, ASTM D6377, sales contracts |
neqsim.statistics |
statistics/ | Parameter fitting, Monte Carlo simulation, data analysis |
| Package | Documentation | Description |
|---|---|---|
neqsim.util |
util/ | Database access, unit conversion, serialization, exceptions |
neqsim.mathlib |
mathlib/ | Mathematical utilities, nonlinear solvers |
docs/
├── README.md # This file - main index
├── modules.md # Module overview
│
├── thermo/ # Thermodynamic package
│ ├── README.md # Package overview
│ ├── system/ # EoS implementations
│ ├── phase/ # Phase modeling
│ ├── component/ # Component properties
│ ├── mixingrule/ # Mixing rules
│ └── characterization/ # Plus fraction handling
│
├── thermodynamicoperations/ # Flash operations
│ └── README.md
│
├── physical_properties/ # Transport properties
│ └── README.md
│
├── process/ # Process simulation
│ ├── README.md # Package overview
│ ├── equipment/ # Equipment documentation
│ ├── processmodel/ # ProcessSystem, modules
│ └── safety/ # Safety systems
│
├── fluidmechanics/ # Pipe flow
│ └── README.md
│
├── pvtsimulation/ # PVT experiments
│ ├── README.md
│ └── flowassurance/ # Flow assurance (asphaltene, wax, hydrates)
│ ├── README.md
│ ├── asphaltene_modeling.md
│ ├── asphaltene_cpa_calculations.md
│ ├── asphaltene_deboer_screening.md
│ ├── asphaltene_parameter_fitting.md
│ ├── asphaltene_method_comparison.md
│ └── asphaltene_validation.md
│
├── blackoil/ # Black oil model
│ └── README.md
│
├── chemicalreactions/ # Reactions
│ └── README.md
│
├── standards/ # Quality standards
│ └── README.md
│
├── statistics/ # Statistics package
│ └── README.md
│
├── util/ # Utilities
│ └── README.md
│
├── mathlib/ # Math utilities
│ └── README.md
│
├── safety/ # Safety system guides
│ ├── ESD_BLOWDOWN_SYSTEM.md
│ ├── HIPPS_SUMMARY.md
│ ├── hipps_implementation.md
│ ├── sis_logic_implementation.md
│ ├── fire_blowdown_capabilities.md
│ ├── psv_dynamic_sizing_example.md
│ └── alarm_system_guide.md
│
├── simulation/ # Process simulation guides
│ ├── advanced_process_logic.md
│ ├── graph_based_process_simulation.md
│ ├── parallel_process_simulation.md
│ ├── recycle_acceleration_guide.md
│ ├── well_simulation_guide.md
│ └── turboexpander_compressor_model.md
│
├── integration/ # Integration guides
│ ├── ai_platform_integration.md
│ ├── ml_integration.md
│ ├── mpc_integration.md
│ ├── REAL_TIME_INTEGRATION_GUIDE.md
│ └── dexpi-reader.md
│
├── development/ # Developer guides
│ ├── DEVELOPER_SETUP.md
│ └── contributing-structure.md
│
├── examples/ # Code examples
│ └── ...
│
└── wiki/ # Additional wiki pages
└── ...
Specialized guides for advanced features and use cases:
| Guide | Description |
|---|---|
| ESD_BLOWDOWN_SYSTEM.md | Emergency shutdown and blowdown systems |
| HIPPS_SUMMARY.md | High Integrity Pressure Protection Systems |
| hipps_implementation.md | HIPPS implementation details |
| hipps_safety_logic.md | HIPPS safety logic |
| INTEGRATED_SAFETY_SYSTEMS.md | Integrated safety systems overview |
| layered_safety_architecture.md | Layered safety architecture |
| sis_logic_implementation.md | SIS logic implementation |
| SAFETY_SIMULATION_ROADMAP.md | Safety simulation roadmap |
| Guide | Description |
|---|---|
| process_logic_framework.md | Process logic framework |
| ProcessLogicEnhancements.md | Logic enhancements |
| advanced_process_logic.md | Advanced process logic |
| alarm_system_guide.md | Alarm system guide |
| alarm_triggered_logic_example.md | Alarm-triggered logic |
| mpc_integration.md | MPC integration |
| Guide | Description |
|---|---|
| fire_blowdown_capabilities.md | Fire and blowdown simulation |
| fire_heat_transfer_enhancements.md | Fire heat transfer |
| psv_dynamic_sizing_example.md | PSV dynamic sizing |
| rupture_disk_dynamic_behavior.md | Rupture disk behavior |
| turboexpander_compressor_model.md | Turboexpander modeling |
| Guide | Description |
|---|---|
| well_simulation_guide.md | Well simulation guide |
| well_and_choke_simulation.md | Choke simulation |
| field_development_engine.md | Field development |
| Guide | Description |
|---|---|
| pvt_workflow.md | PVT workflow |
| blackoil_pvt_export.md | Black oil PVT export |
| whitson_pvt_reader.md | Whitson PVT reader |
| fluid_characterization_mathematics.md | Characterization math |
| Guide | Description |
|---|---|
| parallel_process_simulation.md | Parallel simulation |
| recycle_acceleration_guide.md | Recycle convergence |
| graph_based_process_simulation.md | Graph-based simulation |
| differentiable_thermodynamics.md | Auto-differentiation |
| equipment_factory.md | Equipment factory |
| dexpi-reader.md | DEXPI P&ID reader |
| Guide | Description |
|---|---|
| ai_platform_integration.md | AI/ML integration |
| ml_integration.md | Machine learning |
| REAL_TIME_INTEGRATION_GUIDE.md | Real-time systems |
| QRA_INTEGRATION_GUIDE.md | QRA integration |
| Guide | Description |
|---|---|
| DEVELOPER_SETUP.md | Development environment setup |
| contributing-structure.md | Contributing guidelines |
| EoS | Class | Application |
|---|---|---|
| SRK | SystemSrkEos |
General hydrocarbon systems |
| PR | SystemPrEos |
General hydrocarbon systems |
| PR-1978 | SystemPrEos1978 |
Improved liquid densities |
| SRK-CPA | SystemSrkCPAstatoil |
Associating fluids (water, alcohols, glycols) |
| PC-SAFT | SystemPCSAFT |
Polymers, associating fluids |
| GERG-2008 | SystemGERG2008Eos |
Natural gas reference |
| EOS-CG | SystemEOSCGEos |
CO₂-rich systems (CCS) |
| UMR-PRU | SystemUMRPRUMCEos |
Wide-range hydrocarbon systems |
| Category | Equipment | Class |
|---|---|---|
| Separation | 2-phase separator | Separator |
| 3-phase separator | ThreePhaseSeparator |
|
| Distillation column | DistillationColumn |
|
| Heat Transfer | Heater | Heater |
| Cooler | Cooler |
|
| Heat exchanger | HeatExchanger |
|
| Compression | Compressor | Compressor |
| Pump | Pump |
|
| Expander | Expander |
|
| Flow Control | Valve | ThrottlingValve |
| Mixer | Mixer, StaticMixer |
|
| Splitter | Splitter |
|
| Well/Reservoir | Well | SimpleWell |
| Choke | ChokeValve |
examples/ and notebooks/ directoriesneqsim-python package)This document provides an overview of the seven foundational modules that make up NeqSim. Each module resides under src/main/java/neqsim and works together to support fluid characterization and process design.
thermo and thermodynamicoperationsphysicalpropertiesfluidmechanicsprocess/equipmentchemicalreactionsstatistics/parameterfittingprocessprocess/safety, process/equipment/tank, process/util/fireUse this page as a launchpad into the NeqSim documentation. It mirrors the high-level structure from the Colab introduction notebook and links directly to reference guides and examples.
Add NeqSim as a dependency in your pom.xml:
<dependency>
<groupId>com.equinor.neqsim</groupId>
<artifactId>neqsim</artifactId>
<version>3.0.0</version>
</dependency>
Download the shaded JAR from the releases page and add to your classpath.
Clone the repository and build with the Maven wrapper:
git clone https://github.com/equinor/neqsim.git
cd neqsim
./mvnw install
On Windows:
mvnw.cmd install
The command downloads dependencies, compiles the project, and runs the test suite. For environment notes and troubleshooting tips, see the README and developer setup guide.
import neqsim.thermo.system.SystemSrkEos;
import neqsim.thermodynamicoperations.ThermodynamicOperations;
public class FirstCalculation {
public static void main(String[] args) {
// 1. Create a natural gas system at 25°C and 50 bar
SystemSrkEos gas = new SystemSrkEos(298.15, 50.0);
gas.addComponent("methane", 0.90);
gas.addComponent("ethane", 0.06);
gas.addComponent("propane", 0.03);
gas.addComponent("n-butane", 0.01);
gas.setMixingRule("classic");
// 2. Perform flash calculation
ThermodynamicOperations ops = new ThermodynamicOperations(gas);
ops.TPflash();
// 3. Print results
System.out.println("Number of phases: " + gas.getNumberOfPhases());
System.out.println("Density: " + gas.getDensity("kg/m3") + " kg/m³");
System.out.println("Z-factor: " + gas.getPhase("gas").getZ());
System.out.println("Molecular weight: " + gas.getMolarMass() * 1000 + " g/mol");
}
}
import neqsim.process.equipment.stream.Stream;
import neqsim.process.equipment.valve.ThrottlingValve;
import neqsim.process.equipment.separator.Separator;
import neqsim.process.processmodel.ProcessSystem;
import neqsim.thermo.system.SystemSrkEos;
public class FirstProcess {
public static void main(String[] args) {
// 1. Create fluid
SystemSrkEos fluid = new SystemSrkEos(320.0, 100.0);
fluid.addComponent("methane", 0.80);
fluid.addComponent("ethane", 0.10);
fluid.addComponent("propane", 0.05);
fluid.addComponent("n-pentane", 0.05);
fluid.setMixingRule("classic");
// 2. Create stream
Stream feed = new Stream("Feed", fluid);
feed.setFlowRate(10000.0, "kg/hr");
feed.setTemperature(50.0, "C");
feed.setPressure(100.0, "bara");
// 3. Add equipment
ThrottlingValve valve = new ThrottlingValve("Valve", feed);
valve.setOutletPressure(20.0, "bara");
Separator separator = new Separator("Separator", valve.getOutletStream());
// 4. Build and run process
ProcessSystem process = new ProcessSystem();
process.add(feed);
process.add(valve);
process.add(separator);
// Use runOptimized() for best performance (auto-selects strategy)
process.runOptimized();
// 5. Results
System.out.println("Gas rate: " + separator.getGasOutStream().getFlowRate("kg/hr") + " kg/hr");
System.out.println("Liquid rate: " + separator.getLiquidOutStream().getFlowRate("kg/hr") + " kg/hr");
}
}
NeqSim provides optimized execution strategies for complex process simulations:
| Method | Best For | Speedup |
|---|---|---|
run() |
Simple processes | baseline |
runOptimized() |
Recommended | 28-40% |
runParallel() |
Feed-forward (no recycles) | 40-57% |
runHybrid() |
Complex recycle processes | 38% |
// Recommended - auto-selects best strategy based on process topology
process.runOptimized();
// Analyze process structure
System.out.println(process.getExecutionPartitionInfo());
See ProcessSystem documentation for details.
NeqSim supports multiple equations of state for different applications:
| EOS | Class | Best For |
|---|---|---|
| SRK | SystemSrkEos |
General hydrocarbon systems |
| Peng-Robinson | SystemPrEos |
Reservoir/liquid density |
| SRK-CPA | SystemSrkCPAstatoil |
Water, glycols, alcohols |
| GERG-2008 | SystemGERG2008Eos |
Natural gas custody transfer |
For oils with C7+ fractions:
SystemSrkEos oil = new SystemSrkEos(350.0, 100.0);
oil.addComponent("methane", 10.0);
oil.addComponent("ethane", 5.0);
// ... light components ...
// Add TBP fractions
oil.addTBPfraction("C7", 5.0, 0.092, 730.0);
oil.addTBPfraction("C8", 4.0, 0.104, 750.0);
oil.addPlusFraction("C10+", 20.0, 0.200, 820.0);
// Characterize
oil.getCharacterization().setTBPModel("PedersenSRK");
oil.getCharacterization().characterisePlusFraction();
NeqSim includes 50+ unit operations:
| Category | Equipment |
|---|---|
| Separation | Separator, ThreePhaseSeparator, DistillationColumn, MembraneSeparator |
| Compression | Compressor, Pump, Expander, Ejector |
| Heat Transfer | Heater, Cooler, HeatExchanger |
| Flow Control | ThrottlingValve, FlowRateController |
| Pipelines | PipeBeggsAndBrills, AdiabaticPipe, WaterHammerPipe |
| Specialty | Electrolyzer, WindTurbine, SolarPanel, Battery |
PipeBeggsAndBrills pipeline = new PipeBeggsAndBrills("Pipeline", inletStream);
pipeline.setLength(10000.0); // 10 km
pipeline.setDiameter(0.2); // 8 inch
pipeline.setElevation(50.0); // 50m elevation gain
pipeline.setPipeWallRoughness(4.5e-5); // Steel
pipeline.run();
NeqSim provides comprehensive safety simulation:
// PSV sizing example
ValveController psv = new ValveController("PSV-001");
psv.setMaxPressure(50.0, "bara");
psv.setReliefPressure(55.0, "bara");
ControllerDeviceBaseClass controller = new ControllerDeviceBaseClass();
controller.setControllerSetPoint(50.0);
controller.setControllerParameters(0.5, 100.0, 0.0); // Kp, Ti, Td
valve.setController(controller);
The NeqSim library as a jar files can be downloaded from the NeqSim release pages. A shaded library is distributed including all dependent libraries used by NeqSim. Use of NeqSim in a Java program is done by adding NeqSim.jar to the classpath.
Building NeqSim from source is done by cloning the project to a local directory on the developer computer. Building the code is done using JDK8+. NeqSim uses a number of libraries (jar files) for various calculations. These libraries must be available as part of the compilation process. The required libraries are listed in the pom.xml file. NeqSim can be built using the Maven build system (https://maven.apache.org/). All NeqSim build dependencies are given in the pom.xml file.
An interactive demonstration of how to get started as a NeqSim developer is presented in this NeqSim Colab demo.
Also see NeqSim JavaDoc.
And also see the Java tests where a lot of the functionality is demonstrated.
Navigate through the documentation organized from introductory concepts to advanced topics.
| # | Topic | Description |
|---|---|---|
| 1 | Getting started with NeqSim and GitHub | Installation and quick start |
| 2 | Getting started as a NeqSim developer | Development environment setup |
| 3 | The NeqSim parameter database | Component database and parameters |
| # | Topic | Description |
|---|---|---|
| 4 | Example of setting up a fluid and running simple flash calculations | Basic fluid creation and flash |
| 5 | Select thermodynamic model and mixing rule | EOS selection and configuration |
| 6 | Flash calculations and phase envelope calculations using NeqSim | Phase equilibria calculations |
| 7 | Calculation of thermodynamic and physical properties using NeqSim | Property calculation methods |
| # | Topic | Description |
|---|---|---|
| 8 | Oil Characterization in NeqSim | Crude oil and condensate characterization |
| 9 | Aqueous fluids and NeqSim | Water and brine systems |
| 10 | Electrolytes and NeqSim | Electrolyte thermodynamics |
| # | Topic | Description |
|---|---|---|
| 11 | Process Calculations in NeqSim | Process simulation fundamentals |
| Topic | Description |
|---|---|
| Compressor calculations | Compressor modeling |
| Compressor curves | Performance curves and maps |
| # | Topic | Description |
|---|---|---|
| 12 | Adding a thermodynamic model in NeqSim | Implement custom EOS models |
| 13 | Adding a viscosity model in NeqSim | Implement custom viscosity correlations |
| 14 | Adding an unit operation in NeqSim | Create custom process equipment |
| # | Topic | Description |
|---|---|---|
| 15 | How to make a NeqSim API | Building REST APIs with NeqSim |
| 16 | Create native image using GraalVM | Native compilation for performance |
| # | Topic | Description |
|---|---|---|
| 17 | Profiling calculations | Performance analysis and optimization |
| 18 | Dynamic process simulations | Transient and dynamic modeling |
The repository contains extensive documentation organized by module:
| Resource | Link |
|---|---|
| Source Code | github.com/equinor/neqsim |
| JavaDoc | NeqSim JavaDoc |
| Java Tests | Test Examples |
| Colab Demo | Interactive Tutorial |
| Releases | Download JAR |
| Discussions | GitHub Discussions |
This document summarizes the basic steps from the NeqSim wiki for setting up a local development environment. For additional details see the Getting started as a NeqSim developer wiki page.
git clone https://github.com/equinor/neqsim.git
cd neqsim
NeqSim requires JDK 8 or newer and uses the Maven build system. Use the provided Maven wrapper to build the code:
./mvnw install
(Windows users can run mvnw.cmd.)
Execute all unit tests with:
./mvnw test
To generate a code coverage report:
./mvnw jacoco:prepare-agent test install jacoco:report
Checkstyle, SpotBugs, and PMD plugins are included in the Maven build and run during the verify phase. Run them locally with:
./mvnw checkstyle:check spotbugs:check pmd:check
The checks do not fail the build by default, but fixing any reported issues is encouraged.
Comprehensive examples demonstrating NeqSim capabilities for thermodynamic calculations and process simulation.
The most common thermodynamic calculation - determining phase equilibrium at fixed temperature and pressure.
import neqsim.thermo.system.SystemSrkEos;
import neqsim.thermodynamicoperations.ThermodynamicOperations;
// Create a natural gas system
SystemSrkEos gas = new SystemSrkEos(298.15, 50.0); // 25°C, 50 bar
gas.addComponent("methane", 0.85);
gas.addComponent("ethane", 0.08);
gas.addComponent("propane", 0.04);
gas.addComponent("n-butane", 0.02);
gas.addComponent("n-pentane", 0.01);
gas.setMixingRule("classic");
// Perform TP flash
ThermodynamicOperations ops = new ThermodynamicOperations(gas);
ops.TPflash();
// Print results
System.out.println("Number of phases: " + gas.getNumberOfPhases());
System.out.println("Gas fraction: " + gas.getPhaseFraction("gas", "mole"));
System.out.println("Liquid fraction: " + gas.getPhaseFraction("oil", "mole"));
System.out.println("Gas density: " + gas.getPhase("gas").getDensity("kg/m3") + " kg/m³");
Calculate the complete phase envelope (bubble and dew point curves).
import neqsim.thermo.system.SystemSrkEos;
import neqsim.thermodynamicoperations.ThermodynamicOperations;
SystemSrkEos fluid = new SystemSrkEos(298.15, 50.0);
fluid.addComponent("methane", 0.80);
fluid.addComponent("ethane", 0.10);
fluid.addComponent("propane", 0.07);
fluid.addComponent("n-heptane", 0.03);
fluid.setMixingRule("classic");
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.calcPTphaseEnvelope();
// Get cricondenbar (maximum pressure point)
double cricondenbarP = ops.get("cricondenbar")[0];
double cricondenbarT = ops.get("cricondenbar")[1];
System.out.println("Cricondenbar: " + cricondenbarP + " bar at " + cricondenbarT + " K");
// Get cricondentherm (maximum temperature point)
double cricondentT = ops.get("cricondentherm")[1];
double cricondentP = ops.get("cricondentherm")[0];
System.out.println("Cricondentherm: " + cricondentT + " K at " + cricondentP + " bar");
Calculate hydrocarbon and water dew points.
import neqsim.thermo.system.SystemSrkCPAstatoil;
import neqsim.thermodynamicoperations.ThermodynamicOperations;
// Use CPA for accurate water modeling
SystemSrkCPAstatoil gas = new SystemSrkCPAstatoil(298.15, 70.0);
gas.addComponent("methane", 0.90);
gas.addComponent("ethane", 0.05);
gas.addComponent("propane", 0.03);
gas.addComponent("water", 0.02);
gas.setMixingRule(10); // CPA mixing rule
ThermodynamicOperations ops = new ThermodynamicOperations(gas);
// Hydrocarbon dew point at fixed pressure
ops.dewPointTemperatureFlash();
System.out.println("HC Dew Point at 70 bar: " + (gas.getTemperature() - 273.15) + " °C");
// Water dew point
ops.waterDewPointTemperatureFlash();
System.out.println("Water Dew Point: " + (gas.getTemperature() - 273.15) + " °C");
Calculate comprehensive physical properties after flash.
import neqsim.thermo.system.SystemSrkEos;
import neqsim.thermodynamicoperations.ThermodynamicOperations;
SystemSrkEos fluid = new SystemSrkEos(300.0, 30.0);
fluid.addComponent("methane", 0.95);
fluid.addComponent("CO2", 0.05);
fluid.setMixingRule("classic");
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
// Gas phase properties
System.out.println("=== Gas Phase Properties ===");
System.out.println("Density: " + fluid.getPhase("gas").getDensity("kg/m3") + " kg/m³");
System.out.println("Viscosity: " + fluid.getPhase("gas").getViscosity("cP") + " cP");
System.out.println("Thermal Conductivity: " + fluid.getPhase("gas").getThermalConductivity("W/mK") + " W/m·K");
System.out.println("Cp: " + fluid.getPhase("gas").getCp("kJ/kgK") + " kJ/kg·K");
System.out.println("Cv: " + fluid.getPhase("gas").getCv("kJ/kgK") + " kJ/kg·K");
System.out.println("Z-factor: " + fluid.getPhase("gas").getZ());
System.out.println("Speed of Sound: " + fluid.getPhase("gas").getSoundSpeed() + " m/s");
System.out.println("Molecular Weight: " + fluid.getPhase("gas").getMolarMass() * 1000 + " g/mol");
System.out.println("Enthalpy: " + fluid.getPhase("gas").getEnthalpy("kJ/kg") + " kJ/kg");
System.out.println("Entropy: " + fluid.getPhase("gas").getEntropy("kJ/kgK") + " kJ/kg·K");
import neqsim.thermo.system.SystemSrkEos;
// Standard SRK for natural gas
SystemSrkEos gas = new SystemSrkEos(298.15, 50.0);
gas.addComponent("nitrogen", 0.02);
gas.addComponent("CO2", 0.03);
gas.addComponent("methane", 0.85);
gas.addComponent("ethane", 0.06);
gas.addComponent("propane", 0.03);
gas.addComponent("i-butane", 0.005);
gas.addComponent("n-butane", 0.005);
gas.setMixingRule("classic");
import neqsim.thermo.system.SystemSrkEos;
SystemSrkEos oil = new SystemSrkEos(350.0, 100.0);
// Light components
oil.addComponent("methane", 10.0);
oil.addComponent("ethane", 5.0);
oil.addComponent("propane", 4.0);
oil.addComponent("n-butane", 3.0);
oil.addComponent("n-pentane", 2.0);
oil.addComponent("n-hexane", 2.0);
// Heavy fractions (TBP cuts)
// addTBPfraction(name, moles, molarMass_kg/mol, density_kg/m3)
oil.addTBPfraction("C7", 5.0, 0.092, 730.0);
oil.addTBPfraction("C8", 4.0, 0.104, 750.0);
oil.addTBPfraction("C9", 3.0, 0.117, 770.0);
// Plus fraction
oil.addPlusFraction("C10+", 20.0, 0.200, 820.0);
// Set mixing rule and characterize
oil.setMixingRule("classic");
// Run characterization to split plus fraction
oil.getCharacterization().setTBPModel("PedersenSRK");
oil.getCharacterization().setPlusFractionModel("Pedersen");
oil.getCharacterization().characterisePlusFraction();
For systems with water, glycols, or alcohols, use CPA equation of state.
import neqsim.thermo.system.SystemSrkCPAstatoil;
// Gas with MEG injection for hydrate inhibition
SystemSrkCPAstatoil fluid = new SystemSrkCPAstatoil(280.0, 100.0);
fluid.addComponent("methane", 0.85);
fluid.addComponent("ethane", 0.08);
fluid.addComponent("propane", 0.04);
fluid.addComponent("water", 0.02);
fluid.addComponent("MEG", 0.01);
fluid.setMixingRule(10); // CPA mixing rule
// Calculate hydrate equilibrium
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.hydrateFormationTemperature();
System.out.println("Hydrate formation temperature: " + (fluid.getTemperature() - 273.15) + " °C");
Two-stage separation with pressure reduction.
import neqsim.process.equipment.stream.Stream;
import neqsim.process.equipment.valve.ThrottlingValve;
import neqsim.process.equipment.separator.Separator;
import neqsim.process.processmodel.ProcessSystem;
import neqsim.thermo.system.SystemSrkEos;
// Create feed fluid
SystemSrkEos fluid = new SystemSrkEos(350.0, 150.0);
fluid.addComponent("methane", 70.0);
fluid.addComponent("ethane", 10.0);
fluid.addComponent("propane", 8.0);
fluid.addComponent("n-butane", 5.0);
fluid.addComponent("n-pentane", 4.0);
fluid.addComponent("n-heptane", 3.0);
fluid.setMixingRule("classic");
// Create feed stream
Stream wellStream = new Stream("Well Stream", fluid);
wellStream.setFlowRate(10000.0, "kg/hr");
wellStream.setTemperature(80.0, "C");
wellStream.setPressure(150.0, "bara");
// First stage separation
ThrottlingValve chokeValve = new ThrottlingValve("Choke Valve", wellStream);
chokeValve.setOutletPressure(50.0, "bara");
Separator hpSeparator = new Separator("HP Separator", chokeValve.getOutletStream());
// Second stage separation
ThrottlingValve lpValve = new ThrottlingValve("LP Valve", hpSeparator.getLiquidOutStream());
lpValve.setOutletPressure(5.0, "bara");
Separator lpSeparator = new Separator("LP Separator", lpValve.getOutletStream());
// Build and run process
ProcessSystem process = new ProcessSystem();
process.add(wellStream);
process.add(chokeValve);
process.add(hpSeparator);
process.add(lpValve);
process.add(lpSeparator);
process.run();
// Results
System.out.println("=== HP Separator ===");
System.out.println("Gas rate: " + hpSeparator.getGasOutStream().getFlowRate("kg/hr") + " kg/hr");
System.out.println("Oil rate: " + hpSeparator.getLiquidOutStream().getFlowRate("kg/hr") + " kg/hr");
System.out.println("=== LP Separator ===");
System.out.println("Flash gas: " + lpSeparator.getGasOutStream().getFlowRate("kg/hr") + " kg/hr");
System.out.println("Stabilized oil: " + lpSeparator.getLiquidOutStream().getFlowRate("kg/hr") + " kg/hr");
Multi-stage compression with intercooling.
import neqsim.process.equipment.stream.Stream;
import neqsim.process.equipment.compressor.Compressor;
import neqsim.process.equipment.heatexchanger.Cooler;
import neqsim.process.equipment.separator.Separator;
import neqsim.process.processmodel.ProcessSystem;
import neqsim.thermo.system.SystemSrkEos;
// Create gas feed
SystemSrkEos gas = new SystemSrkEos(298.15, 5.0);
gas.addComponent("methane", 0.95);
gas.addComponent("ethane", 0.03);
gas.addComponent("propane", 0.02);
gas.setMixingRule("classic");
Stream feed = new Stream("Feed Gas", gas);
feed.setFlowRate(50000.0, "Sm3/day");
feed.setTemperature(30.0, "C");
feed.setPressure(5.0, "bara");
// Stage 1: 5 -> 20 bar
Compressor stage1 = new Compressor("Stage 1", feed);
stage1.setOutletPressure(20.0, "bara");
stage1.setPolytropicEfficiency(0.78);
stage1.setUsePolytropicCalc(true);
Cooler intercooler1 = new Cooler("Intercooler 1", stage1.getOutletStream());
intercooler1.setOutTemperature(35.0, "C");
Separator scrubber1 = new Separator("Scrubber 1", intercooler1.getOutletStream());
// Stage 2: 20 -> 80 bar
Compressor stage2 = new Compressor("Stage 2", scrubber1.getGasOutStream());
stage2.setOutletPressure(80.0, "bara");
stage2.setPolytropicEfficiency(0.78);
stage2.setUsePolytropicCalc(true);
Cooler aftercooler = new Cooler("Aftercooler", stage2.getOutletStream());
aftercooler.setOutTemperature(40.0, "C");
// Build process
ProcessSystem process = new ProcessSystem();
process.add(feed);
process.add(stage1);
process.add(intercooler1);
process.add(scrubber1);
process.add(stage2);
process.add(aftercooler);
process.run();
// Results
System.out.println("Stage 1 power: " + stage1.getPower("kW") + " kW");
System.out.println("Stage 1 outlet T: " + stage1.getOutletStream().getTemperature("C") + " °C");
System.out.println("Stage 2 power: " + stage2.getPower("kW") + " kW");
System.out.println("Stage 2 outlet T: " + stage2.getOutletStream().getTemperature("C") + " °C");
System.out.println("Total power: " + (stage1.getPower("kW") + stage2.getPower("kW")) + " kW");
Shell and tube heat exchanger with two streams.
import neqsim.process.equipment.stream.Stream;
import neqsim.process.equipment.heatexchanger.HeatExchanger;
import neqsim.process.processmodel.ProcessSystem;
import neqsim.thermo.system.SystemSrkEos;
// Hot stream (gas to be cooled)
SystemSrkEos hotFluid = new SystemSrkEos(373.15, 50.0);
hotFluid.addComponent("methane", 0.9);
hotFluid.addComponent("ethane", 0.1);
hotFluid.setMixingRule("classic");
Stream hotStream = new Stream("Hot Gas", hotFluid);
hotStream.setFlowRate(5000.0, "kg/hr");
hotStream.setTemperature(100.0, "C");
hotStream.setPressure(50.0, "bara");
// Cold stream (cooling water)
SystemSrkEos coldFluid = new SystemSrkEos(293.15, 3.0);
coldFluid.addComponent("water", 1.0);
coldFluid.setMixingRule("classic");
Stream coldStream = new Stream("Cooling Water", coldFluid);
coldStream.setFlowRate(20000.0, "kg/hr");
coldStream.setTemperature(20.0, "C");
coldStream.setPressure(3.0, "bara");
// Heat exchanger
HeatExchanger hx = new HeatExchanger("Gas Cooler");
hx.setFeedStream(0, hotStream);
hx.setFeedStream(1, coldStream);
hx.setUAvalue(5000.0); // W/K
ProcessSystem process = new ProcessSystem();
process.add(hotStream);
process.add(coldStream);
process.add(hx);
process.run();
System.out.println("Hot stream outlet T: " + hx.getOutStream(0).getTemperature("C") + " °C");
System.out.println("Cold stream outlet T: " + hx.getOutStream(1).getTemperature("C") + " °C");
System.out.println("Duty: " + hx.getDuty() / 1000.0 + " kW");
A more complete example with multiple unit operations.
import neqsim.process.equipment.stream.Stream;
import neqsim.process.equipment.valve.ThrottlingValve;
import neqsim.process.equipment.separator.*;
import neqsim.process.equipment.compressor.Compressor;
import neqsim.process.equipment.heatexchanger.*;
import neqsim.process.equipment.mixer.StaticMixer;
import neqsim.process.processmodel.ProcessSystem;
import neqsim.thermo.system.SystemSrkEos;
// Rich gas feed
SystemSrkEos richGas = new SystemSrkEos(310.0, 70.0);
richGas.addComponent("nitrogen", 0.01);
richGas.addComponent("CO2", 0.02);
richGas.addComponent("methane", 0.75);
richGas.addComponent("ethane", 0.10);
richGas.addComponent("propane", 0.06);
richGas.addComponent("i-butane", 0.02);
richGas.addComponent("n-butane", 0.02);
richGas.addComponent("i-pentane", 0.01);
richGas.addComponent("n-pentane", 0.01);
richGas.setMixingRule("classic");
Stream feed = new Stream("Rich Gas Feed", richGas);
feed.setFlowRate(100000.0, "Sm3/day");
feed.setTemperature(35.0, "C");
feed.setPressure(70.0, "bara");
// Inlet scrubber
Separator inletScrubber = new Separator("Inlet Scrubber", feed);
// Gas cooling
Cooler gasCooler = new Cooler("Gas Cooler", inletScrubber.getGasOutStream());
gasCooler.setOutTemperature(-20.0, "C");
// Cold separator (NGL recovery)
Separator coldSeparator = new Separator("Cold Separator", gasCooler.getOutletStream());
// Sales gas compression
Compressor salesCompressor = new Compressor("Sales Gas Compressor", coldSeparator.getGasOutStream());
salesCompressor.setOutletPressure(150.0, "bara");
salesCompressor.setIsentropicEfficiency(0.75);
// Build and run
ProcessSystem process = new ProcessSystem();
process.add(feed);
process.add(inletScrubber);
process.add(gasCooler);
process.add(coldSeparator);
process.add(salesCompressor);
process.run();
// Results
System.out.println("=== Gas Processing Results ===");
System.out.println("Feed rate: " + feed.getFlowRate("MSm3/day") + " MSm³/day");
System.out.println("Sales gas rate: " + salesCompressor.getOutletStream().getFlowRate("MSm3/day") + " MSm³/day");
System.out.println("NGL rate: " + coldSeparator.getLiquidOutStream().getFlowRate("kg/hr") + " kg/hr");
System.out.println("Compressor power: " + salesCompressor.getPower("MW") + " MW");
Multiphase pipeline pressure drop calculation.
import neqsim.process.equipment.stream.Stream;
import neqsim.process.equipment.pipeline.PipeBeggsAndBrills;
import neqsim.process.processmodel.ProcessSystem;
import neqsim.thermo.system.SystemSrkEos;
// Multiphase fluid
SystemSrkEos fluid = new SystemSrkEos(320.0, 80.0);
fluid.addComponent("methane", 0.75);
fluid.addComponent("ethane", 0.10);
fluid.addComponent("propane", 0.08);
fluid.addComponent("n-pentane", 0.05);
fluid.addComponent("n-heptane", 0.02);
fluid.setMixingRule("classic");
Stream inlet = new Stream("Pipeline Inlet", fluid);
inlet.setFlowRate(50000.0, "kg/hr");
inlet.setTemperature(45.0, "C");
inlet.setPressure(80.0, "bara");
// Pipeline
PipeBeggsAndBrills pipeline = new PipeBeggsAndBrills("Export Pipeline", inlet);
pipeline.setLength(50000.0); // 50 km
pipeline.setDiameter(0.3048); // 12 inch
pipeline.setElevation(100.0); // 100m elevation gain
pipeline.setPipeWallRoughness(4.5e-5); // Steel pipe
pipeline.setNumberOfIncrements(20);
ProcessSystem process = new ProcessSystem();
process.add(inlet);
process.add(pipeline);
process.run();
System.out.println("=== Pipeline Results ===");
System.out.println("Inlet pressure: " + inlet.getPressure("bara") + " bara");
System.out.println("Outlet pressure: " + pipeline.getOutletStream().getPressure("bara") + " bara");
System.out.println("Pressure drop: " + (inlet.getPressure("bara") - pipeline.getOutletStream().getPressure("bara")) + " bar");
System.out.println("Outlet temperature: " + pipeline.getOutletStream().getTemperature("C") + " °C");
System.out.println("Flow regime: " + pipeline.getFlowRegime());
System.out.println("Liquid holdup: " + pipeline.getLiquidHoldup());
The WindTurbine unit converts kinetic energy in wind into electrical power using a simple
actuator-disk formulation. Air density is assumed constant at 1.225 kg/m³ and all inefficiencies
are lumped into the power coefficient.
import neqsim.process.equipment.powergeneration.WindTurbine;
WindTurbine turbine = new WindTurbine("turbine");
turbine.setWindSpeed(12.0); // m/s
turbine.setRotorArea(50.0); // m²
turbine.setPowerCoefficient(0.4);
turbine.run();
System.out.println("Power produced: " + turbine.getPower() + " W");
Water electrolysis for hydrogen production.
import neqsim.process.equipment.electrolyzer.Electrolyzer;
import neqsim.process.equipment.stream.Stream;
import neqsim.thermo.system.SystemSrkEos;
// Water feed
SystemSrkEos water = new SystemSrkEos(298.15, 1.0);
water.addComponent("water", 1.0);
water.setMixingRule("classic");
Stream waterFeed = new Stream("Water Feed", water);
waterFeed.setFlowRate(100.0, "kg/hr");
Electrolyzer electrolyzer = new Electrolyzer("PEM Electrolyzer", waterFeed);
electrolyzer.setEfficiency(0.70);
electrolyzer.run();
System.out.println("H2 production rate: " + electrolyzer.getHydrogenOutStream().getFlowRate("kg/hr") + " kg/hr");
System.out.println("O2 production rate: " + electrolyzer.getOxygenOutStream().getFlowRate("kg/hr") + " kg/hr");
System.out.println("Power consumption: " + electrolyzer.getPower("kW") + " kW");
Gas separation using membrane technology.
import neqsim.process.equipment.separator.MembraneSeparator;
import neqsim.process.equipment.stream.Stream;
import neqsim.thermo.system.SystemSrkEos;
SystemSrkEos gas = new SystemSrkEos(298.15, 50.0);
gas.addComponent("CO2", 0.30);
gas.addComponent("methane", 0.70);
gas.setMixingRule("classic");
Stream feed = new Stream("Feed", gas);
feed.setFlowRate(1000.0, "Sm3/hr");
MembraneSeparator membrane = new MembraneSeparator("CO2 Membrane", feed);
membrane.setRelativePermability("CO2", 20.0);
membrane.setRelativePermability("methane", 1.0);
membrane.setMembranePressureDrop(30.0, "bara");
membrane.run();
System.out.println("Permeate CO2: " + membrane.getPermeateStream().getFluid().getComponent("CO2").getx() * 100 + " mol%");
System.out.println("Retentate CO2: " + membrane.getRetentateStream().getFluid().getComponent("CO2").getx() * 100 + " mol%");
For more examples, see:
NeqSim (Non-Equilibrium Simulator) is a Java library for thermodynamic calculations and process simulation, specializing in oil and gas applications. It provides:
The full JavaDoc is available at https://htmlpreview.github.io/?https://github.com/equinor/neqsimhome/blob/master/javadoc/site/apidocs/index.html.
Yes, NeqSim is open source under the Apache 2.0 license. You can freely use, modify, and distribute it.
The project is developed by Equinor with contributions from the community. Contact Even Solbraa (esolbraa@gmail.com) for questions.
NeqSim requires Java 8 or higher. Java 11+ is recommended for best performance.
Maven:
<dependency>
<groupId>com.equinor.neqsim</groupId>
<artifactId>neqsim</artifactId>
<version>3.0.0</version>
</dependency>
Gradle:
implementation 'com.equinor.neqsim:neqsim:3.0.0'
Direct Download: Download the shaded JAR from GitHub releases.
git clone https://github.com/equinor/neqsim.git
cd neqsim
./mvnw install
On Windows, use mvnw.cmd install.
After cloning the repository, execute:
./mvnw test
To run a specific test:
./mvnw test -Dtest=YourTestClassName
| Application | Recommended EOS |
|---|---|
| General natural gas | SystemSrkEos |
| Reservoir/PVT | SystemPrEos |
| Water + hydrocarbons | SystemSrkCPAstatoil |
| Glycol dehydration | SystemSrkCPAstatoil |
| High-accuracy natural gas | SystemGERG2008Eos |
| CO2 capture/CCS | SystemSrkCPAstatoil or SystemSpanWagnerEos |
| Electrolytes/brine | SystemElectrolyteCPAstatoil |
// For SRK/PR - use classic van der Waals mixing rule
system.setMixingRule("classic"); // or system.setMixingRule(2);
// For CPA - use CPA mixing rule
system.setMixingRule(10);
// For advanced mixing (Huron-Vidal with NRTL)
system.setMixingRule("HV", "NRTL");
Common causes and solutions:
ThermodynamicOperations ops = new ThermodynamicOperations(system);
ops.calcPTphaseEnvelope();
double[] cricondenbar = ops.get("cricondenbar");
double[] cricondentherm = ops.get("cricondentherm");
// Add plus fraction with average properties
system.addPlusFraction("C10+", 10.0, 0.200, 820.0);
// Set characterization model
system.getCharacterization().setTBPModel("PedersenSRK");
system.getCharacterization().setPlusFractionModel("Pedersen");
system.getCharacterization().characterisePlusFraction();
ProcessSystem process = new ProcessSystem();
// Add feed stream
Stream feed = new Stream("Feed", fluid);
process.add(feed);
// Add equipment
Separator sep = new Separator("Sep", feed);
process.add(sep);
// Run
process.run();
ProcessSystem uses equipment names as identifiers. Use unique names:
Separator sep1 = new Separator("HP Separator", stream1);
Separator sep2 = new Separator("LP Separator", stream2); // Different name
Recycle recycle = new Recycle("Recycle");
recycle.addStream(separator.getLiquidOutStream());
recycle.setTolerance(1e-6);
process.add(recycle);
// Connect output to upstream mixer
mixer.addStream(recycle.getOutletStream());
// Isentropic efficiency
compressor.setIsentropicEfficiency(0.75);
compressor.setUsePolytropicCalc(false);
// OR polytropic efficiency
compressor.setPolytropicEfficiency(0.80);
compressor.setUsePolytropicCalc(true);
// After running flash
ops.TPflash();
// Gas viscosity
double gasVisc = system.getPhase("gas").getViscosity("cP");
// Liquid viscosity
double liqVisc = system.getPhase("oil").getViscosity("cP");
See Viscosity Models for details.
double soundSpeed = system.getPhase("gas").getSoundSpeed(); // m/s
double ift = system.getInterfacialTension(0, 1); // Phase indices
Always run flash before accessing properties:
ThermodynamicOperations ops = new ThermodynamicOperations(system);
ops.TPflash(); // REQUIRED before accessing properties
Check component name spelling. Use exact names from the database:
// Correct
system.addComponent("methane", 1.0);
system.addComponent("n-butane", 1.0); // Note: n-butane, not nbutane
// Find available components
// Check database or use system.getComponentNames()
./mvnw verify to check code styleSee Contributing Structure for details.
Open an issue at https://github.com/equinor/neqsim/issues with:
Welcome to the NeqSim documentation. This comprehensive wiki provides guides, tutorials, and reference materials for using the library and contributing to development.
NeqSim (Non-Equilibrium Simulator) is a Java library for estimating fluid properties and process design. The library contains models for:
Development was initiated at the Norwegian University of Science and Technology (NTNU). NeqSim is part of the NeqSim project.
import neqsim.thermo.system.SystemSrkEos;
import neqsim.thermodynamicoperations.ThermodynamicOperations;
// Create a fluid
SystemSrkEos fluid = new SystemSrkEos(298.15, 10.0); // T(K), P(bara)
fluid.addComponent("methane", 0.9);
fluid.addComponent("ethane", 0.07);
fluid.addComponent("propane", 0.03);
fluid.setMixingRule("classic");
// Run flash calculation
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
// Get results
System.out.println("Z-factor: " + fluid.getZ());
System.out.println("Density: " + fluid.getDensity("kg/m3") + " kg/m3");
| Guide | Description |
|---|---|
| Getting Started | Installation, first calculations, and basic concepts |
| Usage Examples | Comprehensive code examples |
| FAQ | Frequently asked questions |
| GitHub Guide | Complete documentation index |
| Guide | Description |
|---|---|
| Thermodynamics Guide | Equations of state, flash calculations, mixing rules |
| Fluid Characterization | Plus fractions, pseudo-components, TBP modeling |
| Flash Equations & Tests | Flash calculations validated by tests |
| Property Flash Workflows | PH, PS, UV flash calculations |
| Guide | Description |
|---|---|
| Process Simulation Guide | Building flowsheets, running simulations |
| Advanced Process Simulation | Recycles, adjusters, complex systems |
| Logical Unit Operations | Controllers, splitters, recycles |
| Transient Simulation Guide | Dynamic process modeling |
| Process Control Framework | PID controllers, automation |
| Equipment | Documentation |
|---|---|
| Distillation Column | Sequential, damped, inside-out solvers |
| Gibbs Reactor | Chemical equilibrium reactor |
| Flow Meter Models | Orifice, venturi, ultrasonic meters |
| Air Cooler | Air-cooled heat exchanger |
| Heat Exchanger Design | Mechanical design methods |
| Water Cooler | Water-cooled systems |
| Steam Heater | Steam heating systems |
| Battery Storage | Energy storage unit |
| Solar Panel | Solar power generation |
| Guide | Description |
|---|---|
| PVT Simulation Workflows | CVD, CCE, DL simulations |
| Black-Oil Flash Playbook | Black-oil modeling techniques |
| Humid Air Mathematics | Psychrometric calculations |
| Guide | Description |
|---|---|
| Gas Quality Standards | ISO 6976, GPA standards |
| Guide | Description |
|---|---|
| Java from Colab | Running NeqSim in Google Colab |
| JUnit Test Overview | Test suite structure |
Maven:
<dependency>
<groupId>com.equinor.neqsim</groupId>
<artifactId>neqsim</artifactId>
<version>3.0.0</version>
</dependency>
Download: GitHub Releases
This folder collects topic-specific documentation for using NeqSim's thermodynamic, PVT, and physical property capabilities. Each page is intended to be self-contained while pointing to related guides so you can jump directly to the workflows you need.
thermo/
├── system/ # Fluid system implementations (58 EoS classes)
├── phase/ # Phase types and calculations (62 classes)
├── component/ # Component properties (65 classes)
├── mixingrule/ # Mixing rules for EoS
└── characterization/ # Plus fraction characterization
| Subpackage | Description | Documentation |
|---|---|---|
| system | Equations of state implementations | system/README.md |
| phase | Phase modeling (gas, liquid, solid, asphaltene) | phase/README.md |
| component | Component property calculations | component/README.md |
| mixingrule | Binary interaction parameters | mixingrule/README.md |
| characterization | Plus fraction and asphaltene characterization | characterization/README.md |
Each document favors short, reproducible code snippets using the Java API so the same ideas transfer to other supported languages (Python/Matlab) with minor syntax changes.
NeqSim (Non-Equilibrium Simulator) is a comprehensive library for thermodynamic calculations, specializing in oil and gas fluids, CO2 systems, and aqueous electrolytes. This guide provides an overview of the available models, methods, and how to use them.
The core of any simulation is the Equation of State (EOS). NeqSim supports a wide range of EOSs tailored for different applications.
Standard models for oil and gas processing.
SystemSrkEos. The industry standard for general hydrocarbon systems.SystemPrEos. Often preferred for reservoir engineering and density predictions.SystemSrkPenelouxEos).Essential for systems containing polar molecules (water, methanol, glycol) and hydrocarbons. It combines a cubic EOS (SRK or PR) with an association term (Wertheim).
SystemSrkCPAstatoil. Recommended for gas-hydrate inhibition (MEG/MeOH) and water-hydrocarbon VLE/LLE.SystemPrCPA.High-precision multiparameter equations for specific fluids or mixtures.
SystemGERG2008Eos. The ISO standard for natural gas properties. Excellent for custody transfer and density calculation.SystemSpanWagnerEos. High-precision EOS for pure CO2.SystemWaterIF97. Industrial standard for water and steam.For systems containing salts and ions.
SystemElectrolyteCPAstatoil. Extends CPA to handle salt solubility and the effect of ions on phase equilibria.SystemFurstElectrolyteEos.Mixing rules define how pure component parameters are combined for mixtures.
system.setMixingRule("classic") or system.setMixingRule(2)system.setMixingRule("HV", "NRTL")NeqSim performs various types of equilibrium calculations (flashes) via the ThermodynamicOperations class.
ops.TPflash()ops.PHflash(enthalpy, unit)ops.PSflash(entropy, unit)ops.bubblePointPressureFlash(false) or ops.bubblePointTemperatureFlash()ops.dewPointPressureFlash() or ops.dewPointTemperatureFlash()ops.waterDewPointTemperatureFlash()ops.hydrateFormationTemperatureFlash()ops.calcWAT()ops.checkScalePotential(phaseNumber)Once a flash is performed, physical properties are available from the Phase objects.
phase.getDensity("kg/m3")phase.getViscosity("kg/msec"). See Viscosity Models.phase.getThermalConductivity("W/mK")system.getInterfacialTension(phase1, phase2)phase.getCp(), phase.getCv()import neqsim.thermo.system.SystemSrkEos;
import neqsim.thermodynamicoperations.ThermodynamicOperations;
public class DewPointExample {
public static void main(String[] args) {
// 1. Create System
SystemSrkEos gas = new SystemSrkEos(298.15, 50.0);
gas.addComponent("methane", 90.0);
gas.addComponent("ethane", 5.0);
gas.addComponent("propane", 3.0);
gas.addComponent("water", 0.1); // Saturated water
// 2. Set Mixing Rule
gas.setMixingRule("classic");
// 3. Initialize Operations
ThermodynamicOperations ops = new ThermodynamicOperations(gas);
// 4. Calculate Hydrocarbon Dew Point
try {
ops.dewPointTemperatureFlash();
System.out.println("HC Dew Point: " + gas.getTemperature("C") + " C");
} catch (Exception e) {
e.printStackTrace();
}
// 5. Calculate Water Dew Point
try {
ops.waterDewPointTemperatureFlash();
System.out.println("Water Dew Point: " + gas.getTemperature("C") + " C");
} catch (Exception e) {
e.printStackTrace();
}
}
}
from neqsim.thermo import SystemGERG2008Eos
from neqsim.thermodynamicoperations import ThermodynamicOperations
# 1. Create System
co2 = SystemGERG2008Eos(300.0, 100.0) # 300 K, 100 bar
co2.addComponent("CO2", 1.0)
# 2. Flash
ops = ThermodynamicOperations(co2)
ops.TPflash()
# 3. Get Properties
rho = co2.getPhase(0).getDensity("kg/m3")
print(f"CO2 Density at 100 bar/300 K: {rho} kg/m3")
For real reservoir fluids containing heavy fractions (C7+), NeqSim provides tools to characterize the fluid based on specific gravity and molecular weight.
system.addTBPfraction() or system.addPlusFraction().ModelLumping.setPlusFractionModel("Pedersen Heavy Oil").setPlusFractionModel("Whitson Gamma") if you have specific gamma distribution parameters.setLumpingModel("no lumping").See Fluid Characterization for details.
Documentation for fluid system implementations in NeqSim.
Location: neqsim.thermo.system
The system package contains 58+ implementations of thermodynamic models, from simple ideal gas to complex associating equations of state.
SystemInterface
└── SystemThermo (abstract base)
├── SystemEos (cubic EoS base)
│ ├── SystemSrkEos
│ ├── SystemPrEos
│ └── ...
├── SystemSrkCPA (CPA base)
│ ├── SystemSrkCPAstatoil
│ └── ...
└── SystemPCSAFT (SAFT base)
└── ...
import neqsim.thermo.system.SystemSrkEos;
SystemSrkEos fluid = new SystemSrkEos(298.15, 50.0);
fluid.addComponent("methane", 1.0);
fluid.setMixingRule("classic");
$$P = \frac{RT}{V-b} - \frac{a\alpha(T)}{V(V+b)}$$
import neqsim.thermo.system.SystemPrEos;
SystemPrEos fluid = new SystemPrEos(298.15, 50.0);
fluid.addComponent("methane", 1.0);
fluid.setMixingRule("classic");
$$P = \frac{RT}{V-b} - \frac{a\alpha(T)}{V(V+b)+b(V-b)}$$
| Class | Description |
|---|---|
SystemSrkEos |
Standard SRK |
SystemPrEos |
Standard PR |
SystemSrkMathiasCopeman |
SRK with Mathias-Copeman alpha |
SystemPrMathiasCopeman |
PR with Mathias-Copeman alpha |
SystemSrkSchwartzentruberRenon |
SRK with GE mixing |
SystemPrSchwartzentruberRenon |
PR with GE mixing |
SystemSrkTwuCoon |
SRK with Twu-Coon alpha |
SystemPrTwuCoon |
PR with Twu-Coon alpha |
SystemPrDanesh |
PR with Danesh modifications |
// Standard Soave alpha
SystemSrkEos std = new SystemSrkEos(T, P);
// Mathias-Copeman alpha (better for polar)
SystemSrkMathiasCopeman mc = new SystemSrkMathiasCopeman(T, P);
// Twu-Coon alpha
SystemSrkTwuCoon tc = new SystemSrkTwuCoon(T, P);
For systems with hydrogen bonding (water, alcohols, glycols, amines).
import neqsim.thermo.system.SystemSrkCPAstatoil;
SystemSrkCPAstatoil fluid = new SystemSrkCPAstatoil(298.15, 10.0);
fluid.addComponent("water", 1.0);
fluid.addComponent("methane", 2.0);
fluid.setMixingRule(10); // CPA mixing rule
| Class | Description |
|---|---|
SystemSrkCPAstatoil |
SRK-CPA (Equinor parameters) |
SystemPrCPA |
PR-CPA |
SystemSrkCPA |
SRK-CPA (generic) |
SystemElectrolyteCPA |
CPA with electrolytes |
// Standard CPA mixing rule
fluid.setMixingRule(10);
// With cross-association
fluid.setMixingRule(10);
Statistical Associating Fluid Theory with perturbed chain.
import neqsim.thermo.system.SystemPCSAFT;
SystemPCSAFT fluid = new SystemPCSAFT(298.15, 10.0);
fluid.addComponent("methane", 1.0);
fluid.addComponent("n-hexane", 0.5);
fluid.setMixingRule("classic");
import neqsim.thermo.system.SystemElectrolytePCSAFT;
SystemElectrolytePCSAFT brine = new SystemElectrolytePCSAFT(298.15, 1.0);
brine.addComponent("water", 1.0);
brine.addComponent("Na+", 0.1);
brine.addComponent("Cl-", 0.1);
import neqsim.thermo.system.SystemNRTL;
SystemNRTL liquid = new SystemNRTL(298.15, 1.0);
liquid.addComponent("ethanol", 0.5);
liquid.addComponent("water", 0.5);
import neqsim.thermo.system.SystemUNIFAC;
SystemUNIFAC liquid = new SystemUNIFAC(298.15, 1.0);
liquid.addComponent("acetone", 0.5);
liquid.addComponent("water", 0.5);
// SRK with UNIFAC for liquid
SystemSrkSchwartzentruberRenon fluid = new SystemSrkSchwartzentruberRenon(T, P);
High-accuracy reference equation for natural gas.
import neqsim.thermo.system.SystemGERG2008;
SystemGERG2008 gas = new SystemGERG2008(288.15, 50.0);
gas.addComponent("nitrogen", 0.02);
gas.addComponent("CO2", 0.01);
gas.addComponent("methane", 0.85);
gas.addComponent("ethane", 0.08);
gas.addComponent("propane", 0.04);
Accuracy:
Universal Mixing Rule with PR EoS.
import neqsim.thermo.system.SystemUMRPRU;
SystemUMRPRU lng = new SystemUMRPRU(110.0, 1.0);
lng.addComponent("methane", 0.92);
lng.addComponent("ethane", 0.05);
lng.addComponent("propane", 0.03);
// Create at specific T, P
SystemSrkEos fluid = new SystemSrkEos(300.0, 10.0); // K, bar
// Change conditions later
fluid.setTemperature(350.0);
fluid.setPressure(50.0);
// With units
fluid.setTemperature(25.0, "C");
fluid.setPressure(50.0, "bara");
// By name and moles
fluid.addComponent("methane", 100.0);
// By index
fluid.addComponent(0, 100.0);
// TBP fraction (for plus fractions)
fluid.addTBPfraction("C7+", 10.0, 150.0, 0.78); // name, moles, MW, SG
// Set mole fractions directly
double[] z = {0.85, 0.10, 0.05};
fluid.setMolarComposition(z);
// Force number of phases
fluid.setNumberOfPhases(2);
// Specify phase types
fluid.setPhaseType(0, "gas");
fluid.setPhaseType(1, "oil");
// Allow solid phases
fluid.setSolidPhaseCheck(true);
// Initialize thermodynamic properties
fluid.init(0); // Molar volumes only
fluid.init(1); // Plus fugacity coefficients
fluid.init(2); // Plus all derivatives
fluid.init(3); // Plus second derivatives
// Bulk properties
double rho = fluid.getDensity("kg/m3");
double MW = fluid.getMolarMass("kg/mol");
double H = fluid.getEnthalpy("kJ/kg");
double S = fluid.getEntropy("kJ/kgK");
double Cp = fluid.getCp("J/molK");
double Cv = fluid.getCv("J/molK");
double Z = fluid.getZ();
double kappa = fluid.getKappa(); // Cp/Cv
// Transport properties
double visc = fluid.getViscosity("cP");
double k = fluid.getThermalConductivity("W/mK");
// Phase properties
double gasZ = fluid.getGasPhase().getZ();
double liqRho = fluid.getLiquidPhase().getDensity("kg/m3");
// Deep copy
SystemInterface copy = fluid.clone();
// Modify copy without affecting original
copy.setTemperature(400.0);
This guide provides comprehensive documentation on how to create and configure thermodynamic fluids in NeqSim, including available equations of state, mixing rules, and best practices.
Creating a fluid in NeqSim follows a consistent pattern:
import neqsim.thermo.system.SystemSrkEos;
import neqsim.thermo.system.SystemInterface;
// 1. Create the fluid with initial temperature (K) and pressure (bara)
SystemInterface fluid = new SystemSrkEos(298.15, 10.0);
// 2. Add components
fluid.addComponent("methane", 0.90); // name, moles
fluid.addComponent("ethane", 0.05);
fluid.addComponent("propane", 0.05);
// 3. Set up the mixing rule
fluid.setMixingRule("classic");
// 4. Initialize the fluid
fluid.init(0);
All fluid system classes accept these constructor signatures:
| Constructor | Description |
|---|---|
SystemXXX() |
Default: 298.15 K, 1.0 bara |
SystemXXX(T, P) |
Temperature (K), Pressure (bara) |
SystemXXX(T, P, checkForSolids) |
With solid phase checking enabled |
NeqSim provides a wide range of thermodynamic models organized into categories:
| Category | Use Cases | Examples |
|---|---|---|
| Cubic EoS | General hydrocarbon processing | SRK, PR, PR-1978 |
| CPA (Cubic Plus Association) | Polar/associating fluids (water, glycols, alcohols) | SRK-CPA, PR-CPA |
| Reference EoS | High-accuracy natural gas, CCS | GERG-2008, EOS-CG |
| SAFT-based | Complex molecular interactions | PC-SAFT |
| Activity Coefficient | Non-ideal liquid mixtures | UNIFAC, NRTL |
| Electrolyte | Aqueous salt solutions | Electrolyte-CPA, Pitzer |
| Specialized | Specific applications | Soreide-Whitson (sour gas/brine) |
The standard SRK equation of state. Best for general gas and light hydrocarbon applications.
SystemInterface fluid = new SystemSrkEos(300.0, 50.0);
fluid.addComponent("methane", 0.8);
fluid.addComponent("CO2", 0.2);
fluid.setMixingRule("classic");
SRK with Peneloux volume correction for improved liquid density predictions.
SystemInterface fluid = new SystemSrkPenelouxEos(300.0, 50.0);
SRK with Mathias-Copeman alpha function for better vapor pressure predictions.
SystemInterface fluid = new SystemSrkMathiasCopeman(300.0, 50.0);
SRK with Twu-Coon alpha function.
SystemInterface fluid = new SystemSrkTwuCoonEos(300.0, 50.0);
Standard Peng-Robinson equation. Widely used for oil and gas applications.
SystemInterface fluid = new SystemPrEos(300.0, 50.0);
fluid.addComponent("methane", 0.7);
fluid.addComponent("n-heptane", 0.3);
fluid.setMixingRule("classic");
The PR equation is expressed as: $$ P = \frac{RT}{v - b} - \frac{a \alpha}{v(v + b) + b(v - b)} $$
Original 1978 Peng-Robinson formulation with modified alpha function.
SystemInterface fluid = new SystemPrEos1978(300.0, 50.0);
PR with Mathias-Copeman alpha function for polar components.
SystemInterface fluid = new SystemPrMathiasCopeman(300.0, 50.0);
Original Redlich-Kwong equation (historical interest, less accurate).
SystemInterface fluid = new SystemRKEos(300.0, 50.0);
Twu-Sim-Tassone equation of state.
SystemInterface fluid = new SystemTSTEos(300.0, 50.0);
CPA models add an association term to handle hydrogen bonding in polar molecules like water, alcohols, and glycols.
The Equinor (formerly Statoil) implementation of SRK-CPA. Recommended for water-hydrocarbon systems.
SystemInterface fluid = new SystemSrkCPAstatoil(300.0, 50.0);
fluid.addComponent("water", 0.1);
fluid.addComponent("methane", 0.85);
fluid.addComponent("MEG", 0.05); // Mono-ethylene glycol
fluid.setMixingRule(10); // CPA mixing rule with temperature/composition dependency
Alternative CPA implementations.
SystemInterface fluid = new SystemSrkCPA(300.0, 50.0);
fluid.setMixingRule(7); // CPA mixing rule
Peng-Robinson with CPA association term.
SystemInterface fluid = new SystemPrCPA(300.0, 50.0);
Perturbed Chain Statistical Associating Fluid Theory. Good for polymers and complex molecules.
SystemInterface fluid = new SystemPCSAFT(300.0, 50.0);
fluid.addComponent("methane", 0.5);
fluid.addComponent("ethane", 0.5);
Peng-Robinson with UNIFAC-based mixing rules for improved predictions.
SystemInterface fluid = new SystemUMRPRUEos(300.0, 50.0);
For high-accuracy applications, NeqSim provides reference equations of state based on the Helmholtz free energy:
$$ \alpha(\delta, \tau, \bar{x}) = \alpha^0(\delta, \tau, \bar{x}) + \alpha^r(\delta, \tau, \bar{x}) $$
The ISO 20765-2 standard for natural gas. Highest accuracy for custody transfer and fiscal metering.
Supported components (21): Methane, Nitrogen, CO2, Ethane, Propane, n-Butane, i-Butane, n-Pentane, i-Pentane, n-Hexane, n-Heptane, n-Octane, n-Nonane, n-Decane, Hydrogen, Oxygen, CO, Water, Helium, Argon.
import neqsim.thermo.system.SystemGERG2008Eos;
SystemInterface fluid = new SystemGERG2008Eos(288.15, 50.0);
fluid.addComponent("methane", 0.90);
fluid.addComponent("ethane", 0.05);
fluid.addComponent("propane", 0.03);
fluid.addComponent("nitrogen", 0.02);
fluid.createDatabase(true);
// Access GERG-specific properties
double density = fluid.getPhase(0).getDensity_GERG2008();
Extension of GERG-2008 for CCS (Carbon Capture and Storage) applications. Includes combustion gas components.
Additional components: SO2, NO, NO2, and others relevant to flue gas.
import neqsim.thermo.system.SystemEOSCGEos;
SystemInterface fluid = new SystemEOSCGEos(300.0, 100.0);
fluid.addComponent("CO2", 0.95);
fluid.addComponent("nitrogen", 0.03);
fluid.addComponent("oxygen", 0.02);
| Class | Description |
|---|---|
SystemSpanWagnerEos |
Span-Wagner equation for CO2 |
SystemLeachmanEos |
Leachman equation for hydrogen |
SystemBWRSEos |
Benedict-Webb-Rubin-Starling |
SystemBnsEos |
BNS equation of state |
For non-ideal liquid mixtures, especially polar and chemical systems:
Group contribution method for activity coefficients.
import neqsim.thermo.system.SystemUNIFAC;
SystemInterface fluid = new SystemUNIFAC(300.0, 1.0);
fluid.addComponent("methanol", 0.3);
fluid.addComponent("water", 0.7);
Non-Random Two-Liquid model.
import neqsim.thermo.system.SystemNRTL;
SystemInterface fluid = new SystemNRTL(300.0, 1.0);
fluid.addComponent("ethanol", 0.4);
fluid.addComponent("water", 0.6);
Wilson equation for activity coefficients.
import neqsim.thermo.system.SystemGEWilson;
SystemInterface fluid = new SystemGEWilson(300.0, 1.0);
For systems containing salts and ions in aqueous solutions:
import neqsim.thermo.system.SystemElectrolyteCPAstatoil;
SystemInterface fluid = new SystemElectrolyteCPAstatoil(298.15, 1.0);
fluid.addComponent("water", 1.0);
fluid.addComponent("Na+", 0.1);
fluid.addComponent("Cl-", 0.1);
Modified PR for sour gas systems and brine.
import neqsim.thermo.system.SystemSoreideWhitson;
SystemSoreideWhitson fluid = new SystemSoreideWhitson(350.0, 200.0);
fluid.addComponent("methane", 0.7);
fluid.addComponent("CO2", 0.15);
fluid.addComponent("H2S", 0.05);
fluid.addComponent("water", 0.1);
fluid.addSalinity(2.0, "mole/sec"); // Add salinity
fluid.setMixingRule(11); // Soreide-Whitson mixing rule
For concentrated electrolyte solutions.
import neqsim.thermo.system.SystemPitzer;
SystemInterface fluid = new SystemPitzer(298.15, 1.0);
Mixing rules determine how pure-component parameters are combined for mixtures. Set via setMixingRule():
| Value | Name | Description |
|---|---|---|
| 1 | NO |
Classic with all kij = 0 (no interaction) |
| 2 | CLASSIC |
Classic van der Waals with kij from database |
| 3 | CLASSIC_HV |
Huron-Vidal with database parameters |
| 4 | HV |
Huron-Vidal including temperature-dependent HVDijT |
| 5 | WS |
Wong-Sandler (NRTL-based coupling) |
| 7 | CPA_MIX |
Classic with CPA kij from database |
| 8 | CLASSIC_T |
Classic with temperature-dependent kij |
| 9 | CLASSIC_T_CPA |
Classic T-dependent kij for CPA |
| 10 | CLASSIC_TX_CPA |
Classic T and composition dependent kij for CPA |
| 11 | SOREIDE_WHITSON |
Søreide-Whitson mixing rule |
| 12 | CLASSIC_T2 |
Alternative temperature-dependent classic |
// By integer value
fluid.setMixingRule(2);
// By name (string)
fluid.setMixingRule("classic");
fluid.setMixingRule("HV");
fluid.setMixingRule("WS");
| Application | Recommended Mixing Rule |
|---|---|
| Light hydrocarbons | classic (2) |
| CO2-hydrocarbon | classic (2) with tuned kij |
| Polar mixtures | HV (4) or WS (5) |
| Water-hydrocarbon (CPA) | CPA_MIX (7) or CLASSIC_TX_CPA (10) |
| Sour gas with brine | SOREIDE_WHITSON (11) |
// Add by name and moles
fluid.addComponent("methane", 0.85);
fluid.addComponent("ethane", 0.10);
fluid.addComponent("propane", 0.05);
// Add with flow rate and unit
fluid.addComponent("methane", 100.0, "kg/hr");
fluid.addComponent("ethane", 50.0, "Sm3/day");
// Add multiple components at once
String[] names = {"methane", "ethane", "propane"};
double[] moles = {0.85, 0.10, 0.05};
fluid.addComponents(names, moles);
For addComponent(name, value, unit):
mol/sec, mol/hrkg/sec, kg/hrSm3/hr, Sm3/day, MSm3/day, Nlitre/minNeqSim's database includes hundreds of components. Common names:
Hydrocarbons:
methane, ethane, propane, i-butane, n-butane, i-pentane, n-pentane, n-hexane, n-heptane, n-octane, n-nonane, n-decane
Inorganics:
nitrogen, oxygen, CO2, H2S, water, hydrogen, helium, argon
Polar/Associating:
methanol, ethanol, MEG (mono-ethylene glycol), TEG (tri-ethylene glycol), DEG
Ions:
Na+, K+, Ca++, Mg++, Cl-, SO4--, HCO3-
For petroleum fluids, NeqSim supports TBP (True Boiling Point) and plus-fraction characterization.
SystemInterface oil = new SystemSrkEos(350.0, 100.0);
oil.createDatabase(true); // Required before adding TBP fractions
// addTBPfraction(name, moles, molarMass [g/mol], density [g/cm3])
oil.addTBPfraction("C7", 0.05, 96.0, 0.738);
oil.addTBPfraction("C8", 0.04, 107.0, 0.765);
oil.addTBPfraction("C9", 0.03, 121.0, 0.781);
oil.addTBPfraction("C10", 0.02, 134.0, 0.792);
oil.setMixingRule("classic");
// addPlusFraction(name, moles, molarMass [g/mol], density [g/cm3])
oil.addPlusFraction("C20+", 0.10, 350.0, 0.88);
NeqSim provides several models for estimating critical properties from TBP data:
// Set TBP model before adding fractions
fluid.getCharacterization().setTBPModel("PedersenSRK"); // Default for SRK
fluid.getCharacterization().setTBPModel("PedersenPR"); // Default for PR
fluid.getCharacterization().setTBPModel("Lee-Kesler");
fluid.getCharacterization().setTBPModel("Twu");
fluid.getCharacterization().setTBPModel("RiaziDaubert");
import neqsim.thermo.system.SystemSrkEos;
import neqsim.thermo.system.SystemInterface;
import neqsim.thermodynamicoperations.ThermodynamicOperations;
public class NaturalGasExample {
public static void main(String[] args) {
// Create SRK fluid at pipeline conditions
SystemInterface gas = new SystemSrkEos(283.15, 70.0);
// Typical natural gas composition
gas.addComponent("nitrogen", 0.02);
gas.addComponent("CO2", 0.01);
gas.addComponent("methane", 0.85);
gas.addComponent("ethane", 0.06);
gas.addComponent("propane", 0.03);
gas.addComponent("i-butane", 0.01);
gas.addComponent("n-butane", 0.01);
gas.addComponent("i-pentane", 0.005);
gas.addComponent("n-pentane", 0.005);
gas.setMixingRule("classic");
// Flash calculation
ThermodynamicOperations ops = new ThermodynamicOperations(gas);
ops.TPflash();
gas.initProperties();
// Display results
System.out.println("Density: " + gas.getDensity("kg/m3") + " kg/m3");
System.out.println("Z-factor: " + gas.getZ());
System.out.println("Molecular weight: " + gas.getMolarMass() * 1000 + " g/mol");
}
}
import neqsim.thermo.system.SystemSrkCPAstatoil;
import neqsim.thermo.system.SystemInterface;
import neqsim.thermodynamicoperations.ThermodynamicOperations;
public class WaterHydrocarbonExample {
public static void main(String[] args) {
// CPA for associating systems
SystemInterface fluid = new SystemSrkCPAstatoil(323.15, 50.0);
fluid.addComponent("methane", 0.70);
fluid.addComponent("ethane", 0.10);
fluid.addComponent("propane", 0.05);
fluid.addComponent("water", 0.10);
fluid.addComponent("MEG", 0.05);
fluid.setMixingRule(10); // Temperature and composition dependent CPA
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
System.out.println("Number of phases: " + fluid.getNumberOfPhases());
fluid.prettyPrint();
}
}
import neqsim.thermo.system.SystemGERG2008Eos;
import neqsim.thermo.system.SystemInterface;
import neqsim.thermodynamicoperations.ThermodynamicOperations;
public class FiscalMeteringExample {
public static void main(String[] args) {
// GERG-2008 for custody transfer accuracy
SystemInterface gas = new SystemGERG2008Eos(288.15, 40.0);
gas.addComponent("methane", 0.92);
gas.addComponent("ethane", 0.04);
gas.addComponent("propane", 0.02);
gas.addComponent("nitrogen", 0.01);
gas.addComponent("CO2", 0.01);
gas.createDatabase(true);
ThermodynamicOperations ops = new ThermodynamicOperations(gas);
ops.TPflash();
// GERG-specific high-accuracy density
double gergDensity = gas.getPhase(0).getDensity_GERG2008();
System.out.println("GERG-2008 Density: " + gergDensity + " kg/m3");
}
}
import neqsim.thermo.system.SystemPrEos;
import neqsim.thermo.system.SystemInterface;
import neqsim.thermodynamicoperations.ThermodynamicOperations;
public class OilCharacterizationExample {
public static void main(String[] args) {
SystemInterface oil = new SystemPrEos(350.0, 150.0);
oil.createDatabase(true);
// Light ends
oil.addComponent("nitrogen", 0.005);
oil.addComponent("CO2", 0.02);
oil.addComponent("methane", 0.35);
oil.addComponent("ethane", 0.08);
oil.addComponent("propane", 0.06);
oil.addComponent("i-butane", 0.02);
oil.addComponent("n-butane", 0.03);
oil.addComponent("i-pentane", 0.02);
oil.addComponent("n-pentane", 0.02);
oil.addComponent("n-hexane", 0.03);
// TBP fractions (moles, MW g/mol, density g/cm3)
oil.addTBPfraction("C7", 0.05, 96.0, 0.738);
oil.addTBPfraction("C8", 0.04, 107.0, 0.765);
oil.addTBPfraction("C9", 0.03, 121.0, 0.781);
oil.addTBPfraction("C10", 0.02, 134.0, 0.792);
// Plus fraction
oil.addPlusFraction("C11+", 0.18, 250.0, 0.85);
oil.setMixingRule("classic");
ThermodynamicOperations ops = new ThermodynamicOperations(oil);
ops.TPflash();
oil.initProperties();
oil.prettyPrint();
}
}
| System Type | Recommended Model | Mixing Rule |
|---|---|---|
| Dry natural gas | SystemSrkEos or SystemPrEos |
classic (2) |
| Wet gas / condensate | SystemPrEos |
classic (2) |
| Black oil | SystemPrEos with TBP |
classic (2) |
| Water-hydrocarbon | SystemSrkCPAstatoil |
CLASSIC_TX_CPA (10) |
| Glycol dehydration | SystemSrkCPAstatoil |
CPA_MIX (7) |
| Sour gas / brine | SystemSoreideWhitson |
SOREIDE_WHITSON (11) |
| Fiscal metering | SystemGERG2008Eos |
N/A |
| CCS / CO2 transport | SystemEOSCGEos |
N/A |
| Electrolyte solutions | SystemElectrolyteCPAstatoil |
N/A |
| Polar organics | SystemUNIFAC or SystemNRTL |
N/A |
| Model Type | Speed | Accuracy | Best For |
|---|---|---|---|
| Cubic (SRK/PR) | Fast | Good | General process simulation |
| CPA | Medium | Very Good | Polar/associating systems |
| GERG-2008 | Slow | Excellent | Fiscal metering, calibration |
| UNIFAC | Medium | Good | Chemical process design |
This guide provides detailed documentation of the COMP database, which stores pure component parameters used by NeqSim's thermodynamic models. Understanding these parameters is essential for model selection, debugging, and extending NeqSim with new components.
The COMP table is the primary pure component property database in NeqSim. It contains over 150 parameters per component, organized into functional groups that support different thermodynamic models and property calculations.
Key characteristics:
| Item | Value |
|---|---|
| File Path | src/main/resources/data/COMP.csv |
| Runtime Path | data/COMP.csv (in JAR) |
| Format | CSV with header row |
| Encoding | UTF-8 |
| Primary Key | ID (integer) |
| Lookup Key | NAME (string, case-sensitive) |
| Column | Description | Unit | Model Usage |
|---|---|---|---|
ID |
Unique component identifier | - | Internal indexing |
NAME |
Component name for lookup | - | addComponent("methane", ...) |
CASnumber |
CAS Registry Number | - | Component identification |
COMPTYPE |
Component type classification | - | Model selection (see Component Types) |
COMPINDEX |
Component index in database | - | Internal ordering |
FORMULA |
Chemical formula | - | Element calculations |
MOLARMASS |
Molar mass | g/mol | All models (stored internally as kg/mol) |
These parameters are fundamental to all cubic equations of state (SRK, PR, etc.).
| Column | Description | Unit | Model Usage |
|---|---|---|---|
TC |
Critical temperature | °C | Converted to K internally: $T_c = T_{C,db} + 273.15$ |
PC |
Critical pressure | bara | SRK, PR, CPA EoS parameter a and b |
ACSFACT |
Acentric factor (ω) | - | Alpha function: $m = f(\omega)$ |
CRITVOL |
Critical molar volume | cm³/mol | Critical compressibility: $Z_c = \frac{P_c V_c}{R T_c}$ |
NORMBOIL |
Normal boiling point | °C | Stored as K internally |
Model linkage:
Parameters for Antoine-type vapor pressure correlations.
| Column | Description | Unit | Model Usage |
|---|---|---|---|
AntoineVapPresLiqType |
Equation type | - | pow10, log, exp, loglog |
ANTOINEA |
Antoine A coefficient | - | Vapor pressure calculation |
ANTOINEB |
Antoine B coefficient | - | Vapor pressure calculation |
ANTOINEC |
Antoine C coefficient | - | Vapor pressure calculation |
ANTOINED |
Antoine D coefficient | - | Extended Antoine |
ANTOINEE |
Antoine E coefficient | - | Extended Antoine |
ANTOINESolidA |
Solid vapor pressure A | - | Sublimation pressure |
ANTOINESolidB |
Solid vapor pressure B | - | Sublimation pressure |
ANTOINESolidC |
Solid vapor pressure C | - | Sublimation pressure |
Antoine equation forms:
pow10: $\log_{10}(P_{sat}) = A - \frac{B}{T + C}$ (P in mmHg, T in °C)log: $\ln(P_{sat}) = A + \frac{B}{T} + C \ln(T) + D T^E$exp: $P_{sat} = \exp(A - \frac{B}{T + C})$Polynomial coefficients for ideal gas heat capacity: $C_p^{ig} = A + BT + CT^2 + DT^3 + ET^4$
| Column | Description | Unit |
|---|---|---|
CPA |
Coefficient A | J/(mol·K) |
CPB |
Coefficient B | J/(mol·K²) |
CPC |
Coefficient C | J/(mol·K³) |
CPD |
Coefficient D | J/(mol·K⁴) |
CPE |
Coefficient E | J/(mol·K⁵) |
CPsolid1-5 |
Solid phase Cp coefficients | J/(mol·K) |
CPliquid1-5 |
Liquid phase Cp coefficients | J/(mol·K) |
Usage: Enthalpy, entropy, and Gibbs energy departure functions for all EoS models.
| Column | Description | Unit | Model Usage |
|---|---|---|---|
LIQDENS |
Liquid density at standard conditions | g/cm³ | Density correlations |
RACKETZ |
Rackett compressibility factor | - | Rackett liquid density: $V = V_c Z_{RA}^{[1+(1-T_r)^{2/7}]}$ |
racketZCPA |
Rackett Z for CPA model | - | CPA volume correction |
volcorrSRK_T |
SRK volume translation | - | Péneloux correction: $V_{corr} = V_{EoS} - c$ |
volcorrCPA_T |
CPA volume translation | - | CPA Péneloux correction |
STDDENS |
Standard density | g/cm³ | Reference conditions |
LIQUIDDENSITYCOEFS1-5 |
Liquid density correlation coefficients | - | Temperature-dependent density |
| Column | Description | Unit | Model Usage |
|---|---|---|---|
DIPOLEMOMENT |
Dipole moment | Debye | Polar corrections |
VISCFACT |
Viscosity correction factor | - | Corresponding states |
LIQVISCMODEL |
Liquid viscosity model type | - | Model selection (1-4) |
LIQVISC1-4 |
Liquid viscosity parameters | - | Andrade equation: $\ln(\eta) = A + B/T + C\ln(T) + DT$ |
LIQUIDCONDUCTIVITY1-3 |
Liquid thermal conductivity | - | $k = A + BT + CT^2$ |
PARACHOR |
Parachor | - | Surface tension: $\sigma^{1/4} = P[\rho_L - \rho_V]$ |
PARACHOR_CPA |
Parachor for CPA model | - | CPA surface tension |
criticalViscosity |
Critical viscosity | Pa·s | Transport correlations |
| Column | Description | Unit | Model Usage |
|---|---|---|---|
PVMODEL |
PV model type | - | Classic for standard EoS |
MC1, MC2, MC3 |
Mathias-Copeman parameters (SRK) | - | Enhanced alpha function |
MCPR1, MCPR2, MCPR3 |
Mathias-Copeman parameters (PR) | - | PR alpha function |
TwuCoon1-3 |
Twu-Coon alpha function parameters | - | Twu-Coon attractive term |
SCHWARTZENTRUBER1-3 |
Schwartzentruber parameters | - | Schwartzentruber EoS |
MC1Solid-MC3Solid |
Solid phase Mathias-Copeman | - | Solid fugacity |
Mathias-Copeman alpha function: $$\alpha = [1 + c_1(1-\sqrt{T_r}) + c_2(1-\sqrt{T_r})^2 + c_3(1-\sqrt{T_r})^3]^2$$
| Column | Description | Unit | Model Usage |
|---|---|---|---|
LJDIAMETER |
LJ molecular diameter | Å | Gas viscosity, diffusion |
LJEPS |
LJ energy parameter | K | ε/k_B |
SphericalCoreRadius |
Hard-core radius | - | LJ potential |
LJDIAMETERHYDRATE |
LJ diameter for hydrates | Å | Hydrate equilibrium |
LJEPSHYDRATE |
LJ energy for hydrates | K | Hydrate cage interaction |
Parameters for Cubic-Plus-Association (CPA) and PC-SAFT models.
| Column | Description | Unit | Model Usage |
|---|---|---|---|
associationsites |
Number of association sites | - | 0, 1, 2, 3, or 4 |
associationscheme |
Association scheme | - | 0, 1A, 2A, 2B, 3B, 4C |
associationenergy |
Association energy (ε^AB) | J/mol | CPA association term |
associationboundingvolume_SRK |
Association volume (β) for SRK-CPA | - | SRK-CPA |
associationboundingvolume_PR |
Association volume (β) for PR-CPA | - | PR-CPA |
aCPA_SRK |
CPA a parameter (SRK base) | Pa·m⁶/mol² | SRK-CPA |
bCPA_SRK |
CPA b parameter (SRK base) | m³/mol | SRK-CPA |
mCPA_SRK |
CPA m parameter (SRK base) | - | SRK-CPA |
aCPA_PR |
CPA a parameter (PR base) | Pa·m⁶/mol² | PR-CPA |
bCPA_PR |
CPA b parameter (PR base) | m³/mol | PR-CPA |
mCPA_PR |
CPA m parameter (PR base) | - | PR-CPA |
| Column | Description | Unit | Model Usage |
|---|---|---|---|
mSAFT |
Number of segments | - | Chain length |
sigmaSAFT |
Segment diameter | Å | Hard-sphere term |
epsikSAFT |
Segment energy | K | ε/k_B |
associationboundingvolume_PCSAFT |
Association volume | - | PC-SAFT association |
associationenergy_PCSAFT |
Association energy | K | PC-SAFT association |
Association schemes:
| Scheme | Sites | Example Molecules |
|---|---|---|
0 |
0 | Non-associating (hydrocarbons) |
1A |
1 | HCl, aromatic compounds |
2A |
2 | CO₂ (electron donor/acceptor) |
2B |
2 | Alcohols (1 proton donor, 1 acceptor) |
3B |
3 | Amines |
4C |
4 | Water, glycols (2 donors, 2 acceptors) |
Parameters for gas hydrate equilibrium calculations.
| Column | Description | Unit | Model Usage |
|---|---|---|---|
HydrateFormer |
Hydrate-forming capability | - | yes or no |
HydrateA1Small, HydrateB1Small |
Type I small cage (512) | - | Langmuir constants |
HydrateA1Large, HydrateB1Large |
Type I large cage (51262) | - | Langmuir constants |
HydrateA2Small, HydrateB2Small |
Type II small cage (512) | - | Langmuir constants |
HydrateA2Large, HydrateB2Large |
Type II large cage (51264) | - | Langmuir constants |
A1_smallGF-B2_largeGF |
Graffis parameters | - | Alternative parameterization |
SphericalCoreRadiusHYDRATE |
Core radius for hydrates | - | Cavity occupation |
| Column | Description | Unit | Model Usage |
|---|---|---|---|
Href |
Reference enthalpy | J/mol | Enthalpy calculations |
GIBBSENERGYOFFORMATION |
Gibbs energy of formation | J/mol | Chemical equilibrium |
ENTHALPYOFFORMATION |
Standard enthalpy of formation | J/mol | Reaction thermodynamics |
ABSOLUTEENTROPY |
Absolute entropy | J/(mol·K) | Entropy calculations |
HEATOFFUSION |
Heat of fusion | J/mol | Solid-liquid equilibrium |
Hsub |
Heat of sublimation | J/mol | Solid-vapor equilibrium |
TRIPLEPOINTTEMPERATURE |
Triple point temperature | K | Phase boundaries |
TRIPLEPOINTPRESSURE |
Triple point pressure | bar | Phase boundaries |
TRIPLEPOINTDENSITY |
Triple point density | kg/m³ | Reference state |
MELTINGPOINTTEMPERATURE |
Melting point | K | Solid calculations |
| Column | Description | Unit | Model Usage |
|---|---|---|---|
IONICCHARGE |
Ionic charge | - | Electrolyte models |
REFERENCESTATETYPE |
Reference state | - | solvent or solute |
DIELECTRICPARAMETER1-5 |
Dielectric parameters | - | Electrolyte activity |
DeshMatIonicDiameter |
Debye-Hückel diameter | Å | Electrolyte models |
calcActivity |
Activity calculation flag | - | 0 or 1 |
| Column | Description | Unit |
|---|---|---|
HenryCoef1 |
Henry constant A | - |
HenryCoef2 |
Henry constant B | - |
HenryCoef3 |
Henry constant C | - |
HenryCoef4 |
Henry constant D | - |
Henry's law correlation: $$\ln(H) = A + \frac{B}{T} + C\ln(T) + DT$$
| Column | Description | Unit |
|---|---|---|
SOLIDDENSITYCOEFS1-5 |
Solid density coefficients | - |
HEATOFVAPORIZATIONCOEFS1-5 |
Heat of vaporization coefficients | - |
waxformer |
Wax-forming component | - |
Complete parameter list with units and typical values:
| Parameter | Unit | Example (methane) | Example (water) |
|---|---|---|---|
MOLARMASS |
g/mol | 16.043 | 18.015 |
TC |
°C | -82.59 | 374.15 |
PC |
bara | 45.99 | 220.89 |
ACSFACT |
- | 0.0115 | 0.344 |
CRITVOL |
cm³/mol | 99.0 | 56.0 |
NORMBOIL |
°C | -161.55 | 100.0 |
LIQDENS |
g/cm³ | 0.422 | 0.999 |
RACKETZ |
- | 0.0 | 0.0 |
DIPOLEMOMENT |
Debye | 0.0 | 1.8 |
associationsites |
- | 0 | 4 |
HydrateFormer |
- | yes | no |
┌─────────────────────────────────────────────────────────────────┐
│ COMP Database │
├─────────────────────────────────────────────────────────────────┤
│ TC, PC, ACSFACT ──────────────> Cubic EoS (SRK, PR) │
│ MC1, MC2, MC3 ──────────────> Mathias-Copeman α(T) │
│ TwuCoon1-3 ──────────────> Twu-Coon α(T) │
│ aCPA, bCPA, mCPA ──────────────> CPA EoS │
│ associationsites ──────────────> CPA/SAFT Association │
│ mSAFT, σSAFT, εSAFT ──────────> PC-SAFT │
│ UNIFAC groups ──────────────> Activity models │
│ LJDIAMETER, LJEPS ─────────────> Transport properties │
│ HydrateA/B params ─────────────> Hydrate equilibrium │
│ CPA, CPB, CPC... ──────────────> Enthalpy/Entropy │
│ ANTOINEA-E ──────────────> Vapor pressure │
└─────────────────────────────────────────────────────────────────┘
| Model Class | Key Parameters |
|---|---|
SystemSrkEos |
TC, PC, ACSFACT, MC1-3, RACKETZ |
SystemPrEos |
TC, PC, ACSFACT, MCPR1-3 |
SystemSrkCPA |
aCPA_SRK, bCPA_SRK, mCPA_SRK, associationsites, associationenergy, associationboundingvolume_SRK |
SystemPrCPA |
aCPA_PR, bCPA_PR, mCPA_PR, associationboundingvolume_PR |
SystemPCSAFT |
mSAFT, sigmaSAFT, epsikSAFT, associationboundingvolume_PCSAFT |
SystemGERG2008Eos |
Uses internal GERG parameters, but TC/PC for initialization |
SystemUNIFAC |
TC, PC (for vapor), UNIFAC groups from UNIFACcomp table |
The COMPTYPE field classifies components for model selection:
| Type | Description | Examples |
|---|---|---|
HC |
Hydrocarbon | methane, ethane, propane, benzene |
inert |
Inert gas | nitrogen, CO2, oxygen, argon |
ion |
Ionic species | Na+, Cl-, HCO3-, Ca++ |
amine |
Amine compounds | MDEA, MEA, DEA |
alcohol |
Alcohols | methanol, ethanol |
glycol |
Glycol compounds | MEG, DEG, TEG |
ice |
Ice/solid water | ice |
TBP |
TBP pseudo-component | Generated from characterization |
plus |
Plus fraction | C7+, C10+, etc. |
// Create a system and access component properties
SystemInterface fluid = new SystemSrkEos(298.15, 10.0);
fluid.addComponent("methane", 1.0);
fluid.init(0);
// Access pure component parameters
ComponentInterface comp = fluid.getPhase(0).getComponent("methane");
double Tc = comp.getTC(); // Critical temperature [K]
double Pc = comp.getPC(); // Critical pressure [bara]
double omega = comp.getAcentricFactor(); // Acentric factor [-]
double Mw = comp.getMolarMass(); // Molar mass [kg/mol]
double Tb = comp.getNormalBoilingPoint(); // Normal boiling point [K]
System.out.println("Methane Tc = " + Tc + " K");
System.out.println("Methane Pc = " + Pc + " bara");
System.out.println("Methane ω = " + omega);
// Modify properties for sensitivity analysis
comp.setTC(190.6); // Set new Tc in Kelvin
comp.setPC(46.0); // Set new Pc in bara
comp.setAcentricFactor(0.012);
// Re-initialize to apply changes
fluid.init(0);
Add a new row to COMP.csv with all required parameters.
// Add a pseudo-component with custom properties
SystemInterface fluid = new SystemSrkEos(298.15, 50.0);
// Add TBP fraction with molar mass and density
fluid.addTBPfraction("C7_custom", 0.1, 95.0, 0.72); // name, moles, MW, SG
// Or add component and modify properties
fluid.addComponent("n-heptane", 1.0);
ComponentInterface comp = fluid.getPhase(0).getComponent("n-heptane");
comp.setTC(540.0);
comp.setPC(27.4);
comp.setAcentricFactor(0.35);
// Initialize database for binary parameters
fluid.createDatabase(true);
fluid.setMixingRule(2);
// Enable temporary tables for session-specific components
NeqSimDataBase.setCreateTemporaryTables(true);
// Components added to "comptemp" table
fluid.getPhase(0).getComponent(0).insertComponentIntoDatabase("comptemp");
// Remember to disable after use
NeqSimDataBase.setCreateTemporaryTables(false);
The COMP table works with several related tables:
| Table | Purpose | Key Columns |
|---|---|---|
INTER |
Binary interaction parameters (kij) | comp1, comp2, kij, model |
UNIFACcomp |
UNIFAC group assignments | compname, group, count |
UNIFACGroupParam |
UNIFAC group parameters | groupid, R, Q |
UNIFACInterParam* |
UNIFAC group interaction parameters | group1, group2, aij |
MBWR32param |
MBWR equation parameters | comp, coefficients |
AdsorptionParameters |
Adsorption isotherm parameters | comp, adsorbent, params |
Documentation for component modeling in NeqSim.
Location: neqsim.thermo.component
The component package contains 65+ classes for modeling pure component properties and their behavior in mixtures.
// By name
ComponentInterface methane = fluid.getComponent("methane");
// By index
ComponentInterface comp = fluid.getComponent(0);
// In specific phase
ComponentInterface methaneInGas = fluid.getGasPhase().getComponent("methane");
ComponentInterface comp = fluid.getComponent("methane");
// Pure component properties
double Tc = comp.getTC(); // Critical temperature (K)
double Pc = comp.getPC(); // Critical pressure (bar)
double omega = comp.getAcentricFactor();
double MW = comp.getMolarMass(); // kg/mol
// Composition
double z = comp.getz(); // Overall mole fraction
double x = comp.getx(); // Phase mole fraction
double n = comp.getNumberOfMolesInPhase();
// Fugacity
double f = comp.getFugacity();
double phi = comp.getFugacityCoefficient();
ComponentInterface comp = fluid.getComponent("propane");
double Tc = comp.getTC(); // 369.83 K
double Pc = comp.getPC(); // 42.48 bar
double Vc = comp.getVc(); // Critical volume (m³/mol)
double Zc = comp.getZc(); // Critical Z-factor
double omega = comp.getAcentricFactor(); // 0.1523
// Molecular properties
double MW = comp.getMolarMass(); // kg/mol
double Tb = comp.getNormalBoilingPoint(); // K
double Tf = comp.getTriplePointTemperature(); // K
// Reference properties
double dHf = comp.getEnthalpyOfFormation(); // kJ/mol
double dGf = comp.getGibbsEnergyOfFormation();
double Href = comp.getReferencePotential();
// SRK/PR parameters
double a = comp.geta(); // Attraction parameter
double b = comp.getb(); // Co-volume parameter
// CPA parameters (for associating)
double eps = comp.getAssociationEnergy();
double beta = comp.getAssociationVolume();
// PC-SAFT parameters
double m = comp.getmSAFTi(); // Segment number
double sigma = comp.getSigmaSAFTi(); // Segment diameter
double epsilon = comp.getEpsSAFTi(); // Dispersion energy
PhaseInterface gas = fluid.getGasPhase();
ComponentInterface methane = gas.getComponent("methane");
// Mole fraction in phase
double x_i = methane.getx();
// Fugacity coefficient
double phi_i = methane.getFugacityCoefficient();
double lnPhi = methane.getLogFugacityCoefficient();
// Fugacity
double f_i = methane.getFugacity();
// Chemical potential
double mu_i = methane.getChemicalPotential();
// Activity (for liquid phases)
double a_i = methane.getActivity();
double gamma = methane.getActivityCoefficient();
// Partial molar volume
double Vbar_i = comp.getPartialMolarVolume();
// Partial molar enthalpy
double Hbar_i = comp.getPartialMolarEnthalpy();
// Partial molar entropy
double Sbar_i = comp.getPartialMolarEntropy();
// Fugacity coefficient derivatives
double dPhidT = comp.getdfugdT(); // d(ln φ)/dT
double dPhidP = comp.getdfugdP(); // d(ln φ)/dP
double dPhidx = comp.getdfugdx(j); // d(ln φ_i)/dx_j
// Standard EoS component
ComponentEos compEos = (ComponentEos) fluid.getComponent("methane");
// Access EoS-specific properties
double ai = compEos.getaT(); // Temperature-dependent a
double bi = compEos.getb();
double alphai = compEos.getAlpha();
// For associating components
ComponentCPA compCPA = (ComponentCPA) fluid.getComponent("water");
// Association properties
double eps = compCPA.getAssociationEnergy();
double beta = compCPA.getAssociationVolume();
int sites = compCPA.getNumberOfAssociationSites();
// Association fraction
double X_A = compCPA.getXsite(0); // Fraction of site A unbonded
ComponentPCSAFT compSAFT = (ComponentPCSAFT) fluid.getComponent("n-hexane");
double m = compSAFT.getmSAFTi(); // Chain length
double sigma = compSAFT.getSigmaSAFTi(); // Segment diameter (Å)
double eps = compSAFT.getEpsSAFTi(); // Dispersion energy (K)
// Ions
ComponentElectrolyte ion = (ComponentElectrolyte) fluid.getComponent("Na+");
double charge = ion.getIonicCharge();
double diameter = ion.getIonicDiameter();
// Plus fraction components
ComponentTBP plus = (ComponentTBP) fluid.getComponent("C7+");
double Tb = plus.getNormalBoilingPoint();
double SG = plus.getSpecificGravity();
double MW = plus.getMolarMass();
NeqSim includes a database with 100+ components:
| Category | Examples |
|---|---|
| Light gases | nitrogen, oxygen, CO2, H2S, hydrogen |
| Hydrocarbons | methane through n-C20 |
| Cyclic | cyclohexane, benzene, toluene |
| Polar | water, methanol, ethanol, MEG, DEG, TEG |
| Amines | MDEA, MEA, DEA, piperazine |
| Refrigerants | R-134a, R-32, ammonia |
// Add from database
fluid.addComponent("methane", 1.0);
// Add with alias
fluid.addComponent("CO2", 0.5); // Carbon dioxide
// Add pseudo-component (TBP method)
fluid.addTBPfraction("C10", 0.1, 140.0, 0.75); // name, moles, MW, SG
// Add plus fraction
fluid.addPlusFraction("C7+", 0.05, 150.0, 0.78);
// Check if component exists
boolean exists = fluid.hasComponent("methane");
// Get component index
int index = fluid.getComponentIndex("ethane");
import neqsim.thermo.system.SystemSrkEos;
SystemSrkEos fluid = new SystemSrkEos(298.15, 50.0);
fluid.addComponent("methane", 0.85);
fluid.addComponent("ethane", 0.10);
fluid.addComponent("propane", 0.05);
fluid.setMixingRule("classic");
fluid.init(0);
System.out.println("Component Properties:");
System.out.println("-------------------------------------------");
System.out.printf("%-10s %8s %8s %8s %8s%n",
"Name", "Tc(K)", "Pc(bar)", "omega", "MW");
System.out.println("-------------------------------------------");
for (int i = 0; i < fluid.getNumberOfComponents(); i++) {
ComponentInterface comp = fluid.getComponent(i);
System.out.printf("%-10s %8.2f %8.2f %8.4f %8.4f%n",
comp.getName(),
comp.getTC(),
comp.getPC(),
comp.getAcentricFactor(),
comp.getMolarMass() * 1000); // g/mol
}
NeqSim bundles several thermodynamic and transport models so you can switch between correlations without rewriting system setup code. The sections below summarize the most commonly used options and when to consider them.
NeqSim primarily uses cubic equations of state of the general form:
[ P = \frac{RT}{v - b} - \frac{a(T)}{(v + \epsilon b)(v + \sigma b)} ]
where $P$ is pressure, $T$ is temperature, $v$ is molar volume, $R$ is the gas constant, and $a(T), b$ are the energy and co-volume parameters.
Peng–Robinson (PR) family ($\epsilon = 1 - \sqrt{2}, \sigma = 1 + \sqrt{2}$):
Standard PR (SystemPrEos), volume-corrected variants (Peneloux), and tuned versions such as the Søreide–Whitson model for sour service. Suitable for general gas/condensate and light-oil systems.
Soave–Redlich–Kwong (SRK) family ($\epsilon = 0, \sigma = 1$):
Core SRK (SystemSrkEos), SRK-Twu, and CPA-SRK for associating fluids such as water and glycols.
Cubic-Plus-Association (CPA): Adds an association term to the SRK or PR equation to represent hydrogen bonding: [ P = P_{\text{cubic}} - \frac{1}{2} RT \rho \sum_i x_i \sum_{A_i} \left( 1 - X_{A_i} \right) \frac{\partial \ln g}{\partial v} ] where $X_{A_i}$ is the fraction of site A on molecule i not bonded to other active sites.
Activity-coefficient hybrids: Huron–Vidal and Wong–Sandler mixing rules combine cubic EoS with excess-Gibbs models ($G^E$) for improved liquid-phase behavior.
SystemInterface fluid = new SystemSrkEos(298.15, 50.0);
fluid.addComponent("methane", 0.9);
fluid.addComponent("n-heptane", 0.1);
fluid.setMixingRule(2); // 2 = Huron–Vidal; use 1 for classical van der Waals
Use SystemSrkCPAstatoil or SystemSrkCPAs when hydrogen bonding is important, and prefer PR variants for high-pressure gas processing.
For high-accuracy applications involving natural gas or CCS mixtures, NeqSim supports multi-parameter equations of state explicit in the Helmholtz free energy:
SystemGERG2008Eos): The ISO 20765-2 standard for natural gas.SystemEOSCGEos): An extension of GERG-2008 for combustion gases and CCS mixtures (including impurities like SO2, NO, NO2).See the GERG-2008 and EOS-CG guide for details.
NeqSim provides NRTL/UNIQUAC/UNIFAC variants for non-ideal liquid mixtures. They can be used directly for gamma-phi flashes or combined with cubic EoS via Wong–Sandler mixing rules.
SystemInterface fluid = new SystemFurstElectrolyteEos(298.15, 1.0);
fluid.addComponent("water", 1.0);
fluid.addComponent("ethanol", 1.0);
fluid.setMixingRule(4); // enables Wong–Sandler (NRTL) coupling
Load binary-interaction parameters via mixingRuleName or by reading custom datasets to align with lab data.
For hydrate prediction, enable hydrateCheck(true) and select a hydrate model (CPA-based or classical van der Waals–Platteeuw) depending on accuracy and speed requirements. Wax precipitation can be modeled using solid-phase enabled systems (e.g., PR with solid checks) and tuned heavy-end characterizations.
Choose property packages that match the flow regime: cubic EoS with corresponding-state transport for gas processing, CPA with association corrections for aqueous systems, and heavy-oil tuned correlations for late-life reservoirs.
This document provides a comprehensive overview of the thermodynamic models available in NeqSim, their theoretical foundations, and practical guidance on when and how to use each model. Models are classified into categories based on their mathematical formulation and application domain.
NeqSim (Non-Equilibrium Simulator) provides a rich library of thermodynamic models for simulating phase equilibria, physical properties, and process operations. Choosing the appropriate thermodynamic model is crucial for obtaining accurate results in process simulation.
import neqsim.thermo.system.SystemSrkEos;
import neqsim.thermo.system.SystemInterface;
// 1. Choose and instantiate the thermodynamic model
SystemInterface fluid = new SystemSrkEos(298.15, 10.0); // T [K], P [bara]
// 2. Add components
fluid.addComponent("methane", 0.90);
fluid.addComponent("ethane", 0.10);
// 3. Set an appropriate mixing rule
fluid.setMixingRule("classic");
// 4. Initialize and perform calculations
fluid.init(0);
| Category | Mathematical Basis | Key Use Cases | Examples |
|---|---|---|---|
| Cubic EoS | $P = \frac{RT}{v-b} - \frac{a(T)}{(v+\epsilon b)(v+\sigma b)}$ | General hydrocarbons, gas processing | SRK, PR |
| CPA | Cubic EoS + Association term | Polar/associating fluids (water, glycols, alcohols) | SRK-CPA, PR-CPA |
| Reference EoS | Helmholtz free energy explicit | High-accuracy metering, CCS | GERG-2008, EOS-CG |
| Activity Coefficient | Excess Gibbs energy ($G^E$) | Non-ideal liquid mixtures | UNIFAC, NRTL |
| Electrolyte | CPA + Electrostatic contributions | Aqueous salt solutions | Electrolyte-CPA, Pitzer |
| Specialized | Modified equations | Specific applications | Søreide-Whitson |
Cubic equations of state express pressure as a function of temperature and molar volume in a cubic polynomial form:
$$ P = \frac{RT}{v - b} - \frac{a(T)}{(v + \epsilon b)(v + \sigma b)} $$
Where:
The energy parameter typically uses an alpha function:
$$ a(T) = a_c \cdot \alpha(T_r, \omega) $$
Where $T_r = T/T_c$ is the reduced temperature and $\omega$ is the acentric factor.
For SRK: $\epsilon = 0$, $\sigma = 1$
$$ P = \frac{RT}{v - b} - \frac{a(T)}{v(v + b)} $$
| Class | Description | Best For |
|---|---|---|
SystemSrkEos |
Standard SRK | General gas/light hydrocarbon |
SystemSrkPenelouxEos |
SRK with Peneloux volume correction | Improved liquid density |
SystemSrkMathiasCopeman |
SRK with Mathias-Copeman alpha function | Better vapor pressure |
SystemSrkTwuCoonEos |
SRK with Twu-Coon alpha function | Polar components |
SystemSrkSchwartzentruberEos |
SRK-Schwartzentruber | Polar/associated fluids |
Example: Standard SRK
SystemInterface fluid = new SystemSrkEos(300.0, 50.0);
fluid.addComponent("methane", 0.85);
fluid.addComponent("CO2", 0.10);
fluid.addComponent("nitrogen", 0.05);
fluid.setMixingRule("classic");
For PR: $\epsilon = 1 - \sqrt{2}$, $\sigma = 1 + \sqrt{2}$
$$ P = \frac{RT}{v - b} - \frac{a(T)}{v(v + b) + b(v - b)} $$
| Class | Description | Best For |
|---|---|---|
SystemPrEos |
Standard PR | General oil & gas |
SystemPrEos1978 |
Original 1978 formulation | Classic applications |
SystemPrMathiasCopeman |
PR with Mathias-Copeman alpha | Polar components |
SystemPrDanesh |
Modified PR | Heavy oil systems |
SystemPrEosvolcor |
PR with volume correction | Liquid density |
SystemPrEosDelft1998 |
Delft 1998 modification | Gas condensates |
Example: Peng-Robinson
SystemInterface fluid = new SystemPrEos(350.0, 150.0);
fluid.addComponent("methane", 0.70);
fluid.addComponent("n-heptane", 0.20);
fluid.addComponent("n-decane", 0.10);
fluid.setMixingRule("classic");
| Class | Description |
|---|---|
SystemRKEos |
Original Redlich-Kwong (historical) |
SystemTSTEos |
Twu-Sim-Tassone equation |
SystemBWRSEos |
Benedict-Webb-Rubin-Starling (extended virial) |
CPA (Cubic Plus Association) extends cubic EoS to handle hydrogen bonding in polar molecules like water, alcohols, and glycols. The pressure is expressed as:
$$ P = P_{\text{cubic}} + P_{\text{association}} $$
The association term accounts for hydrogen bonding:
$$ P_{\text{association}} = -\frac{1}{2} RT \rho \sum_i x_i \sum_{A_i} \left( 1 - X_{A_i} \right) \frac{\partial \ln g}{\partial v} $$
Where:
The non-bonded site fraction $X_{A_i}$ is determined by solving:
$$ X_{A_i} = \frac{1}{1 + \rho \sum_j x_j \sum_{B_j} X_{B_j} \Delta^{A_i B_j}} $$
Where $\Delta^{A_i B_j}$ is the association strength between site A on molecule $i$ and site B on molecule $j$.
| Class | Description | Mixing Rule |
|---|---|---|
SystemSrkCPAstatoil |
Recommended Equinor SRK-CPA implementation | 10 |
SystemSrkCPA |
Standard SRK-CPA | 7 |
SystemSrkCPAs |
Alternative SRK-CPA | 7 |
SystemPrCPA |
Peng-Robinson with CPA | 7 |
SystemUMRCPAEoS |
UMR-CPA with UNIFAC | - |
| Scheme | Sites | Examples |
|---|---|---|
| 4C | 2 donors + 2 acceptors | Water, glycols |
| 2B | 1 donor + 1 acceptor | Alcohols |
| CR-1 | Cross-association | Water-MEG, Water-methanol |
import neqsim.thermo.system.SystemSrkCPAstatoil;
SystemInterface fluid = new SystemSrkCPAstatoil(323.15, 50.0);
fluid.addComponent("methane", 0.70);
fluid.addComponent("ethane", 0.10);
fluid.addComponent("propane", 0.05);
fluid.addComponent("water", 0.10);
fluid.addComponent("MEG", 0.05); // Mono-ethylene glycol
// Use mixing rule 10 (recommended for CPA)
fluid.setMixingRule(10); // CLASSIC_TX_CPA
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
Reference equations of state are explicit in the dimensionless Helmholtz free energy $\alpha$:
$$ \alpha(\delta, \tau, \bar{x}) = \frac{a(\rho, T, \bar{x})}{RT} = \alpha^0(\delta, \tau, \bar{x}) + \alpha^r(\delta, \tau, \bar{x}) $$
Where:
The residual contribution is fitted to high-accuracy experimental data:
$$ \alpha^r(\delta, \tau, \bar{x}) = \sum_{i=1}^{N} x_i \alpha_{0i}^r(\delta, \tau) + \sum_{i=1}^{N-1} \sum_{j=i+1}^{N} x_i x_j F_{ij} \alpha_{ij}^r(\delta, \tau) $$
These models provide superior accuracy for density, speed of sound, and heat capacity compared to cubic EoS.
Standard: ISO 20765-2
Application: Natural gas custody transfer, fiscal metering
Accuracy: ±0.1% in density for typical natural gas
Supported Components (21): Methane, Nitrogen, CO2, Ethane, Propane, n-Butane, i-Butane, n-Pentane, i-Pentane, n-Hexane, n-Heptane, n-Octane, n-Nonane, n-Decane, Hydrogen, Oxygen, CO, Water, Helium, Argon
import neqsim.thermo.system.SystemGERG2008Eos;
SystemInterface fluid = new SystemGERG2008Eos(288.15, 50.0);
fluid.addComponent("methane", 0.92);
fluid.addComponent("ethane", 0.04);
fluid.addComponent("propane", 0.02);
fluid.addComponent("nitrogen", 0.01);
fluid.addComponent("CO2", 0.01);
fluid.createDatabase(true);
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
// Access GERG-specific high-accuracy density
double gergDensity = fluid.getPhase(0).getDensity_GERG2008();
Extension for hydrogen-rich blends with improved H2 binary parameters:
SystemGERG2008Eos fluid = new SystemGERG2008Eos(300.0, 50.0);
fluid.addComponent("methane", 0.7);
fluid.addComponent("hydrogen", 0.3);
// Enable hydrogen-enhanced model
fluid.useHydrogenEnhancedModel();
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
Application: Carbon Capture and Storage (CCS), combustion gases
Extension: Includes SO2, NO, NO2, HCl, Cl2, COS in addition to GERG-2008 components
import neqsim.thermo.system.SystemEOSCGEos;
SystemInterface fluid = new SystemEOSCGEos(300.0, 100.0);
fluid.addComponent("CO2", 0.95);
fluid.addComponent("nitrogen", 0.03);
fluid.addComponent("SO2", 0.02);
fluid.createDatabase(true);
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
double density = fluid.getPhase(0).getDensity_EOSCG();
| Class | Description | Application |
|---|---|---|
SystemSpanWagnerEos |
Span-Wagner equation | Pure CO2 |
SystemLeachmanEos |
Leachman equation | Pure hydrogen |
SystemVegaEos |
Vega equation | Specialized applications |
SystemAmmoniaEos |
Ammonia-specific | Ammonia systems |
Activity coefficient models describe non-ideal liquid behavior through the excess Gibbs energy:
$$ G^E = RT \sum_i x_i \ln \gamma_i $$
Where $\gamma_i$ is the activity coefficient of component $i$.
Group contribution method that predicts activity coefficients from molecular group interactions:
$$ \ln \gamma_i = \ln \gamma_i^C + \ln \gamma_i^R $$
Where the combinatorial ($C$) and residual ($R$) contributions are calculated from group properties.
import neqsim.thermo.system.SystemUNIFAC;
SystemInterface fluid = new SystemUNIFAC(300.0, 1.0);
fluid.addComponent("methanol", 0.3);
fluid.addComponent("water", 0.7);
Local composition model with binary interaction parameters:
$$ \ln \gamma_i = \frac{\sum_j x_j \tau_{ji} G_{ji}}{\sum_k x_k G_{ki}} + \sum_j \frac{x_j G_{ij}}{\sum_k x_k G_{kj}} \left( \tau_{ij} - \frac{\sum_m x_m \tau_{mj} G_{mj}}{\sum_k x_k G_{kj}} \right) $$
import neqsim.thermo.system.SystemNRTL;
SystemInterface fluid = new SystemNRTL(300.0, 1.0);
fluid.addComponent("ethanol", 0.4);
fluid.addComponent("water", 0.6);
| Class | Description |
|---|---|
SystemGEWilson |
Wilson equation |
SystemUNIFACpsrk |
UNIFAC with PSRK parameters |
SystemUMRPRUEos |
Peng-Robinson with UNIFAC mixing |
SystemUMRPRUMCEos |
UMR-PRU with Mathias-Copeman |
Electrolyte models extend CPA with electrostatic contributions for aqueous salt solutions:
$$ A^{res} = A^{CPA} + A^{elec} $$
The electrostatic contribution typically includes:
$$ A^{elec} = A^{MSA} + A^{Born} + A^{SR} $$
Where:
Born Solvation Term:
$$ \frac{A^{Born}}{RT} = -\frac{e^2 N_A}{8\pi\varepsilon_0 k_B T} \sum_i n_i \frac{z_i^2}{\sigma_i} \left(1 - \frac{1}{\varepsilon_r}\right) $$
Recommended for: Aqueous electrolyte solutions with associating solvents
import neqsim.thermo.system.SystemElectrolyteCPAstatoil;
SystemInterface system = new SystemElectrolyteCPAstatoil(298.15, 1.01325);
system.addComponent("water", 55.5);
system.addComponent("Na+", 1.0);
system.addComponent("Cl-", 1.0);
system.chemicalReactionInit(); // Enable pH and speciation
system.createDatabase(true);
system.setMixingRule(10); // Required for electrolyte CPA
ThermodynamicOperations ops = new ThermodynamicOperations(system);
ops.TPflash();
// Access activity coefficients
double meanGamma = system.getPhase(0).getMeanIonicActivityCoefficient("Na+", "Cl-");
Application: Sour gas systems with brine
Uses salinity-dependent binary interaction parameters for CO2-water, H2S-water, and hydrocarbon-water systems:
$$ k_{ij,\text{CO}_2-\text{water}} = -0.31092(1 + 0.156 S^{0.75}) + 0.236(1 + 0.178 S^{0.98}) T_r - 21.26 e^{-6.72^{T_r} - S} $$
import neqsim.thermo.system.SystemSoreideWhitson;
SystemSoreideWhitson fluid = new SystemSoreideWhitson(350.0, 200.0);
fluid.addComponent("methane", 0.70);
fluid.addComponent("CO2", 0.15);
fluid.addComponent("H2S", 0.05);
fluid.addComponent("water", 0.10);
fluid.addSalinity(2.0, "mole/sec"); // Add NaCl salinity
fluid.setMixingRule(11); // Søreide-Whitson mixing rule
| Class | Description | Use Case |
|---|---|---|
SystemPitzer |
Pitzer model | Concentrated brines |
SystemDesmukhMather |
Desmukh-Mather | Amine systems |
SystemKentEisenberg |
Kent-Eisenberg | CO2/H2S in amines |
SystemDuanSun |
Duan-Sun | CO2 solubility in brine |
SystemFurstElectrolyteEos |
Fürst electrolyte EoS | General electrolytes |
Mixing rules determine how pure-component parameters are combined for mixtures. The choice of mixing rule is as important as the choice of equation of state.
van der Waals One-Fluid Mixing Rule:
$$ a_{mix} = \sum_i \sum_j x_i x_j a_{ij} $$
$$ b_{mix} = \sum_i x_i b_i $$
With combining rule:
$$ a_{ij} = \sqrt{a_i a_j}(1 - k_{ij}) $$
| Type | Name | Description |
|---|---|---|
| 1 | NO |
All $k_{ij} = 0$ (no interactions) |
| 2 | CLASSIC |
Classic with database $k_{ij}$ |
| 8 | CLASSIC_T |
Temperature-dependent: $k_{ij}(T) = k_{ij,0} + k_{ij,T} \cdot T$ |
| 12 | CLASSIC_T2 |
Inverse T-dependency: $k_{ij}(T) = k_{ij,0} + k_{ij,T}/T$ |
Combines cubic EoS with activity coefficient models at infinite pressure:
$$ a_{mix} = b_{mix} \left( \sum_i x_i \frac{a_i}{b_i} - \frac{G^E}{\Lambda} \right) $$
Where $\Lambda \approx 0.693$ for SRK.
| Type | Name | Description |
|---|---|---|
| 3 | CLASSIC_HV |
Huron-Vidal with database NRTL parameters |
| 4 | HV |
HV with temperature-dependent parameters |
Usage with GE model:
fluid.setMixingRule("HV", "UNIFAC_UMRPRU");
fluid.setMixingRule("HV", "NRTL");
Matches both second virial coefficient and excess Gibbs energy:
$$ b_{mix} = \frac{\sum_i \sum_j x_i x_j \left( b - \frac{a}{RT} \right)_{ij}}{1 - \frac{A_\infty^E}{CRT} - \sum_i x_i \frac{a_i}{RTb_i}} $$
| Type | Name | Description |
|---|---|---|
| 5 | WS |
Wong-Sandler (NRTL-based) |
fluid.setMixingRule("WS", "NRTL");
| Type | Name | Description |
|---|---|---|
| 7 | CPA_MIX |
Classic for CPA systems |
| 9 | CLASSIC_T_CPA |
Temperature-dependent CPA |
| 10 | CLASSIC_TX_CPA |
Recommended T and x dependent for CPA |
Mixing Rule 10 Auto-Selection:
Mixing rule 10 automatically selects the appropriate sub-type:
| Condition | Sub-Type | Description |
|---|---|---|
| Symmetric $k_{ij}$, no T-dependency | classic-CPA |
Simple symmetric |
| Symmetric $k_{ij}$, with T-dependency | classic-CPA_T |
Temperature-dependent |
| Asymmetric $k_{ij}$ ($k_{ij} \neq k_{ji}$) | classic-CPA_Tx |
Full asymmetric |
| Type | Name | Description |
|---|---|---|
| 11 | SOREIDE_WHITSON |
Salinity-dependent for sour gas/brine |
// By integer value
fluid.setMixingRule(2);
// By name
fluid.setMixingRule("classic");
fluid.setMixingRule("HV");
fluid.setMixingRule("WS");
// By enum
fluid.setMixingRule(EosMixingRuleType.CLASSIC);
// With GE model (for HV/WS)
fluid.setMixingRule("HV", "UNIFAC_UMRPRU");
fluid.setMixingRule("WS", "NRTL");
EosMixingRulesInterface mixRule = fluid.getPhase(0).getMixingRule();
// Set symmetric kij
mixRule.setBinaryInteractionParameter(0, 1, 0.12);
// Set asymmetric kij (for CPA)
mixRule.setBinaryInteractionParameterij(0, 1, 0.08); // kij
mixRule.setBinaryInteractionParameterji(0, 1, 0.12); // kji
// Set temperature-dependent kij
mixRule.setBinaryInteractionParameter(0, 1, 0.10); // kij0
mixRule.setBinaryInteractionParameterT1(0, 1, 0.001); // kijT
NeqSim provides an autoSelectModel() method that automatically chooses an appropriate thermodynamic model based on the components in your fluid. This is useful for quick setups or when you're unsure which model to use.
The autoSelectModel() method follows this decision tree:
public SystemInterface autoSelectModel() {
if (hasComponent("MDEA") && hasComponent("water") && hasComponent("CO2")) {
return setModel("Electrolyte-ScRK-EOS"); // Amine systems
}
else if (hasComponent("water") || hasComponent("methanol") ||
hasComponent("MEG") || hasComponent("TEG") ||
hasComponent("ethanol") || hasComponent("DEG")) {
if (hasComponent("Na+") || hasComponent("K+") ||
hasComponent("Br-") || hasComponent("Mg++") ||
hasComponent("Cl-") || hasComponent("Ca++") ||
hasComponent("Fe++") || hasComponent("SO4--")) {
return setModel("Electrolyte-CPA-EOS-statoil"); // Electrolytes
} else {
return setModel("CPAs-SRK-EOS-statoil"); // Polar/associating
}
}
else if (hasComponent("water")) {
return setModel("ScRK-EOS"); // Water present
}
else if (hasComponent("mercury")) {
return setModel("SRK-TwuCoon-Statoil-EOS"); // Mercury
}
else {
return setModel("SRK-EOS"); // Default: standard SRK
}
}
| Components Present | Selected Model |
|---|---|
| MDEA + water + CO2 | Electrolyte-ScRK-EOS |
| Water/glycols + ions | Electrolyte-CPA-EOS-statoil |
| Water/glycols (no ions) | CPAs-SRK-EOS-statoil |
| Only water (no glycols) | ScRK-EOS |
| Mercury | SRK-TwuCoon-Statoil-EOS |
| Default (hydrocarbons only) | SRK-EOS |
import neqsim.thermo.Fluid;
// Method 1: Using Fluid builder with autoSelectModel
Fluid fluidBuilder = new Fluid();
fluidBuilder.setAutoSelectModel(true);
SystemInterface fluid = fluidBuilder.create("black oil with water");
// Method 2: Calling autoSelectModel() on existing fluid
SystemInterface gas = new SystemSrkEos(300.0, 50.0);
gas.addComponent("methane", 0.80);
gas.addComponent("water", 0.15);
gas.addComponent("MEG", 0.05);
gas.createDatabase(true);
// Auto-select will switch to CPA model
SystemInterface optimizedFluid = gas.autoSelectModel();
There is also an autoSelectMixingRule() method that selects an appropriate mixing rule based on the model type:
fluid.autoSelectMixingRule(); // Automatically sets appropriate mixing rule
| System Type | Recommended Model | Mixing Rule | Notes |
|---|---|---|---|
| Dry natural gas | SystemSrkEos |
classic (2) |
Simple, fast |
| Wet gas / condensate | SystemPrEos |
classic (2) |
Better for C7+ |
| Black oil with TBP | SystemPrEos |
classic (2) |
With characterization |
| Water-hydrocarbon | SystemSrkCPAstatoil |
CLASSIC_TX_CPA (10) |
Handles association |
| Glycol dehydration | SystemSrkCPAstatoil |
CPA_MIX (7) or (10) |
MEG, TEG, DEG |
| Sour gas with brine | SystemSoreideWhitson |
SOREIDE_WHITSON (11) |
Salinity-dependent |
| Amine gas treating | SystemSrkEos |
HV (4) |
With NRTL |
| Fiscal metering | SystemGERG2008Eos |
N/A | ISO 20765-2 |
| CCS / CO2 transport | SystemEOSCGEos |
N/A | Flue gas components |
| Electrolyte solutions | SystemElectrolyteCPAstatoil |
(10) | With chemicalReactionInit |
| Polar organics | SystemUNIFAC |
N/A | Group contribution |
| High-pressure polar | SystemPrEos |
WS (5) |
Wong-Sandler |
1. Is high accuracy required for custody transfer?
└─ YES → Use GERG-2008 or EOS-CG
2. Does the system contain electrolytes (Na+, Cl-, etc.)?
└─ YES → Use Electrolyte-CPA
3. Does the system contain water, glycols, or alcohols?
└─ YES → Use CPA models (SystemSrkCPAstatoil)
4. Is it a sour gas system with brine?
└─ YES → Use Søreide-Whitson
5. Is it a non-ideal organic mixture?
└─ YES → Use UNIFAC or NRTL with HV/WS mixing
6. Is it a standard hydrocarbon system?
└─ YES → Use SRK or PR with classic mixing
| Model Type | Speed | Accuracy | Memory |
|---|---|---|---|
| Cubic (SRK/PR) | ⚡⚡⚡ Fast | ★★★ Good | Low |
| CPA | ⚡⚡ Medium | ★★★★ Very Good | Medium |
| GERG-2008 | ⚡ Slow | ★★★★★ Excellent | High |
| UNIFAC | ⚡⚡ Medium | ★★★ Good | Medium |
| Electrolyte-CPA | ⚡ Slow | ★★★★ Very Good | High |
| Class | Category | Description |
|---|---|---|
SystemSrkEos |
Cubic | Standard SRK |
SystemSrkPenelouxEos |
Cubic | SRK with volume correction |
SystemSrkMathiasCopeman |
Cubic | SRK with MC alpha function |
SystemSrkTwuCoonEos |
Cubic | SRK with Twu-Coon alpha |
SystemSrkTwuCoonParamEos |
Cubic | SRK Twu-Coon parameterized |
SystemSrkTwuCoonStatoilEos |
Cubic | SRK Twu-Coon Equinor version |
SystemSrkSchwartzentruberEos |
Cubic | SRK-Schwartzentruber |
SystemSrkEosvolcor |
Cubic | SRK with volume correction |
SystemPrEos |
Cubic | Standard PR |
SystemPrEos1978 |
Cubic | Original 1978 PR |
SystemPrMathiasCopeman |
Cubic | PR with MC alpha |
SystemPrDanesh |
Cubic | Modified PR |
SystemPrEosvolcor |
Cubic | PR with volume correction |
SystemPrEosDelft1998 |
Cubic | Delft 1998 PR |
SystemPrGassemEos |
Cubic | Gassem PR |
SystemRKEos |
Cubic | Original RK |
SystemTSTEos |
Cubic | Twu-Sim-Tassone |
SystemBWRSEos |
Cubic | Benedict-Webb-Rubin-Starling |
SystemCSPsrkEos |
Cubic | CSP-SRK |
SystemPsrkEos |
Cubic | PSRK |
SystemSrkCPA |
CPA | Standard SRK-CPA |
SystemSrkCPAs |
CPA | Alternative SRK-CPA |
SystemSrkCPAstatoil |
CPA | Recommended Equinor SRK-CPA |
SystemPrCPA |
CPA | PR-CPA |
SystemUMRCPAEoS |
CPA | UMR-CPA |
SystemPCSAFT |
SAFT | PC-SAFT |
SystemPCSAFTa |
SAFT | PC-SAFT variant |
SystemGERG2008Eos |
Reference | GERG-2008 (ISO 20765-2) |
SystemGERG2004Eos |
Reference | GERG-2004 (older version) |
SystemGERGwaterEos |
Reference | GERG with water |
SystemEOSCGEos |
Reference | EOS-CG for CCS |
SystemSpanWagnerEos |
Reference | Span-Wagner for CO2 |
SystemLeachmanEos |
Reference | Leachman for H2 |
SystemVegaEos |
Reference | Vega equation |
SystemAmmoniaEos |
Reference | Ammonia-specific |
SystemBnsEos |
Reference | BNS equation |
SystemUNIFAC |
GE | UNIFAC |
SystemUNIFACpsrk |
GE | UNIFAC-PSRK |
SystemNRTL |
GE | NRTL |
SystemGEWilson |
GE | Wilson |
SystemUMRPRUEos |
GE-EoS | UMR-PRU |
SystemUMRPRUMCEos |
GE-EoS | UMR-PRU-MC |
SystemElectrolyteCPA |
Electrolyte | Electrolyte CPA |
SystemElectrolyteCPAstatoil |
Electrolyte | Recommended Equinor Electrolyte CPA |
SystemElectrolyteCPAMM |
Electrolyte | Electrolyte CPA-MM |
SystemFurstElectrolyteEos |
Electrolyte | Fürst electrolyte |
SystemFurstElectrolyteEosMod2004 |
Electrolyte | Modified Fürst (2004) |
SystemSoreideWhitson |
Electrolyte | Søreide-Whitson for sour gas |
SystemPitzer |
Electrolyte | Pitzer model |
SystemDesmukhMather |
Electrolyte | Desmukh-Mather |
SystemKentEisenberg |
Electrolyte | Kent-Eisenberg |
SystemDuanSun |
Electrolyte | Duan-Sun |
SystemIdealGas |
Ideal | Ideal gas law |
SystemWaterIF97 |
Water | IF-97 for steam |
| Type | Name | String Key | Best For |
|---|---|---|---|
| 1 | NO | "NO" |
Ideal mixtures |
| 2 | CLASSIC | "classic" |
General hydrocarbons |
| 3 | CLASSIC_HV | "CLASSIC_HV" |
Polar mixtures |
| 4 | HV | "HV" |
Alcohol-water-HC |
| 5 | WS | "WS" |
High-pressure polar |
| 7 | CPA_MIX | "CPA_MIX" |
CPA systems |
| 8 | CLASSIC_T | "CLASSIC_T" |
T-dependent kij |
| 9 | CLASSIC_T_CPA | "CLASSIC_T_CPA" |
CPA with T-dependency |
| 10 | CLASSIC_TX_CPA | "CLASSIC_TX_CPA" |
Recommended for CPA |
| 11 | SOREIDE_WHITSON | "SOREIDE_WHITSON" |
Sour gas, brine |
| 12 | CLASSIC_T2 | "CLASSIC_T2" |
Inverse T-dependency |
Last updated: January 2026
NeqSim supports the GERG-2008 and EOS-CG equations of state, which are reference-quality models explicit in the Helmholtz free energy. These models are widely used for high-accuracy property calculations in natural gas and CCS (Carbon Capture and Storage) applications.
Both GERG-2008 and EOS-CG share the same fundamental mathematical structure. They are fundamental equations of state explicit in the dimensionless Helmholtz free energy $\alpha$.
The dimensionless Helmholtz energy $\alpha$ is separated into an ideal gas part $\alpha^0$ and a residual part $\alpha^r$:
$$ \alpha(\delta, \tau, \bar{x}) = \frac{a(\rho, T, \bar{x})}{RT} = \alpha^0(\delta, \tau, \bar{x}) + \alpha^r(\delta, \tau, \bar{x}) $$
Where:
The ideal gas part is determined from the ideal gas heat capacity of the mixture components:
$$ \alpha^0(\delta, \tau, \bar{x}) = \sum_{i=1}^{N} x_i \left[ \alpha_{0i}^0(\delta, \tau) + \ln x_i \right] $$
The residual part accounts for intermolecular forces and real fluid behavior. It is typically expressed as a sum of polynomial and exponential terms fitted to high-accuracy experimental data:
$$ \alpha^r(\delta, \tau, \bar{x}) = \sum_{i=1}^{N} x_i \alpha_{0i}^r(\delta, \tau) + \sum_{i=1}^{N-1} \sum_{j=i+1}^{N} x_i x_j F_{ij} \alpha_{ij}^r(\delta, \tau) $$
This structure allows for extremely high accuracy in density, speed of sound, and heat capacity calculations, often superior to cubic equations of state (like SRK or PR), especially in the supercritical region.
Full Name: GERG-2008 Wide-Range Equation of State for Natural Gases and Other Mixtures.
Authors: O. Kunz and W. Wagner (Ruhr-Universität Bochum).
Standard: ISO 20765-2.
GERG-2008 is the standard reference equation for natural gas transport, processing, and custody transfer. It covers 21 components typical of natural gas.
Methane, Nitrogen, Carbon Dioxide, Ethane, Propane, Butanes, Pentanes, Hexane, Heptane, Octane, Nonane, Decane, Hydrogen, Oxygen, Carbon Monoxide, Water, Helium, Argon.
To use GERG-2008 in NeqSim, use the SystemGERG2008Eos class.
import neqsim.thermo.system.SystemGERG2008Eos;
import neqsim.thermo.system.SystemInterface;
public class GergExample {
public static void main(String[] args) {
// Create system
SystemInterface fluid = new SystemGERG2008Eos(298.15, 10.0); // T in K, P in bara
// Add components
fluid.addComponent("methane", 0.9);
fluid.addComponent("ethane", 0.1);
// Initialize
fluid.createDatabase(true);
fluid.setMixingRule("classic"); // Not strictly used by GERG but good practice for init
// Flash calculation
neqsim.thermodynamicoperations.ThermodynamicOperations ops =
new neqsim.thermodynamicoperations.ThermodynamicOperations(fluid);
ops.TPflash();
// Retrieve properties
// Note: GERG-2008 properties are often accessed via specific methods
double density = fluid.getPhase(0).getDensity_GERG2008();
double[] props = fluid.getPhase(0).getProperties_GERG2008();
System.out.println("Density (GERG): " + density + " kg/m3");
}
}
Full Name: Extension of the equation of state for natural gases GERG-2008 with improved hydrogen parameters.
Authors: R. Beckmüller, M. Thol, I. Sampson, E.W. Lemmon, R. Span (Ruhr-Universität Bochum, NIST).
GERG-2008-H2 is an extension of GERG-2008 with improved hydrogen binary interaction parameters. This extension is particularly important for:
The GERG-2008-H2 model includes:
| Binary System | Typical Density Difference |
|---|---|
| CH₄-H₂ | ~0.1-0.25% |
| N₂-H₂ | ~0.05-0.5% |
| CO₂-H₂ | ~1-1.5% (largest) |
| C₂H₆-H₂ | ~0.5-0.8% |
Differences increase with:
The GERG-2008-H2 model is available through SystemGERG2008Eos by enabling the hydrogen-enhanced mode:
import neqsim.thermo.system.SystemGERG2008Eos;
import neqsim.thermo.util.gerg.GERG2008Type;
import neqsim.thermodynamicoperations.ThermodynamicOperations;
public class Gerg2008H2Example {
public static void main(String[] args) {
// Create system with GERG-2008
SystemGERG2008Eos fluid = new SystemGERG2008Eos(300.0, 50.0); // T in K, P in bara
// Add hydrogen-rich mixture
fluid.addComponent("methane", 0.7);
fluid.addComponent("hydrogen", 0.3);
// Enable GERG-2008-H2 model with improved hydrogen parameters
fluid.useHydrogenEnhancedModel();
// or equivalently:
// fluid.setGergModelType(GERG2008Type.HYDROGEN_ENHANCED);
// Flash calculation
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
// Retrieve properties
double density = fluid.getPhase(0).getDensity();
System.out.println("Density (GERG-2008-H2): " + density + " kg/m3");
System.out.println("Model: " + fluid.getModelName()); // "GERG2008-H2-EOS"
// Check which model is active
if (fluid.isUsingHydrogenEnhancedModel()) {
System.out.println("Using hydrogen-enhanced GERG-2008-H2 model");
}
}
}
| Method | Description |
|---|---|
useHydrogenEnhancedModel() |
Enable GERG-2008-H2 model |
setGergModelType(GERG2008Type.STANDARD) |
Use standard GERG-2008 |
setGergModelType(GERG2008Type.HYDROGEN_ENHANCED) |
Use GERG-2008-H2 |
getGergModelType() |
Get current model type |
isUsingHydrogenEnhancedModel() |
Check if H2 model is active |
Full Name: EOS-CG: A Helmholtz energy equation of state for combustion gases and CCS mixtures.
Authors: J. Gernert and R. Span (Ruhr-Universität Bochum).
EOS-CG is an extension of the GERG framework designed for Carbon Capture and Storage (CCS) and combustion gas applications. It includes additional components found in flue gases and impurities relevant to CO2 transport.
Includes all 21 components from GERG-2008, plus:
Recent PRs refreshed the EOS-CG component tables with updated critical properties and binary interaction data, improving phase behavior for acid-gas heavy blends. The refresh aligns the library with the latest GERG-compatible datasets so CCS mixtures match reference densities and sound speed benchmarks more closely.
To use EOS-CG in NeqSim, use the SystemEOSCGEos class.
import neqsim.thermo.system.SystemEOSCGEos;
import neqsim.thermo.system.SystemInterface;
public class EosCgExample {
public static void main(String[] args) {
// Create system
SystemInterface fluid = new SystemEOSCGEos(298.15, 50.0);
// Add components (including CCS impurities)
fluid.addComponent("CO2", 0.95);
fluid.addComponent("SO2", 0.05);
// Initialize and Flash
fluid.createDatabase(true);
neqsim.thermodynamicoperations.ThermodynamicOperations ops =
new neqsim.thermodynamicoperations.ThermodynamicOperations(fluid);
ops.TPflash();
// Retrieve properties
double density = fluid.getPhase(0).getDensity_EOSCG();
System.out.println("Density (EOS-CG): " + density + " kg/m3");
}
}
This guide provides comprehensive documentation on mixing rules available in NeqSim, including mathematical formulations, usage patterns, and recommendations for different applications.
Mixing rules determine how pure-component parameters from an equation of state are combined to calculate mixture properties. For cubic equations of state like SRK and PR, the key parameters are:
The general form of cubic EoS mixing rules is:
$$ a_{mix} = \sum_i \sum_j x_i x_j a_{ij} $$
$$ b_{mix} = \sum_i x_i b_i $$
Where the cross-parameter $a_{ij}$ is typically calculated using a combining rule:
$$ a_{ij} = \sqrt{a_i a_j}(1 - k_{ij}) $$
The binary interaction parameter $k_{ij}$ is crucial for accurate phase equilibrium predictions.
NeqSim provides three ways to set mixing rules:
SystemInterface fluid = new SystemSrkEos(300.0, 50.0);
fluid.addComponent("methane", 0.8);
fluid.addComponent("CO2", 0.2);
fluid.setMixingRule(2); // Classic mixing rule
fluid.setMixingRule("classic");
fluid.setMixingRule("HV");
fluid.setMixingRule("WS");
import neqsim.thermo.mixingrule.EosMixingRuleType;
fluid.setMixingRule(EosMixingRuleType.CLASSIC);
fluid.setMixingRule(EosMixingRuleType.byName("classic"));
fluid.setMixingRule(EosMixingRuleType.byValue(2));
// Huron-Vidal with UNIFAC
fluid.setMixingRule("HV", "UNIFAC_UMRPRU");
// Wong-Sandler with NRTL
fluid.setMixingRule("WS", "NRTL");
The simplest mixing rule with no binary interactions. All $k_{ij}$ values are set to zero.
$$ a_{ij} = \sqrt{a_i a_j} $$
Use case: Quick calculations, ideal mixture approximations, or when no interaction parameters are available.
SystemInterface fluid = new SystemSrkEos(300.0, 50.0);
fluid.addComponent("methane", 0.7);
fluid.addComponent("ethane", 0.3);
fluid.setMixingRule(1); // or fluid.setMixingRule("NO");
The standard van der Waals one-fluid mixing rule with binary interaction parameters from the NeqSim database.
$$ a_{mix} = \sum_i \sum_j x_i x_j \sqrt{a_i a_j}(1 - k_{ij}) $$
$$ b_{mix} = \sum_i x_i b_i $$
Use case: General hydrocarbon systems, natural gas processing, most industrial applications.
SystemInterface fluid = new SystemSrkEos(300.0, 50.0);
fluid.addComponent("methane", 0.85);
fluid.addComponent("CO2", 0.10);
fluid.addComponent("nitrogen", 0.05);
fluid.setMixingRule(2); // or fluid.setMixingRule("classic");
Classic mixing rule with temperature-dependent binary interaction parameters:
$$ k_{ij}(T) = k_{ij,0} + k_{ij,T} \cdot T $$
Where $k_{ij,0}$ is the reference value and $k_{ij,T}$ is the temperature coefficient.
Use case: Systems where kij varies significantly with temperature.
SystemInterface fluid = new SystemSrkEos(300.0, 50.0);
fluid.addComponent("methane", 0.8);
fluid.addComponent("H2S", 0.2);
fluid.setMixingRule(8); // Temperature-dependent classic
Alternative temperature-dependent formulation:
$$ k_{ij}(T) = k_{ij,0} + \frac{k_{ij,T}}{T} $$
Use case: Systems requiring inverse temperature dependency.
fluid.setMixingRule(12);
Huron-Vidal (HV) mixing rules combine cubic EoS with activity coefficient models (like NRTL or UNIFAC) for improved liquid-phase behavior.
The excess Gibbs energy from an activity coefficient model is incorporated into the EoS:
$$ a_{mix} = b_{mix} \left( \sum_i x_i \frac{a_i}{b_i} - \frac{G^E}{\Lambda} \right) $$
Where:
Basic Huron-Vidal mixing rule with NRTL parameters from the database.
SystemInterface fluid = new SystemSrkEos(350.0, 10.0);
fluid.addComponent("methanol", 0.3);
fluid.addComponent("water", 0.4);
fluid.addComponent("methane", 0.3);
fluid.setMixingRule(3); // or fluid.setMixingRule("CLASSIC_HV");
Enhanced Huron-Vidal with temperature-dependent parameters (HVDijT):
$$ D_{ij}(T) = D_{ij,0} + D_{ij,T} \cdot T $$
Use case: Polar/non-polar mixtures, alcohol-water-hydrocarbon systems.
SystemInterface fluid = new SystemPrEos(320.0, 20.0);
fluid.addComponent("ethanol", 0.2);
fluid.addComponent("water", 0.5);
fluid.addComponent("propane", 0.3);
fluid.setMixingRule(4); // or fluid.setMixingRule("HV");
For predictive calculations without fitted parameters, use UNIFAC:
SystemInterface fluid = new SystemSrkEos(320.0, 15.0);
fluid.addComponent("ethanol", 0.3);
fluid.addComponent("n-hexane", 0.4);
fluid.addComponent("water", 0.3);
// HV with UNIFAC activity coefficients
fluid.setMixingRule("HV", "UNIFAC_UMRPRU");
Available GE models for HV:
"NRTL" - Non-Random Two-Liquid (default)"UNIFAC_UMRPRU" - UNIFAC with UMR-PRU parameters"UNIFAC" - Standard UNIFAC"UNIFAC_PSRK" - UNIFAC with PSRK parameters// Get the mixing rule interface
HVMixingRulesInterface hvRule = (HVMixingRulesInterface) fluid.getPhase(0).getMixingRule();
// Get/Set HV parameters
double dij = hvRule.getHVDijParameter(0, 1);
hvRule.setHVDijParameter(0, 1, 500.0); // Set Dij in K
double alpha = hvRule.getHValphaParameter(0, 1);
hvRule.setHValphaParameter(0, 1, 0.3); // Set alpha (non-randomness)
The Wong-Sandler (WS) mixing rule provides theoretically correct behavior at both low and high densities by matching:
$$ b_{mix} = \frac{\sum_i \sum_j x_i x_j \left( b - \frac{a}{RT} \right)_{ij}}{1 - \frac{A_\infty^E}{CRT} - \sum_i x_i \frac{a_i}{RTb_i}} $$
$$ a_{mix} = b_{mix} \left( \sum_i x_i \frac{a_i}{b_i} + \frac{A_\infty^E}{C} \right) $$
Where $C$ is an EoS-specific constant and $A_\infty^E$ is the excess Helmholtz energy at infinite pressure.
SystemInterface fluid = new SystemPrEos(350.0, 30.0);
fluid.addComponent("methanol", 0.2);
fluid.addComponent("water", 0.5);
fluid.addComponent("CO2", 0.3);
fluid.setMixingRule(5); // or fluid.setMixingRule("WS");
SystemInterface fluid = new SystemSrkEos(320.0, 20.0);
fluid.addComponent("acetone", 0.3);
fluid.addComponent("water", 0.4);
fluid.addComponent("methane", 0.3);
// WS with NRTL activity coefficients
fluid.setMixingRule("WS", "NRTL");
HVMixingRulesInterface wsRule = (HVMixingRulesInterface) fluid.getPhase(0).getMixingRule();
// Get/Set Wong-Sandler kij parameter
double kijWS = wsRule.getKijWongSandler(0, 1);
wsRule.setKijWongSandler(0, 1, 0.1);
Use case: High-pressure VLE, polar/non-polar mixtures, CO2 capture systems.
For Cubic-Plus-Association (CPA) equations of state, specialized mixing rules handle both the cubic EoS part and the association term.
Classic mixing rule with CPA-specific binary interaction parameters from the database.
SystemInterface fluid = new SystemSrkCPAstatoil(320.0, 50.0);
fluid.addComponent("water", 0.1);
fluid.addComponent("methane", 0.8);
fluid.addComponent("MEG", 0.1);
fluid.setMixingRule(7); // CPA classic mixing
Temperature-dependent classic mixing rule for CPA systems:
fluid.setMixingRule(9);
Temperature and composition dependent mixing rule for CPA. This is the recommended mixing rule for CPA systems as it:
SystemInterface fluid = new SystemSrkCPAstatoil(330.0, 100.0);
fluid.addComponent("water", 0.15);
fluid.addComponent("methane", 0.70);
fluid.addComponent("MEG", 0.10);
fluid.addComponent("CO2", 0.05);
fluid.setMixingRule(10); // Recommended for CPA
CPA mixing rules also handle association site interactions between different molecules:
| Association Scheme | Description | Examples |
|---|---|---|
| CR-1 | Cross-association with single site | Water-MEG |
| ER | Elliott combining rule | General polar systems |
| 4C | Four-site model | Water, glycols |
| 2B | Two-site model | Alcohols |
The Søreide-Whitson mixing rule is specifically designed for systems containing:
It uses composition and salinity-dependent binary interaction parameters.
import neqsim.thermo.system.SystemSoreideWhitson;
SystemSoreideWhitson fluid = new SystemSoreideWhitson(350.0, 200.0);
fluid.addComponent("methane", 0.70);
fluid.addComponent("CO2", 0.15);
fluid.addComponent("H2S", 0.05);
fluid.addComponent("water", 0.10);
// Add salinity (important for sour gas solubility)
fluid.addSalinity(2.0, "mole/sec");
fluid.setMixingRule(11); // Søreide-Whitson mixing rule
The mixing rule calculates kij for water-gas interactions based on salinity (S in mol/kg):
For CO2-water: $$ k_{ij} = -0.31092(1 + 0.156 S^{0.75}) + 0.236(1 + 0.178 S^{0.98}) T_r - 21.26 e^{-6.72^{T_r} - S} $$
For N2-water: $$ k_{ij} = -1.702(1 + 0.026 S^{0.75}) + 0.443(1 + 0.081 S^{0.75}) T_r $$
For hydrocarbons-water: $$ k_{ij} = (1 + a_0 S) A_0 + (1 + a_1 S) A_1 T_r + (1 + a_2 S) A_2 T_r^2 $$
Where $T_r$ is the reduced temperature and the $A$ and $a$ coefficients depend on the acentric factor.
Use case: Reservoir fluids with aqueous phases, sour gas processing, CCS with brine.
NeqSim loads kij values from its internal database when you call setMixingRule(). These are stored in the inter table.
SystemInterface fluid = new SystemSrkEos(300.0, 50.0);
fluid.addComponent("methane", 0.9);
fluid.addComponent("CO2", 0.1);
fluid.setMixingRule("classic");
// Get the mixing rule interface
EosMixingRulesInterface mixRule = fluid.getPhase(0).getMixingRule();
// Set custom kij (symmetric: kij = kji)
mixRule.setBinaryInteractionParameter(0, 1, 0.12);
// Get current kij value
double kij = mixRule.getBinaryInteractionParameter(0, 1);
System.out.println("kij(CH4-CO2) = " + kij);
// For CPA systems with asymmetric parameters
mixRule.setBinaryInteractionParameterij(0, 1, 0.08); // kij
mixRule.setBinaryInteractionParameterji(0, 1, 0.12); // kji
// For mixing rules 8, 9, 12
// kij(T) = kij0 + kijT * f(T)
mixRule.setBinaryInteractionParameter(0, 1, 0.10); // kij0
mixRule.setBinaryInteractionParameterT1(0, 1, 0.001); // kijT
double[][] kijMatrix = mixRule.getBinaryInteractionParameters();
for (int i = 0; i < fluid.getNumberOfComponents(); i++) {
for (int j = 0; j < fluid.getNumberOfComponents(); j++) {
System.out.printf("kij[%d][%d] = %.4f ", i, j, kijMatrix[i][j]);
}
System.out.println();
}
| Type | Name | String Key | Description | Best For |
|---|---|---|---|---|
| 1 | NO |
"NO" |
All kij = 0 | Quick estimates, ideal mixtures |
| 2 | CLASSIC |
"classic" |
Classic van der Waals with database kij | General hydrocarbons |
| 3 | CLASSIC_HV |
"CLASSIC_HV" |
Huron-Vidal with database parameters | Polar mixtures |
| 4 | HV |
"HV" |
Huron-Vidal with T-dependent parameters | Alcohol-water-HC |
| 5 | WS |
"WS" |
Wong-Sandler | High-pressure polar systems |
| 7 | CPA_MIX |
"CPA_MIX" |
Classic for CPA systems | Water-HC with CPA |
| 8 | CLASSIC_T |
"CLASSIC_T" |
T-dependent kij (linear) | Temperature-sensitive kij |
| 9 | CLASSIC_T_CPA |
"CLASSIC_T_CPA" |
T-dependent for CPA | CPA with T-dependency |
| 10 | CLASSIC_TX_CPA |
"CLASSIC_TX_CPA" |
T and x dependent for CPA | Recommended for CPA |
| 11 | SOREIDE_WHITSON |
"SOREIDE_WHITSON" |
Salinity-dependent | Sour gas, brine systems |
| 12 | CLASSIC_T2 |
"CLASSIC_T2" |
T-dependent (inverse) | Alternative T-dependency |
SystemInterface gas = new SystemSrkEos(280.0, 70.0);
gas.addComponent("nitrogen", 0.02);
gas.addComponent("CO2", 0.03);
gas.addComponent("methane", 0.85);
gas.addComponent("ethane", 0.06);
gas.addComponent("propane", 0.04);
gas.setMixingRule("classic"); // Type 2
ThermodynamicOperations ops = new ThermodynamicOperations(gas);
ops.TPflash();
SystemInterface fluid = new SystemSrkCPAstatoil(320.0, 50.0);
fluid.addComponent("methane", 0.80);
fluid.addComponent("water", 0.05);
fluid.addComponent("TEG", 0.15); // Triethylene glycol
fluid.setMixingRule(10); // CLASSIC_TX_CPA - recommended for CPA
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
SystemInterface fluid = new SystemSrkEos(323.0, 20.0);
fluid.addComponent("CO2", 0.15);
fluid.addComponent("H2S", 0.05);
fluid.addComponent("methane", 0.60);
fluid.addComponent("MDEA", 0.10); // Methyldiethanolamine
fluid.addComponent("water", 0.10);
fluid.setMixingRule("HV", "NRTL"); // Huron-Vidal with NRTL
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
SystemInterface fluid = new SystemPrEos(350.0, 150.0);
fluid.addComponent("CO2", 0.90);
fluid.addComponent("nitrogen", 0.05);
fluid.addComponent("oxygen", 0.02);
fluid.addComponent("water", 0.03);
fluid.setMixingRule("WS"); // Wong-Sandler for high pressure
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
SystemSoreideWhitson fluid = new SystemSoreideWhitson(380.0, 250.0);
fluid.addComponent("methane", 0.65);
fluid.addComponent("CO2", 0.20);
fluid.addComponent("H2S", 0.05);
fluid.addComponent("water", 0.10);
fluid.addSalinity("NaCl", 3.0, "mole/sec"); // Add NaCl salinity
fluid.setMixingRule(11); // Søreide-Whitson
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
System.out.println("Number of phases: " + fluid.getNumberOfPhases());
fluid.prettyPrint();
SystemInterface fluid = new SystemSrkEos(298.0, 1.0);
fluid.addComponent("ethanol", 0.3);
fluid.addComponent("acetone", 0.3);
fluid.addComponent("water", 0.4);
fluid.setMixingRule("HV", "UNIFAC_UMRPRU"); // Predictive UNIFAC
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
Documentation for mixing rules in NeqSim equations of state.
Location: neqsim.thermo.mixingrule
Mixing rules define how pure component EoS parameters combine in mixtures.
$$a_m = \sum_i \sum_j x_i x_j \sqrt{a_i a_j}(1 - k_{ij})$$
$$b_m = \sum_i x_i b_i$$
// Classic mixing rule
fluid.setMixingRule("classic");
// or
fluid.setMixingRule(2);
// Set binary interaction parameter
fluid.getInterphaseProperties().setParameter("kij", "CO2", "methane", 0.12);
For associating systems (water, alcohols, amines).
// CPA-specific mixing rule
SystemSrkCPAstatoil fluid = new SystemSrkCPAstatoil(T, P);
fluid.setMixingRule(10);
Cross-association between different associating molecules is handled via combining rules:
$$\epsilon^{A_iB_j} = \frac{\epsilon^{A_i} + \epsilon^{B_j}}{2}$$
$$\beta^{A_iB_j} = \sqrt{\beta^{A_i}\beta^{B_j}}$$
// SRK with Huron-Vidal mixing
SystemSrkHuronVidal fluid = new SystemSrkHuronVidal(T, P);
fluid.setMixingRule("HV");
// Wong-Sandler mixing rule
fluid.setMixingRule("WS");
Combines EoS with UNIFAC.
SystemSrkSchwartzentruberRenon fluid = new SystemSrkSchwartzentruberRenon(T, P);
double kij = fluid.getInterphaseProperties().getParameter("kij", "CO2", "methane");
fluid.getInterphaseProperties().setParameter("kij", "CO2", "methane", 0.12);
$$k_{ij}(T) = k_{ij}^0 + k_{ij}^1 \cdot T + k_{ij}^2 \cdot T^2$$
| Number | Mixing Rule |
|---|---|
| 1 | No mixing |
| 2 | Classic (Van der Waals) |
| 4 | Huron-Vidal |
| 7 | Wong-Sandler |
| 9 | Schwarzentruber-Renon |
| 10 | CPA |
Documentation for phase modeling in NeqSim.
Location: neqsim.thermo.phase
The phase package contains 62+ classes for modeling different phase types. Each phase type inherits from a base class that implements the PhaseInterface.
| Category | Classes |
|---|---|
| Gas | PhaseGas, PhaseGasEos, PhaseGasCPA, PhaseGasPCSAFT |
| Liquid | PhaseLiquid, PhaseLiquidEos, PhaseLiquidCPA, PhaseLiquidPCSAFT |
| Aqueous | PhaseAqueous, PhaseAqueousEos |
| Solid | PhaseSolid, PhaseSolidComplex, PhaseHydrate, PhaseWax |
PhaseInterface
└── Phase (abstract base)
├── PhaseEos (EoS phases)
│ ├── PhaseGasEos
│ ├── PhaseLiquidEos
│ └── ...
├── PhaseCPA (CPA phases)
│ ├── PhaseGasCPA
│ ├── PhaseLiquidCPA
│ └── ...
└── PhaseSolid
├── PhaseHydrate
└── PhaseWax
PhaseInterface phase = fluid.getPhase(0);
// Phase fraction
double beta = phase.getBeta(); // Mole fraction of total
double betaV = phase.getBetaV(); // Volume fraction
// Thermodynamic properties
double T = phase.getTemperature();
double P = phase.getPressure();
double V = phase.getMolarVolume();
double rho = phase.getDensity("kg/m3");
double Z = phase.getZ();
// Energetic properties
double H = phase.getEnthalpy("kJ/kg");
double S = phase.getEntropy("kJ/kgK");
double G = phase.getGibbsEnergy();
double U = phase.getInternalEnergy();
double A = phase.getHelmholtzEnergy();
// Heat capacities
double Cp = phase.getCp("J/molK");
double Cv = phase.getCv("J/molK");
// Transport properties
double visc = phase.getViscosity("cP");
double k = phase.getThermalConductivity("W/mK");
double D = phase.getDiffusionCoefficient("m2/s");
// Speed of sound
double u = phase.getSoundSpeed("m/s");
// Get component in phase
ComponentInterface comp = phase.getComponent("methane");
// Mole fraction in phase
double x = comp.getx();
// Fugacity
double f = comp.getFugacity();
double phi = comp.getFugacityCoefficient();
PhaseInterface gas = fluid.getGasPhase();
// Compressibility
double Z = gas.getZ();
// Density
double rhoGas = gas.getDensity("kg/m3");
// Viscosity
double muGas = gas.getViscosity("cP");
// Specific volume
double Vm = gas.getMolarVolume();
// Standard EoS gas phase
PhaseGasEos gasEos = (PhaseGasEos) fluid.getGasPhase();
// CPA gas phase (for associating components)
PhaseGasCPA gasCPA = (PhaseGasCPA) fluid.getGasPhase();
// PC-SAFT gas phase
PhaseGasPCSAFT gasSAFT = (PhaseGasPCSAFT) fluid.getGasPhase();
PhaseInterface oil = fluid.getLiquidPhase();
// Liquid density
double rhoLiq = oil.getDensity("kg/m3");
// API gravity
double API = 141.5 / (oil.getDensity("g/cm3") / 0.999) - 131.5;
// Viscosity
double muOil = oil.getViscosity("cP");
// When system has multiple liquid phases
if (fluid.getNumberOfLiquidPhases() > 1) {
PhaseInterface oil = fluid.getPhase("oil");
PhaseInterface aqueous = fluid.getPhase("aqueous");
}
For water-rich liquid phases.
PhaseInterface aqueous = fluid.getPhase("aqueous");
// Water activity
double aW = aqueous.getComponent("water").getActivity();
// pH (if electrolytes present)
double pH = aqueous.getpH();
// Ionic strength
double I = aqueous.getIonicStrength();
// Enable solid phase check
fluid.setSolidPhaseCheck(true);
// Get solid phase if formed
if (fluid.hasSolidPhase()) {
PhaseInterface solid = fluid.getSolidPhase();
double solidFraction = solid.getBeta();
}
// Hydrate formation check
fluid.setHydrateCheck(true);
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
if (fluid.hasHydrate()) {
PhaseInterface hydrate = fluid.getPhase("hydrate");
double hydrateTemp = fluid.getHydrateTemperature();
}
// Wax formation check
fluid.setWaxCheck(true);
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
if (fluid.hasWax()) {
PhaseInterface wax = fluid.getPhase("wax");
double waxFraction = wax.getBeta();
}
NeqSim supports two approaches for modeling asphaltene precipitation:
Solid Asphaltene (PhaseType.ASPHALTENE): Traditional approach where precipitated asphaltene is treated as a solid phase with literature-based physical properties.
Liquid Asphaltene (PhaseType.LIQUID_ASPHALTENE): Pedersen's approach where asphaltene is modeled as a liquid phase using cubic EOS (SRK/PR), enabling liquid-liquid equilibrium calculations.
// Enable solid phase check for asphaltene
fluid.setSolidPhaseCheck("asphaltene");
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
// Check for asphaltene phase using PhaseType
if (fluid.hasPhaseType(PhaseType.ASPHALTENE)) {
PhaseInterface asphaltene = fluid.getPhaseOfType("asphaltene");
double asphalteneFraction = asphaltene.getBeta();
double density = asphaltene.getDensity("kg/m3"); // ~1150 kg/m³
double viscosity = asphaltene.getViscosity("Pa*s"); // ~10,000 Pa·s
}
Pedersen's method treats asphaltene as a heavy liquid component using cubic EOS with estimated critical properties. This allows liquid-liquid equilibrium (LLE) calculations.
import neqsim.thermo.characterization.PedersenAsphalteneCharacterization;
// Create fluid system
SystemInterface fluid = new SystemSrkEos(373.15, 50.0);
fluid.addComponent("methane", 0.30);
fluid.addComponent("n-pentane", 0.25); // Precipitant
fluid.addComponent("n-heptane", 0.20);
fluid.addComponent("nC10", 0.15);
// Create and configure asphaltene characterization
PedersenAsphalteneCharacterization asphChar = new PedersenAsphalteneCharacterization();
asphChar.setAsphalteneMW(750.0); // g/mol
asphChar.setAsphalteneDensity(1.10); // g/cm³
// Add asphaltene as pseudo-component (before mixing rule)
asphChar.addAsphalteneToSystem(fluid, 0.10); // 10 mol%
fluid.setMixingRule("classic");
// Perform TPflash with automatic asphaltene detection
boolean hasAsphaltene = PedersenAsphalteneCharacterization.TPflash(fluid);
// Check for asphaltene-rich liquid phase
if (fluid.hasPhaseType(PhaseType.LIQUID_ASPHALTENE)) {
PhaseInterface asphLiquid = fluid.getPhaseOfType("asphaltene liquid");
System.out.println("Asphaltene liquid fraction: " + asphLiquid.getBeta());
}
// Check if any phase is asphaltene-rich (works for both approaches)
for (int i = 0; i < fluid.getNumberOfPhases(); i++) {
PhaseInterface phase = fluid.getPhase(i);
if (phase.isAsphalteneRich()) {
System.out.println("Phase " + i + " is asphaltene-rich");
}
}
// Using StateOfMatter helper
import neqsim.thermo.phase.StateOfMatter;
boolean isAsph = StateOfMatter.isAsphaltene(phase.getType());
Asphaltene Phase Properties:
| Property | Solid Approach | Liquid (Pedersen) | Unit |
|---|---|---|---|
| Density | ~1150 (literature) | EOS-calculated | kg/m³ |
| Heat Capacity (Cp) | ~0.9 (literature) | EOS-calculated | kJ/kgK |
| Thermal Conductivity | ~0.20 (literature) | EOS-calculated | W/mK |
| Viscosity | ~10,000 (literature) | EOS-calculated | Pa·s |
| Speed of Sound | ~1745 (literature) | EOS-calculated | m/s |
// Get phase type
String type = phase.getPhaseTypeName(); // "gas", "oil", "aqueous", "asphaltene", etc.
// Check phase type using enum
boolean isGas = phase.getType() == PhaseType.GAS;
boolean isLiquid = phase.getType() == PhaseType.LIQUID;
boolean isAqueous = phase.getType() == PhaseType.AQUEOUS;
boolean isAsphaltene = phase.getType() == PhaseType.ASPHALTENE;
boolean isSolid = phase.getType() == PhaseType.SOLID;
// Check using string-based lookup (convenient API)
boolean hasGas = fluid.hasPhaseType("gas");
boolean hasAsphaltene = fluid.hasPhaseType("asphaltene");
| PhaseType | Description | Value | StateOfMatter |
|---|---|---|---|
GAS |
Gas phase | 1 | GAS |
LIQUID |
Generic liquid | 0 | LIQUID |
OIL |
Oil/hydrocarbon liquid | 2 | LIQUID |
AQUEOUS |
Water-rich liquid | 3 | LIQUID |
HYDRATE |
Gas hydrate solid | 4 | SOLID |
WAX |
Wax solid | 5 | SOLID |
SOLID |
Generic solid | 6 | SOLID |
SOLIDCOMPLEX |
Complex solid | 7 | SOLID |
ASPHALTENE |
Asphaltene solid | 8 | SOLID |
LIQUID_ASPHALTENE |
Asphaltene liquid (Pedersen) | 9 | LIQUID |
// Phase stability check
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.checkStability();
boolean stable = fluid.isPhaseStable();
// Fugacity coefficients
for (int i = 0; i < phase.getNumberOfComponents(); i++) {
double phi = phase.getComponent(i).getFugacityCoefficient();
double lnPhi = phase.getComponent(i).getLogFugacityCoefficient();
}
// Compressibility factor derivatives
double dZdT = phase.getdZdT();
double dZdP = phase.getdZdP();
// Fugacity coefficient derivatives
double dPhidT = phase.getComponent(0).getdfugdT();
double dPhidP = phase.getComponent(0).getdfugdP();
// Excess properties
double GE = phase.getExcessGibbsEnergy();
double HE = phase.getExcessEnthalpy();
double SE = phase.getExcessEntropy();
double VE = phase.getExcessVolume();
// Activity coefficients (for liquid)
double gamma = phase.getComponent("ethanol").getActivityCoefficient();
import neqsim.thermo.system.SystemSrkEos;
import neqsim.thermodynamicoperations.ThermodynamicOperations;
SystemSrkEos fluid = new SystemSrkEos(300.0, 50.0);
fluid.addComponent("methane", 0.80);
fluid.addComponent("ethane", 0.10);
fluid.addComponent("propane", 0.05);
fluid.addComponent("n-pentane", 0.05);
fluid.setMixingRule("classic");
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
System.out.println("Number of phases: " + fluid.getNumberOfPhases());
for (int p = 0; p < fluid.getNumberOfPhases(); p++) {
PhaseInterface phase = fluid.getPhase(p);
System.out.println("\nPhase " + (p + 1) + ": " + phase.getPhaseTypeName());
System.out.println(" Mole fraction: " + phase.getBeta());
System.out.println(" Density: " + phase.getDensity("kg/m3") + " kg/m³");
System.out.println(" Z-factor: " + phase.getZ());
System.out.println(" Viscosity: " + phase.getViscosity("cP") + " cP");
System.out.println(" Composition:");
for (int i = 0; i < phase.getNumberOfComponents(); i++) {
String name = phase.getComponent(i).getName();
double x = phase.getComponent(i).getx();
System.out.printf(" %s: %.4f%n", name, x);
}
}
The electrolyte CPA (Cubic Plus Association) model in NeqSim extends the standard CPA equation of state to handle aqueous electrolyte solutions. The model is based on the work of Solbraa (2002) and combines:
The Statoil (now Equinor) implementation of the electrolyte CPA model:
import neqsim.thermo.system.SystemElectrolyteCPAstatoil;
SystemElectrolyteCPAstatoil system = new SystemElectrolyteCPAstatoil(298.15, 1.01325);
system.addComponent("water", 55.5);
system.addComponent("Na+", 1.0);
system.addComponent("Cl-", 1.0);
system.chemicalReactionInit();
system.createDatabase(true);
system.setMixingRule(10); // Required: CPA mixing rule with temperature/composition dependency
| Feature | Description |
|---|---|
| Model Name | Electrolyte-CPA-EOS-statoil |
| Base Class | Extends SystemFurstElectrolyteEos |
| Phase Class | PhaseElectrolyteCPAstatoil |
| Component Class | ComponentElectrolyteCPAstatoil |
| Attractive Term | Term 15 (Mathias-Copeman alpha function) |
| Volume Correction | Enabled by default |
| Fürst Parameters | Uses electrolyteCPA parameter set |
SystemThermo
└── SystemSrkEos
└── SystemFurstElectrolyteEos
└── SystemElectrolyteCPAstatoil
├── PhaseElectrolyteCPAstatoil (phase calculations)
└── ComponentElectrolyteCPAstatoil (component properties)
Mixing rule 10 is the recommended mixing rule for all CPA and electrolyte CPA systems. It automatically selects the appropriate sub-type based on the binary interaction parameters:
system.setMixingRule(10); // Automatically selects optimal sub-type
The mixing rule analyzes the binary interaction parameter matrices and selects:
| Condition | Sub-Type | Class | Description |
|---|---|---|---|
| Symmetric kij, no T-dependency | classic-CPA |
ClassicSRK |
Simple symmetric mixing |
| Symmetric kij, with T-dependency | classic-CPA_T |
ClassicSRKT2 |
Temperature-dependent symmetric |
| Asymmetric kij (kij ≠ kji) | classic-CPA_Tx |
ClassicSRKT2x |
Full asymmetric + T-dependent |
The a parameter mixing rule:
$$a = \sum_i \sum_j x_i x_j \sqrt{a_i a_j} (1 - k_{ij})$$
For asymmetric mixing (ClassicSRKT2x):
$$k_{ij} \neq k_{ji}$$
Temperature dependency:
$$k_{ij}(T) = k_{ij,0} + k_{ij,T} \cdot T$$
The total residual Helmholtz energy is decomposed as:
$$A^{res} = A^{CPA} + A^{elec}$$
Where:
The electrostatic contribution follows the Fürst model, which combines:
$$A^{elec} = A^{MSA} + A^{Born} + A^{SR}$$
The MSA term accounts for the electrostatic screening between ions:
$$\frac{A^{MSA}}{RT} = -\frac{V}{3\pi} \left[ \Gamma^3 + \frac{3\Gamma\sigma_+ \sigma_-}{1 + \Gamma\sigma_{+-}} \right]$$
Where:
The Born term accounts for the solvation energy of ions in the dielectric medium:
$$\frac{A^{Born}}{RT} = -\frac{e^2 N_A}{8\pi\varepsilon_0 k_B T} \sum_i n_i \frac{z_i^2}{\sigma_i} \left(1 - \frac{1}{\varepsilon_r}\right)$$
Where:
The short-range Wij parameters capture specific ion-solvent and ion-ion interactions not described by the electrostatic terms. These are fitted to experimental activity coefficient and osmotic coefficient data.
The Wij values are calculated using linear correlations with ionic diameter:
Wij(cation-water) = furstParamsCPA[2] × stokesDiameter + furstParamsCPA[3]
Wij(cation-anion) = furstParamsCPA[4] × (d_cat + d_an)^4 + furstParamsCPA[5]
Current fitted values (2024):
[2] = 4.985e-05 (slope for cation-water)[3] = -1.215e-04 (intercept for cation-water)[4] = -2.059e-08 (prefactor for cation-anion)[5] = -9.495e-05 (intercept for cation-anion)Wij(2+ cation-water) = furstParamsCPA[6] × stokesDiameter + furstParamsCPA[7]
Wij(2+ cation-anion) = furstParamsCPA[8] × (d_cat + d_an)^4 + furstParamsCPA[9]
Current fitted values (refitted December 2024):
[6] = 5.40e-05 (slope for 2+ cation-water)[7] = -1.72e-04 (intercept for 2+ cation-water)[8] = -4.398e-08 (prefactor for 2+ cation-anion)[9] = -5.970e-17 (intercept for 2+ cation-anion)The divalent cation parameters differ significantly from monovalent:
Using unified parameters would give dramatically wrong Wij values for divalent cations (sometimes even wrong sign), making separate parameters essential for accuracy.
| Parameter | Monovalent | Divalent | Ratio |
|---|---|---|---|
| Slope | 4.98e-05 | 5.40e-05 | 1.08 |
| Intercept | -1.22e-04 | -1.72e-04 | 1.42 |
The model has been validated against Robinson & Stokes experimental data for mean activity coefficients (γ±) and osmotic coefficients (φ) at 25°C.
| Salt | Type | γ± Error | φ Error |
|---|---|---|---|
| NaCl | 1-1 | 2.4% | 1.6% |
| KCl | 1-1 | 4.3% | 1.0% |
| LiCl | 1-1 | 3.4% | 2.5% |
| NaBr | 1-1 | 2.8% | 2.0% |
| KBr | 1-1 | 1.4% | 2.0% |
| Salt | Type | γ± Error | φ Error |
|---|---|---|---|
| CaCl₂ | 2-1 | 7.0% | 4.2% |
| MgCl₂ | 2-1 | 9.6% | 4.6% |
| BaCl₂ | 2-1 | 2.3% | 1.5% |
| Salt | Type | γ± Error | φ Error |
|---|---|---|---|
| Na₂SO₄ | 1-2 | 20.0% | 19.7% |
| K₂SO₄ | 1-2 | 2.9% | 1.6% |
The model supports three dielectric constant mixing rules:
$$\varepsilon_{mix} = \sum_i x_i \varepsilon_i$$
$$\varepsilon_{mix} = \sum_i \phi_i \varepsilon_i$$
Where $\phi_i$ is the volume fraction.
$$\varepsilon_{mix}^{1/3} = \sum_i \phi_i \varepsilon_i^{1/3}$$
The model has been verified for thermodynamic consistency using built-in checks:
✅ $\sum_i x_i \ln\phi_i = \frac{G^{res}}{RT}$ - PASSED
✅ $\left(\frac{\partial \ln\phi_i}{\partial P}\right)_T = \frac{\bar{V}_i - V_{ig}}{RT}$ - PASSED
✅ $\left(\frac{\partial \ln\phi_i}{\partial T}\right)_P = \frac{H_{ig} - \bar{H}_i}{RT^2}$ - PASSED
✅ $\left(\frac{\partial \ln\phi_i}{\partial n_j}\right)_{T,P,n_{k\neq j}} = \left(\frac{\partial \ln\phi_j}{\partial n_i}\right)_{T,P,n_{k\neq i}}$ (symmetry) - PASSED
// Create electrolyte CPA system
SystemInterface system = new SystemElectrolyteCPAstatoil(298.15, 1.01325);
// Add water (solvent)
system.addComponent("water", 55.5); // mol
// Add electrolyte
system.addComponent("Na+", 1.0);
system.addComponent("Cl-", 1.0);
// Initialize
system.chemicalReactionInit();
system.createDatabase(true);
system.setMixingRule(10); // Electrolyte CPA mixing rule
// Run flash calculation
ThermodynamicOperations ops = new ThermodynamicOperations(system);
ops.TPflash();
// Get activity coefficients
int aq = system.getPhaseNumberOfPhase("aqueous");
int naIdx = system.getPhase(aq).getComponent("Na+").getComponentNumber();
int clIdx = system.getPhase(aq).getComponent("Cl-").getComponentNumber();
int waterIdx = system.getPhase(aq).getComponent("water").getComponentNumber();
double gammaNa = system.getPhase(aq).getActivityCoefficient(naIdx, waterIdx);
double gammaCl = system.getPhase(aq).getActivityCoefficient(clIdx, waterIdx);
double meanGamma = Math.sqrt(gammaNa * gammaCl); // Mean activity coefficient
double phi = system.getPhase(aq).getOsmoticCoefficientOfWater();
The model supports mixed solvent systems including:
Separate Wij parameters are available for each solvent system.
| Feature | SystemSrkCPAstatoil | SystemElectrolyteCPAstatoil |
|---|---|---|
| Use Case | Non-ionic associating systems | Aqueous electrolyte solutions |
| Electrostatics | None | MSA + Born solvation |
| Ions | Not supported | Na+, K+, Ca++, Mg++, Cl-, etc. |
| Mixing Rule | 10 (recommended) | 10 (required) |
| Chemical Reactions | Optional | Recommended (pH, speciation) |
| Phase Class | PhaseSrkCPAs |
PhaseElectrolyteCPAstatoil |
SystemInterface system = new SystemElectrolyteCPAstatoil(298.15, 1.01325);
system.addComponent("water", 55.5);
system.addComponent("Na+", 0.5);
system.addComponent("Cl-", 0.5);
system.createDatabase(true);
system.setMixingRule(10);
system.init(0);
system.init(1);
// Get mean activity coefficient
double gammaMean = system.getPhase(0).getMeanIonicActivityCoefficient("Na+", "Cl-");
SystemInterface system = new SystemElectrolyteCPAstatoil(298.15, 1.01325);
system.addComponent("water", 55.5);
system.addComponent("CO2", 0.01);
system.addComponent("Na+", 0.1);
system.addComponent("Cl-", 0.1);
system.chemicalReactionInit(); // Enable pH and speciation
system.createDatabase(true);
system.setMixingRule(10);
ThermodynamicOperations ops = new ThermodynamicOperations(system);
ops.TPflash();
// Access aqueous phase
int aq = system.getPhaseNumberOfPhase("aqueous");
double pH = -Math.log10(system.getPhase(aq).getComponent("H3O+").getx() * 55.5);
SystemInterface system = new SystemElectrolyteCPAstatoil(323.15, 50.0);
system.addComponent("methane", 10.0);
system.addComponent("water", 100.0);
system.addComponent("MEG", 20.0);
system.addComponent("Na+", 1.0);
system.addComponent("Cl-", 1.0);
system.createDatabase(true);
system.setMixingRule(10);
ThermodynamicOperations ops = new ThermodynamicOperations(system);
ops.TPflash();
// Check phase compositions
for (int i = 0; i < system.getNumberOfPhases(); i++) {
System.out.println("Phase " + i + ": " + system.getPhase(i).getType());
}
Solbraa, E. (2002). "Measurement and Modelling of Absorption of Carbon Dioxide into Methyldiethanolamine Solutions at High Pressures." PhD Thesis, Norwegian University of Science and Technology.
Fürst, W., & Renon, H. (1993). "Representation of excess properties of electrolyte solutions using a new equation of state." AIChE Journal, 39(2), 335-343.
Robinson, R.A., & Stokes, R.H. (1965). "Electrolyte Solutions." 2nd Edition, Butterworths, London.
Kontogeorgis, G.M., & Folas, G.K. (2010). "Thermodynamic Models for Industrial Applications." Wiley.
Michelsen, M.L., & Mollerup, J.M. (2007). "Thermodynamic Models: Fundamentals & Computational Aspects." Tie-Line Publications.
| Date | Change | Impact |
|---|---|---|
| 2002 | Initial parameters from Solbraa thesis | Baseline model |
| 2024 | Refitted monovalent parameters to Robinson & Stokes | γ± error: 2.8% |
| Dec 2024 | Refitted divalent cation parameters [6-9] | CaCl₂: 16%→7%, MgCl₂: 22%→10% |
| Dec 2024 | Updated chemical equilibrium solver | Improved pH accuracy |
SystemElectrolyteCPAstatoil.java - Main system class (Statoil implementation)SystemElectrolyteCPA.java - Generic electrolyte CPA systemSystemSrkCPAstatoil.java - Non-electrolyte CPA (for comparison)PhaseElectrolyteCPAstatoil.java - Phase calculations (Statoil g-function)PhaseElectrolyteCPA.java - Base electrolyte CPA phasePhaseModifiedFurstElectrolyteEos.java - Fürst electrostatic contributionsComponentElectrolyteCPAstatoil.java - Component propertiesComponentElectrolyteCPA.java - Base electrolyte CPA componentEosMixingRuleHandler.java - Mixing rule selection (line 552 for rule 10)CPAMixingRuleHandler.java - CPA association mixing rulesFurstElectrolyteConstants.java - Wij correlation parametersSystemElectrolyteCPATest.java - Basic electrolyte CPA testsElectrolyteCPAThermodynamicConsistencyTest.java - Thermodynamic consistencyElectrolyteCPARobinsonValidationTest.java - Validation against experimental dataLast updated: December 27, 2024
This guide provides comprehensive documentation of flash calculations available in NeqSim via the ThermodynamicOperations class. Flash calculations determine the equilibrium state of a thermodynamic system by solving phase equilibrium equations under specified constraints.
Flash calculations solve phase equilibrium problems by finding:
The mathematical basis is the equality of chemical potentials (or fugacities) for all components across all phases:
$$f_i^{vapor} = f_i^{liquid} = f_i^{solid}$$
where $f_i$ is the fugacity of component $i$.
All flash calculations use the ThermodynamicOperations class:
import neqsim.thermo.system.SystemSrkEos;
import neqsim.thermodynamicoperations.ThermodynamicOperations;
// 1. Create a fluid system
SystemInterface fluid = new SystemSrkEos(298.15, 50.0); // T in K, P in bara
fluid.addComponent("methane", 0.8);
fluid.addComponent("ethane", 0.15);
fluid.addComponent("propane", 0.05);
fluid.setMixingRule("classic");
// 2. Create operations object
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
// 3. Run the flash calculation
ops.TPflash();
// 4. Access results
System.out.println("Vapor fraction: " + fluid.getBeta());
System.out.println("Number of phases: " + fluid.getNumberOfPhases());
System.out.println("Temperature: " + fluid.getTemperature("C") + " °C");
System.out.println("Pressure: " + fluid.getPressure("bara") + " bara");
The most common flash type. Given temperature and pressure, find phase split and compositions. NeqSim implements the classical Michelsen flash algorithm with stability analysis.
Method signatures:
void TPflash()
void TPflash(boolean checkForSolids)
Example:
SystemInterface fluid = new SystemSrkEos(300.0, 50.0);
fluid.addComponent("methane", 0.9);
fluid.addComponent("n-heptane", 0.1);
fluid.setMixingRule("classic");
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
// Results
double vaporFraction = fluid.getBeta(); // Molar vapor fraction
double liquidDensity = fluid.getPhase("oil").getDensity("kg/m3");
With solid phase checking:
fluid.setSolidPhaseCheck(true);
ops.TPflash(true); // Includes solid equilibrium
By default, NeqSim checks for two-phase (gas-liquid) equilibrium. For systems that may form multiple liquid phases (VLLE, LLE), enable multi-phase checking:
fluid.setMultiPhaseCheck(true);
ops.TPflash();
// Will detect gas + multiple liquid phases (e.g., oil, aqueous)
For complex mixtures where standard stability analysis may miss additional phases (e.g., sour gas with CO₂/H₂S, LLE systems), enable enhanced stability analysis:
fluid.setMultiPhaseCheck(true);
fluid.setEnhancedMultiPhaseCheck(true); // Enable enhanced phase detection
ops.TPflash();
The enhanced stability analysis:
Example - Sour Gas Three-Phase Detection:
// Sour gas mixture: methane/CO2/H2S at low temperature
SystemInterface sourGas = new SystemPrEos(210.0, 55.0); // ~-63°C, 55 bar
sourGas.addComponent("methane", 49.88);
sourGas.addComponent("CO2", 9.87);
sourGas.addComponent("H2S", 40.22);
sourGas.setMixingRule("classic");
sourGas.setMultiPhaseCheck(true);
sourGas.setEnhancedMultiPhaseCheck(true); // Critical for finding 3 phases
ThermodynamicOperations ops = new ThermodynamicOperations(sourGas);
ops.TPflash();
sourGas.initProperties();
System.out.println("Number of phases: " + sourGas.getNumberOfPhases());
// May find: vapor + CO2-rich liquid + H2S-rich liquid
When to use enhanced stability analysis:
Note: Enhanced stability analysis adds computational overhead. For simple VLE systems, the standard analysis is sufficient.
Given pressure and total enthalpy, find temperature and phase split. Essential for:
Method signatures:
void PHflash(double Hspec) // H in J
void PHflash(double Hspec, String unit) // Supported: J, J/mol, J/kg, kJ/kg
void PHflash(double Hspec, int type) // type 0 = standard
Example - Joule-Thomson expansion:
SystemInterface fluid = new SystemSrkEos(350.0, 100.0);
fluid.addComponent("methane", 1.0);
fluid.setMixingRule("classic");
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
fluid.initPhysicalProperties();
// Store inlet enthalpy
double inletH = fluid.getEnthalpy("J");
// Reduce pressure (isenthalpic process)
fluid.setPressure(10.0, "bara");
// Find new temperature at same enthalpy
ops.PHflash(inletH);
System.out.println("Outlet temperature: " + fluid.getTemperature("C") + " °C");
// Demonstrates Joule-Thomson cooling
With unit specification:
// Enthalpy specified in kJ/kg
ops.PHflash(-150.0, "kJ/kg");
Given pressure and total entropy, find temperature and phase split. Used for:
Method signatures:
void PSflash(double Sspec) // S in J/K
void PSflash(double Sspec, String unit) // Supported: J/K, J/molK, J/kgK, kJ/kgK
Example - Isentropic compression:
SystemInterface fluid = new SystemSrkEos(300.0, 10.0);
fluid.addComponent("methane", 1.0);
fluid.setMixingRule("classic");
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
fluid.initPhysicalProperties();
// Inlet conditions
double T1 = fluid.getTemperature("K");
double S_inlet = fluid.getEntropy("J/K");
// Compress to higher pressure (isentropic)
fluid.setPressure(50.0, "bara");
ops.PSflash(S_inlet);
double T2 = fluid.getTemperature("K");
System.out.println("Isentropic outlet T: " + (T2 - 273.15) + " °C");
// Compare to actual with polytropic efficiency
double eta_poly = 0.85;
double T2_actual = T1 + (T2 - T1) / eta_poly;
Given pressure and internal energy, find temperature and phase split.
Method signatures:
void PUflash(double Uspec) // U in J
void PUflash(double Uspec, String unit) // Supported: J, J/mol, J/kg, kJ/kg
void PUflash(double Pspec, double Uspec, String unitP, String unitU)
Example:
ops.PUflash(100.0, -500.0, "bara", "kJ/kg");
Given temperature and total volume, find pressure and phase split. Used for:
Method signatures:
void TVflash(double Vspec) // V in cm³
void TVflash(double Vspec, String unit) // Supported: m3
Example - Fixed volume vessel:
SystemInterface fluid = new SystemSrkEos(300.0, 10.0);
fluid.addComponent("nitrogen", 1.0);
fluid.setMixingRule("classic");
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
// Vessel volume = 1 m³
double vesselVolume = 1.0; // m³
// Heat the vessel (isochoric process)
fluid.setTemperature(400.0, "K");
ops.TVflash(vesselVolume, "m3");
System.out.println("New pressure: " + fluid.getPressure("bara") + " bara");
Given temperature and entropy, find pressure and phase split. Uses the Q-function methodology based on Michelsen (1999).
Method signatures:
void TSflash(double Sspec) // S in J/K
void TSflash(double Sspec, String unit) // Supported: J/K, J/molK, J/kgK, kJ/kgK
Thermodynamic derivative: $$\left(\frac{\partial S}{\partial P}\right)_T = -\left(\frac{\partial V}{\partial T}\right)_P$$
Given temperature and enthalpy, find pressure and phase split. Uses the Q-function methodology with Newton iteration.
Applications:
Method signatures:
void THflash(double Hspec) // H in J
void THflash(double Hspec, String unit) // Supported: J, J/mol, J/kg, kJ/kg
Example:
SystemInterface fluid = new SystemSrkEos(300.0, 50.0);
fluid.addComponent("methane", 0.9);
fluid.addComponent("ethane", 0.1);
fluid.setMixingRule("classic");
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
// Store enthalpy at initial pressure
double H_target = fluid.getEnthalpy("J");
// Change temperature (enthalpy will change)
fluid.setTemperature(280.0, "K");
// Find pressure that gives same enthalpy at new temperature
ops.THflash(H_target);
System.out.println("Pressure for same H at new T: " + fluid.getPressure("bara") + " bara");
Thermodynamic derivative: $$\left(\frac{\partial H}{\partial P}\right)_T = V - T\left(\frac{\partial V}{\partial T}\right)_P$$
Given temperature and internal energy, find pressure and phase split. Uses the Q-function methodology with Newton iteration.
Applications:
Method signatures:
void TUflash(double Uspec) // U in J
void TUflash(double Uspec, String unit) // Supported: J, J/mol, J/kg, kJ/kg
Example:
SystemInterface fluid = new SystemSrkEos(350.0, 20.0);
fluid.addComponent("methane", 1.0);
fluid.setMixingRule("classic");
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
// Store internal energy
double U_target = fluid.getInternalEnergy("J");
// Change temperature
fluid.setTemperature(300.0, "K");
// Find pressure that maintains same internal energy
ops.TUflash(U_target);
System.out.println("Pressure for same U at new T: " + fluid.getPressure("bara") + " bara");
Thermodynamic derivative: $$\left(\frac{\partial U}{\partial P}\right)_T = -T\left(\frac{\partial V}{\partial T}\right)_P - P\left(\frac{\partial V}{\partial P}\right)_T$$
Given pressure and volume, find temperature and phase split. Uses the Q-function methodology with Newton iteration.
Applications:
Method signatures:
void PVflash(double Vspec) // V in m³
void PVflash(double Vspec, String unit) // Supported: m3
Example:
SystemInterface fluid = new SystemSrkEos(300.0, 50.0);
fluid.addComponent("nitrogen", 1.0);
fluid.setMixingRule("classic");
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
// Store volume at initial conditions
double V_target = fluid.getVolume("m3");
// Change pressure
fluid.setPressure(100.0, "bara");
// Find temperature that gives same volume at new pressure
ops.PVflash(V_target);
System.out.println("Temperature for same V at new P: " + fluid.getTemperature("C") + " °C");
Thermodynamic derivative: $$\left(\frac{\partial V}{\partial T}\right)_P$$
Given volume and enthalpy, find temperature, pressure, and phase split. Used for:
Method signatures:
void VHflash(double Vspec, double Hspec)
void VHflash(double V, double H, String unitV, String unitH)
Example:
ops.VHflash(0.5, -50000.0, "m3", "J");
Given volume and internal energy, find temperature, pressure, and phase split. Critical for:
Method signatures:
void VUflash(double Vspec, double Uspec)
void VUflash(double V, double U, String unitV, String unitU)
Example - Dynamic depressurization:
SystemInterface fluid = new SystemSrkCPAstatoil(300.0, 100.0);
fluid.addComponent("methane", 0.9);
fluid.addComponent("CO2", 0.1);
fluid.setMixingRule(10);
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
// Initial state
double V0 = fluid.getVolume("m3");
double U0 = fluid.getInternalEnergy("J");
// Simulate adiabatic expansion (U constant, V increases)
double V_new = V0 * 2.0; // Volume doubles
ops.VUflash(V_new, U0, "m3", "J");
System.out.println("New T: " + fluid.getTemperature("C") + " °C");
System.out.println("New P: " + fluid.getPressure("bara") + " bara");
Given volume and entropy, find temperature, pressure, and phase split.
Method signatures:
void VSflash(double Vspec, double Sspec)
void VSflash(double V, double S, String unitV, String unitS)
Several flash types in NeqSim use the Q-function methodology described by Michelsen (1999). This approach is particularly effective for state function specifications (entropy, enthalpy, internal energy, volume) where the flash must solve for temperature and/or pressure.
The Q-function method uses a nested iteration approach:
The key advantage is that the inner TP flash handles all the phase equilibrium complexity, while the outer loop only needs to adjust T or P based on the state function derivative.
The Q-function flashes use analytical thermodynamic derivatives computed via init(3):
| Flash | Specification | Solved | Derivative Used |
|---|---|---|---|
| TSflash | T, S | P | $\left(\frac{\partial S}{\partial P}\right)_T = -\left(\frac{\partial V}{\partial T}\right)_P$ |
| THflash | T, H | P | $\left(\frac{\partial H}{\partial P}\right)_T = V - T\left(\frac{\partial V}{\partial T}\right)_P$ |
| TUflash | T, U | P | $\left(\frac{\partial U}{\partial P}\right)_T = -T\left(\frac{\partial V}{\partial T}\right)_P - P\left(\frac{\partial V}{\partial P}\right)_T$ |
| TVflash | T, V | P | $\left(\frac{\partial V}{\partial P}\right)_T$ |
| PVflash | P, V | T | $\left(\frac{\partial V}{\partial T}\right)_P$ |
| VUflash | V, U | T, P | 2D Newton with $\frac{\partial U}{\partial T}$, $\frac{\partial U}{\partial P}$, $\frac{\partial V}{\partial T}$, $\frac{\partial V}{\partial P}$ |
| VHflash | V, H | T, P | 2D Newton with $\frac{\partial H}{\partial T}$, $\frac{\partial H}{\partial P}$, $\frac{\partial V}{\partial T}$, $\frac{\partial V}{\partial P}$ |
| VSflash | V, S | T, P | 2D Newton with $\frac{\partial S}{\partial T}$, $\frac{\partial S}{\partial P}$, $\frac{\partial V}{\partial T}$, $\frac{\partial V}{\partial P}$ |
All Q-function flashes follow a similar pattern:
// Example: TH flash pseudocode
public double solveQ() {
do {
system.init(3); // Calculate derivatives
double residual = system.getEnthalpy() - Hspec;
// Analytical derivative: (dH/dP)_T = V - T*(dV/dT)_P
double V = system.getVolume();
double T = system.getTemperature();
double dVdT = -system.getdVdTpn(); // Note sign convention
double dHdP = (V - T * dVdT) * 1e5; // Convert to J/bar
// Newton step with damping
double deltaP = -factor * residual / dHdP;
nyPres = oldPres + deltaP;
system.setPressure(nyPres);
tpFlash.run();
} while (error > tolerance && iterations < maxIter);
}
Note the sign conventions used in NeqSim for thermodynamic derivatives:
getdVdTpn() returns $-\left(\frac{\partial V}{\partial T}\right)_P$getdVdPtn() returns $\left(\frac{\partial V}{\partial P}\right)_T$Calculate the bubble point (onset of vaporization) at a given temperature or pressure.
Temperature flash (find T at given P):
void bubblePointTemperatureFlash()
Pressure flash (find P at given T):
void bubblePointPressureFlash()
void bubblePointPressureFlash(boolean derivatives) // Include dP/dT, dP/dx
Example:
SystemInterface fluid = new SystemSrkEos(298.15, 10.0);
fluid.addComponent("propane", 0.5);
fluid.addComponent("n-butane", 0.5);
fluid.setMixingRule("classic");
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
// Find bubble point pressure at 25°C
fluid.setTemperature(298.15, "K");
ops.bubblePointPressureFlash();
System.out.println("Bubble point pressure: " + fluid.getPressure("bara") + " bara");
// Find bubble point temperature at 5 bar
fluid.setPressure(5.0, "bara");
ops.bubblePointTemperatureFlash();
System.out.println("Bubble point temperature: " + fluid.getTemperature("C") + " °C");
Calculate the dew point (onset of condensation) at a given temperature or pressure.
Temperature flash:
void dewPointTemperatureFlash()
void dewPointTemperatureFlash(boolean derivatives)
Pressure flash:
void dewPointPressureFlash()
Example:
// Natural gas dew point
SystemInterface gas = new SystemSrkEos(298.15, 50.0);
gas.addComponent("methane", 0.85);
gas.addComponent("ethane", 0.08);
gas.addComponent("propane", 0.04);
gas.addComponent("n-butane", 0.02);
gas.addComponent("n-pentane", 0.01);
gas.setMixingRule("classic");
ThermodynamicOperations ops = new ThermodynamicOperations(gas);
// Find dew point at 50 bar
ops.dewPointTemperatureFlash();
System.out.println("Hydrocarbon dew point: " + gas.getTemperature("C") + " °C");
Calculate the water dew point (onset of water condensation).
Methods:
void waterDewPointTemperatureFlash()
void waterDewPointTemperatureMultiphaseFlash() // For complex systems
Example:
SystemInterface wetGas = new SystemSrkCPAstatoil(298.15, 80.0);
wetGas.addComponent("methane", 0.95);
wetGas.addComponent("water", 0.05);
wetGas.setMixingRule(10);
ThermodynamicOperations ops = new ThermodynamicOperations(wetGas);
ops.waterDewPointTemperatureFlash();
System.out.println("Water dew point: " + wetGas.getTemperature("C") + " °C");
Calculate the cricondentherm (maximum temperature for two-phase region).
void dewPointPressureFlashHC()
For systems with potential solid precipitation (wax, ice, hydrates).
void TPSolidflash()
void PHsolidFlash(double Hspec)
void freezingPointTemperatureFlash()
Example - Wax precipitation:
SystemInterface oil = new SystemSrkEos(320.0, 10.0);
oil.addComponent("n-C20", 0.1);
oil.addComponent("n-C10", 0.9);
oil.setSolidPhaseCheck("n-C20");
oil.setMixingRule("classic");
ThermodynamicOperations ops = new ThermodynamicOperations(oil);
ops.freezingPointTemperatureFlash();
System.out.println("Wax appearance temperature: " + oil.getTemperature("C") + " °C");
Find the critical point of a mixture.
void criticalPointFlash()
Example:
SystemInterface mix = new SystemSrkEos(300.0, 50.0);
mix.addComponent("methane", 0.7);
mix.addComponent("ethane", 0.3);
mix.setMixingRule("classic");
ThermodynamicOperations ops = new ThermodynamicOperations(mix);
ops.criticalPointFlash();
System.out.println("Critical T: " + mix.getTemperature("K") + " K");
System.out.println("Critical P: " + mix.getPressure("bara") + " bara");
Calculate composition variation with depth (gravitational segregation).
SystemInterface TPgradientFlash(double height, double temperature)
Parameters:
height: Depth in meterstemperature: Temperature at depth in KelvinFind conditions for a specified phase fraction.
void constantPhaseFractionPressureFlash(double fraction) // Find P at given vapor fraction
void constantPhaseFractionTemperatureFlash(double fraction) // Find T at given vapor fraction
void TVfractionFlash(double Vfraction) // Volume fraction based
For high-accuracy calculations, NeqSim provides flash methods using reference equations of state.
For natural gas systems with GERG-2008 accuracy:
void PHflashGERG2008(double Hspec)
void PSflashGERG2008(double Sspec)
Example:
SystemInterface gas = new SystemGERG2008Eos(280.0, 100.0);
gas.addComponent("methane", 0.9);
gas.addComponent("ethane", 0.06);
gas.addComponent("CO2", 0.04);
ThermodynamicOperations ops = new ThermodynamicOperations(gas);
ops.TPflash();
double H = gas.getEnthalpy("J");
gas.setPressure(50.0);
ops.PHflashGERG2008(H); // High-accuracy isenthalpic flash
For pure hydrogen systems:
void PHflashLeachman(double Hspec)
void PSflashLeachman(double Sspec)
For pure CO₂ systems:
void PHflashVega(double Hspec)
void PSflashVega(double Sspec)
NeqSim provides comprehensive calculations for gas hydrate formation, including multi-phase equilibrium with hydrate, inhibitor effects, and cavity occupancy calculations.
📚 Detailed Documentation:
- Hydrate Models - Thermodynamic models (vdWP, CPA, PVTsim)
- Hydrate Flash Operations - Complete flash API
Calculate phase equilibrium including hydrate at given T and P:
SystemInterface fluid = new SystemSrkCPAstatoil(273.15 + 5.0, 100.0);
fluid.addComponent("methane", 0.85);
fluid.addComponent("ethane", 0.08);
fluid.addComponent("propane", 0.04);
fluid.addComponent("water", 0.03);
fluid.setMixingRule(10);
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.hydrateTPflash();
// Check phases: GAS, AQUEOUS, HYDRATE
fluid.prettyPrint();
For systems with trace water where all water can be consumed by hydrate:
SystemInterface fluid = new SystemSrkCPAstatoil(273.15 - 15.0, 250.0);
fluid.addComponent("methane", 0.9998);
fluid.addComponent("water", 0.0002); // 200 ppm water
fluid.setMixingRule(10);
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.gasHydrateTPflash(); // Targets gas-hydrate equilibrium
// Result: GAS + HYDRATE phases (no AQUEOUS)
void hydrateFormationTemperature()
void hydrateFormationTemperature(double initialGuess)
void hydrateFormationTemperature(int structure) // 0=ice, 1=sI, 2=sII
void hydrateFormationPressure()
void hydrateFormationPressure(int structure)
void hydrateInhibitorConcentration(String inhibitor, double targetT)
void hydrateInhibitorConcentrationSet(String inhibitor, double wtFrac)
Supported inhibitors: MEG, TEG, methanol, ethanol
SystemInterface gas = new SystemSrkCPAstatoil(280.0, 100.0);
gas.addComponent("methane", 0.9);
gas.addComponent("ethane", 0.05);
gas.addComponent("CO2", 0.02);
gas.addComponent("water", 0.03);
gas.setMixingRule(10);
gas.setHydrateCheck(true);
ThermodynamicOperations ops = new ThermodynamicOperations(gas);
// Calculate hydrate formation temperature
ops.hydrateFormationTemperature();
System.out.println("Hydrate formation T: " + gas.getTemperature("C") + " °C");
// Check if hydrate forms at 5°C
gas.setTemperature(273.15 + 5.0);
ops.hydrateTPflash();
if (gas.hasHydratePhase()) {
System.out.println("Hydrate fraction: " + gas.getBeta(PhaseType.HYDRATE));
}
SystemInterface fluid = new SystemSrkCPAstatoil(273.15 + 4.0, 100.0);
fluid.addComponent("methane", 0.70);
fluid.addComponent("ethane", 0.08);
fluid.addComponent("propane", 0.05);
fluid.addComponent("n-hexane", 0.02);
fluid.addComponent("n-heptane", 0.05);
fluid.addComponent("water", 0.10);
fluid.setMixingRule(10);
ops.hydrateTPflash();
// Phases: GAS, OIL, AQUEOUS, HYDRATE
Most flash methods accept unit specifications for flexibility:
| Unit | Description |
|---|---|
J |
Joules (total) |
J/mol |
Joules per mole |
J/kg |
Joules per kilogram |
kJ/kg |
Kilojoules per kilogram |
| Unit | Description |
|---|---|
J/K |
Joules per Kelvin (total) |
J/molK |
Joules per mole-Kelvin |
J/kgK |
Joules per kg-Kelvin |
kJ/kgK |
Kilojoules per kg-Kelvin |
| Unit | Description |
|---|---|
m3 |
Cubic meters |
| (default) | cm³ for internal methods |
Flash calculations can fail if:
Best practice:
try {
ops.dewPointTemperatureFlash();
} catch (IsNaNException e) {
System.err.println("No dew point found: " + e.getMessage());
}
// Check for valid result
if (Double.isNaN(fluid.getTemperature())) {
System.err.println("Flash calculation did not converge");
}
Always initialize before flashing:
fluid.init(0); // Basic initialization
ops.TPflash();
fluid.init(3); // Full thermodynamic initialization after flash
Reuse ThermodynamicOperations:
// Good - single instance
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
for (double T : temperatures) {
fluid.setTemperature(T, "K");
ops.TPflash();
}
Clone fluids for independent calculations:
SystemInterface fluid2 = fluid.clone();
ThermodynamicOperations ops2 = new ThermodynamicOperations(fluid2);
Check phase existence before accessing:
if (fluid.hasPhaseType("gas")) {
double gasRho = fluid.getPhase("gas").getDensity("kg/m3");
}
Use appropriate EoS for the application:
| Method | Input Specs | Output | Use Case |
|---|---|---|---|
TPflash() |
T, P | phases, compositions | General equilibrium |
PHflash(H) |
P, H | T, phases | Valves, heat exchangers |
PSflash(S) |
P, S | T, phases | Compressors, turbines |
PUflash(U) |
P, U | T, phases | Energy balance |
TVflash(V) |
T, V | P, phases | Fixed-volume systems |
TSflash(S) |
T, S | P, phases | Process analysis |
VHflash(V,H) |
V, H | T, P, phases | Dynamic simulation |
VUflash(V,U) |
V, U | T, P, phases | Blowdown, depressurization |
VSflash(V,S) |
V, S | T, P, phases | Isentropic vessel |
bubblePointTemperatureFlash() |
P | T_bubble | Evaporator design |
bubblePointPressureFlash() |
T | P_bubble | Vapor pressure |
dewPointTemperatureFlash() |
P | T_dew | Condenser design |
dewPointPressureFlash() |
T | P_dew | Dew point control |
waterDewPointTemperatureFlash() |
P | T_wdp | Gas dehydration |
hydrateFormationTemperature() |
P | T_hydrate | Hydrate prevention |
criticalPointFlash() |
- | T_c, P_c | Mixture critical |
NeqSim's flash algorithms are exercised heavily in the JUnit suite under src/test/java/neqsim/thermodynamicoperations/flashops. The tests document how the solvers are configured and what outputs they must reproduce, giving a reproducible view of the underlying theory.
RachfordRiceTest switches between the Nielsen (2023) and Michelsen (2001) variants of the Rachford–Rice solver to verify that all implementations converge to the same vapor fraction for the same K-values and overall composition.【F:src/test/java/neqsim/thermodynamicoperations/flashops/RachfordRiceTest.java†L14-L39】 The test uses a binary mixture with z=[0.7, 0.3] and K=[2.0, 0.01] and asserts a vapor fraction ((\beta)) of 0.40707, which is the root of the classic balance equation:
[ \sum_i z_i \frac{K_i - 1}{1 + \beta (K_i - 1)} = 0 ]
The converged solution satisfies material balance between vapor and liquid while honoring the phase equilibrium ratios supplied by the K-values. Switching RachfordRice.setMethod(...) in the test demonstrates that NeqSim exposes multiple solver strategies for the same equation without altering the target root.【F:src/test/java/neqsim/thermodynamicoperations/flashops/RachfordRiceTest.java†L21-L33】 When modeling your own flashes, choose a method that matches your numerical preferences; the test shows that the default and named methods must agree on the fundamental solution.
TPFlashTest configures multicomponent systems with cubic equations of state (Peng–Robinson, UMR-PRU-MC, SRK-CPA) and validates both phase splits and energy properties after a TPflash() call. The tests cover low and high pressure regimes, multi-phase checks, and heavy pseudo-component handling.【F:src/test/java/neqsim/thermodynamicoperations/flashops/TPFlashTest.java†L19-L140】 Assertions include vapor fraction (getBeta()), number of phases, and total enthalpy, confirming that the flash calculation preserves the combined internal energy and molar balance implied by the Rachford–Rice solution and the chosen EOS.
To mirror the test configuration:
SystemInterface instance with the appropriate EOS and reference conditions.setMixingRule("classic") or numeric variants) and enable multiphase detection if solids or water are expected.new ThermodynamicOperations(system).TPflash().The enthalpy checks in testRun2 and testRun3 highlight that the flash solution must satisfy both material balance and the caloric EOS relationships at the specified state points.【F:src/test/java/neqsim/thermodynamicoperations/flashops/TPFlashTest.java†L43-L82】 If discrepancies appear in your own models, align your setup with the tested recipe before exploring alternative property packages.
QfuncFlashTest provides comprehensive testing for state-function based flash calculations following Michelsen's (1999) Q-function methodology. The test class validates multiple flash specifications:
These flashes solve for one unknown (T or P) given a state function constraint:
| Test | Flash Type | Specification | Validates |
|---|---|---|---|
testTSFlash_* |
TSflash | Temperature, Entropy | Pressure convergence |
testTHFlash_* |
THflash | Temperature, Enthalpy | Pressure convergence |
testTUFlash_* |
TUflash | Temperature, Internal Energy | Pressure convergence |
testTVFlash_* |
TVflash | Temperature, Volume | Pressure convergence |
testPVFlash_* |
PVflash | Pressure, Volume | Temperature convergence |
These flashes solve for both T and P simultaneously:
| Test | Flash Type | Specification | Validates |
|---|---|---|---|
testVUFlash_* |
VUflash | Volume, Internal Energy | T, P convergence |
testVHFlash_* |
VHflash | Volume, Enthalpy | T, P convergence |
testVSFlash_* |
VSflash | Volume, Entropy | T, P convergence |
Each Q-function flash test follows this pattern:
Example from the test suite:
// Store enthalpy at initial conditions
ops.TPflash();
double targetH = system.getEnthalpy();
// Change temperature (perturb the system)
system.setTemperature(newTemperature);
// Flash should find pressure that recovers original enthalpy
ops.THflash(targetH);
assertEquals(targetH, system.getEnthalpy(), tolerance);
The Q-function flashes use analytical derivatives computed via system.init(3):
getdVdTpn() returns $-(\partial V/\partial T)_P$getdVdPtn() returns $(\partial V/\partial P)_T$These are combined to form the Newton iteration Jacobians for each flash type.
Michelsen, M.L. (1999). "State function based flash specifications." Fluid Phase Equilibria, 158-160, 617-626.
Thermodynamic operations execute equilibrium and property tasks using a configured fluid. Most workflows create a ThermodynamicOperations object once and reuse it for multiple calls.
TPflash(): Calculates phase split at specified temperature and pressure. Run initProperties() afterward for density/viscosity.PHflash(P, H) and PSflash(P, S): Solve for temperature/phase split given enthalpy or entropy targets—useful for compressors and turbines.TVflash(T, V): Volume-constrained flash for fixed-volume cells.UVflash(U, V): Energy- and volume-constrained flash for transient simulations.ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
double vaporFraction = fluid.getBeta();
ops.calcPTphaseEnvelope() fills critical point, cricondenbar, cricondentherm, and two-phase boundary.hydrateCheck(true) before calling ops.hydrateFormationTemperature(pressure).ops.calcSolidFormationTemperature().After flashes, properties are available on each phase:
getDensity() or getNumberOfMoles() for molar/volume properties.getEnthalpy(), getEntropy(), and getCp() for energy balances.getViscosity(), getThermalConductivity(), and getInterfacialTension() for transport analyses.SystemFurstElectrolyteEos or SystemElectrolyteCPAstatoil, add salts/acids, and enable charge balance. Use ops.electrolyteFlash() for salt precipitation studies.ops.calcChemicalEquilibrium() to couple them into flashes.fluid.init(3)) after changing temperature, pressure, or composition significantly.ThermodynamicOperations instance when sweeping conditions to avoid rebuilding internal caches.The Temperature-Pressure (TP) flash calculation is a fundamental operation in chemical engineering thermodynamics. Given a mixture composition, temperature, and pressure, the TP flash determines:
NeqSim implements the classical Michelsen flash algorithm with stability analysis, as described in the landmark work Thermodynamic Models: Fundamentals and Computational Aspects (Michelsen & Mollerup, 2007). The implementation supports:
The following flowchart shows the complete two-phase flash algorithm as implemented in TPflash.run():
╔═══════════════════════════════════════════════════════════════════════════════╗
║ TPflash.run() ALGORITHM FLOW ║
╚═══════════════════════════════════════════════════════════════════════════════╝
┌─────────────────────────────────────────────────────────────────────────────────┐
│ STEP 1: INITIALIZATION │
├─────────────────────────────────────────────────────────────────────────────────┤
│ • system.init(0) - Initialize molar composition │
│ • system.init(1) - Calculate thermodynamic properties │
│ • Determine minimum Gibbs energy phase (gas or liquid) │
│ • Store reference: minGibsPhaseLogZ[i], minGibsLogFugCoef[i] │
│ • Handle single-component or single-phase systems → return early │
└─────────────────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────────────────┐
│ STEP 2: INITIAL K-VALUES (Wilson Equation) │
├─────────────────────────────────────────────────────────────────────────────────┤
│ K-values are pre-initialized using Wilson's correlation: │
│ │
│ Ki = (Pc,i / P) × exp[5.373(1 + ωi)(1 - Tc,i/T)] │
│ │
│ • Solve Rachford-Rice equation to get initial β │
│ • Calculate initial x, y from material balance │
│ • system.init(1) - Update fugacity coefficients │
└─────────────────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────────────────┐
│ STEP 3: INITIAL SSI (3 iterations) │
├─────────────────────────────────────────────────────────────────────────────────┤
│ IF β is at bounds (all liquid or all vapor): │
│ • Reset β = 0.5 │
│ • Run 1 sucsSubs() iteration │
│ │
│ FOR k = 0 to 2: (exactly 3 preliminary SSI iterations) │
│ • IF β is in valid range (not at bounds): │
│ - Run sucsSubs() iteration │
│ - IF Gibbs energy decreased significantly → break early │
└─────────────────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────────────────┐
│ STEP 4: QUICK STABILITY CHECK (TPD-based) │
├─────────────────────────────────────────────────────────────────────────────────┤
│ Calculate tangent plane distances for both phases: │
│ │
│ tpdy = Σ yi × [ln(φi^V) + ln(yi) - ln(zi) - ln(φi^ref)] │
│ tpdx = Σ xi × [ln(φi^L) + ln(xi) - ln(zi) - ln(φi^ref)] │
│ dgonRT = β × tpdy + (1-β) × tpdx │
│ │
│ IF dgonRT > 0 AND tpdx > 0 AND tpdy > 0: │
│ → Single phase is stable │
│ → Run full stability analysis if checkStability() enabled │
│ → If multiPhaseCheck: delegate to TPmultiflash │
│ → return │
│ │
│ ELSE IF tpdx < 0 or tpdy < 0: │
│ → Re-estimate K-values from fugacity ratios │
│ → Continue to main iteration loop │
└─────────────────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────────────────┐
│ STEP 5: PHASE TYPE DETERMINATION │
├─────────────────────────────────────────────────────────────────────────────────┤
│ Compare Gibbs energy of phase 0 as GAS vs LIQUID: │
│ • Calculate G(gas), G(liquid) │
│ • Set phase type to lower Gibbs energy option │
└─────────────────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────────────────┐
│ STEP 6: MAIN ITERATION LOOP │
├─────────────────────────────────────────────────────────────────────────────────┤
│ Parameters: │
│ • accelerateInterval = 7 (use DEM every 7 iterations) │
│ • newtonLimit = 20 (switch to Newton after 20 SSI iterations) │
│ • maxNumberOfIterations = 50 (default) │
│ • convergence tolerance = 1e-10 │
│ │
│ DO (outer loop for chemical systems): │
│ │ iterations = 0 │
│ │ DO (inner loop): │
│ │ │ iterations++ │
│ │ │ │
│ │ │ IF iterations < 20 (or chemical system, or no fugacity derivatives): │
│ │ │ │ IF timeFromLastGibbsFail > 6 AND iterations % 7 == 0: │
│ │ │ │ → accselerateSucsSubs() [DEM acceleration] │
│ │ │ │ ELSE: │
│ │ │ │ → sucsSubs() [standard SSI] │
│ │ │ │ │
│ │ │ ELSE IF iterations >= 20: │
│ │ │ │ IF iterations == 20: │
│ │ │ │ → Create SysNewtonRhapsonTPflash solver │
│ │ │ │ → secondOrderSolver.solve() [Newton-Raphson] │
│ │ │ │ │
│ │ │ Check Gibbs energy: │
│ │ │ IF G increased OR β at bounds: │
│ │ │ → resetK() [restore previous K-values] │
│ │ │ → timeFromLastGibbsFail = 0 │
│ │ │ ELSE: │
│ │ │ → setNewK() [store current K-values] │
│ │ │ → timeFromLastGibbsFail++ │
│ │ │ │
│ │ WHILE (deviation > 1e-10 AND iterations < 50) │
│ │ │
│ │ IF chemical system: │
│ │ → Solve chemical equilibrium in liquid phase │
│ │ → Calculate chemical equilibrium deviation │
│ │ │
│ WHILE (chemdev > 1e-6 AND totiter < 300) OR (chemical system AND totiter < 2) │
└─────────────────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────────────────┐
│ STEP 7: POST-PROCESSING │
├─────────────────────────────────────────────────────────────────────────────────┤
│ IF multiPhaseCheck enabled: │
│ → Delegate to TPmultiflash for stability analysis and phase split │
│ ELSE: │
│ → Final phase type check (gas vs liquid Gibbs energy) │
│ │
│ IF solidCheck enabled: │
│ → Run solid phase flash │
│ │
│ Remove phases with β < βmin │
│ Order phases by density │
│ Final system.init(1) │
│ │
│ IF chemical system: │
│ → Final chemical equilibrium solve in aqueous/liquid phases │
└─────────────────────────────────────────────────────────────────────────────────┘
| Parameter | Value | Description |
|---|---|---|
phaseFractionMinimumLimit |
~1e-12 | Minimum allowed phase fraction |
| Initial SSI iterations | 3 | Preliminary iterations before stability check |
accelerateInterval |
7 | Apply DEM every 7th iteration |
newtonLimit |
20 | Switch to Newton-Raphson after 20 SSI iterations |
maxNumberOfIterations |
50 | Maximum iterations per convergence loop |
| Convergence tolerance | 1e-10 | Deviation threshold for K-value convergence |
| Gibbs increase tolerance | 1e-8 | Relative increase that triggers K-reset |
Consider a mixture of $N_c$ components with overall mole fractions $z_i$ at temperature $T$ and pressure $P$. The two-phase flash problem seeks the vapor fraction $\beta$ (also called $V$ for vapor) and the mole fractions in each phase ($x_i$ for liquid, $y_i$ for vapor) such that thermodynamic equilibrium is satisfied.
Equilibrium Conditions:
At equilibrium, the fugacity of each component must be equal in all phases:
$$f_i^V = f_i^L \quad \text{for } i = 1, 2, \ldots, N_c$$
This can be rewritten using fugacity coefficients $\phi_i$:
$$y_i \phi_i^V P = x_i \phi_i^L P$$
Defining the equilibrium ratio (K-factor):
$$K_i = \frac{y_i}{x_i} = \frac{\phi_i^L}{\phi_i^V}$$
Material Balance:
The overall material balance constrains the phase compositions:
$$z_i = \beta y_i + (1 - \beta) x_i$$
Combining with the K-factor definition:
$$y_i = \frac{K_i z_i}{1 + \beta(K_i - 1)}$$
$$x_i = \frac{z_i}{1 + \beta(K_i - 1)}$$
The vapor fraction $\beta$ is found by solving the Rachford-Rice equation, derived from the constraint $\sum_i y_i = \sum_i x_i = 1$:
$$g(\beta) = \sum_{i=1}^{N_c} \frac{z_i (K_i - 1)}{1 + \beta(K_i - 1)} = 0$$
Properties of $g(\beta)$:
Where the bounds ensure positive mole fractions:
$$\beta_{\min} = \max_i \left( \frac{K_i z_i - 1}{K_i - 1} \right) \quad \text{for } K_i > 1$$
$$\beta_{\max} = \min_i \left( \frac{1 - z_i}{1 - K_i} \right) \quad \text{for } K_i < 1$$
Derivative for Newton's Method:
$$\frac{dg}{d\beta} = -\sum_{i=1}^{N_c} \frac{z_i (K_i - 1)^2}{[1 + \beta(K_i - 1)]^2}$$
NeqSim implements two Rachford-Rice solvers:
The method can be selected via:
RachfordRice.setMethod("Nielsen2023"); // or "Michelsen2001"
See RachfordRice.java for implementation details.
The standard approach to solve the two-phase flash is Successive Substitution Iteration (SSI), which iteratively updates K-factors until convergence.
Algorithm:
Initialize K-factors using Wilson's correlation: $$K_i^{(0)} = \frac{P_{c,i}}{P} \exp\left[ 5.373(1 + \omega_i)\left(1 - \frac{T_{c,i}}{T}\right) \right]$$
Solve Rachford-Rice to obtain $\beta$
Calculate phase compositions using material balance: $$x_i = \frac{z_i}{1 + \beta(K_i - 1)}, \quad y_i = K_i x_i$$
Update K-factors from fugacity coefficients: $$K_i^{(n+1)} = \frac{\phi_i^L(x, T, P)}{\phi_i^V(y, T, P)}$$
Check convergence: $$\sum_i \left| \ln K_i^{(n+1)} - \ln K_i^{(n)} \right| < \epsilon$$
If not converged, return to step 2.
NeqSim Implementation:
// From TPflash.java - sucsSubs() method
public void sucsSubs() {
for (i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) {
Kold = system.getPhase(0).getComponent(i).getK();
system.getPhase(0).getComponent(i).setK(
system.getPhase(1).getComponent(i).getFugacityCoefficient()
/ system.getPhase(0).getComponent(i).getFugacityCoefficient() * presdiff);
deviation += Math.abs(Math.log(system.getPhase(0).getComponent(i).getK())
- Math.log(Kold));
}
RachfordRice rachfordRice = new RachfordRice();
system.setBeta(rachfordRice.calcBeta(system.getKvector(), system.getzvector()));
system.calc_x_y();
system.init(1);
}
Near the critical point or for systems with similar K-factors, standard SSI converges slowly. NeqSim implements the Dominant Eigenvalue Method (DEM) for acceleration.
Theory (Michelsen, 1982):
The convergence of SSI is limited by the dominant eigenvalue of the iteration matrix. The acceleration factor $\lambda$ is estimated from the last three iterates:
$$\lambda = \frac{\sum_i (\Delta \ln K_i^{(n)}) \cdot (\Delta \ln K_i^{(n-1)})}{\sum_i (\Delta \ln K_i^{(n-1)})^2}$$
Where $\Delta \ln K_i^{(n)} = \ln K_i^{(n)} - \ln K_i^{(n-1)}$.
Accelerated Update:
$$\ln K_i^{(n+1)} = \ln K_i^{(n)} + \frac{\lambda}{1 - \lambda} \Delta \ln K_i^{(n)}$$
NeqSim Implementation:
// From TPflash.java - accselerateSucsSubs() method
public void accselerateSucsSubs() {
double prod1 = 0.0, prod2 = 0.0;
for (i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) {
prod1 += oldDeltalnK[i] * oldoldDeltalnK[i];
prod2 += oldoldDeltalnK[i] * oldoldDeltalnK[i];
}
double lambda = prod1 / prod2;
for (i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) {
lnK[i] += lambda / (1.0 - lambda) * deltalnK[i];
system.getPhase(0).getComponent(i).setK(Math.exp(lnK[i]));
}
// ... Rachford-Rice and update
}
For difficult systems or near-critical conditions, NeqSim employs a second-order Newton-Raphson method using fugacity derivatives.
Formulation:
Define the objective function vector $\mathbf{f}$ with components:
$$f_i = \ln \left( \frac{y_i \phi_i^V}{x_i \phi_i^L} \right) = 0$$
The solution is found by iterating:
$$\mathbf{u}^{(n+1)} = \mathbf{u}^{(n)} - \mathbf{J}^{-1} \mathbf{f}(\mathbf{u}^{(n)})$$
Where $\mathbf{u} = (\beta y_1, \beta y_2, \ldots, \beta y_{N_c})^T$ and the Jacobian $\mathbf{J}$ includes composition derivatives of fugacity coefficients:
$$J_{ij} = \frac{\partial f_i}{\partial u_j} = \frac{1}{\beta}\left(\frac{\delta_{ij}}{x_i} - 1 + \frac{\partial \ln \phi_i^V}{\partial x_j}\right) + \frac{1}{1-\beta}\left(\frac{\delta_{ij}}{y_i} - 1 + \frac{\partial \ln \phi_i^L}{\partial y_j}\right)$$
NeqSim Implementation:
See SysNewtonRhapsonTPflash.java for the full implementation.
A phase is thermodynamically stable if it has the lowest Gibbs energy among all possible phase configurations. The stability analysis determines whether a given phase will spontaneously split into multiple phases.
Gibbs Energy Criterion:
For a single-phase mixture with mole numbers $\mathbf{n}$, the mixture is stable if and only if the Gibbs energy $G(\mathbf{n})$ is at its global minimum. This is equivalent to requiring that no other phase can exist with lower chemical potential.
Michelsen (1982) introduced the Tangent Plane Distance (TPD) function for stability analysis. Consider a reference phase with composition $\mathbf{z}$ and a trial phase with composition $\mathbf{w}$.
TPD Definition:
$$\text{TPD}(\mathbf{w}) = \sum_{i=1}^{N_c} w_i \left[ \mu_i(\mathbf{w}) - \mu_i(\mathbf{z}) \right]$$
In terms of fugacity coefficients:
$$\text{TPD}(\mathbf{w}) = \sum_{i=1}^{N_c} w_i \left[ \ln w_i + \ln \phi_i(\mathbf{w}) - d_i \right]$$
Where: $$d_i = \ln z_i + \ln \phi_i(\mathbf{z})$$
Stationary Point Condition:
At a stationary point of TPD, the gradient is zero:
$$\frac{\partial \text{TPD}}{\partial w_i} = \ln w_i + \ln \phi_i(\mathbf{w}) - d_i + 1 = 0$$
Using the substitution $W_i = \exp(\ln w_i)$, define:
$$\ln W_i = d_i - \ln \phi_i(\mathbf{w})$$
Stability Test:
The reduced TPD at a stationary point is:
$$\text{tm} = 1 - \sum_{i=1}^{N_c} W_i$$
Criterion:
NeqSim implements a hybrid algorithm combining successive substitution with Newton's method:
Phase 1: Successive Substitution
Initialize trial phase with pure component or Wilson K-factor estimate: $$W_i^{(0)} = z_i \cdot K_i \quad \text{(vapor-like)} \quad \text{or} \quad W_i^{(0)} = z_i / K_i \quad \text{(liquid-like)}$$
Iterate: $$\ln W_i^{(n+1)} = d_i - \ln \phi_i(\mathbf{w}^{(n)})$$
Where $w_i = W_i / \sum_j W_j$ (normalized composition)
Accelerate using DEM (every 7 iterations): $$\lambda = \frac{\sum_i \Delta(\ln W_i)^{(n)} \cdot \Delta(\ln W_i)^{(n-1)}}{\sum_i [\Delta(\ln W_i)^{(n-1)}]^2}$$ $$\ln W_i^{(n+1)} = \ln W_i^{(n)} + \frac{\lambda}{1-\lambda} \Delta(\ln W_i)^{(n)}$$
Continue until $\sum_i |\ln W_i^{(n+1)} - \ln W_i^{(n)}| < \epsilon$
Phase 2: Second-Order Newton (if needed)
For difficult cases (iteration > 150), switch to Newton's method using the variable $\alpha_i = 2\sqrt{W_i}$:
Objective function: $$F_i = \sqrt{W_i} \left[ \ln W_i + \ln \phi_i(\mathbf{w}) - d_i \right]$$
Jacobian: $$\frac{\partial F_i}{\partial \alpha_j} = \delta_{ij} + \sqrt{W_i W_j} \frac{\partial \ln \phi_i}{\partial n_j}$$
Newton step: $$\boldsymbol{\alpha}^{(n+1)} = \boldsymbol{\alpha}^{(n)} - (\mathbf{I} + \mathbf{H})^{-1} \mathbf{F}$$
NeqSim Implementation:
// From TPmultiflash.java - stabilityAnalysis() method
// Successive substitution phase
for (int i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) {
logWi[i] = d[i] - clonedSystem.getPhase(1).getComponent(i).getLogFugacityCoefficient();
Wi[j][i] = safeExp(logWi[i]);
}
// Check convergence and compute tm
tm[j] = 1.0;
for (int i = 0; i < system.getPhase(1).getNumberOfComponents(); i++) {
tm[j] -= safeExp(logWi[i]);
}
// Phase is unstable if tm < -1e-8
if (tm[j] < -1e-8) {
system.addPhase(); // Add new phase
// Set composition from stationary point
}
Trivial Solution Check:
To avoid converging to trivial solutions (identical to existing phases):
$$\sum_i |w_i - x_i^{\text{existing}}| < \epsilon_{\text{trivial}}$$
If the trial composition is too close to an existing phase, it is rejected.
When system.setMultiPhaseCheck(true) is called, NeqSim uses the TPmultiflash class which extends the basic two-phase flash with comprehensive stability analysis and support for three or more equilibrium phases.
╔═══════════════════════════════════════════════════════════════════════════════╗
║ TPmultiflash.run() ALGORITHM FLOW ║
╚═══════════════════════════════════════════════════════════════════════════════╝
┌─────────────────────────────────────────────────────────────────────────────────┐
│ STEP 1: ELECTROLYTE PREPROCESSING │
├─────────────────────────────────────────────────────────────────────────────────┤
│ IF system is chemical/electrolyte: │
│ • Store ionic component compositions: ionicZ[i] = z[i] for ions │
│ • Temporarily set ion z = 1e-100 (remove from stability analysis) │
│ • hasIons = true │
│ • system.init(1) - Recalculate properties without ions │
└─────────────────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────────────────┐
│ STEP 2: PRIMARY STABILITY ANALYSIS │
├─────────────────────────────────────────────────────────────────────────────────┤
│ IF doStabilityAnalysis == true: │
│ → stabilityAnalysis() [see detailed flow below] │
│ → Sets multiPhaseTest = true if unstable phase found │
│ → Adds new phase with composition from stationary point │
└─────────────────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────────────────┐
│ STEP 3: HEURISTIC PHASE SEEDING │
├─────────────────────────────────────────────────────────────────────────────────┤
│ IF NOT multiPhaseTest AND seedAdditionalPhaseFromFeed(): │
│ → Add gas phase seeded from feed composition │
│ → multiPhaseTest = true │
│ │
│ IF seedHydrocarbonLiquidFromFeed(): │
│ → Add hydrocarbon liquid phase if conditions met │
│ → multiPhaseTest = true │
└─────────────────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────────────────┐
│ STEP 4: ION RESTORATION (Electrolyte Systems) │
├─────────────────────────────────────────────────────────────────────────────────┤
│ IF hasIons: │
│ FOR each ionic component: │
│ • Restore z[i] = ionicZ[i] in all phases │
│ • IF phase is AQUEOUS: set x[i] = ionicZ[i] │
│ • ELSE: set x[i] = 1e-50 (ions only in aqueous) │
│ • Normalize all phases │
│ • system.init(1) │
└─────────────────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────────────────┐
│ STEP 5: INITIAL CHEMICAL EQUILIBRIUM │
├─────────────────────────────────────────────────────────────────────────────────┤
│ IF chemical system AND has aqueous phase: │
│ → solveChemEq(aqueousPhaseNumber, 0) [stoichiometric] │
│ → solveChemEq(aqueousPhaseNumber, 1) [full Newton] │
└─────────────────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────────────────┐
│ STEP 6: MULTIPHASE SPLIT CALCULATION │
├─────────────────────────────────────────────────────────────────────────────────┤
│ IF multiPhaseTest == true: │
│ maxerr = 1e-12 │
│ │
│ DO (outer loop - chemical equilibrium): │
│ │ iterOut++ │
│ │ │
│ │ IF chemical system with aqueous phase: │
│ │ → Solve chemical equilibrium │
│ │ → Calculate chemical deviation │
│ │ │
│ │ setDoubleArrays() [allocate Q-function arrays] │
│ │ iterations = 0 │
│ │ │
│ │ DO (inner loop - Q-function minimization): │
│ │ │ iterations++ │
│ │ │ oldDiff = diff │
│ │ │ diff = solveBeta() [Newton step on Q-function] │
│ │ │ │
│ │ │ IF iterations % 50 == 0: │
│ │ │ maxerr *= 100 [relax tolerance] │
│ │ │ │
│ │ WHILE (diff > maxerr AND NOT removePhase │
│ │ AND (diff < oldDiff OR iterations < 50) │
│ │ AND iterations < 200) │
│ │ │
│ WHILE (|chemdev| > 1e-10 AND iterOut < 100) │
│ OR (iterOut < 3 AND chemical AND aqueous) │
└─────────────────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────────────────┐
│ STEP 7: AQUEOUS PHASE SEEDING (if water present but no aqueous) │
├─────────────────────────────────────────────────────────────────────────────────┤
│ IF has water component AND NOT aqueousPhaseSeedAttempted │
│ AND multiPhaseCheck AND NOT hasAqueousPhase: │
│ │
│ IF waterZ > 1e-6 AND numberOfPhases < 3: │
│ → Add new phase │
│ → Set phase type = AQUEOUS │
│ → Initialize with water-rich composition │
│ → Set β = max(1e-5, 10 × βmin) │
│ → multiPhaseTest = true │
│ → aqueousPhaseSeedAttempted = true │
└─────────────────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────────────────┐
│ STEP 8: SINGLE AQUEOUS PHASE ENFORCEMENT (Electrolytes) │
├─────────────────────────────────────────────────────────────────────────────────┤
│ IF chemical system: │
│ → ensureSingleAqueousPhase() │
│ → Reclassify extra "aqueous" phases as OIL │
│ → Move ions to the true aqueous phase │
└─────────────────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────────────────┐
│ STEP 9: PHASE CLEANUP │
├─────────────────────────────────────────────────────────────────────────────────┤
│ Remove negligible phases: │
│ FOR each phase: │
│ IF β < 1.1 × βmin: │
│ → removePhaseKeepTotalComposition() │
│ → hasRemovedPhase = true │
│ │
│ Detect trivial solutions (phases with same density): │
│ FOR each pair of phases (i, j): │
│ IF |ρi - ρj| < 1.1e-5: │
│ → Remove phase j │
│ → hasRemovedPhase = true │
└─────────────────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────────────────┐
│ STEP 10: RECURSIVE STABILITY CHECK │
├─────────────────────────────────────────────────────────────────────────────────┤
│ IF hasRemovedPhase AND NOT secondTime: │
│ → secondTime = true │
│ → stabilityAnalysis3() [re-check stability] │
│ → run() [RECURSIVE CALL - restart algorithm] │
└─────────────────────────────────────────────────────────────────────────────────┘
The stabilityAnalysis() method tests multiple trial phases to find instabilities:
╔═══════════════════════════════════════════════════════════════════════════════╗
║ stabilityAnalysis() DETAILED FLOW ║
╚═══════════════════════════════════════════════════════════════════════════════╝
┌─────────────────────────────────────────────────────────────────────────────────┐
│ INITIALIZATION │
├─────────────────────────────────────────────────────────────────────────────────┤
│ • Clone system for trial phase calculations │
│ • Calculate reference chemical potentials: │
│ d[k] = ln(x[k]) + ln(φ[k]) for each component k │
│ • Initialize logWi[j] = 1.0 for components with z > 1e-100 │
│ • Find heaviest and lightest hydrocarbon components │
└─────────────────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────────────────┐
│ COMPONENT SELECTION │
├─────────────────────────────────────────────────────────────────────────────────┤
│ Components to test (loop j from Nc-1 down to 0): │
│ SKIP if: │
│ • x[j] < 1e-100 (negligible) │
│ • Component is ionic │
│ • Hydrocarbon but NOT heaviest AND NOT lightest │
│ │
│ This typically tests: water, CO2, H2S, heaviest HC, lightest HC, etc. │
└─────────────────────────────────────────────────────────────────────────────────┘
↓
╔═════════════════════════════════════════════════════════════════════════════╗
║ FOR EACH SELECTED COMPONENT j: ║
╚═════════════════════════════════════════════════════════════════════════════╝
↓
┌─────────────────────────────────────────────────────────────────────────────────┐
│ TRIAL PHASE INITIALIZATION │
├─────────────────────────────────────────────────────────────────────────────────┤
│ Initialize trial phase composition (nearly pure component j): │
│ w[i] = 1.0 if i == j │
│ w[i] = 1e-12 if i ≠ j (trace amounts) │
│ w[i] = 0 if z[i] < 1e-100 │
└─────────────────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────────────────┐
│ SSI LOOP (up to 150 iterations) │
├─────────────────────────────────────────────────────────────────────────────────┤
│ Parameters: │
│ • maxsucssubiter = 150 (max SSI iterations) │
│ • maxiter = 200 (absolute max with Newton) │
│ • convergence = 1e-9 │
│ │
│ iter = 0 │
│ DO: │
│ │ iter++ │
│ │ errOld = err │
│ │ err = 0 │
│ │ │
│ │ IF iter <= 150 (SSI phase): │
│ │ │ │
│ │ │ IF iter % 7 == 0 AND useaccsubst (DEM acceleration): │
│ │ │ │ Calculate acceleration factor λ: │
│ │ │ │ λ = Σ(ΔlnW^n × ΔlnW^n-1 × (ΔlnW^n-1)²) │
│ │ │ │ / Σ(ΔlnW^n-1)⁴ │
│ │ │ │ Apply acceleration: │
│ │ │ │ lnW[i] += λ/(1-λ) × ΔlnW[i] │
│ │ │ │ │
│ │ │ ELSE (standard SSI): │
│ │ │ │ Store old values for acceleration │
│ │ │ │ Calculate fugacity coefficients: clonedSystem.init(1,1) │
│ │ │ │ Update: │
│ │ │ │ lnW[i] = d[i] - ln(φ[i]) │
│ │ │ │ W[j][i] = exp(lnW[i]) │
│ │ │ │ err += |lnW[i] - lnW_old[i]| │
│ │ │ │ │
│ │ │ IF err > errOld after 2 iters: │
│ │ │ useaccsubst = false (disable acceleration) │
│ │ │ │
│ │ ELSE (iter > 150 - Newton phase): │
│ │ │ clonedSystem.init(3,1) [compute fugacity derivatives] │
│ │ │ α[i] = 2√(W[j][i]) │
│ │ │ │
│ │ │ Build objective function F and Jacobian J: │
│ │ │ F[i] = √W[i] × (lnW[i] + ln(φ[i]) - d[i]) │
│ │ │ J[i,k] = δ[i,k] + √(W[i]×W[k]) × ∂ln(φ[i])/∂n[k] │
│ │ │ │
│ │ │ Solve Newton step: │
│ │ │ Δα = -(I + J)⁻¹ × F │
│ │ │ (with regularization fallback if singular) │
│ │ │ │
│ │ │ Update: │
│ │ │ α_new = α + Δα │
│ │ │ W[j][i] = (α_new/2)² │
│ │ │ lnW[i] = ln(W[j][i]) │
│ │ │ │
│ │ Normalize and update trial phase composition: │
│ │ sumw = Σ exp(lnW[i]) │
│ │ x[i] = exp(lnW[i]) / sumw │
│ │ │
│ WHILE (|err| > 1e-9 OR err > errOld) AND iter < 200 │
└─────────────────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────────────────┐
│ CONVERGENCE CHECK AND tm CALCULATION │
├─────────────────────────────────────────────────────────────────────────────────┤
│ Calculate tangent plane distance: │
│ tm[j] = 1 - Σ exp(lnW[i]) │
│ │
│ Check for trivial solution: │
│ trivialCheck0 = Σ |w[i] - x_phase0[i]| │
│ trivialCheck1 = Σ |w[i] - x_phase1[i]| │
│ IF trivialCheck0 < 1e-4 OR trivialCheck1 < 1e-4: │
│ tm[j] = 10.0 (mark as stable - trivial solution) │
│ │
│ IF tm[j] < -1e-8: │
│ → UNSTABLE! Break loop, proceed to phase addition │
└─────────────────────────────────────────────────────────────────────────────────┘
║ ║
║ END FOR EACH COMPONENT ║
╚══════════════════════════════════════════════════════════════════════════════╝
↓
┌─────────────────────────────────────────────────────────────────────────────────┐
│ PHASE ADDITION (if instability found) │
├─────────────────────────────────────────────────────────────────────────────────┤
│ FOR k = Nc-1 down to 0: │
│ IF tm[k] < -1e-8 AND NOT NaN: │
│ • system.addPhase() │
│ • Set new phase composition = x[k][i] (from stationary point) │
│ • Normalize new phase │
│ • multiPhaseTest = true │
│ • Set initial β = z[destabilizing_component] │
│ • system.init(1) │
│ • system.normalizeBeta() │
│ → RETURN (exit stability analysis) │
│ │
│ IF no instability found: │
│ → system.normalizeBeta() │
│ → RETURN (system is stable) │
└─────────────────────────────────────────────────────────────────────────────────┘
| Parameter | Value | Description |
|---|---|---|
maxsucssubiter |
150 | Maximum SSI iterations before Newton |
maxiter |
200 | Absolute maximum iterations |
| DEM interval | 7 | Apply acceleration every 7th iteration |
| Convergence tolerance | 1e-9 | Error threshold for lnW convergence |
| Instability threshold | -1e-8 | tm value indicating phase split |
| Trivial solution threshold | 1e-4 | Composition difference to detect trivial |
The multiphase stability analysis in NeqSim is more sophisticated than the basic two-phase version. It systematically tests multiple trial phase compositions to ensure no additional phases can form.
Instead of using only Wilson K-factor estimates, the multiphase stability analysis tests component-seeded trial phases:
Pure component initialization: For each component $j$, create a trial phase with: $$w_i^{(0)} = \begin{cases} 1.0 & \text{if } i = j \ 10^{-12} & \text{if } i \neq j \end{cases}$$
Hydrocarbon optimization: To reduce computational cost, only two hydrocarbon components are tested:
This captures both potential liquid-liquid separation (heavy components) and vapor formation (light components).
Non-hydrocarbon components: All non-hydrocarbon components (water, CO₂, H₂S, etc.) are tested individually.
Ion exclusion: Components with ionic charge are excluded from stability testing since they cannot exist in separate non-aqueous phases.
// From TPmultiflash.java - component selection logic
for (int j = system.getPhase(0).getNumberOfComponents() - 1; j >= 0; j--) {
// Skip negligible components
if (minimumGibbsEnergySystem.getPhase(0).getComponent(j).getx() < 1e-100)
continue;
// Skip ions
if (minimumGibbsEnergySystem.getPhase(0).getComponent(j).getIonicCharge() != 0)
continue;
// For hydrocarbons, only test heaviest and lightest
if (minimumGibbsEnergySystem.getPhase(0).getComponent(j).isHydrocarbon()
&& j != hydrocarbonTestCompNumb && j != lightTestCompNumb)
continue;
// Perform stability test for this component...
}
The reference chemical potential $d_i$ is computed from the current phase (typically the phase with lowest Gibbs energy):
$$d_i = \ln x_i^{\text{ref}} + \ln \phi_i^{\text{ref}}$$
Where superscript "ref" denotes the reference phase. This is computed once before the iteration loop:
for (int k = 0; k < system.getPhase(0).getNumberOfComponents(); k++) {
if (system.getPhase(0).getComponent(k).getx() > 1e-100) {
d[k] = Math.log(system.getPhase(0).getComponent(k).getx())
+ system.getPhase(0).getComponent(k).getLogFugacityCoefficient();
}
}
The multiphase stability analysis maintains both unnormalized ($W_i$) and normalized ($w_i$) compositions:
Iteration update: $$\ln W_i^{(n+1)} = d_i - \ln \phi_i(\mathbf{w}^{(n)})$$
Normalization for fugacity calculation: $$w_i = \frac{W_i}{\sum_j W_j}$$
This is important because fugacity coefficients must be evaluated at normalized compositions, but the TPD criterion uses the unnormalized $W_i$ values.
// Compute sum for normalization
sumw[j] = 0;
for (int i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) {
sumw[j] += safeExp(logWi[i]);
}
// Set normalized composition for fugacity calculation
for (int i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) {
clonedSystem.get(0).getPhase(1).getComponent(i).setx(safeExp(logWi[i]) / sumw[j]);
}
Every 7 iterations, the Dominant Eigenvalue Method accelerates convergence:
$$\lambda = \frac{\sum_i (\Delta \ln W_i^{(n)}) \cdot (\Delta \ln W_i^{(n-1)}) \cdot (\Delta \ln W_i^{(n-1)})^2}{\sum_i (\Delta \ln W_i^{(n-1)})^4}$$
$$\ln W_i^{\text{acc}} = \ln W_i^{(n)} + \frac{\lambda}{1 - \lambda} \Delta \ln W_i^{(n)}$$
Acceleration is disabled if the error increases (indicating divergence):
if (iter > 2 && err > errOld) {
useaccsubst = false;
}
After 150 successive substitution iterations, NeqSim switches to a second-order Newton method using the substitution $\alpha_i = 2\sqrt{W_i}$:
Objective function: $$F_i = \sqrt{W_i} \left[ \ln W_i + \ln \phi_i(\mathbf{w}) - d_i \right]$$
Jacobian with fugacity derivatives: $$\frac{\partial F_i}{\partial \alpha_k} = \delta_{ik} + \sqrt{W_i W_k} \cdot \frac{\partial \ln \phi_i}{\partial n_k}$$
Newton update with regularization: $$\boldsymbol{\alpha}^{(n+1)} = \boldsymbol{\alpha}^{(n)} - (\mathbf{I} + \mathbf{J})^{-1} \mathbf{F}$$
The implementation includes robust fallbacks for singular matrices:
After convergence, the algorithm checks if the solution is trivial (identical to an existing phase):
$$\text{trivialCheck}_k = \sum_i |w_i - x_i^{(k)}|$$
If $\text{trivialCheck}_k < 10^{-4}$ for any existing phase $k$, the stationary point is rejected:
double xTrivialCheck0 = 0.0;
double xTrivialCheck1 = 0.0;
for (int i = 0; i < system.getPhase(1).getNumberOfComponents(); i++) {
xTrivialCheck0 += Math.abs(x[j][i] - system.getPhase(0).getComponent(i).getx());
xTrivialCheck1 += Math.abs(x[j][i] - system.getPhase(1).getComponent(i).getx());
}
if (Math.abs(xTrivialCheck0) < 1e-4 || Math.abs(xTrivialCheck1) < 1e-4) {
tm[j] = 10.0; // Mark as stable (trivial solution)
}
When an unstable stationary point is found ($\text{tm} < -10^{-8}$):
if (tm[k] < -1e-8 && !(Double.isNaN(tm[k]))) {
system.addPhase();
unstabcomp = k;
// Set composition from stationary point
for (int i = 0; i < system.getPhase(1).getNumberOfComponents(); i++) {
system.getPhase(system.getNumberOfPhases() - 1).getComponent(i).setx(x[k][i]);
}
system.getPhases()[system.getNumberOfPhases() - 1].normalize();
// Set initial phase fraction
multiPhaseTest = true;
system.setBeta(system.getNumberOfPhases() - 1,
system.getPhase(0).getComponent(unstabcomp).getz());
system.init(1);
system.normalizeBeta();
return; // Exit stability analysis, proceed to phase split
}
NeqSim implements three stability analysis methods in TPmultiflash:
| Method | Description | Use Case |
|---|---|---|
stabilityAnalysis() |
Single cloned system, optimized for performance | Primary method |
stabilityAnalysis2() |
Multiple cloned systems (one per component) | Alternative for difficult cases |
stabilityAnalysis3() |
Re-run after phase removal | Post-processing verification |
The main run() method orchestrates these:
if (doStabilityAnalysis) {
stabilityAnalysis(); // Primary stability check
}
// ... phase equilibrium calculation ...
if (hasRemovedPhase && !secondTime) {
secondTime = true;
stabilityAnalysis3(); // Re-check after phase removal
run(); // Recursive call
}
When system.setEnhancedMultiPhaseCheck(true) is enabled, an additional stability analysis is performed using Wilson K-value based initialization. This is particularly useful for detecting liquid-liquid equilibria in complex mixtures such as sour gas systems (methane/CO₂/H₂S).
The standard stability analysis may fail to detect additional phases in certain systems because:
The enhanced stability analysis (stabilityAnalysisEnhanced()) addresses these limitations:
╔═══════════════════════════════════════════════════════════════════════════════╗
║ stabilityAnalysisEnhanced() ALGORITHM FLOW ║
╚═══════════════════════════════════════════════════════════════════════════════╝
┌─────────────────────────────────────────────────────────────────────────────────┐
│ STEP 1: WILSON K-VALUE CALCULATION │
├─────────────────────────────────────────────────────────────────────────────────┤
│ FOR each valid component i (z > 1e-100, not ionic): │
│ K[i] = (Pc[i] / P) × exp[5.373 × (1 + ω[i]) × (1 - Tc[i]/T)] │
│ log(K[i]) = ln(K[i]) │
└─────────────────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────────────────┐
│ STEP 2: PRE-CALCULATE REFERENCE FUGACITIES │
├─────────────────────────────────────────────────────────────────────────────────┤
│ FOR each existing phase p = 0 to numPhases-1: │
│ FOR each component k: │
│ d_ref[p][k] = ln(x[p][k]) + ln(φ[p][k]) │
└─────────────────────────────────────────────────────────────────────────────────┘
↓
╔═════════════════════════════════════════════════════════════════════════════╗
║ FOR EACH EXISTING PHASE AS REFERENCE (p = 0 to numPhases-1): ║
╠═════════════════════════════════════════════════════════════════════════════╣
║ FOR EACH TRIAL TYPE (vapor-like, liquid-like, LLE): ║
╚═════════════════════════════════════════════════════════════════════════════╝
↓
┌─────────────────────────────────────────────────────────────────────────────────┐
│ STEP 3: TRIAL PHASE INITIALIZATION │
├─────────────────────────────────────────────────────────────────────────────────┤
│ trialType = 1: VAPOR-LIKE (VLE gas detection) │
│ W[i] = exp(ln(K[i])) → volatile components enriched │
│ │
│ trialType = -1: LIQUID-LIKE (VLE liquid detection) │
│ W[i] = exp(-ln(K[i])) = 1/K[i] → heavy components enriched │
│ │
│ trialType = 0: LLE TRIAL (polarity-based perturbation) │
│ perturbFactor = 2.0 if ω[i] > 0.15 (polar), else 0.5 (non-polar) │
│ W[i] = z[i] × perturbFactor │
│ │
│ Note: LLE uses acentric factor as polarity proxy since Wilson K-values │
│ are derived from vapor pressure and don't capture activity coefficient- │
│ driven liquid-liquid splits. │
└─────────────────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────────────────┐
│ STEP 4: SSI LOOP WITH WEGSTEIN ACCELERATION │
├─────────────────────────────────────────────────────────────────────────────────┤
│ FOR iter = 1 to maxIter (300): │
│ Calculate fugacity coefficients at normalized w[i] │
│ Update: ln(W[i]) = d_ref[p] - ln(φ[i]) │
│ │
│ IF iter % 5 == 0 AND iter > 5 (Wegstein acceleration): │
│ λ = Σ(Δln(W)^n × Δln(W)^n-1) / Σ(Δln(W)^n-1)² │
│ λ = clamp(λ, -0.5, 0.9) │
│ ln(W[i]) += λ/(1-λ) × Δln(W[i]) │
│ │
│ Check convergence: err = Σ|ln(W[i])^n - ln(W[i])^n-1| │
│ IF err < 1e-10: BREAK │
└─────────────────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────────────────┐
│ STEP 5: STABILITY CHECK AND PHASE ADDITION │
├─────────────────────────────────────────────────────────────────────────────────┤
│ tm = 1 - Σ W[i] │
│ │
│ Check for trivial solution (composition too close to existing phase) │
│ │
│ IF tm < -1e-8 AND NOT trivial: │
│ → Add new phase with composition w[i] = W[i]/ΣW[j] │
│ → multiPhaseTest = true │
│ → RETURN │
└─────────────────────────────────────────────────────────────────────────────────┘
| Feature | Standard Analysis | Enhanced Analysis |
|---|---|---|
| Initial guess | Pure component | Wilson K-values |
| Trial types | Single | Vapor-like, Liquid-like, LLE |
| Reference phase | Phase 0 only | All existing phases |
| LLE detection | Component-based | Polarity perturbation |
| Acceleration | DEM every 7 iterations | Wegstein every 5 iterations |
| Hydrocarbon filtering | Yes (only heaviest/lightest) | No (all components tested) |
Enable setEnhancedMultiPhaseCheck(true) for:
Example usage:
SystemInterface fluid = new SystemPrEos(210.0, 55.0); // Low T, moderate P
fluid.addComponent("methane", 49.88);
fluid.addComponent("CO2", 9.87);
fluid.addComponent("H2S", 40.22);
fluid.setMixingRule("classic");
fluid.setMultiPhaseCheck(true);
fluid.setEnhancedMultiPhaseCheck(true); // Enable enhanced detection
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
// May find vapor + CO2-rich liquid + H2S-rich liquid
Note: Enhanced stability analysis adds computational overhead. For simple VLE systems, the standard analysis is sufficient and more efficient.
For systems with $N_p$ phases, the equilibrium conditions become:
$$f_i^{(1)} = f_i^{(2)} = \cdots = f_i^{(N_p)} \quad \text{for } i = 1, \ldots, N_c$$
Material Balance:
$$z_i = \sum_{k=1}^{N_p} \beta_k x_i^{(k)}$$
With the constraint:
$$\sum_{k=1}^{N_p} \beta_k = 1$$
Michelsen (1982) introduced the Q-function for multiphase flash:
$$Q = \sum_{k=1}^{N_p} \beta_k - \sum_{i=1}^{N_c} z_i \ln E_i$$
Where: $$E_i = \sum_{k=1}^{N_p} \frac{\beta_k}{\phi_i^{(k)}}$$
Gradient: $$\frac{\partial Q}{\partial \beta_k} = 1 - \sum_{i=1}^{N_c} \frac{z_i}{E_i \phi_i^{(k)}}$$
Hessian: $$\frac{\partial^2 Q}{\partial \beta_k \partial \beta_l} = \sum_{i=1}^{N_c} \frac{z_i}{E_i^2 \phi_i^{(k)} \phi_i^{(l)}}$$
Newton Update:
$$\boldsymbol{\beta}^{(n+1)} = \boldsymbol{\beta}^{(n)} - \mathbf{H}^{-1} \nabla Q$$
Phase Compositions:
$$x_i^{(k)} = \frac{z_i}{E_i \phi_i^{(k)}}$$
NeqSim Implementation:
// From TPmultiflash.java - calcQ() and solveBeta()
public double calcQ() {
this.calcE(); // Calculate E_i
// Compute gradient dQ/dβ
for (int k = 0; k < system.getNumberOfPhases(); k++) {
dQdbeta[k][0] = 1.0;
for (int i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) {
dQdbeta[k][0] -= multTerm[i] / system.getPhase(k).getComponent(i).getFugacityCoefficient();
}
}
// Compute Hessian Q_matrix
for (int i = 0; i < system.getNumberOfPhases(); i++) {
for (int j = 0; j < system.getNumberOfPhases(); j++) {
Qmatrix[i][j] = 0.0;
for (int k = 0; k < system.getPhase(0).getNumberOfComponents(); k++) {
Qmatrix[i][j] += multTerm2[k] / (phi_j[k] * phi_i[k]);
}
}
}
return Q;
}
Phase Addition:
When stability analysis indicates an unstable phase (tm < 0):
Phase Removal:
Phases with negligible fractions ($\beta_k < \beta_{\min}$) are removed:
// From TPmultiflash.java - run()
for (int i = 0; i < system.getNumberOfPhases(); i++) {
if (system.getBeta(i) < 1.1 * phaseFractionMinimumLimit) {
system.removePhaseKeepTotalComposition(i);
}
}
Trivial Solution Detection:
Phases with nearly identical densities are merged:
if (Math.abs(system.getPhase(i).getDensity() - system.getPhase(j).getDensity()) < 1.1e-5) {
system.removePhaseKeepTotalComposition(j);
}
The complete workflow in TPmultiflash.run() is:
┌─────────────────────────────────────────────────────────────┐
│ 1. PREPROCESSING │
│ - For electrolyte systems: temporarily remove ions │
│ - Store ionic compositions for later restoration │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 2. STABILITY ANALYSIS │
│ - Test component-seeded trial phases │
│ - Use SSI + DEM acceleration + Newton fallback │
│ - If tm < -1e-8: add new phase, set multiPhaseTest=true │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 3. ADDITIONAL PHASE SEEDING (if stability didn't add) │
│ - seedAdditionalPhaseFromFeed(): gas phase seeding │
│ - seedHydrocarbonLiquidFromFeed(): oil phase seeding │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 4. ION RESTORATION (electrolyte systems) │
│ - Restore ions to aqueous phase(s) only │
│ - Set ion x = 1e-50 in non-aqueous phases │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 5. CHEMICAL EQUILIBRIUM (if applicable) │
│ - Solve chemical equilibrium in aqueous phase │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 6. PHASE SPLIT CALCULATION (if multiPhaseTest = true) │
│ - Q-function minimization with Newton's method │
│ - Nested iteration with chemical equilibrium │
│ - Continue until convergence │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 7. AQUEOUS PHASE SEEDING (if water present but no aq) │
│ - Add aqueous phase seeded with water │
│ - Re-run phase split if phase added │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 8. PHASE CLEANUP │
│ - Remove phases with β < βmin │
│ - Detect and merge trivial solutions (same density) │
│ - ensureSingleAqueousPhase() for electrolytes │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 9. POST-REMOVAL STABILITY CHECK │
│ - If phase removed: run stabilityAnalysis3() │
│ - Recursive call to run() if new phase found │
└─────────────────────────────────────────────────────────────┘
Beyond stability analysis, NeqSim uses heuristic phase seeding to improve convergence:
When an aqueous phase exists without a gas phase, seed a gas phase:
private boolean seedAdditionalPhaseFromFeed() {
// Only if multiphase check enabled and < 3 phases
if (!system.doMultiPhaseCheck() || system.getNumberOfPhases() >= 3)
return false;
// Need aqueous phase but no gas phase
boolean hasAqueous = false, hasGas = false;
for (int phase = 0; phase < system.getNumberOfPhases(); phase++) {
if (type == PhaseType.GAS) hasGas = true;
if (type == PhaseType.AQUEOUS) hasAqueous = true;
}
if (!hasAqueous || hasGas) return false;
// Seed gas phase with feed composition
system.addPhase();
system.setPhaseType(phaseIndex, PhaseType.GAS);
for (int comp = 0; comp < ncomp; comp++) {
system.getPhase(phaseIndex).getComponent(comp).setx(z[comp]);
}
system.setBeta(phaseIndex, 1e-3);
return true;
}
When water is present but no aqueous phase exists:
if (waterZ > 1.0e-6 && !system.hasPhaseType(PhaseType.AQUEOUS)) {
system.addPhase();
system.setPhaseType(aquPhaseIndex, PhaseType.AQUEOUS);
// Initialize with water-concentrated composition
for (int comp = 0; comp < ncomp; comp++) {
double x = 1.0e-16;
if (comp == waterComponentIndex) {
x = Math.max(waterZ, 1.0e-12); // Concentrate water
} else if (!isHydrocarbon(comp) && !isInert(comp)) {
x = Math.min(z[comp] * 1.0e-2, 1.0e-8); // Trace aqueous components
}
system.getPhase(aquPhaseIndex).getComponent(comp).setx(x);
}
system.setBeta(aquPhaseIndex, 1e-5);
}
For systems with chemical reactions (electrolytes, acid-base equilibria), the flash calculation must be coupled with chemical equilibrium. NeqSim solves this as a nested iteration:
Outer Loop: Phase equilibrium (flash) Inner Loop: Chemical equilibrium within each phase
Chemical Equilibrium Condition:
For a reaction $\sum_i \nu_i A_i = 0$:
$$\sum_i \nu_i \mu_i = 0$$
Or equivalently:
$$\prod_i a_i^{\nu_i} = K_{eq}(T)$$
Where $a_i$ is the activity and $K_{eq}$ is the equilibrium constant.
NeqSim Implementation:
// From TPflash.java - chemical equilibrium integration
if (system.isChemicalSystem()) {
for (int phaseNum = 0; phaseNum < system.getNumberOfPhases(); phaseNum++) {
if ("aqueous".equalsIgnoreCase(phaseType)) {
system.getChemicalReactionOperations().solveChemEq(phaseNum, 0);
system.getChemicalReactionOperations().solveChemEq(phaseNum, 1);
}
}
}
The chemical equilibrium solver uses:
Ionic species present special challenges for stability analysis because they cannot exist in non-aqueous phases. NeqSim handles this by:
Temporarily removing ions before stability analysis:
if (system.isChemicalSystem()) {
ionicZ = new double[system.getPhase(0).getNumberOfComponents()];
for (int i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) {
if (system.getPhase(0).getComponent(i).getIonicCharge() != 0) {
ionicZ[i] = system.getPhase(0).getComponent(i).getz();
// Temporarily set to near-zero
system.getPhase(phase).getComponent(i).setz(1e-100);
}
}
}
Running stability analysis on the neutral system
Restoring ions to aqueous phases after phase configuration is determined:
if (hasIons && ionicZ != null) {
for (int i = 0; i < system.getPhase(0).getNumberOfComponents(); i++) {
if (system.getPhase(0).getComponent(i).getIonicCharge() != 0) {
// Restore z values, put ions only in aqueous phase
if (system.getPhase(phase).getType() == PhaseType.AQUEOUS) {
system.getPhase(phase).getComponent(i).setx(ionicZ[i]);
} else {
system.getPhase(phase).getComponent(i).setx(1e-50);
}
}
}
}
For electrolyte systems, NeqSim ensures proper aqueous phase handling:
Single Aqueous Phase Constraint:
The system ensures only one aqueous phase exists, containing all ionic species:
private void ensureSingleAqueousPhase() {
if (!system.isChemicalSystem() || system.getNumberOfPhases() < 2) {
return;
}
// Find phase with highest aqueous component content
int bestAqueousPhase = -1;
double maxAqueousContent = 0.0;
for (int phase = 0; phase < system.getNumberOfPhases(); phase++) {
double aqueousContent = 0.0;
for (int comp = 0; comp < ncomp; comp++) {
// Count water, glycols, alcohols, and ions
if (isAqueousComponent(component)) {
aqueousContent += component.getx();
}
}
if (aqueousContent > maxAqueousContent) {
maxAqueousContent = aqueousContent;
bestAqueousPhase = phase;
}
}
// Reclassify other phases as OIL
}
Aqueous Phase Seeding:
When water is present but no aqueous phase exists, a seed aqueous phase can be created:
if (waterZ > 1.0e-6 && !system.hasPhaseType(PhaseType.AQUEOUS)) {
system.addPhase();
system.setPhaseType(aquPhaseIndex, PhaseType.AQUEOUS);
// Initialize with water-rich composition
}
Michelsen, M. L. (1982). "The isothermal flash problem. Part I. Stability." Fluid Phase Equilibria, 9(1), 1-19.
Michelsen, M. L. (1982). "The isothermal flash problem. Part II. Phase-split calculation." Fluid Phase Equilibria, 9(1), 21-40.
Michelsen, M. L. & Mollerup, J. M. (2007). Thermodynamic Models: Fundamentals and Computational Aspects, 2nd Ed. Tie-Line Publications.
Rachford, H. H. & Rice, J. D. (1952). "Procedure for use of electronic digital computers in calculating flash vaporization hydrocarbon equilibrium." Journal of Petroleum Technology, 4(10), 19-3.
Nielsen, R. F. & Lia, A. (2023). "Avoiding round-off error in the Rachford–Rice equation." Fluid Phase Equilibria, 571, 113801.
Smith, W. R. & Missen, R. W. (1982). Chemical Reaction Equilibrium Analysis: Theory and Algorithms. Wiley-Interscience.
Michelsen, M. L. (1989). "Calculation of multiphase equilibrium in ideal solutions." Fluid Phase Equilibria, 53, 73-80.
| File | Description |
|---|---|
| TPflash.java | Two-phase flash with SSI and Newton |
| TPmultiflash.java | Multi-phase flash with stability analysis |
| RachfordRice.java | Rachford-Rice equation solvers |
| SysNewtonRhapsonTPflash.java | Second-order Newton solver |
| ChemicalReactionOperations.java | Chemical equilibrium solver |
import neqsim.thermo.system.SystemSrkEos;
import neqsim.thermodynamicoperations.ThermodynamicOperations;
// Create system
SystemSrkEos system = new SystemSrkEos(298.15, 10.0);
system.addComponent("methane", 0.7);
system.addComponent("ethane", 0.2);
system.addComponent("propane", 0.1);
system.setMixingRule("classic");
// Enable multi-phase check for stability analysis
system.setMultiPhaseCheck(true);
// Perform TP flash
ThermodynamicOperations ops = new ThermodynamicOperations(system);
ops.TPflash();
// Results
System.out.println("Number of phases: " + system.getNumberOfPhases());
System.out.println("Vapor fraction: " + system.getBeta(0));
system.display();
For electrolyte systems:
import neqsim.thermo.system.SystemElectrolyteCPA;
SystemElectrolyteCPA system = new SystemElectrolyteCPA(298.15, 1.0);
system.addComponent("CO2", 1.0);
system.addComponent("water", 100.0);
system.setMixingRule(10); // CPA mixing rule with electrolyte support
system.setMultiPhaseCheck(true);
ThermodynamicOperations ops = new ThermodynamicOperations(system);
ops.TPflash(); // Automatically solves chemical equilibrium in aqueous phase
The thermodynamicoperations package provides flash calculations, phase envelope construction, and chemical equilibrium solvers.
Location: neqsim.thermodynamicoperations
Purpose:
Main Entry Point: ThermodynamicOperations
import neqsim.thermodynamicoperations.ThermodynamicOperations;
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash(); // Temperature-Pressure flash
thermodynamicoperations/
├── ThermodynamicOperations.java # Main facade class
├── BaseOperation.java # Base class for operations
├── OperationInterface.java # Operation interface
│
├── flashops/ # Flash calculations
│ ├── TPflash.java # Temperature-Pressure flash
│ ├── PHflash.java # Pressure-Enthalpy flash
│ ├── PSFlash.java # Pressure-Entropy flash
│ ├── TVflash.java # Temperature-Volume flash
│ ├── TSFlash.java # Temperature-Entropy flash (Q-function)
│ ├── THflash.java # Temperature-Enthalpy flash (Q-function)
│ ├── TUflash.java # Temperature-Internal Energy flash (Q-function)
│ ├── PVflash.java # Pressure-Volume flash (Q-function)
│ ├── VUflash.java # Volume-Internal Energy flash (Q-function)
│ ├── VHflash.java # Volume-Enthalpy flash (Q-function)
│ ├── VSflash.java # Volume-Entropy flash (Q-function)
│ ├── PUflash.java # Pressure-Internal Energy flash
│ ├── TVfractionFlash.java # Temperature-Vapor fraction flash
│ ├── dTPflash.java # Dual temperature flash
│ ├── TPmultiflash.java # Multiphase TP flash
│ ├── SolidFlash.java # Flash with solids
│ ├── CriticalPointFlash.java # Critical point calculation
│ ├── QfuncFlash.java # Base class for Q-function flashes
│ ├── RachfordRice.java # Rachford-Rice solver
│ └── saturationops/ # Saturation calculations
│ ├── BubblePointPressureFlash.java
│ ├── BubblePointTemperatureFlash.java
│ ├── DewPointPressureFlash.java
│ ├── DewPointTemperatureFlash.java
│ ├── WaterDewPointFlash.java
│ └── HydrateEquilibrium.java
│
├── phaseenvelopeops/ # Phase envelope calculations
│ ├── multicomponentenvelopeops/
│ │ ├── PTPhaseEnvelope.java
│ │ └── PHPhaseEnvelope.java
│ └── reactivecurves/
│ └── ReactivePhaseEnvelope.java
│
├── chemicalequilibrium/ # Chemical equilibrium
│ └── ChemicalEquilibrium.java
│
└── propertygenerator/ # Property tables
└── OLGApropertyTableGenerator.java
| Flash Type | Method | Known Variables | Solved Variables |
|---|---|---|---|
| TP | TPflash() |
T, P | Phase amounts, compositions |
| PH | PHflash(H) |
P, H | T, phase amounts, compositions |
| PS | PSflash(S) |
P, S | T, phase amounts, compositions |
| PU | PUflash(U) |
P, U | T, phase amounts, compositions |
| TV | TVflash(V) |
T, V | P, phase amounts, compositions |
| TS | TSflash(S) |
T, S | P, phase amounts, compositions |
| TH | THflash(H) |
T, H | P, phase amounts, compositions |
| TU | TUflash(U) |
T, U | P, phase amounts, compositions |
| PV | PVflash(V) |
P, V | T, phase amounts, compositions |
| VU | VUflash(V, U) |
V, U | T, P, phase amounts |
| VH | VHflash(V, H) |
V, H | T, P, phase amounts |
| VS | VSflash(V, S) |
V, S | T, P, phase amounts |
The most common flash calculation - given temperature and pressure, find equilibrium phases.
SystemInterface fluid = new SystemSrkEos(298.15, 50.0);
fluid.addComponent("methane", 0.9);
fluid.addComponent("ethane", 0.1);
fluid.setMixingRule("classic");
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
System.out.println("Number of phases: " + fluid.getNumberOfPhases());
System.out.println("Vapor fraction: " + fluid.getBeta());
Find temperature given pressure and enthalpy - essential for adiabatic processes.
// Initial state
double H = fluid.getEnthalpy();
// Change pressure
fluid.setPressure(20.0);
// Find new temperature at same enthalpy
ops.PHflash(H);
System.out.println("New temperature: " + fluid.getTemperature("C") + " °C");
Find temperature given pressure and entropy - for isentropic compression/expansion.
double S = fluid.getEntropy();
fluid.setPressure(100.0);
ops.PSflash(S);
System.out.println("Isentropic temperature: " + fluid.getTemperature("C") + " °C");
For dynamic simulations - given volume and internal energy, find T and P.
double V = fluid.getVolume();
double U = fluid.getInternalEnergy();
// Simulate heat addition
double Unew = U + 10000.0; // Add 10 kJ
ops.VUflash(V, Unew);
System.out.println("New T: " + fluid.getTemperature("C") + " °C");
System.out.println("New P: " + fluid.getPressure() + " bar");
Find pressure at given vapor/liquid fraction.
// Find pressure where vapor fraction = 0.5
ops.TVfractionFlash(0.5);
System.out.println("Pressure at 50% vapor: " + fluid.getPressure() + " bar");
// Bubble point pressure at current temperature
ops.bubblePointPressureFlash(false);
double Pbub = fluid.getPressure();
// Bubble point temperature at current pressure
ops.bubblePointTemperatureFlash();
double Tbub = fluid.getTemperature();
// Dew point pressure at current temperature
ops.dewPointPressureFlash();
double Pdew = fluid.getPressure();
// Dew point temperature at current pressure
ops.dewPointTemperatureFlash();
double Tdew = fluid.getTemperature();
// Water dew point temperature at given pressure
ops.waterDewPointTemperatureFlash();
double TwaterDew = fluid.getTemperature();
📚 See Hydrate Flash Operations for complete documentation
// Hydrate formation temperature
ops.hydrateFormationTemperature();
double Thyd = fluid.getTemperature();
// Hydrate formation pressure
fluid.setTemperature(278.15);
ops.hydrateFormationPressure();
double Phyd = fluid.getPressure();
// Hydrate TPflash (phase equilibrium with hydrate)
ops.hydrateTPflash();
// Gas-Hydrate equilibrium (no aqueous phase)
ops.gasHydrateTPflash();
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.calcPTphaseEnvelope();
// Get results
double[][] envelope = ops.getOperation().get2DData();
// envelope[0] = temperatures (K)
// envelope[1] = pressures (bar)
// Get cricondenbar and cricondentherm
double cricondenbar = ops.getOperation().getCricondenbar();
double cricondentherm = ops.getOperation().getCricondentherm();
ops.calcPHenveloppe();
double[][] phEnvelope = ops.getOperation().get2DData();
Generate property tables for multiphase flow simulators.
import neqsim.thermodynamicoperations.propertygenerator.OLGApropertyTableGenerator;
OLGApropertyTableGenerator generator = new OLGApropertyTableGenerator(fluid);
generator.setFileName("fluid_properties");
// Set ranges
generator.setPressureRange(1.0, 200.0, 50); // 1-200 bar, 50 points
generator.setTemperatureRange(250.0, 400.0, 30); // 250-400 K, 30 points
generator.setWaterCutRange(0.0, 1.0, 5); // 0-100% water cut, 5 points
generator.run();
For reactive systems, calculate equilibrium composition considering reactions.
// Set up reactive system
SystemInterface reactive = new SystemSrkEos(700.0, 10.0);
reactive.addComponent("methane", 1.0);
reactive.addComponent("water", 2.0);
reactive.addComponent("CO2", 0.0);
reactive.addComponent("hydrogen", 0.0);
// Enable chemical reactions
reactive.setChemicalReactions(true);
ThermodynamicOperations ops = new ThermodynamicOperations(reactive);
ops.calcChemicalEquilibrium();
// Get equilibrium composition
for (int i = 0; i < reactive.getNumberOfComponents(); i++) {
System.out.println(reactive.getComponent(i).getName() +
": " + reactive.getComponent(i).getx() + " mol/mol");
}
Handle systems with multiple liquid phases, solids, or hydrates.
SystemInterface fluid = new SystemSrkCPAstatoil(273.15 + 5, 100.0);
fluid.addComponent("methane", 0.90);
fluid.addComponent("water", 0.10);
fluid.setMixingRule("CPA_Statoil");
fluid.setMultiPhaseCheck(true); // Enable multi-phase check
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
System.out.println("Number of phases: " + fluid.getNumberOfPhases());
for (int i = 0; i < fluid.getNumberOfPhases(); i++) {
System.out.println("Phase " + i + ": " + fluid.getPhase(i).getPhaseTypeName());
}
fluid.setSolidPhaseCheck("wax");
ops.TPsolidflash();
Track calculations with UUIDs for parallel processing.
UUID calcId = UUID.randomUUID();
ops.TPflash(calcId);
// Set maximum iterations
ops.setMaxIterations(100);
// Set convergence tolerance
ops.setTolerance(1e-10);
createDatabase(true) for new componentsgetNumberOfPhases() makes senseReal reservoir fluids often contain a complex mixture of heavy hydrocarbons (C7+) that cannot be represented by standard pure components. NeqSim provides a robust characterization framework to model these fluids using TBP (True Boiling Point) fractions and Plus fractions.
Related Documentation:
- TBP Fraction Models - Detailed guide on all available TBP models (Pedersen, Lee-Kesler, Riazi-Daubert, Twu, Cavett, Standing), model selection, and mathematical correlations
You can add heavy fractions to a system using two primary methods: addTBPfraction and addPlusFraction.
Use addTBPfraction when you have data for specific carbon number cuts (e.g., C7, C8, C9) with defined properties.
// addTBPfraction(name, moles, molarMass_kg_mol, density_kg_m3)
system.addTBPfraction("C7", 1.0, 0.092, 0.73);
system.addTBPfraction("C8", 1.0, 0.104, 0.76);
Use addPlusFraction for the final residue or "plus" fraction (e.g., C10+, C20+) where you only have average properties.
// addPlusFraction(name, moles, molarMass_kg_mol, density_kg_m3)
system.addPlusFraction("C10+", 10.0, 0.250, 0.85);
After adding the components, you must run the characterization routine to split the plus fraction into pseudo-components and estimate their critical properties (Tc, Pc, w).
NeqSim supports several characterization models. The most common is the Pedersen model.
// Set the TBP Model (affects how TBP fractions are treated)
system.getCharacterization().setTBPModel("PedersenSRK");
// Set the Plus Fraction Model (affects how the plus fraction is split)
system.getCharacterization().setPlusFractionModel("Pedersen");
NeqSim provides 10 TBP models for estimating critical properties (Tc, Pc, ω) from molecular weight and density:
| Model | Best Application |
|---|---|
PedersenSRK |
General SRK EOS (default) |
PedersenPR |
General Peng-Robinson EOS |
PedersenSRKHeavyOil |
Heavy oils with SRK |
PedersenPRHeavyOil |
Heavy oils with PR |
Lee-Kesler |
General purpose, uses Watson K-factor |
RiaziDaubert |
Light fractions (MW < 300 g/mol) |
Twu |
Paraffinic fluids, gas condensates |
Cavett |
Refining industry, API gravity corrections |
Standing |
Reservoir engineering |
See TBP Fraction Models for detailed mathematical correlations and model selection guidelines.
The standard Pedersen model assumes an exponential distribution for the mole fraction $z_i$ of each carbon number fraction $i$:
[ z_i = \exp(A + B \cdot i) ]
where $i$ is the carbon number, and $A$ and $B$ are coefficients determined to match the total mole fraction and average molar mass of the plus fraction.
The density $\rho_i$ is modeled as a logarithmic function of the carbon number:
[ \rho_i = C + D \cdot \ln(i) ]
where $C$ and $D$ are fitted coefficients.
The Whitson Gamma model uses a three-parameter Gamma probability density function (PDF) to describe the molar mass distribution:
[ p(M) = \frac{(M - \eta)^{\alpha - 1} \exp\left(-\frac{M - \eta}{\beta}\right)}{\beta^\alpha \Gamma(\alpha)} ]
where:
The mole fraction $z_i$ for a pseudo-component covering the molar mass range $[M_{L}, M_{U}]$ is obtained by integrating the PDF:
[ z_i = z_{plus} \int_{M_{L}}^{M_{U}} p(M) \, dM ]
The density of each pseudo-component is calculated using the Watson UOP characterization factor $K_w$:
[ K_w = 4.5579 \cdot (M_{plus})^{0.15178} \cdot \rho_{plus}^{-1.18241} ]
[ \rho_i = 6.0108 \cdot M_i^{0.17947} \cdot K_w^{-1.18241} ]
(Note: Molar masses are in g/mol and densities in g/cm³ for these correlations).
Once models are set, execute the characterization.
system.getCharacterization().characterisePlusFraction();
This process will:
To reduce simulation time, it is often necessary to group the many characterized components into a smaller number of "lumped" pseudo-components.
You can control the lumping behavior via the LumpingModel.
// Set the lumping method (Default is "PVTlumpingModel")
system.getCharacterization().setLumpingModel("PVTlumpingModel");
// Configure the number of pseudo-components to generate
system.getCharacterization().getLumpingModel().setNumberOfLumpedComponents(12);
import neqsim.thermo.system.SystemSrkEos;
import neqsim.thermodynamicoperations.ThermodynamicOperations;
public class CharacterizationExample {
public static void main(String[] args) {
// 1. Create System
SystemSrkEos fluid = new SystemSrkEos(298.15, 50.0);
fluid.addComponent("nitrogen", 0.5);
fluid.addComponent("CO2", 1.0);
fluid.addComponent("methane", 60.0);
fluid.addComponent("ethane", 5.0);
fluid.addComponent("propane", 3.0);
// 2. Add Heavy Fractions
fluid.addTBPfraction("C6", 1.0, 0.086, 0.66);
fluid.addTBPfraction("C7", 2.0, 0.092, 0.73);
fluid.addTBPfraction("C8", 2.0, 0.104, 0.76);
fluid.addTBPfraction("C9", 1.0, 0.118, 0.78);
fluid.addPlusFraction("C10+", 15.0, 0.280, 0.84); // The Plus Fraction
// 3. Configure Characterization
fluid.getCharacterization().setTBPModel("PedersenSRK");
fluid.getCharacterization().setPlusFractionModel("Pedersen");
// 4. Configure Lumping
// We want to lump the C10+ distribution into 5 pseudo-components
fluid.getCharacterization().setLumpingModel("PVTlumpingModel");
fluid.getCharacterization().getLumpingModel().setNumberOfLumpedComponents(5);
// 5. Run Characterization
fluid.getCharacterization().characterisePlusFraction();
// 6. Use the Fluid
fluid.setMixingRule("classic");
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
fluid.prettyPrint();
}
}
setPlusFractionModel("Pedersen Heavy Oil").setPlusFractionModel("Whitson Gamma") if you have specific gamma distribution parameters.setLumpingModel("no lumping"). Note that this will result in a system with many components, which is slower to simulate.This guide provides comprehensive documentation on True Boiling Point (TBP) fraction models available in NeqSim for petroleum fluid characterization.
TBP (True Boiling Point) models are empirical correlations that estimate the critical properties of petroleum pseudo-components from easily measured bulk properties like molecular weight (MW) and specific gravity (SG). These critical properties are essential inputs for cubic equations of state (EOS) such as SRK and Peng-Robinson.
Petroleum fluids contain thousands of individual hydrocarbon species that cannot all be individually identified and characterized. Instead, heavy fractions (typically C7+) are lumped into pseudo-components. TBP models provide the thermodynamic properties needed for EOS calculations:
NeqSim provides 10 TBP models, each optimized for different applications:
| Model Name | Best Application | Key Feature |
|---|---|---|
PedersenSRK |
General SRK EOS | Default, auto light/heavy switching |
PedersenSRKHeavyOil |
Heavy oils with SRK | Optimized for MW > 500 g/mol |
PedersenPR |
General PR EOS | Optimized for Peng-Robinson |
PedersenPR2 |
PR EOS alternate | Søreide boiling point correlation |
PedersenPRHeavyOil |
Heavy oils with PR | For viscous/heavy crude |
RiaziDaubert |
Light fractions | Best for MW < 300 g/mol |
Lee-Kesler |
General purpose | Uses Watson K-factor |
Twu |
Paraffinic fluids | n-alkane reference method |
Cavett |
Refining industry | API gravity corrections |
Standing |
Reservoir engineering | Simple, widely used |
Best for: General purpose petroleum characterization
The Pedersen correlations are the default and most widely used TBP models in NeqSim. They were specifically developed for use with cubic equations of state.
Critical Temperature: $$T_c = a_0 \cdot \rho + a_1 \cdot \ln(M) + a_2 \cdot M + \frac{a_3}{M}$$
Critical Pressure: $$P_c = \exp\left(b_0 + b_1 \cdot \rho^{b_4} + \frac{b_2}{M} + \frac{b_3}{M^2}\right)$$
EOS m-parameter: $$m = c_0 + c_1 \cdot M + c_2 \cdot \rho + c_3 \cdot M^2$$
The model automatically switches between light oil and heavy oil coefficients at MW = 1120 g/mol.
import neqsim.thermo.system.SystemSrkEos;
// For SRK equation of state
SystemSrkEos fluid = new SystemSrkEos(298.15, 50.0);
fluid.getCharacterization().setTBPModel("PedersenSRK");
fluid.addTBPfraction("C7", 1.0, 0.092, 0.73);
import neqsim.thermo.system.SystemPrEos;
// For Peng-Robinson equation of state
SystemPrEos fluid = new SystemPrEos(298.15, 50.0);
fluid.getCharacterization().setTBPModel("PedersenPR");
fluid.addTBPfraction("C7", 1.0, 0.092, 0.73);
Reference: Pedersen, K.S., Thomassen, P., Fredenslund, A. (1984). "Thermodynamics of Petroleum Mixtures Containing Heavy Hydrocarbons." Ind. Eng. Chem. Process Des. Dev., 23, 566-573.
Best for: Light to medium petroleum fractions (MW < 300 g/mol)
The Riazi-Daubert model uses a simple exponential-power law form that works well for lighter fractions. For heavier fractions (MW > 300), it automatically falls back to the Pedersen model.
Critical Temperature (K): $$T_c = \frac{5}{9} \times 554.4 \times \exp(-1.3478 \times 10^{-4} \cdot M - 0.61641 \cdot SG) \times M^{0.2998} \times SG^{1.0555}$$
Critical Pressure (bar): $$P_c = 0.068947 \times 4.5203 \times 10^{4} \times \exp(-1.8078 \times 10^{-3} \cdot M - 0.3084 \cdot SG) \times M^{-0.8063} \times SG^{1.6015}$$
Boiling Point (K): $$T_b = 97.58 \times M^{0.3323} \times SG^{0.04609}$$
SystemSrkEos fluid = new SystemSrkEos(298.15, 50.0);
fluid.getCharacterization().setTBPModel("RiaziDaubert");
fluid.addTBPfraction("C7", 1.0, 0.092, 0.73); // Light fraction - uses Riazi-Daubert
fluid.addTBPfraction("C30", 0.5, 0.400, 0.90); // Heavy fraction - falls back to Pedersen
Reference: Riazi, M.R. and Daubert, T.E. (1980). "Simplify Property Predictions." Hydrocarbon Processing, 59(3), 115-116.
Best for: General purpose characterization, especially when Watson K-factor is known
The Lee-Kesler model is based on generalized correlations using boiling point and specific gravity as primary inputs. It is widely used in the petroleum industry.
Critical Temperature (K): $$T_c = 189.8 + 450.6 \cdot SG + (0.4244 + 0.1174 \cdot SG) \cdot T_b + (0.1441 - 1.0069 \cdot SG) \times \frac{10^5}{T_b}$$
Critical Pressure (bar): $$\ln(P_c) = 3.3864 - \frac{0.0566}{SG} - f(T_b, SG)$$
where $f(T_b, SG)$ is a polynomial function of boiling point and specific gravity.
Acentric Factor (Kesler-Lee):
For $T_{br} < 0.8$: $$\omega = \frac{\ln(P_{br}) - 5.92714 + \frac{6.09649}{T_{br}} + 1.28862 \cdot \ln(T_{br}) - 0.169347 \cdot T_{br}^6}{15.2518 - \frac{15.6875}{T_{br}} - 13.4721 \cdot \ln(T_{br}) + 0.43577 \cdot T_{br}^6}$$
For $T_{br} \geq 0.8$: $$\omega = -7.904 + 0.1352 \cdot K_w - 0.007465 \cdot K_w^2 + 8.359 \cdot T_{br} + \frac{1.408 - 0.01063 \cdot K_w}{T_{br}}$$
where $T_{br} = T_b/T_c$ and $K_w$ is the Watson characterization factor.
SystemSrkEos fluid = new SystemSrkEos(298.15, 50.0);
fluid.getCharacterization().setTBPModel("Lee-Kesler");
fluid.addTBPfraction("C10", 1.0, 0.142, 0.78);
Reference: Kesler, M.G. and Lee, B.I. (1976). "Improve Prediction of Enthalpy of Fractions." Hydrocarbon Processing, 55(3), 153-158.
Best for: Paraffinic fluids and gas condensates (Watson K > 12)
The Twu model uses n-alkanes as reference compounds and applies perturbation corrections based on specific gravity differences. This approach is particularly accurate for waxy/paraffinic petroleum fractions.
n-Alkane Critical Temperature: $$T_{c,alk} = T_b \times \left[0.533272 + 0.343831 \times 10^{-3} \cdot T_b + 2.526167 \times 10^{-7} \cdot T_b^2 - 1.65848 \times 10^{-10} \cdot T_b^3 + \frac{4.60774 \times 10^{24}}{T_b^{13}}\right]^{-1}$$
Perturbation Function: $$f_T = \Delta S_T \times \left(-0.270159 \cdot T_b^{-0.5} + (0.0398285 - 0.706691 \cdot T_b^{-0.5}) \cdot \Delta S_T\right)$$
where $\Delta S_T = \exp(5.0 \cdot (SG_{alk} - SG)) - 1$
Corrected Critical Temperature: $$T_c = T_{c,alk} \times \left(\frac{1 + 2f_T}{1 - 2f_T}\right)^2$$
SystemSrkEos fluid = new SystemSrkEos(298.15, 50.0);
fluid.getCharacterization().setTBPModel("Twu");
fluid.addTBPfraction("C10", 1.0, 0.142, 0.78);
Reference: Twu, C.H. (1984). "An Internally Consistent Correlation for Predicting the Critical Properties and Molecular Weights of Petroleum and Coal-Tar Liquids." Fluid Phase Equilibria, 16, 137-150.
Best for: Refining industry applications, heavy oils with API gravity data
The Cavett model in NeqSim uses a hybrid Lee-Kesler/Cavett approach with API gravity corrections. This provides robust results across a wide range of petroleum fractions while maintaining the API gravity sensitivity important for refining applications.
$$API = \frac{141.5}{SG} - 131.5$$
| API Range | Classification | SG Range |
|---|---|---|
| > 31.1° | Light crude | < 0.87 |
| 22.3° - 31.1° | Medium crude | 0.87 - 0.92 |
| < 22.3° | Heavy crude | > 0.92 |
The model uses Lee-Kesler correlations as the base, with API corrections for heavy fractions:
Critical Temperature (API < 30°): $$T_c = T_{c,LK} \times [1 + 0.002 \cdot (30 - API)]$$
Critical Pressure (API < 30°): $$P_c = P_{c,LK} \times [1 + 0.001 \cdot (30 - API)]$$
Acentric Factor (Edmister): $$\omega = \frac{3}{7} \times \frac{\log_{10}(P_c/P_{ref})}{T_c/T_b - 1} - 1$$
Bounded to range [0.0, 1.5] for physical validity.
SystemSrkEos fluid = new SystemSrkEos(298.15, 50.0);
fluid.getCharacterization().setTBPModel("Cavett");
// Heavy oil example (API ~ 20°)
fluid.addTBPfraction("HeavyFrac", 1.0, 0.300, 0.93);
Reference: Cavett, R.H. (1962). "Physical Data for Distillation Calculations, Vapor-Liquid Equilibria." Proc. 27th API Meeting, San Francisco.
Best for: Reservoir engineering, quick estimates, black oil PVT
The Standing model uses Riazi-Daubert style correlations for robust critical property estimation. It's widely used in reservoir simulation tools.
Same as Riazi-Daubert (see Section 3.2).
SystemSrkEos fluid = new SystemSrkEos(298.15, 50.0);
fluid.getCharacterization().setTBPModel("Standing");
fluid.addTBPfraction("C7", 1.0, 0.092, 0.73);
Reference: Standing, M.B. (1977). "Volumetric and Phase Behavior of Oil Field Hydrocarbon Systems." SPE, Dallas.
The Watson characterization factor ($K_w$) is useful for classifying petroleum fractions and selecting appropriate TBP models:
$$K_w = \frac{(1.8 \cdot T_b)^{1/3}}{SG}$$
| K_w Range | Fluid Type | Recommended Model |
|---|---|---|
| > 12.5 | Paraffinic (gas condensates) | Twu |
| 11.5 - 12.5 | Mixed/intermediate | Pedersen or Lee-Kesler |
| 10.5 - 11.5 | Naphthenic | Pedersen or RiaziDaubert |
| < 10.5 | Aromatic | Pedersen |
TBPfractionModel tbpModel = new TBPfractionModel();
double Kw = tbpModel.calcWatsonKFactor(0.142, 0.78); // MW in kg/mol, density in g/cm³
System.out.println("Watson K-factor: " + Kw);
Is EOS = Peng-Robinson?
├── Yes → Is fluid heavy (MW > 500)?
│ ├── Yes → PedersenPRHeavyOil
│ └── No → PedersenPR
└── No (SRK) → Is fluid heavy (MW > 500)?
├── Yes → PedersenSRKHeavyOil
└── No → Is K_w > 12 (paraffinic)?
├── Yes → Twu
└── No → Is MW < 300?
├── Yes → RiaziDaubert or Lee-Kesler
└── No → PedersenSRK
NeqSim can recommend an appropriate model based on fluid properties:
TBPfractionModel tbpModel = new TBPfractionModel();
String recommended = tbpModel.recommendTBPModel(
0.200, // Average MW in kg/mol
0.85, // Average density in g/cm³
"SRK" // EOS type: "SRK" or "PR"
);
System.out.println("Recommended model: " + recommended);
String[] models = TBPfractionModel.getAvailableModels();
for (String model : models) {
System.out.println(model);
}
Reference values for common petroleum fractions:
| Component | MW (g/mol) | SG | T_c (K) | P_c (bar) | ω |
|---|---|---|---|---|---|
| n-Heptane (C7) | 100 | 0.684 | 540 | 27.4 | 0.35 |
| C7 (typical) | 96-100 | 0.72-0.74 | 540-560 | 27-30 | 0.30-0.35 |
| C10 | 134-142 | 0.76-0.79 | 600-640 | 20-25 | 0.45-0.55 |
| C15 | 200-210 | 0.81-0.83 | 680-720 | 15-18 | 0.65-0.75 |
| C20 | 275-285 | 0.85-0.87 | 750-800 | 12-15 | 0.85-0.95 |
| C30 | 400-420 | 0.88-0.90 | 850-900 | 8-10 | 1.0-1.2 |
import neqsim.thermo.system.SystemSrkEos;
import neqsim.thermodynamicoperations.ThermodynamicOperations;
public class BasicCharacterization {
public static void main(String[] args) {
// Create SRK fluid
SystemSrkEos fluid = new SystemSrkEos(298.15, 50.0);
// Add light components
fluid.addComponent("methane", 70.0);
fluid.addComponent("ethane", 10.0);
fluid.addComponent("propane", 5.0);
// Set TBP model before adding heavy fractions
fluid.getCharacterization().setTBPModel("PedersenSRK");
// Add TBP fractions
fluid.addTBPfraction("C7", 3.0, 0.092, 0.73);
fluid.addTBPfraction("C8", 2.5, 0.104, 0.76);
fluid.addTBPfraction("C9", 2.0, 0.118, 0.78);
fluid.addTBPfraction("C10", 1.5, 0.134, 0.79);
// Set mixing rule and initialize
fluid.setMixingRule("classic");
// Perform flash calculation
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
// Print results
fluid.prettyPrint();
// Access individual component properties
System.out.println("\nC7 Critical Properties:");
System.out.println("Tc = " + fluid.getComponent("C7_PC").getTC() + " K");
System.out.println("Pc = " + fluid.getComponent("C7_PC").getPC() + " bar");
System.out.println("omega = " + fluid.getComponent("C7_PC").getAcentricFactor());
}
}
import neqsim.thermo.system.SystemSrkEos;
public class ModelComparison {
public static void main(String[] args) {
String[] models = {"PedersenSRK", "Lee-Kesler", "RiaziDaubert", "Twu", "Cavett", "Standing"};
System.out.println("=== TBP Model Comparison for C10 (MW=142 g/mol, SG=0.78) ===");
System.out.printf("%-15s %10s %10s %10s%n", "Model", "Tc (K)", "Pc (bar)", "omega");
System.out.println(StringUtils.repeat("-", 50));
for (String modelName : models) {
SystemSrkEos fluid = new SystemSrkEos(298.15, 50.0);
fluid.getCharacterization().setTBPModel(modelName);
fluid.addTBPfraction("C10", 1.0, 0.142, 0.78);
double Tc = fluid.getComponent(0).getTC();
double Pc = fluid.getComponent(0).getPC();
double omega = fluid.getComponent(0).getAcentricFactor();
System.out.printf("%-15s %10.2f %10.2f %10.4f%n", modelName, Tc, Pc, omega);
}
}
}
import neqsim.thermo.system.SystemSrkEos;
import neqsim.thermodynamicoperations.ThermodynamicOperations;
public class GasCondensateExample {
public static void main(String[] args) {
SystemSrkEos fluid = new SystemSrkEos(350.0, 150.0);
// Lean gas condensate composition
fluid.addComponent("nitrogen", 1.5);
fluid.addComponent("CO2", 2.0);
fluid.addComponent("methane", 80.0);
fluid.addComponent("ethane", 6.0);
fluid.addComponent("propane", 3.0);
fluid.addComponent("i-butane", 0.8);
fluid.addComponent("n-butane", 1.2);
fluid.addComponent("i-pentane", 0.5);
fluid.addComponent("n-pentane", 0.5);
// Use Twu model for paraffinic gas condensate
fluid.getCharacterization().setTBPModel("Twu");
// Add C6+ fractions
fluid.addTBPfraction("C6", 1.0, 0.086, 0.68);
fluid.addTBPfraction("C7+", 3.5, 0.130, 0.76);
fluid.setMixingRule("classic");
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
System.out.println("Gas Condensate Flash at " + fluid.getTemperature() + " K, "
+ fluid.getPressure() + " bar");
fluid.prettyPrint();
}
}
import neqsim.thermo.system.SystemPrEos;
import neqsim.thermodynamicoperations.ThermodynamicOperations;
public class HeavyOilExample {
public static void main(String[] args) {
// Use Peng-Robinson for heavy oil
SystemPrEos fluid = new SystemPrEos(333.15, 10.0);
// Light ends
fluid.addComponent("methane", 5.0);
fluid.addComponent("ethane", 2.0);
fluid.addComponent("propane", 3.0);
fluid.addComponent("n-butane", 2.0);
fluid.addComponent("n-pentane", 3.0);
// Use heavy oil model
fluid.getCharacterization().setTBPModel("PedersenPRHeavyOil");
// Heavy fractions (API ~ 15°, SG ~ 0.96)
fluid.addTBPfraction("C6-C10", 15.0, 0.120, 0.80);
fluid.addTBPfraction("C11-C20", 25.0, 0.250, 0.88);
fluid.addTBPfraction("C21-C30", 20.0, 0.380, 0.92);
fluid.addTBPfraction("C31+", 25.0, 0.550, 0.96);
fluid.setMixingRule("classic");
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
System.out.println("Heavy Oil Flash at " + fluid.getTemperature() + " K");
fluid.prettyPrint();
}
}
from neqsim.thermo import fluid
# Create fluid with TBP model selection
oil = fluid('srk')
oil.getCharacterization().setTBPModel("PedersenSRK")
# Add components
oil.addComponent("methane", 70.0)
oil.addComponent("ethane", 10.0)
oil.addTBPfraction("C7", 5.0, 0.092, 0.73)
oil.addTBPfraction("C10", 3.0, 0.142, 0.78)
oil.setMixingRule("classic")
oil.setTemperature(298.15, "K")
oil.setPressure(50.0, "bara")
# Flash and print
from neqsim.thermodynamicoperations import TPflash
TPflash(oil)
oil.prettyPrint()
from jpype import JClass
# Import Java classes directly
SystemSrkEos = JClass('neqsim.thermo.system.SystemSrkEos')
TBPfractionModel = JClass('neqsim.thermo.characterization.TBPfractionModel')
# Create fluid
fluid = SystemSrkEos(298.15, 50.0)
# Get model recommendation
tbpModel = TBPfractionModel()
recommended = tbpModel.recommendTBPModel(0.200, 0.85, "SRK")
print(f"Recommended model: {recommended}")
# Set model and add fractions
fluid.getCharacterization().setTBPModel(recommended)
fluid.addTBPfraction("C10", 1.0, 0.142, 0.78)
# Print critical properties
print(f"Tc = {fluid.getComponent(0).getTC()} K")
print(f"Pc = {fluid.getComponent(0).getPC()} bar")
Unrealistic Tc values (> 1000 K or < 400 K)
Negative acentric factor
Flash convergence issues with heavy fractions
Model not found error
TBPfractionModel.getAvailableModels() to see valid names| Property | Required Unit | Common Mistake |
|---|---|---|
| Molecular Weight | kg/mol | Using g/mol (divide by 1000) |
| Density | g/cm³ | Using kg/m³ (divide by 1000) |
| Temperature | K | (internal) |
| Pressure | bar | (internal) |
Pedersen, K.S., Thomassen, P., Fredenslund, A. (1984). "Thermodynamics of Petroleum Mixtures Containing Heavy Hydrocarbons." Ind. Eng. Chem. Process Des. Dev., 23, 566-573.
Kesler, M.G., Lee, B.I. (1976). "Improve Prediction of Enthalpy of Fractions." Hydrocarbon Processing, 55(3), 153-158.
Riazi, M.R., Daubert, T.E. (1980). "Simplify Property Predictions." Hydrocarbon Processing, 59(3), 115-116.
Twu, C.H. (1984). "An Internally Consistent Correlation for Predicting the Critical Properties and Molecular Weights of Petroleum and Coal-Tar Liquids." Fluid Phase Equilibria, 16, 137-150.
Cavett, R.H. (1962). "Physical Data for Distillation Calculations, Vapor-Liquid Equilibria." Proc. 27th API Meeting, San Francisco.
Standing, M.B. (1977). "Volumetric and Phase Behavior of Oil Field Hydrocarbon Systems." SPE, Dallas.
Edmister, W.C. (1958). "Applied Hydrocarbon Thermodynamics, Part 4: Compressibility Factors and Equations of State." Petroleum Refiner, 37(4), 173-179.
Accurate phase behavior predictions start with a realistic fluid description. NeqSim supports full compositional models, TBP cuts, and black-oil style pseudo-components.
addPlusFraction(name, moles, molarMass, density) when only overall heavy fraction data are available.addTBPfraction(name, moles, density, molarMass) to preserve multiple heavy cuts with their own boiling ranges.SystemInterface oil = new SystemSrkEos(323.15, 150.0);
oil.createDatabase(true);
oil.addComponent("nitrogen", 0.01);
oil.addComponent("methane", 0.60);
oil.addTBPfraction("C7", 0.08, 0.73, 7.5);
oil.addTBPfraction("C10", 0.10, 0.80, 10.5);
oil.addPlusFraction("C20+", 0.21, 0.92, 22.0);
oil.setMixingRule(2);
setBinaryInteractionParameter) to match dew/bubble points.splitTBPfraction to subdivide heavy cuts using predefined distillation curves.For fluids with asphaltene precipitation risk, use the PedersenAsphalteneCharacterization class:
import neqsim.thermo.characterization.PedersenAsphalteneCharacterization;
// Create asphaltene characterization
PedersenAsphalteneCharacterization asphChar = new PedersenAsphalteneCharacterization();
asphChar.setAsphalteneMW(750.0); // Molecular weight g/mol
asphChar.setAsphalteneDensity(1.10); // Density g/cm³
// Add asphaltene as pseudo-component (before mixing rule)
asphChar.addAsphalteneToSystem(oil, 0.02); // 2 mol% asphaltene
oil.setMixingRule("classic");
// Perform TPflash with asphaltene detection
boolean hasAsphaltene = PedersenAsphalteneCharacterization.TPflash(oil);
NeqSim supports two asphaltene phase types:
PhaseType.ASPHALTENE: Solid asphaltene with literature-based propertiesPhaseType.LIQUID_ASPHALTENE: Pedersen's liquid approach using cubic EOSAfter running ThermodynamicOperations.TPflash(), collect standard PVT outputs:
oil.initProperties();
System.out.println("Bo at separator: " + oil.getPhase("oil").getVolume() / oil.getTotalNumberOfMoles());
System.out.println("GOR at separator: " + oil.getPhase("gas").getNumberOfMoles()/oil.getPhase("oil").getNumberOfMoles());
For multi-stage separators, clone the fluid after each flash and continue flashing at downstream conditions.
toJson() for reproducible studies.addFluid(existingSystem) to combine live-oil and gas-cap fluids or to merge lab and model data sets.Documentation for plus fraction and asphaltene characterization in NeqSim.
Location: neqsim.thermo.characterization
The characterization package handles petroleum plus fraction and asphaltene characterization:
import neqsim.thermo.system.SystemSrkEos;
SystemSrkEos fluid = new SystemSrkEos(373.15, 100.0);
// Light components
fluid.addComponent("methane", 0.70);
fluid.addComponent("ethane", 0.10);
fluid.addComponent("propane", 0.08);
fluid.addComponent("n-butane", 0.05);
// C7+ as single pseudo-component
fluid.addTBPfraction("C7+", 0.07, 150.0, 0.78); // name, moles, MW, SG
// Split C7+ into multiple fractions
fluid.addTBPfraction("C7", 0.02, 96.0, 0.727);
fluid.addTBPfraction("C8", 0.015, 107.0, 0.749);
fluid.addTBPfraction("C9", 0.01, 121.0, 0.768);
fluid.addTBPfraction("C10+", 0.025, 180.0, 0.82);
import neqsim.thermo.characterization.PedersenCharacterization;
// Characterize using Pedersen correlations
PedersenCharacterization charPedersen = new PedersenCharacterization(fluid);
charPedersen.characterize();
import neqsim.thermo.characterization.WhitsonCharacterization;
// Characterize using Whitson gamma distribution
WhitsonCharacterization charWhitson = new WhitsonCharacterization(fluid);
charWhitson.setAlpha(1.0); // Shape parameter
charWhitson.characterize();
// addTBPfraction(name, moles, MW, specificGravity)
fluid.addTBPfraction("C7", moles, 96.0, 0.727);
// addPlusFraction with characterization
fluid.addPlusFraction("C20+", moles, 400.0, 0.90);
For pseudo-components, critical properties are estimated using correlations:
| Correlation | Properties Estimated |
|---|---|
| Twu | Tc, Pc, omega from MW, SG |
| Lee-Kesler | Tc, Pc, omega from Tb, SG |
| Riazi-Daubert | Tb from MW, SG |
| Pedersen | Tc, Pc, omega for petroleum |
The PedersenAsphalteneCharacterization class implements Pedersen's approach for treating asphaltene as a heavy liquid pseudo-component. This enables liquid-liquid equilibrium (LLE) calculations for asphaltene precipitation.
import neqsim.thermo.characterization.PedersenAsphalteneCharacterization;
import neqsim.thermo.system.SystemSrkEos;
// Create fluid system
SystemInterface fluid = new SystemSrkEos(373.15, 50.0);
fluid.addComponent("methane", 0.40);
fluid.addComponent("n-pentane", 0.25);
fluid.addComponent("n-heptane", 0.20);
fluid.addComponent("nC10", 0.10);
// Create and configure asphaltene characterization
PedersenAsphalteneCharacterization asphChar = new PedersenAsphalteneCharacterization();
asphChar.setAsphalteneMW(750.0); // Molecular weight g/mol
asphChar.setAsphalteneDensity(1.10); // Density g/cm³
// Add asphaltene pseudo-component (BEFORE setting mixing rule)
asphChar.addAsphalteneToSystem(fluid, 0.05); // 5 mol% asphaltene
// Set mixing rule (AFTER adding all components)
fluid.setMixingRule("classic");
// Print estimated critical properties
System.out.println(asphChar.toString());
Pedersen's method estimates critical properties from molecular weight (MW) and liquid density (ρ):
| Property | Correlation |
|---|---|
| Critical Temperature (Tc) | f(MW, ρ) |
| Critical Pressure (Pc) | f(MW, ρ) |
| Acentric Factor (ω) | f(MW, ρ) |
| Normal Boiling Point (Tb) | f(MW, ρ) |
Typical values for asphaltene (MW=750 g/mol, ρ=1.10 g/cm³):
| Property | Value | Unit |
|---|---|---|
| Tc | 996 | K |
| Pc | 16.3 | bar |
| ω | 0.925 | - |
| Tb | 838 | K |
The class provides static methods for TPflash with automatic detection of asphaltene-rich phases:
// Static TPflash - marks asphaltene-rich liquid phases as LIQUID_ASPHALTENE
boolean hasAsphaltene = PedersenAsphalteneCharacterization.TPflash(fluid);
// With explicit T,P specification
boolean hasAsphaltene = PedersenAsphalteneCharacterization.TPflash(fluid, 373.15, 50.0);
// Check result
if (hasAsphaltene) {
System.out.println("Asphaltene-rich liquid phase detected");
fluid.prettyPrint(); // Shows "ASPHALTENE LIQUID" column
}
A liquid phase is marked as PhaseType.LIQUID_ASPHALTENE when:
import neqsim.thermo.system.SystemSrkEos;
import neqsim.thermodynamicoperations.ThermodynamicOperations;
SystemSrkEos gas = new SystemSrkEos(373.15, 100.0);
// Wellstream composition
gas.addComponent("nitrogen", 0.015);
gas.addComponent("CO2", 0.020);
gas.addComponent("methane", 0.750);
gas.addComponent("ethane", 0.080);
gas.addComponent("propane", 0.045);
gas.addComponent("i-butane", 0.012);
gas.addComponent("n-butane", 0.020);
gas.addComponent("i-pentane", 0.008);
gas.addComponent("n-pentane", 0.010);
gas.addComponent("n-hexane", 0.015);
// C7+ fraction
gas.addTBPfraction("C7+", 0.025, 145.0, 0.78);
gas.setMixingRule("classic");
ThermodynamicOperations ops = new ThermodynamicOperations(gas);
ops.TPflash();
System.out.println("Gas fraction: " + gas.getGasPhase().getBeta());
System.out.println("C7+ in gas: " + gas.getGasPhase().getComponent("C7+_PC").getx());
// Black oil with detailed C7+ split
SystemSrkEos oil = new SystemSrkEos(350.0, 50.0);
oil.addComponent("methane", 0.40);
oil.addComponent("ethane", 0.08);
oil.addComponent("propane", 0.06);
oil.addComponent("n-butane", 0.04);
oil.addComponent("n-pentane", 0.03);
oil.addComponent("n-hexane", 0.03);
// Detailed C7+ split
oil.addTBPfraction("C7", 0.05, 96.0, 0.727);
oil.addTBPfraction("C8", 0.05, 107.0, 0.749);
oil.addTBPfraction("C9", 0.04, 121.0, 0.768);
oil.addTBPfraction("C10", 0.04, 134.0, 0.782);
oil.addTBPfraction("C11", 0.03, 147.0, 0.793);
oil.addTBPfraction("C12+", 0.15, 250.0, 0.85);
oil.setMixingRule("classic");
This guide explains how to add a fluid into another's characterization framework in NeqSim, making them compatible for use with a common Equation of State (EOS).
When working with multiple reservoir fluids or combining fluids from different sources, it is often necessary to:
NeqSim provides several approaches based on the methods described in Pedersen et al., "Phase Behavior of Petroleum Reservoir Fluids".
The mathematical foundations for fluid combining and re-characterization are based on Pedersen et al. (2014), Chapters 5.5 and 5.6.
When combining fluids, total mass and moles must be conserved:
$$n_{total} = \sum_{f=1}^{N_f} \sum_{i=1}^{N_c} n_{f,i}$$
$$m_{total} = \sum_{f=1}^{N_f} \sum_{i=1}^{N_c} n_{f,i} \cdot M_{f,i}$$
where:
For combining pseudo-components from multiple fluids into unified groups, NeqSim uses mass-weighted averaging for intensive properties.
The combined molar mass of a pseudo-component group $g$ is:
$$M_g = \frac{m_g}{n_g} = \frac{\sum_{j \in g} m_j}{\sum_{j \in g} n_j}$$
where $m_j$ and $n_j$ are the mass and moles of contributing pseudo-component $j$.
Combined density uses volume-weighted averaging to ensure volume additivity:
$$\rho_g = \frac{m_g}{V_g} = \frac{\sum_{j \in g} m_j}{\sum_{j \in g} \frac{m_j}{\rho_j}}$$
These properties use mass-weighted averaging within each group:
$$T_{c,g} = \frac{\sum_{j \in g} m_j \cdot T_{c,j}}{\sum_{j \in g} m_j}$$
$$P_{c,g} = \frac{\sum_{j \in g} m_j \cdot P_{c,j}}{\sum_{j \in g} m_j}$$
$$\omega_g = \frac{\sum_{j \in g} m_j \cdot \omega_j}{\sum_{j \in g} m_j}$$
$$T_{b,g} = \frac{\sum_{j \in g} m_j \cdot T_{b,j}}{\sum_{j \in g} m_j}$$
When combining multiple fluids with different pseudo-component structures, a two-level weighting scheme is applied:
For property $\theta$ (such as $T_c$, $P_c$, $\omega$, or $T_b$) in combined group $g$:
$$\theta_g = \frac{\sum_{f=1}^{N_f} w_f \cdot x_{f,g} \cdot \theta_{f,g}}{\sum_{f=1}^{N_f} w_f \cdot x_{f,g}}$$
where:
NeqSim determines boundaries between pseudo-component groups using mass-based quantiles of the boiling point (or molar mass) distribution.
For $N_{PC}$ target pseudo-components:
$$M_{cumulative,k} = \frac{k}{N_{PC}} \cdot m_{total,pseudo}$$
where $k = 1, 2, ..., N_{PC}-1$ defines the boundary positions.
The boundary boiling point $T_{b,boundary,k}$ is the boiling point at which cumulative mass equals $M_{cumulative,k}$.
When re-characterizing a source fluid to match a reference fluid's pseudo-component structure:
Extract boundary points from the reference fluid: $$T_{b,boundary,k} = \frac{T_{b,k} + T_{b,k+1}}{2}$$
where $T_{b,k}$ is the boiling point of reference pseudo-component $k$.
Redistribute source components into reference bins based on these boundaries
Calculate weighted properties for each bin using the mass-weighted formulas above
When combining fluids, density is calculated to preserve volume additivity:
$$V_{total} = \sum_{j=1}^{N} \frac{m_j}{\rho_j}$$
$$\rho_{combined} = \frac{\sum_{j=1}^{N} m_j}{\sum_{j=1}^{N} \frac{m_j}{\rho_j}}$$
This is equivalent to the harmonic mean weighted by mass fractions.
addFluid() for Simple Fluid AdditionThe simplest way to add one fluid to another is using the addFluid() method. This works best when:
import neqsim.thermo.system.SystemSrkEos;
import neqsim.thermo.system.SystemInterface;
// Create first fluid
SystemInterface fluid1 = new SystemSrkEos(298.15, 50.0);
fluid1.addComponent("methane", 0.6);
fluid1.addComponent("ethane", 0.1);
fluid1.addTBPfraction("C7", 0.2, 0.100, 0.80);
fluid1.addTBPfraction("C10", 0.1, 0.200, 0.85);
fluid1.setMixingRule("classic");
// Create second fluid
SystemInterface fluid2 = new SystemSrkEos(298.15, 50.0);
fluid2.addComponent("methane", 0.4);
fluid2.addComponent("propane", 0.15);
fluid2.addTBPfraction("C8", 0.3, 0.120, 0.82);
fluid2.setMixingRule("classic");
// Add fluid2 to fluid1
fluid1.addFluid(fluid2);
// The combined fluid now contains all components from both fluids
// - Existing components (methane) have moles added together
// - New components (propane, C8_PC) are added with their properties
addFluid():createDatabase(true) is called automatically when new components are addedaddFluids() for Creating New Combined FluidTo create a new fluid without modifying the originals:
import neqsim.thermo.system.SystemInterface;
// Create a new combined fluid (originals are unchanged)
SystemInterface combined = SystemInterface.addFluids(fluid1, fluid2);
This clones fluid1, then adds fluid2 to it.
When combining fluids with different pseudo-component characterizations, use combineReservoirFluids() to redistribute heavy fractions into a common pseudo-component structure:
import neqsim.thermo.system.SystemInterface;
import neqsim.thermo.system.SystemPrEos;
import neqsim.thermo.component.ComponentInterface;
// Fluid 1: Has C7 and C10 pseudo-components
SystemInterface fluid1 = new SystemPrEos(298.15, 50.0);
fluid1.addComponent("methane", 0.6);
fluid1.addComponent("ethane", 0.1);
fluid1.addTBPfraction("C7", 0.2, 0.100, 0.80);
ComponentInterface c7 = fluid1.getComponent("C7_PC");
c7.setNormalBoilingPoint(350.0);
c7.setTC(540.0);
c7.setPC(28.0);
c7.setAcentricFactor(0.32);
fluid1.addTBPfraction("C10", 0.1, 0.200, 0.85);
ComponentInterface c10 = fluid1.getComponent("C10_PC");
c10.setNormalBoilingPoint(410.0);
c10.setTC(620.0);
c10.setPC(22.0);
c10.setAcentricFactor(0.36);
// Fluid 2: Has C8 and C11 pseudo-components (different structure!)
SystemInterface fluid2 = new SystemPrEos(298.15, 50.0);
fluid2.addComponent("methane", 0.4);
fluid2.addComponent("n-butane", 0.05);
fluid2.addTBPfraction("C8", 0.15, 0.120, 0.82);
ComponentInterface c8 = fluid2.getComponent("C8_PC");
c8.setNormalBoilingPoint(380.0);
c8.setTC(580.0);
c8.setPC(26.0);
c8.setAcentricFactor(0.34);
fluid2.addTBPfraction("C11", 0.05, 0.220, 0.86);
ComponentInterface c11 = fluid2.getComponent("C11_PC");
c11.setNormalBoilingPoint(440.0);
c11.setTC(660.0);
c11.setPC(20.0);
c11.setAcentricFactor(0.38);
// Combine into 2 unified pseudo-components using Pedersen mixing weights
int targetPseudoComponents = 2;
SystemInterface combined = SystemInterface.combineReservoirFluids(
targetPseudoComponents,
fluid1,
fluid2
);
// Result: Combined fluid has:
// - methane: 1.0 mol (summed)
// - ethane: 0.1 mol
// - n-butane: 0.05 mol
// - PC1_PC: Lower boiling pseudo-component (weighted blend)
// - PC2_PC: Higher boiling pseudo-component (weighted blend)
Consider combining two fluids with pseudo-components:
| Fluid | Component | Moles | MW (kg/mol) | Mass (kg) | Tb (K) |
|---|---|---|---|---|---|
| 1 | C7_PC | 0.20 | 0.100 | 0.020 | 350 |
| 1 | C10_PC | 0.10 | 0.200 | 0.020 | 410 |
| 2 | C8_PC | 0.15 | 0.120 | 0.018 | 380 |
| 2 | C11_PC | 0.05 | 0.220 | 0.011 | 440 |
Step 1: Sort by boiling point: C7 (350K) → C8 (380K) → C10 (410K) → C11 (440K)
Step 2: For 2 target groups, find mass boundary at 50%:
Step 3: Group 1 (C7 + C8): $$M_1 = \frac{0.020 + 0.018}{0.20 + 0.15} = 0.1086 \text{ kg/mol}$$
$$T_{b,1} = \frac{0.020 \times 350 + 0.018 \times 380}{0.020 + 0.018} = 364.2 \text{ K}$$
Step 4: Group 2 (C10 + C11): $$M_2 = \frac{0.020 + 0.011}{0.10 + 0.05} = 0.207 \text{ kg/mol}$$
$$T_{b,2} = \frac{0.020 \times 410 + 0.011 \times 440}{0.020 + 0.011} = 420.6 \text{ K}$$
When you need to make one fluid's characterization match another (e.g., for blending in simulation), use characterizeToReference():
import neqsim.thermo.system.SystemInterface;
import neqsim.thermo.system.SystemPrEos;
// Reference fluid defines the target pseudo-component structure
SystemInterface reference = new SystemPrEos(298.15, 50.0);
reference.addTBPfraction("C7", 0.10, 0.090, 0.78);
reference.getComponent("C7_PC").setNormalBoilingPoint(340.0);
reference.getComponent("C7_PC").setTC(530.0);
reference.getComponent("C7_PC").setPC(29.0);
reference.getComponent("C7_PC").setAcentricFactor(0.31);
reference.addTBPfraction("C8", 0.12, 0.110, 0.81);
reference.getComponent("C8_PC").setNormalBoilingPoint(360.0);
reference.getComponent("C8_PC").setTC(550.0);
reference.getComponent("C8_PC").setPC(27.0);
reference.getComponent("C8_PC").setAcentricFactor(0.33);
reference.addTBPfraction("C9", 0.15, 0.150, 0.84);
reference.getComponent("C9_PC").setNormalBoilingPoint(380.0);
reference.getComponent("C9_PC").setTC(570.0);
reference.getComponent("C9_PC").setPC(25.0);
reference.getComponent("C9_PC").setAcentricFactor(0.35);
// Source fluid has different characterization
SystemInterface source = new SystemPrEos(298.15, 50.0);
source.addComponent("methane", 0.7);
source.addTBPfraction("S1", 0.05, 0.090, 0.79);
source.addTBPfraction("S2", 0.07, 0.095, 0.80);
source.addTBPfraction("S3", 0.08, 0.120, 0.82);
source.addTBPfraction("S4", 0.09, 0.150, 0.83);
// Re-characterize source to match reference's pseudo-component structure
SystemInterface characterized = SystemInterface.characterizeToReference(source, reference);
// Result: characterized has C7_PC, C8_PC, C9_PC (same names as reference)
// with properties redistributed from S1-S4 components
Given a reference fluid with $N$ pseudo-components having boiling points $T_{b,1} < T_{b,2} < ... < T_{b,N}$:
Step 1: Calculate boundary boiling points: $$T_{boundary,k} = \frac{T_{b,k} + T_{b,k+1}}{2}, \quad k = 1, ..., N-1$$
Step 2: Assign source pseudo-components to bins:
Step 3: Calculate properties for each bin using mass-weighted averaging: $$\theta_{bin,k} = \frac{\sum_{j \in bin_k} m_j \cdot \theta_j}{\sum_{j \in bin_k} m_j}$$
The resulting fluid has pseudo-components with the same names as the reference (C7_PC, C8_PC, etc.) but with properties derived from the source fluid's heavy fractions.
For more control, use CharacterizationOptions:
import neqsim.thermo.characterization.PseudoComponentCombiner;
import neqsim.thermo.characterization.CharacterizationOptions;
CharacterizationOptions options = new CharacterizationOptions();
options.setTransferBinaryInteractionParameters(true); // Copy BIPs from reference
options.setNormalizeComposition(true); // Normalize mole fractions
options.setGenerateValidationReport(true); // Log validation info
SystemInterface characterized = PseudoComponentCombiner.characterizeToReference(
source,
reference,
options
);
BIPs are critical for accurate phase behavior. When characterizing to a reference:
import neqsim.thermo.characterization.PseudoComponentCombiner;
// Transfer BIPs from reference to characterized fluid
PseudoComponentCombiner.transferBinaryInteractionParameters(reference, characterized);
// BIPs are matched by:
// - Component name for base components
// - Position for pseudo-components (1st PC to 1st PC, etc.)
// Correct: Both use Peng-Robinson
SystemInterface fluid1 = new SystemPrEos(298.15, 50.0);
SystemInterface fluid2 = new SystemPrEos(298.15, 50.0);
// Caution: Mixing EOS types may cause inconsistencies
// SystemInterface fluid1 = new SystemSrkEos(298.15, 50.0);
// SystemInterface fluid2 = new SystemPrEos(298.15, 50.0);
fluid.addTBPfraction("C7", moles, molarMass, density);
ComponentInterface pc = fluid.getComponent("C7_PC");
pc.setNormalBoilingPoint(350.0); // Important for redistribution
pc.setTC(540.0);
pc.setPC(28.0);
pc.setAcentricFactor(0.32);
fluid.setMixingRule("classic");
// or
fluid.setMixingRule(2); // Numeric code for specific mixing rule
combined.init(0); // Initialize mole numbers
combined.init(1); // Initialize thermodynamic properties
// Check total mass conservation
double originalMass = fluid1.getMolarMass() * fluid1.getTotalNumberOfMoles()
+ fluid2.getMolarMass() * fluid2.getTotalNumberOfMoles();
double combinedMass = combined.getMolarMass() * combined.getTotalNumberOfMoles();
// These should be equal within tolerance
| Method | Use Case | Creates New Fluid? | Modifies Original? |
|---|---|---|---|
fluid1.addFluid(fluid2) |
Simple addition | No | Yes (fluid1) |
SystemInterface.addFluids(f1, f2) |
Non-destructive addition | Yes | No |
combineReservoirFluids(n, fluids...) |
Unified characterization | Yes | No |
characterizeToReference(src, ref) |
Match reference structure | Yes | No |
When combining pseudo-components, NeqSim calculates mass-weighted averages for the following properties:
| Property | Symbol | Unit | Weighting Method |
|---|---|---|---|
| Molar Mass | $M$ | kg/mol | Mass/Moles ratio |
| Normal Liquid Density | $\rho$ | kg/m³ | Volume-weighted (harmonic) |
| Normal Boiling Point | $T_b$ | K | Mass-weighted |
| Critical Temperature | $T_c$ | K | Mass-weighted |
| Critical Pressure | $P_c$ | bar | Mass-weighted |
| Acentric Factor | $\omega$ | - | Mass-weighted |
| Critical Volume | $V_c$ | m³/kmol | Mass-weighted |
| Rackett Z-factor | $Z_{RA}$ | - | Mass-weighted |
| Parachor | $P$ | - | Mass-weighted |
| Critical Viscosity | $\mu_c$ | Pa·s | Mass-weighted |
| Triple Point Temperature | $T_{tp}$ | K | Mass-weighted |
| Heat of Fusion | $\Delta H_f$ | J/mol | Mass-weighted |
| Ideal Gas Enthalpy of Formation | $\Delta H_f^{ig}$ | J/mol | Mass-weighted |
| Heat Capacity Coefficients | $C_{p,A}, C_{p,B}, C_{p,C}, C_{p,D}$ | various | Mass-weighted |
| EOS Attractive Term Parameter | $m$ | - | Mass-weighted |
This document provides detailed mathematical documentation of the fluid characterization methods implemented in NeqSim, with emphasis on plus fraction (C7+) modeling, TBP fraction property correlations, and pseudo-component generation.
Petroleum fluids contain thousands of hydrocarbon compounds. For practical thermodynamic calculations, heavy fractions (typically C7+) must be characterized using continuous distribution functions or discrete pseudo-components. NeqSim implements industry-standard characterization methods from Whitson (1983), Pedersen et al. (1984), and others.
The characterization workflow consists of three main steps:
The Whitson gamma distribution (SPE 12233, 1983) is the most widely used continuous distribution for petroleum C7+ fractions.
The molar distribution of the plus fraction is described by a three-parameter gamma distribution:
$$p(M) = \frac{(M - \eta)^{\alpha - 1}}{\beta^\alpha \cdot \Gamma(\alpha)} \exp\left(-\frac{M - \eta}{\beta}\right)$$
where:
NeqSim uses a polynomial approximation for the gamma function:
$$\Gamma(\alpha) \approx \alpha \cdot \left(1 + \sum_{i=1}^{8} b_i \cdot (\alpha - 1)^i\right)$$
with coefficients:
| $i$ | $b_i$ |
|---|---|
| 1 | -0.577191652 |
| 2 | 0.988205891 |
| 3 | -0.897056937 |
| 4 | 0.918206857 |
| 5 | -0.756704078 |
| 6 | 0.482199394 |
| 7 | -0.193527818 |
| 8 | 0.035868343 |
The mean of the gamma distribution equals the plus fraction average molecular weight:
$$\bar{M}_{C7+} = \eta + \alpha \cdot \beta$$
Therefore, the scale parameter is calculated as:
$$\beta = \frac{\bar{M}_{C7+} - \eta}{\alpha}$$
For splitting the plus fraction into SCN groups, NeqSim computes:
$$P_0(M_b) = \int_\eta^{M_b} p(M) \, dM = Q(M_b) \cdot S(M_b)$$
where:
$$Q = \frac{\exp(-Y) \cdot Y^\alpha}{\Gamma(\alpha)}, \quad Y = \frac{M_b - \eta}{\beta}$$
$$S = \sum_{j=0}^{\infty} \frac{Y^j}{\prod_{k=0}^{j}(\alpha + k)} = \frac{1}{\alpha} + \frac{Y}{\alpha(\alpha+1)} + \frac{Y^2}{\alpha(\alpha+1)(\alpha+2)} + \cdots$$
The first moment $P_1$ is used for average molecular weight calculation:
$$P_1(M_b) = Q(M_b) \cdot \left(S(M_b) - \frac{1}{\alpha}\right)$$
For an SCN group bounded by molecular weights $M_L$ (lower) and $M_U$ (upper):
$$z_i = z_{C7+} \cdot \left[P_0(M_U) - P_0(M_L)\right]$$
$$\bar{M}_i = \eta + \alpha \cdot \beta \cdot \frac{P_1(M_U) - P_1(M_L)}{P_0(M_U) - P_0(M_L)}$$
The shape parameter $\alpha$ characterizes the fluid type:
| Fluid Type | Typical $\alpha$ Range | Watson $K_w$ |
|---|---|---|
| Gas condensates | 0.5 - 1.0 | > 12.5 |
| Black oils | 1.0 - 2.0 | 11.5 - 12.5 |
| Heavy/naphthenic oils | 2.0 - 4.0 | < 11.5 |
NeqSim can automatically estimate $\alpha$ using the Watson characterization factor:
$$K_w = 4.5579 \cdot M_{C7+}^{0.15178} \cdot \rho_{C7+}^{-1.18241}$$
where $\rho$ is specific gravity (g/cm³). The estimated alpha is:
$$\alpha = \begin{cases} 0.5 + 0.1(K_w - 12.5) & K_w \geq 12.5 \text{ (paraffinic)} \ 1.0 + 0.5(K_w - 11.5) & 11.5 \leq K_w < 12.5 \text{ (mixed)} \ 1.5 + 0.5(K_w - 10.5) & 10.5 \leq K_w < 11.5 \text{ (naphthenic)} \ 2.0 + 0.5(10.5 - K_w) & K_w < 10.5 \text{ (aromatic)} \end{cases}$$
The Pedersen model (Pedersen et al., 1984) uses an exponential distribution:
$$\ln(z_n) = A + B \cdot n$$
where:
The coefficients are determined by matching:
$$\rho_n = C_1 + C_2 \cdot \ln(n)$$
where $C_1$ and $C_2$ are fitted to match the measured plus fraction density.
For SRK equation of state, critical properties are calculated using Pedersen et al. correlations:
$$T_c = a_0 \cdot \rho + a_1 \cdot \ln(M) + a_2 \cdot M + \frac{a_3}{M}$$
| Coefficient | Light Oil (M < 1120 g/mol) | Heavy Oil |
|---|---|---|
| $a_0$ | 163.12 | 830.63 |
| $a_1$ | 86.052 | 17.5228 |
| $a_2$ | 0.43475 | 0.0455911 |
| $a_3$ | -1877.4 | -11348.4 |
$$P_c = \exp\left(0.01325 + b_0 + b_1 \cdot \rho^{b_4} + \frac{b_2}{M} + \frac{b_3}{M^2}\right)$$
| Coefficient | Light Oil | Heavy Oil |
|---|---|---|
| $b_0$ | -0.13408 | 0.802988 |
| $b_1$ | 2.5019 | 1.78396 |
| $b_2$ | 208.46 | 156.740 |
| $b_3$ | -3987.2 | -6965.59 |
| $b_4$ | 1.0 | 0.25 |
$$m = c_0 + c_1 \cdot M + c_2 \cdot \rho + c_3 \cdot M^2$$
For Peng-Robinson, different coefficients are used.
Alternative correlations based on molecular weight and specific gravity:
$$T_c = \frac{5}{9} \cdot 554.4 \cdot \exp(-1.3478 \times 10^{-4} M - 0.61641 \cdot \rho) \cdot M^{0.2998} \cdot \rho^{1.0555}$$
$$P_c = 0.068947 \cdot 4.5203 \times 10^4 \cdot \exp(-1.8078 \times 10^{-3} M - 0.3084 \cdot \rho) \cdot M^{-0.8063} \cdot \rho^{1.6015}$$
For $T_{br} = T_b/T_c < 0.8$:
$$\omega = \frac{\ln(P_{br}) - 5.92714 + \frac{6.09649}{T_{br}} + 1.28862 \ln(T_{br}) - 0.169347 T_{br}^6}{15.2518 - \frac{15.6875}{T_{br}} - 13.4721 \ln(T_{br}) + 0.43577 T_{br}^6}$$
For $T_{br} \geq 0.8$:
$$\omega = -7.904 + 0.1352 K_w - 0.007465 K_w^2 + 8.359 T_{br} + \frac{1.408 - 0.01063 K_w}{T_{br}}$$
where $P_{br} = 1.01325 / P_c$ and $K_w = T_b^{1/3} / \rho$ is the Watson K-factor.
The Universal Oil Products (UOP) characterization assumes constant Watson K for all SCN groups:
$$K_w = 4.5579 \cdot M_{C7+}^{0.15178} \cdot \rho_{C7+}^{-1.18241}$$
Individual SCN densities:
$$\rho_i = 6.0108 \cdot M_i^{0.17947} \cdot K_w^{-1.18241}$$
Limitation: Less accurate for heavy fractions (C20+).
Søreide (1989) developed a more accurate correlation for heavy fractions:
$$SG = 0.2855 + C_f \cdot (M - 66)^{0.13}$$
where $SG$ is specific gravity (same as $\rho$ in g/cm³) and $C_f$ is calculated from the plus fraction:
$$C_f = \frac{SG_{C7+} - 0.2855}{(M_{C7+} - 66)^{0.13}}$$
Individual SCN densities:
$$\rho_i = 0.2855 + C_f \cdot (M_i - 66)^{0.13}$$
Constrained to physical limits: $0.6 \leq \rho_i \leq 1.2$ g/cm³.
$$T_b = \frac{1}{1.8}\left(1928.3 - 1.695 \times 10^5 \cdot M^{-0.03522} \cdot \rho^{3.266} \cdot \exp\left(-4.922 \times 10^{-3} M - 4.7685 \rho + 3.462 \times 10^{-3} M \cdot \rho\right)\right)$$
$$T_b = \left(\frac{M}{5.805 \times 10^{-5} \cdot \rho^{0.9371}}\right)^{1/2.3776}$$
For $M < 540$ g/mol:
$$T_b = 2 \times 10^{-6} M^3 - 0.0035 M^2 + 2.4003 M + 171.74$$
SCN pseudo-components are grouped into $N$ lumps with approximately equal weight fractions:
$$w_{target} = \frac{\sum_i z_i \cdot M_i}{N}$$
For each lump $k$, the properties are averaged:
$$z_k = \sum_{i \in k} z_i$$
$$M_k = \frac{\sum_{i \in k} z_i \cdot M_i}{z_k}$$
$$\rho_k = \frac{\sum_{i \in k} z_i \cdot M_i}{\sum_{i \in k} \frac{z_i \cdot M_i}{\rho_i}}$$
Mixing rules (typically mole-fraction weighted):
$$T_{c,k} = \sum_{i \in k} \frac{z_i}{z_k} \cdot T_{c,i}$$
$$P_{c,k} = \sum_{i \in k} \frac{z_i}{z_k} \cdot P_{c,i}$$
$$\omega_k = \sum_{i \in k} \frac{z_i}{z_k} \cdot \omega_i$$
import neqsim.thermo.system.SystemInterface;
import neqsim.thermo.system.SystemSrkEos;
// Create fluid with plus fraction
SystemInterface fluid = new SystemSrkEos(350.0, 150.0);
fluid.addComponent("methane", 0.70);
fluid.addComponent("ethane", 0.10);
fluid.addComponent("propane", 0.05);
fluid.addPlusFraction("C7+", 0.15, 150.0 / 1000.0, 0.82); // MW in kg/mol, density in g/cm³
fluid.setMixingRule(2);
// Configure Whitson Gamma Model
fluid.getCharacterization()
.setPlusFractionModel("Whitson Gamma Model")
.setGammaShapeParameter(1.5) // alpha for black oil
.setGammaMinMW(84.0) // eta for C7+
.setGammaDensityModel("Soreide"); // Use Søreide density correlation
// Run characterization
fluid.getCharacterization().characterisePlusFraction();
// Let NeqSim estimate alpha based on Watson K-factor
fluid.getCharacterization()
.setPlusFractionModel("Whitson Gamma Model")
.setAutoEstimateGammaAlpha(true)
.setGammaDensityModel("Soreide");
fluid.getCharacterization().characterisePlusFraction();
// Check the estimated alpha
double alpha = ((PlusFractionModel.WhitsonGammaModel)
fluid.getCharacterization().getPlusFractionModel()).getAlpha();
double watsonK = ((PlusFractionModel.WhitsonGammaModel)
fluid.getCharacterization().getPlusFractionModel()).getWatsonKFactor();
System.out.println("Estimated alpha: " + alpha);
System.out.println("Watson K-factor: " + watsonK);
fluid.getCharacterization()
.setPlusFractionModel("Pedersen")
.setLumpingModel("Standard");
fluid.getCharacterization().getLumpingModel().setNumberOfPseudoComponents(6);
fluid.getCharacterization().characterisePlusFraction();
PlusFractionModelInterface model = fluid.getCharacterization().getPlusFractionModel();
double[] moleFractions = model.getZ();
double[] molecularWeights = model.getM();
double[] densities = model.getDens();
for (int i = model.getFirstPlusFractionNumber(); i < model.getLastPlusFractionNumber(); i++) {
if (moleFractions[i] > 1e-10) {
System.out.printf("SCN %d: z=%.6f, M=%.1f g/mol, rho=%.4f g/cm³%n",
i, moleFractions[i], molecularWeights[i] * 1000, densities[i]);
}
}
This section documents the PVT regression framework for automatic EOS model tuning based on experimental PVT report data. The framework is implemented in the neqsim.pvtsimulation.regression package.
| Class | Description |
|---|---|
PVTRegression |
Main regression framework class |
RegressionParameter |
Enum defining tunable parameters (BIPs, volume shifts, critical properties) |
ExperimentType |
Enum for experiment types (CCE, CVD, DLE, SEPARATOR, etc.) |
CCEDataPoint, CVDDataPoint, DLEDataPoint, SeparatorDataPoint |
Data point classes for each experiment type |
RegressionParameterConfig |
Configuration for each regression parameter with bounds |
PVTRegressionFunction |
Objective function extending LevenbergMarquardtFunction |
RegressionResult |
Result container with tuned fluid and uncertainty analysis |
UncertaintyAnalysis |
Statistical uncertainty quantification |
PVT regression involves adjusting equation of state parameters to minimize the deviation between calculated and experimental properties. The framework handles multiple experiment types simultaneously while maintaining physical consistency.
The total objective function combines weighted contributions from different PVT experiments:
$$F_{obj} = \sum_{k} w_k \cdot F_k$$
where $w_k$ are user-defined weights and $F_k$ are individual experiment objective functions.
$$F_{CCE} = \sum_{i=1}^{N_{CCE}} \left[ \left(\frac{V_{rel,i}^{calc} - V_{rel,i}^{exp}}{V_{rel,i}^{exp}}\right)^2 + \lambda_Y \left(\frac{Y_i^{calc} - Y_i^{exp}}{Y_i^{exp}}\right)^2 \right]$$
where:
Key match points:
$$F_{CVD} = \sum_{i=1}^{N_{CVD}} \left[ \left(\frac{L_i^{calc} - L_i^{exp}}{L_i^{exp}}\right)^2 + \left(\frac{Z_i^{calc} - Z_i^{exp}}{Z_i^{exp}}\right)^2 + \sum_{j} \left(\frac{y_{j,i}^{calc} - y_{j,i}^{exp}}{y_{j,i}^{exp}}\right)^2 \right]$$
where:
Key match points:
$$F_{DLE} = \sum_{i=1}^{N_{DLE}} \left[ \left(\frac{R_{s,i}^{calc} - R_{s,i}^{exp}}{R_{s,i}^{exp}}\right)^2 + \left(\frac{B_{o,i}^{calc} - B_{o,i}^{exp}}{B_{o,i}^{exp}}\right)^2 + \left(\frac{\rho_{o,i}^{calc} - \rho_{o,i}^{exp}}{\rho_{o,i}^{exp}}\right)^2 \right]$$
where:
Key match points:
$$F_{SEP} = \left(\frac{GOR^{calc} - GOR^{exp}}{GOR^{exp}}\right)^2 + \left(\frac{B_o^{calc} - B_o^{exp}}{B_o^{exp}}\right)^2 + \left(\frac{API^{calc} - API^{exp}}{API^{exp}}\right)^2$$
$$F_{\mu} = \sum_{i} \left(\frac{\mu_i^{calc} - \mu_i^{exp}}{\mu_i^{exp}}\right)^2$$
For a system with $N_c$ components, the symmetric BIP matrix has $N_c(N_c-1)/2$ independent parameters. To reduce dimensionality, group-based BIPs are used:
| Group Pairs | Typical Starting $k_{ij}$ | Regression Range |
|---|---|---|
| CH₄ - C₂-C₆ | 0.00 - 0.02 | Fixed or narrow |
| CH₄ - C7+ | 0.02 - 0.05 | Primary target |
| CO₂ - HC | 0.10 - 0.15 | If CO₂ present |
| N₂ - HC | 0.04 - 0.08 | If N₂ present |
| H₂S - HC | 0.05 - 0.10 | If H₂S present |
| C7+ - C7+ | 0.00 | Usually fixed |
For C7+ pseudo-components, BIPs can be correlated:
$$k_{ij} = k_{CH_4-C7+} \cdot \left(1 - \left(\frac{2\sqrt{T_{c,i} \cdot T_{c,j}}}{T_{c,i} + T_{c,j}}\right)^n\right)$$
where $n$ is a tunable exponent (typically 0.5-2.0).
The Peneloux (1982) volume translation corrects liquid density without affecting VLE:
$$V_{corrected} = V_{EOS} - \sum_i x_i \cdot c_i$$
where $c_i$ is the component volume shift parameter.
For pseudo-components, express volume shift as:
$$c_i = c_0 + c_1 \cdot M_i + c_2 \cdot M_i^2$$
Objective: Minimize density deviation in single-phase liquid region:
$$F_c = \sum_{i} \left(\frac{\rho_i^{calc} - \rho_i^{exp}}{\rho_i^{exp}}\right)^2$$
$$Z_{RA,i} = Z_{RA}^{ref} + a \cdot (M_i - M_{ref})$$
where $Z_{RA}$ is the Rackett compressibility factor and $a$ is a tunable coefficient.
Instead of regressing individual $T_c$, $P_c$, $\omega$ values, tune the correlation coefficients:
Critical Temperature: $$T_c = (a_0 + \Delta a_0) \cdot \rho + (a_1 + \Delta a_1) \cdot \ln(M) + a_2 \cdot M + \frac{a_3}{M}$$
Critical Pressure: $$\ln(P_c) = (b_0 + \Delta b_0) + b_1 \cdot \rho^{b_4} + \frac{b_2}{M} + \frac{b_3}{M^2}$$
Acentric Factor: $$\omega = (\omega_{base} + \Delta\omega) \cdot f(M, \rho)$$
where $\Delta a_0$, $\Delta a_1$, $\Delta b_0$, $\Delta\omega$ are regression parameters.
Physical bounds must be enforced:
$$T_c > T_b > 0$$ $$P_c > 0$$ $$0 < \omega < 2$$ $$\frac{\partial T_c}{\partial M} > 0 \text{ (monotonic increase)}$$
Compute the Jacobian matrix at the optimum:
$$J_{ij} = \frac{\partial F_i}{\partial \theta_j}$$
where $F_i$ are individual residuals and $\theta_j$ are parameters.
$$\text{Cov}(\theta) = s^2 \cdot (J^T J)^{-1}$$
where $s^2$ is the residual variance:
$$s^2 = \frac{F_{obj}}{N_{data} - N_{params}}$$
95% confidence interval for parameter $\theta_j$:
$$\theta_j \pm t_{0.975, N-p} \cdot \sqrt{\text{Cov}(\theta)_{jj}}$$
$$P_{95\%} = \left[\mu - 1.96\sigma, \mu + 1.96\sigma\right]$$
import neqsim.pvtsimulation.regression.*;
import neqsim.thermo.system.SystemInterface;
import neqsim.thermo.system.SystemSrkEos;
// Create base fluid
SystemInterface fluid = new SystemSrkEos(373.15, 200.0);
fluid.addComponent("methane", 0.70);
fluid.addComponent("ethane", 0.10);
fluid.addComponent("propane", 0.05);
fluid.addComponent("n-pentane", 0.05);
fluid.addPlusFraction("C7+", 0.10, 0.150, 0.82);
fluid.setMixingRule(2);
// Create PVT regression framework
PVTRegression regression = new PVTRegression(fluid);
// Add experimental CCE data
double[] pressures = {300.0, 250.0, 200.0, 150.0, 100.0};
double[] relativeVolumes = {0.98, 1.00, 1.08, 1.25, 1.55};
regression.addCCEData(pressures, relativeVolumes, 373.15);
// Add DLE data
double[] dlePressures = {250.0, 200.0, 150.0, 100.0};
double[] rs = {150.0, 120.0, 85.0, 50.0};
double[] bo = {1.45, 1.38, 1.30, 1.20};
double[] oilDensity = {720.0, 740.0, 760.0, 780.0};
regression.addDLEData(dlePressures, rs, bo, oilDensity, 373.15);
// Configure regression parameters (with custom bounds or use defaults)
regression.addRegressionParameter(RegressionParameter.BIP_METHANE_C7PLUS, 0.0, 0.10, 0.03);
regression.addRegressionParameter(RegressionParameter.VOLUME_SHIFT_C7PLUS); // Uses defaults
// Set weights for multi-objective optimization
regression.setExperimentWeight(ExperimentType.CCE, 1.0);
regression.setExperimentWeight(ExperimentType.DLE, 1.5); // Prioritize DLE matching
// Configure optimization
regression.setMaxIterations(100);
regression.setVerbose(true);
// Run regression
RegressionResult result = regression.runRegression();
// Get tuned fluid
SystemInterface tunedFluid = result.getTunedFluid();
// Get optimized parameter values
double optimizedBIP = result.getOptimizedValue(RegressionParameter.BIP_METHANE_C7PLUS);
System.out.println("Optimized BIP (CH4-C7+): " + optimizedBIP);
// Uncertainty analysis
UncertaintyAnalysis uncertainty = result.getUncertainty();
double[] ci = uncertainty.getConfidenceIntervalBounds(0);
System.out.println("95% CI for BIP: [" + ci[0] + ", " + ci[1] + "]");
// Generate summary report
String summary = result.generateSummary();
System.out.println(summary);
| Parameter | Description | Default Bounds |
|---|---|---|
BIP_METHANE_C7PLUS |
BIP between methane and C7+ fractions | [0.0, 0.10, 0.03] |
BIP_C2C6_C7PLUS |
BIP between C2-C6 and C7+ fractions | [0.0, 0.05, 0.01] |
BIP_CO2_HC |
BIP between CO₂ and hydrocarbons | [0.08, 0.18, 0.12] |
BIP_N2_HC |
BIP between N₂ and hydrocarbons | [0.02, 0.12, 0.05] |
VOLUME_SHIFT_C7PLUS |
Volume shift multiplier for C7+ | [0.8, 1.2, 1.0] |
TC_MULTIPLIER_C7PLUS |
Critical temperature multiplier | [0.95, 1.05, 1.0] |
PC_MULTIPLIER_C7PLUS |
Critical pressure multiplier | [0.95, 1.05, 1.0] |
OMEGA_MULTIPLIER_C7PLUS |
Acentric factor multiplier | [0.90, 1.10, 1.0] |
PLUS_MOLAR_MASS_MULTIPLIER |
Plus fraction MW multiplier | [0.90, 1.10, 1.0] |
GAMMA_ALPHA |
Gamma distribution shape parameter | [0.5, 4.0, 1.0] |
GAMMA_ETA |
Gamma distribution minimum MW | [75.0, 95.0, 84.0] |
CCE Data:
Pressure(bara),RelativeVolume,YFactor
350.0,0.9850,
300.0,0.9912,
250.0,1.0000, # Saturation point
200.0,1.0523,1.0234
150.0,1.1876,1.0456
DLE Data:
Pressure(bara),Rs(Sm3/Sm3),Bo(m3/Sm3),OilDensity(kg/m3),GasGravity
250.0,150.5,1.425,725.3,0.85
200.0,120.2,1.380,742.1,0.82
150.0,85.6,1.312,761.5,0.79
| Phase | Feature | Status |
|---|---|---|
| 1 | CCE/DLE simulation with objective function | ✅ Implemented |
| 2 | BIP regression for saturation pressure | ✅ Implemented |
| 3 | Volume translation optimization | ✅ Implemented |
| 4 | CVD simulation and regression | ✅ Implemented |
| 5 | Critical property correlation tuning | ✅ Implemented |
| 6 | Multi-objective optimization framework | ✅ Implemented |
| 7 | Uncertainty quantification | ✅ Implemented |
| 8 | GUI/Report generation | 🔲 Future work |
When working with multiple reservoir fluids in a simulation model (e.g., compositional reservoir simulation, commingled production), all fluids must share the same pseudo-component (PC) structure. NeqSim provides utilities for this workflow based on Pedersen et al. (Chapter 5.5-5.6).
The PseudoComponentCombiner class provides methods for matching fluid characterizations:
import neqsim.thermo.characterization.PseudoComponentCombiner;
// Match source fluid to reference's PC structure
SystemInterface matched = PseudoComponentCombiner.characterizeToReference(
sourceFluid, referenceFluid);
// Combine multiple fluids with automatic common PC structure
SystemInterface combined = PseudoComponentCombiner.combineReservoirFluids(
Arrays.asList(fluid1, fluid2, fluid3),
Arrays.asList(0.5, 0.3, 0.2)); // volume fractions
For advanced control, use the CharacterizationOptions builder:
import neqsim.thermo.characterization.CharacterizationOptions;
import neqsim.thermo.characterization.CharacterizationOptions.NamingScheme;
CharacterizationOptions options = CharacterizationOptions.builder()
.transferBinaryInteractionParameters(true) // Copy BIPs from reference
.normalizeComposition(true) // Ensure mole fractions sum to 1.0
.namingScheme(NamingScheme.REFERENCE) // Use reference component names
.generateValidationReport(true) // Create before/after comparison
.build();
SystemInterface matched = PseudoComponentCombiner.characterizeToReference(
sourceFluid, referenceFluid, options);
| Option | Description | Default |
|---|---|---|
transferBinaryInteractionParameters |
Copy BIPs from reference fluid | false |
normalizeComposition |
Normalize mole fractions to sum to 1.0 | true |
namingScheme |
Use SOURCE, REFERENCE, or MERGED names | REFERENCE |
generateValidationReport |
Generate validation report | false |
Binary Interaction Parameters (BIPs) can be transferred between fluids:
// Transfer BIPs during characterization
PseudoComponentCombiner.transferBinaryInteractionParameters(
sourceFluid, referenceFluid);
// Fluent API on Characterise class
SystemInterface fluid = new SystemSrkEos(298, 50);
fluid.addComponent("methane", 0.7);
fluid.addPlusFraction("C7+", 0.3, 0.200, 0.85);
fluid.getCharacterization()
.setTBPModel("PedersenSRK")
.characterize()
.transferBipsFrom(tunedReferenceFluid);
The CharacterizationValidationReport provides before/after comparison:
CharacterizationValidationReport report =
PseudoComponentCombiner.generateValidationReport(sourceFluid, matchedFluid);
System.out.println("Mass conserved: " + report.isMassConserved());
System.out.println("Moles conserved: " + report.isMolesConserved());
System.out.println("PC count before: " + report.getSourcePseudoComponentCount());
System.out.println("PC count after: " + report.getResultPseudoComponentCount());
System.out.println(report.toReportString());
When matching a source fluid to a reference PC structure:
$$z_i^{matched} = z_{C7+}^{source} \cdot \frac{z_i^{ref}}{\sum_{j \in PC} z_j^{ref}}$$
Whitson, C.H. (1983). "Characterizing Hydrocarbon Plus Fractions." SPE Journal, 23(4), 683-694. SPE-12233-PA.
Pedersen, K.S., Thomassen, P., and Fredenslund, A. (1984). "Thermodynamics of Petroleum Mixtures Containing Heavy Hydrocarbons. 1. Phase Envelope Calculations by Use of the Soave-Redlich-Kwong Equation of State." Industrial & Engineering Chemistry Process Design and Development, 23(1), 163-170.
Søreide, I. (1989). "Improved Phase Behavior Predictions of Petroleum Reservoir Fluids from a Cubic Equation of State." Dr.Ing. Thesis, Norwegian Institute of Technology (NTH), Trondheim.
Riazi, M.R. and Daubert, T.E. (1980). "Simplify Property Predictions." Hydrocarbon Processing, 59(3), 115-116.
Kesler, M.G. and Lee, B.I. (1976). "Improve Prediction of Enthalpy of Fractions." Hydrocarbon Processing, 55(3), 153-158.
Whitson, C.H. and Brulé, M.R. (2000). "Phase Behavior." SPE Monograph Series, Vol. 20. Society of Petroleum Engineers.
| Property | Internal Unit | Common Input Unit |
|---|---|---|
| Molecular weight | kg/mol | g/mol (÷1000) |
| Density | g/cm³ | g/cm³ or kg/m³ (÷1000) |
| Temperature | K | °C (+273.15) |
| Pressure | bar | bara |
| Critical temperature | K | K |
| Critical pressure | bar | bar |
Note: When using addPlusFraction(), molecular weight should be in kg/mol and density in g/cm³ (specific gravity).
| Symbol | Description | Typical Range |
|---|---|---|
| $\alpha$ | Gamma shape parameter | 0.5 - 4.0 |
| $\beta$ | Gamma scale parameter | Calculated |
| $\eta$ | Minimum molecular weight | 84 - 90 g/mol |
| $M$ | Molecular weight | g/mol |
| $\rho$ | Density (specific gravity) | 0.6 - 1.0 g/cm³ |
| $K_w$ | Watson characterization factor | 10 - 13 |
| $T_c$ | Critical temperature | K |
| $P_c$ | Critical pressure | bar |
| $\omega$ | Acentric factor | 0.2 - 1.5 |
| $T_b$ | Normal boiling point | K |
| $z$ | Mole fraction | 0 - 1 |
NeqSim computes phase and mixture properties after thermodynamic initialization. This guide highlights the most used methods and how to choose appropriate models.
fluid.initPhysicalProperties() or fluid.initProperties() after a flash to populate density (getDensity()) and compressibility (getZ()).getViscosity() on a phase returns dynamic viscosity. Correlations switch automatically based on system type:
setMixingRule(7) or CPA fluids when hydrogen bonding impacts rheology (water, glycols).getThermalConductivity() provides phase thermal conductivity using dense-gas corrections.getCp() and getEnthalpy() support energy balances. Reinitialize (init(3)) if temperature changes substantially between calls.getInterfacialTension(phase1, phase2) calculates tension between phases (e.g., gas-oil, gas-water) using parachor correlations tied to the active EOS.getDiffusionCoefficient() is available on phases for estimating film and molecular diffusion coefficients.TPflash, PHflash, etc.) before requesting properties; raw composition-only systems do not hold valid properties.initPhysicalProperties() after each state change to refresh transport properties without repeating equilibrium calculations.This documentation covers NeqSim's physical properties calculation system, including transport properties (viscosity, thermal conductivity, diffusivity), interfacial properties (surface tension), and density correlations.
The physical properties package follows a modular design with clear separation between:
physicalproperties/
├── PhysicalPropertyHandler.java # Main entry point
├── PhysicalPropertyType.java # Property type enum
├── system/
│ ├── PhysicalProperties.java # Abstract base class
│ ├── PhysicalPropertyModel.java # Model selection enum
│ ├── gasphysicalproperties/ # Gas phase implementations
│ ├── liquidphysicalproperties/ # Liquid phase implementations
│ └── solidphysicalproperties/ # Solid phase implementations
├── methods/
│ ├── gasphysicalproperties/
│ │ ├── viscosity/
│ │ ├── conductivity/
│ │ └── diffusivity/
│ ├── liquidphysicalproperties/
│ │ ├── viscosity/
│ │ ├── conductivity/
│ │ ├── diffusivity/
│ │ └── density/
│ └── commonphasephysicalproperties/
│ ├── viscosity/ # Models valid for all phases
│ ├── conductivity/
│ └── diffusivity/
├── mixingrule/
│ └── PhysicalPropertyMixingRule.java
└── interfaceproperties/
├── InterfaceProperties.java
└── surfacetension/
Physical properties are calculated after thermodynamic equilibrium has been established:
import neqsim.thermo.system.SystemSrkEos;
import neqsim.thermodynamicoperations.ThermodynamicOperations;
// Create fluid and run flash
SystemInterface fluid = new SystemSrkEos(298.15, 50.0);
fluid.addComponent("methane", 0.9);
fluid.addComponent("ethane", 0.1);
fluid.setMixingRule("classic");
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
// Initialize physical properties
fluid.initPhysicalProperties();
// Access properties
double gasViscosity = fluid.getPhase("gas").getViscosity("kg/msec");
double gasConductivity = fluid.getPhase("gas").getThermalConductivity("W/mK");
double gasDensity = fluid.getPhase("gas").getDensity("kg/m3");
NeqSim provides several pre-configured physical property model sets:
| Model | Description | Best For |
|---|---|---|
DEFAULT |
Standard models for oil/gas | General hydrocarbon systems |
WATER |
Water-specific correlations | Aqueous systems |
SALT_WATER |
Salt water correlations | Brine systems |
GLYCOL |
Glycol-specific models | Glycol dehydration |
AMINE |
Amine solution models | Gas sweetening |
CO2WATER |
CO₂-water system models | CCS applications |
BASIC |
Minimal calculations | Fast approximations |
// Set physical property model
fluid.initPhysicalProperties("GLYCOL");
// Or use the enum directly
import neqsim.physicalproperties.system.PhysicalPropertyModel;
fluid.initPhysicalProperties(PhysicalPropertyModel.AMINE);
You can override specific property models while keeping others at defaults:
fluid.initPhysicalProperties();
// Set viscosity model for a specific phase
fluid.getPhase("gas").getPhysicalProperties().setViscosityModel("friction theory");
fluid.getPhase("oil").getPhysicalProperties().setViscosityModel("LBC");
Available viscosity models:
"polynom" - Polynomial correlation"friction theory" - Quiñones-Cisneros friction theory"LBC" - Lohrenz-Bray-Clark (tunable)"PFCT" - Pedersen corresponding states"PFCT-Heavy-Oil" - Pedersen for heavy oils"KTA" - KTA method"Muzny" - Muzny (for hydrogen)"CO2Model" - CO₂ reference"MethaneModel" - Methane referencefluid.getPhase("gas").getPhysicalProperties().setConductivityModel("Chung");
fluid.getPhase("oil").getPhysicalProperties().setConductivityModel("PFCT");
Available conductivity models:
"Chung" - Chung method (gases)"PFCT" - Pedersen corresponding states"polynom" - Polynomial correlation"CO2Model" - CO₂ referencefluid.getPhase("gas").getPhysicalProperties().setDiffusionCoefficientModel("Wilke Lee");
fluid.getPhase("oil").getPhysicalProperties().setDiffusionCoefficientModel("Siddiqi Lucas");
Available diffusivity models:
"Wilke Lee" - Wilke-Lee (gases)"Siddiqi Lucas" - Siddiqi-Lucas (liquids)"CSP" - Corresponding states"Alkanol amine" - Amine solutionsfluid.getPhase("oil").getPhysicalProperties().setDensityModel("Costald");
Available density models:
"Peneloux volume shift" - EoS with volume translation"Costald" - COSTALD correlationSeveral models support parameter tuning for better match with experimental data:
The LBC model has 5 tunable parameters for the dense-fluid contribution:
// Set all parameters at once
double[] lbcParams = {0.1023, 0.023364, 0.058533, -0.040758, 0.0093324};
fluid.getPhase("oil").getPhysicalProperties().setLbcParameters(lbcParams);
// Or set individual parameters
fluid.getPhase("oil").getPhysicalProperties().setLbcParameter(0, 0.105);
For non-SRK/PR equations of state:
FrictionTheoryViscosityMethod viscModel =
(FrictionTheoryViscosityMethod) fluid.getPhase("oil")
.getPhysicalProperties().getViscosityModel();
viscModel.setFrictionTheoryConstants(
kapac, // Attractive constant
kaprc, // Repulsive constant
kaprrc, // Repulsive-repulsive constant
kapa, // Attractive matrix (3x3)
kapr, // Repulsive matrix (3x3)
kaprr // Repulsive-repulsive constant
);
After initialization, properties are available through the phase interface:
// Viscosity
double viscosity = fluid.getPhase("gas").getViscosity(); // Pa·s
double viscosity_cP = fluid.getPhase("gas").getViscosity("cP"); // cP
// Thermal conductivity
double k = fluid.getPhase("gas").getThermalConductivity(); // W/(m·K)
double k_alt = fluid.getPhase("gas").getThermalConductivity("W/mK");
// Density
double rho = fluid.getPhase("gas").getDensity(); // kg/m³
double rho_alt = fluid.getPhase("gas").getDensity("kg/m3");
// Kinematic viscosity
double nu = fluid.getPhase("gas").getKinematicViscosity(); // m²/s
// Binary diffusion coefficients
double[][] Dij = fluid.getPhase("gas").getPhysicalProperties()
.getDiffusivityCalc().getBinaryDiffusionCoefficients(); // m²/s
// Pure component viscosity (for mixing rule debugging)
double pureVisc = fluid.getPhase("gas").getPhysicalProperties()
.getPureComponentViscosity(0);
// Surface tension between phases
fluid.initPhysicalProperties();
double sigma = fluid.getInterphaseProperties().getSurfaceTension(0, 1); // N/m
To add a custom physical property model:
package neqsim.physicalproperties.methods.liquidphysicalproperties.viscosity;
import neqsim.physicalproperties.system.PhysicalProperties;
public class MyCustomViscosityModel extends Viscosity {
public MyCustomViscosityModel(PhysicalProperties phase) {
super(phase);
}
@Override
public double calcViscosity() {
// Your implementation here
double viscosity = 0.0;
// Access phase properties
double T = phase.getPhase().getTemperature(); // K
double P = phase.getPhase().getPressure(); // bar
double rho = phase.getPhase().getDensity(); // kg/m³
// Access component properties
for (int i = 0; i < phase.getPhase().getNumberOfComponents(); i++) {
double x = phase.getPhase().getComponent(i).getx();
double Tc = phase.getPhase().getComponent(i).getTC();
// ... calculate contribution
}
return viscosity; // Pa·s
}
@Override
public double getPureComponentViscosity(int i) {
// Return pure component viscosity for component i
return 0.0;
}
}
Add to setViscosityModel() in PhysicalProperties.java:
public void setViscosityModel(String model) {
// ... existing models ...
else if ("MyCustomModel".equals(model)) {
viscosityCalc = new MyCustomViscosityModel(this);
}
}
fluid.getPhase("oil").getPhysicalProperties().setViscosityModel("MyCustomModel");
initPhysicalProperties() is calledinit(phase, PropertyType) to update only specific properties// Efficient property sweep
SystemInterface baseFluid = createFluid();
baseFluid.initPhysicalProperties();
for (double T : temperatures) {
SystemInterface fluid = baseFluid.clone();
fluid.setTemperature(T, "K");
ops.TPflash();
fluid.initPhysicalProperties();
// ... use properties
}
NeqSim provides a comprehensive suite of methods for calculating fluid viscosity, ranging from standard empirical correlations to advanced corresponding states models and specialized pure-component equations. This document details the available models, their applications, and how to use them in simulations.
NeqSim viscosity models are organized into several categories:
| Category | Models | Applicability |
|---|---|---|
| General Purpose | LBC, PFCT, Friction Theory | Hydrocarbon mixtures, reservoir fluids |
| Pure Component | Muzny, MethaneModel, CO2Model, KTA | Specialized high-accuracy correlations |
| Aqueous Systems | Salt Water (Laliberté), polynom | Brine and water solutions |
| Heavy Oils | PFCT-Heavy-Oil | Viscous crude oils, bitumen |
The LBC method is the industry-standard correlation for calculating viscosity of reservoir fluids. It combines a low-pressure gas viscosity term with a dense-fluid contribution based on reduced density.
"LBC"The CSP method (referred to as PFCT in NeqSim) uses the Corresponding States Principle to relate mixture viscosity to a reference substance (typically Methane) at corresponding thermodynamic conditions.
"PFCT"A variant of the CSP model specifically tuned for heavy oil systems with additional terms to represent the viscous behavior of heavy fractions.
"PFCT-Heavy-Oil"The Friction Theory (f-theory) model links viscosity to the equation of state (EOS) by separating total viscosity into a dilute gas contribution and a residual friction contribution.
"friction theory"The Chung method is a corresponding states correlation for gas-phase viscosity based on the Chapman-Enskog kinetic theory with empirical corrections.
A simple empirical correlation for natural gas viscosity estimation.
High-accuracy correlation for pure hydrogen viscosity based on the work of Muzny et al. Includes dilute-gas, first-density, and higher-density contributions.
"Muzny"Extended version of the Muzny correlation with additional correction terms for improved accuracy at specific conditions.
"Muzny_mod"Specialized correlation for pure methane viscosity using LBC as base with empirical correction terms.
"MethaneModel"Reference-quality correlation for pure carbon dioxide based on Laesecke et al. (JPCRD 2017).
"CO2Model"Simple power-law correlation for pure helium viscosity.
"KTA"Extended KTA model with pressure-dependent corrections for improved high-pressure accuracy.
"KTA_mod"Viscosity correlation for aqueous salt solutions using the Laliberté (2007) model with erratum corrections.
"Salt Water"General liquid viscosity calculation using the Grunberg-Nissan mixing rule with pure-component correlations.
"polynom"To use a specific viscosity model, you must set it on the PhysicalProperties object of a phase. This is typically done after creating the system but before performing calculations.
import neqsim.thermo.system.SystemInterface;
import neqsim.thermo.system.SystemSrkEos;
import neqsim.thermodynamicoperations.ThermodynamicOperations;
public class ViscosityExample {
public static void main(String[] args) {
// 1. Create a system
SystemInterface system = new SystemSrkEos(298.15, 100.0); // 298.15 K, 100 bar
system.addComponent("methane", 0.5);
system.addComponent("n-heptane", 0.5);
// 2. Set mixing rule and initialize
system.setMixingRule("classic");
ThermodynamicOperations ops = new ThermodynamicOperations(system);
ops.TPflash();
// 3. Set Viscosity Model for a specific phase (e.g., oil/liquid)
// Available options: "LBC", "PFCT", "PFCT-Heavy-Oil", "friction theory"
// Example: Using LBC
system.getPhase("oil").getPhysicalProperties().setViscosityModel("LBC");
system.initProperties();
double lbcViscosity = system.getPhase("oil").getViscosity();
System.out.println("LBC Viscosity: " + lbcViscosity + " kg/(m*s)");
// Example: Using PFCT
system.getPhase("oil").getPhysicalProperties().setViscosityModel("PFCT");
system.initProperties();
double pfctViscosity = system.getPhase("oil").getViscosity();
System.out.println("PFCT Viscosity: " + pfctViscosity + " kg/(m*s)");
// Example: Using PFCT for Heavy Oil
system.getPhase("oil").getPhysicalProperties().setViscosityModel("PFCT-Heavy-Oil");
system.initProperties();
double pfctHeavyViscosity = system.getPhase("oil").getViscosity();
System.out.println("PFCT Heavy Oil Viscosity: " + pfctHeavyViscosity + " kg/(m*s)");
// Example: Using Friction Theory
system.getPhase("oil").getPhysicalProperties().setViscosityModel("friction theory");
system.initProperties();
double frictionViscosity = system.getPhase("oil").getViscosity();
System.out.println("Friction Theory Viscosity: " + frictionViscosity + " kg/(m*s)");
}
}
The LBC implementation exposes the dense-fluid polynomial coefficients ("Whitson/Bray-Clark"
$a_0 \dots a_4$ parameters) so you can tune the model against laboratory data. After selecting the
"LBC" viscosity model, update the coefficients via setLbcParameters or setLbcParameter, then
re-initialize properties to apply them:
system.getPhase(1).getPhysicalProperties().setViscosityModel("LBC");
system.initProperties();
double baseViscosity = system.getPhase(1).getViscosity();
double[] tunedCoefficients = new double[] {0.11, 0.030, 0.065, -0.045, 0.010};
system.getPhase(1).getPhysicalProperties().setLbcParameters(tunedCoefficients);
system.getPhase(1).getPhysicalProperties().setLbcParameter(2, 0.070); // tweak a single term
system.initProperties();
double tunedViscosity = system.getPhase(1).getViscosity();
System.out.println("Base viscosity: " + baseViscosity);
System.out.println("Tuned viscosity: " + tunedViscosity);
from neqsim.thermo import SystemSrkEos
from neqsim.thermodynamicoperations import ThermodynamicOperations
# 1. Create system
system = SystemSrkEos(298.15, 100.0)
system.addComponent("methane", 0.5)
system.addComponent("n-heptane", 0.5)
system.setMixingRule("classic")
# 2. Flash
ops = ThermodynamicOperations(system)
ops.TPflash()
# 3. Set Viscosity Model
# Note: Phase index 0 is usually gas, 1 is oil/liquid
system.getPhase(1).getPhysicalProperties().setViscosityModel("LBC")
system.initProperties()
print("LBC Viscosity:", system.getPhase(1).getViscosity(), "kg/(m*s)")
system.getPhase(1).getPhysicalProperties().setViscosityModel("PFCT")
system.initProperties()
print("PFCT Viscosity:", system.getPhase(1).getViscosity(), "kg/(m*s)")
system.getPhase(1).getPhysicalProperties().setViscosityModel("friction theory")
system.initProperties()
print("Friction Theory Viscosity:", system.getPhase(1).getViscosity(), "kg/(m*s)")
The LBC model calculates the viscosity of a fluid ($\eta$) as the sum of a low-pressure gas contribution ($\eta^*$) and a dense-fluid contribution ($\eta_{dense}$):
$$ \eta = \eta^* + \frac{\eta_{dense}}{\xi_m} $$
where $\xi_m$ is the mixture viscosity parameter:
$$ \xi_m = \frac{T_{cm}^{1/6}}{M_m^{1/2} P_{cm}^{2/3}} $$
The dense-fluid contribution is a function of the reduced density $\rho_r = \rho_m / \rho_{cm}$:
$$ [(\eta - \eta^*) \xi_m + 10^{-4}]^{1/4} = a_0 + a_1 \rho_r + a_2 \rho_r^2 + a_3 \rho_r^3 + a_4 \rho_r^4 $$
Mixing Rules:
The CSP model uses the Corresponding States Principle to relate the viscosity of a mixture to that of a reference substance (typically Methane) at a corresponding state ($T_0, P_0$).
Viscosity Mapping: $$ \eta_{mix}(T, P) = \eta_{ref}(T_0, P_0) \cdot F_{\eta} \cdot \frac{\alpha_{mix}}{\alpha_{ref}} $$
where the scaling factor $F_{\eta}$ is: $$ F_{\eta} = \left(\frac{T_{cm}}{T_{c,ref}}\right)^{-1/6} \left(\frac{P_{cm}}{P_{c,ref}}\right)^{2/3} \left(\frac{M_{mix}}{M_{ref}}\right)^{1/2} $$
Corresponding State ($T_0, P_0$): The reference substance is evaluated at: $$ T_0 = T \cdot \frac{T_{c,ref}}{T_{cm}} \cdot \frac{\alpha_{ref}}{\alpha_{mix}} $$ $$ P_0 = P \cdot \frac{P_{c,ref}}{P_{cm}} \cdot \frac{\alpha_{ref}}{\alpha_{mix}} $$
The parameter $\alpha$ accounts for deviations from the simple CSP and is typically a function of reduced density and molecular weight.
The Friction Theory model separates the total viscosity into a dilute gas term ($\eta_0$) and a friction term ($\eta_f$):
$$ \eta = \eta_0 + \eta_f $$
The friction term is derived from mechanical friction concepts applied to the van der Waals repulsive and attractive pressure terms of the Equation of State (EOS):
$$ \eta_f = \kappa_r P_r + \kappa_a P_a + \kappa_{rr} P_r^2 $$
where:
This approach ensures that the viscosity model is consistent with the thermodynamic behavior predicted by the EOS, making it robust across a wide range of conditions, including high pressure and near-critical regions.
The Muzny correlation for pure hydrogen viscosity follows a multi-term structure:
$$ \eta = \eta_0 + \eta_1 \rho + \Delta\eta(\rho_r, T_r) $$
where:
The dilute-gas term is:
$$ \eta_0 = \frac{0.021357 \sqrt{MT}}{\sigma^2 S^*} $$
where $S^*$ is the reduced collision integral and $\sigma = 0.297$ nm is the Lennard-Jones size parameter.
The CO2 viscosity correlation consists of dilute-gas and residual terms:
$$ \eta = \eta_0(T) + \Delta\eta(\rho, T) $$
The dilute-gas term follows an empirical correlation, and the residual term is expressed as:
$$ \Delta\eta = \eta_{t,L} \left[ c_1 T_r \rho_r^3 + \frac{\rho_r^2 + \rho_r^\gamma}{T_r - c_2} \right] $$
where $T_t = 216.592$ K is the triple point temperature and $\rho_{t,L} = 1178.53$ kg/m³ is the triple point liquid density.
The Laliberté mixture rule for aqueous salt solutions:
$$ \eta_m = \eta_w^{w_w} \prod_i \eta_i^{w_i} $$
where $\eta_w$ is pure-water viscosity and $\eta_i$ are solute viscosities:
$$ \eta_i = \frac{\exp\left[\frac{\nu_1(1-w_w)^{\nu_2} + \nu_3}{\nu_4 t + 1}\right]}{\nu_5(1-w_w)^{\nu_6} + 1} $$
with $w_w$ = water mass fraction and $t$ = temperature in °C.
| Model Keyword | Applicability | Phase | Multi-Component |
|---|---|---|---|
"LBC" |
Hydrocarbons, reservoir fluids | Gas/Liquid | Yes |
"PFCT" |
Light-medium hydrocarbons | Gas/Liquid | Yes |
"PFCT-Heavy-Oil" |
Heavy oils, bitumen | Liquid | Yes |
"friction theory" |
General fluids, EOS-consistent | Gas/Liquid | Yes |
"polynom" |
Liquids with database parameters | Liquid | Yes |
"Muzny" |
Pure hydrogen | Gas/Liquid | No |
"Muzny_mod" |
Pure hydrogen (extended) | Gas/Liquid | No |
"MethaneModel" |
Pure methane | Gas/Liquid | No |
"CO2Model" |
Pure CO2 | Gas/Liquid | No |
"KTA" |
Pure helium | Gas | No |
"KTA_mod" |
Pure helium (extended) | Gas | No |
"Salt Water" |
Brine, salt solutions | Liquid | Yes (aqueous) |
// Pure Hydrogen Viscosity
SystemInterface h2System = new SystemSrkEos(300.0, 50.0);
h2System.addComponent("hydrogen", 1.0);
h2System.setMixingRule("classic");
ThermodynamicOperations h2Ops = new ThermodynamicOperations(h2System);
h2Ops.TPflash();
h2System.getPhase(0).getPhysicalProperties().setViscosityModel("Muzny");
h2System.initProperties();
System.out.println("H2 Viscosity (Muzny): " + h2System.getPhase(0).getViscosity() + " Pa·s");
// Pure CO2 Viscosity
SystemInterface co2System = new SystemSrkEos(350.0, 100.0);
co2System.addComponent("CO2", 1.0);
co2System.setMixingRule("classic");
ThermodynamicOperations co2Ops = new ThermodynamicOperations(co2System);
co2Ops.TPflash();
co2System.getPhase(0).getPhysicalProperties().setViscosityModel("CO2Model");
co2System.initProperties();
System.out.println("CO2 Viscosity (Laesecke): " + co2System.getPhase(0).getViscosity() + " Pa·s");
// Brine viscosity calculation
SystemInterface brine = new SystemSrkCPAstatoil(323.15, 10.0);
brine.addComponent("water", 0.95);
brine.addComponent("NaCl", 0.05);
brine.setMixingRule(10); // CPA mixing rule
ThermodynamicOperations brineOps = new ThermodynamicOperations(brine);
brineOps.TPflash();
// Set Laliberté salt water model
brine.getPhase("aqueous").getPhysicalProperties().setViscosityModel("Salt Water");
brine.initProperties();
System.out.println("Brine Viscosity: " + brine.getPhase("aqueous").getViscosity() + " Pa·s");
"Model only supports PURE X" error: Pure-component models (Muzny, CO2Model, MethaneModel, KTA) only work with single-component systems. Use LBC or PFCT for mixtures.
Unexpected viscosity values: Ensure initProperties() is called after setting the viscosity model and after any flash calculations.
Phase selection: Use getPhase("oil"), getPhase("gas"), or getPhase("aqueous") to select the correct phase, or use phase index (0, 1, 2).
Heavy oil predictions too low: Try "PFCT-Heavy-Oil" or tune LBC parameters using setLbcParameters().
This guide documents the viscosity calculation methods available in NeqSim for gas, liquid, and multiphase systems.
Viscosity describes a fluid's resistance to flow. NeqSim provides several viscosity models suitable for different applications:
Units:
Setting a viscosity model:
fluid.initPhysicalProperties();
fluid.getPhase("gas").getPhysicalProperties().setViscosityModel("friction theory");
fluid.getPhase("oil").getPhysicalProperties().setViscosityModel("LBC");
The Lohrenz-Bray-Clark (1964) method is widely used in reservoir simulation. It combines a dilute gas correlation with a dense fluid polynomial correction.
Class: LBCViscosityMethod
Equation: $$\eta = \eta^* + \frac{(\eta_r - 0.0001)^4}{\xi}$$
where:
The dense fluid contribution uses a polynomial: $$\eta_r = a_0 + a_1\rho_r + a_2\rho_r^2 + a_3\rho_r^3 + a_4\rho_r^4$$
Default parameters: {0.10230, 0.023364, 0.058533, -0.040758, 0.0093324}
Applicable phases: Gas, Oil
Best for:
Usage:
fluid.getPhase("oil").getPhysicalProperties().setViscosityModel("LBC");
The Quiñones-Cisneros and Firoozabadi (2000) friction theory relates viscosity to the repulsive and attractive pressure contributions from the equation of state.
Class: FrictionTheoryViscosityMethod
Equation: $$\eta = \eta_0 + \kappa_a P_a + \kappa_{aa} P_a^2 + \kappa_r P_r + \kappa_{rr} P_r^2$$
where:
Applicable phases: Gas, Oil (any EoS-based phase)
Best for:
Automatic EoS detection: The method automatically selects SRK or PR constants based on the phase type.
Usage:
fluid.getPhase("oil").getPhysicalProperties().setViscosityModel("friction theory");
Custom constants (for other EoS):
FrictionTheoryViscosityMethod viscModel =
(FrictionTheoryViscosityMethod) fluid.getPhase("oil")
.getPhysicalProperties().getViscosityModel();
// Set custom friction theory constants
viscModel.setFrictionTheoryConstants(kapac, kaprc, kaprrc, kapa, kapr, kaprr);
The Pedersen Friction Corresponding States Theory uses methane as a reference fluid with shape factors for mixture calculations.
Classes:
PFCTViscosityMethodMod86 - Standard Pedersen method (1987)PFCTViscosityMethodHeavyOil - Extended for heavy oilsEquation: $$\eta_{mix} = \eta_{ref} \cdot \frac{f_\eta \cdot \alpha_{mix}}{\alpha_0}$$
where:
Applicable phases: Gas, Oil
Best for:
"PFCT-Heavy-Oil")Usage:
// Standard Pedersen
fluid.getPhase("oil").getPhysicalProperties().setViscosityModel("PFCT");
// Heavy oil variant
fluid.getPhase("oil").getPhysicalProperties().setViscosityModel("PFCT-Heavy-Oil");
The Chung method (1984, 1988) is a corresponding states method for dilute gas and dense fluid viscosity.
Class: ChungViscosityMethod
Equation (dilute gas): $$\eta_0 = \frac{40.785 F_c \sqrt{M T}}{\Omega_v V_c^{2/3}}$$
where:
Applicable phases: Primarily gas phase
Best for:
Usage:
fluid.getPhase("gas").getPhysicalProperties().setViscosityModel("Chung");
Uses component-specific polynomial coefficients from the database.
Class: Viscosity (liquid), GasViscosity (gas)
Equation: $$\ln(\eta) = A + \frac{B}{T} + C\ln(T) + DT$$
where A, B, C, D are component-specific parameters from COMP database.
Database columns: LIQVISC1, LIQVISC2, LIQVISC3, LIQVISC4
Applicable phases: Primarily liquid
Best for:
Usage:
fluid.getPhase("oil").getPhysicalProperties().setViscosityModel("polynom");
Specialized high-accuracy methods for specific fluids:
Class: MethaneViscosityMethod
Class: CO2ViscosityMethod
Classes: MuznyViscosityMethod, MuznyModViscosityMethod
Usage:
fluid.getPhase("gas").getPhysicalProperties().setViscosityModel("MethaneModel");
fluid.getPhase("gas").getPhysicalProperties().setViscosityModel("CO2Model");
fluid.getPhase("gas").getPhysicalProperties().setViscosityModel("Muzny");
| Application | Recommended Model | Notes |
|---|---|---|
| Reservoir simulation | LBC | Tunable, industry standard |
| Wide P-T range | Friction Theory | Good near critical |
| Heavy oils | PFCT-Heavy-Oil | Extended for high MW |
| Characterized crudes | PFCT | Works with pseudo-components |
| Gas processing | Chung | Good for gases |
| Pure CO₂ | CO2Model | High accuracy |
| Pure H₂ | Muzny | Reference accuracy |
| Aqueous systems | Salt Water | Water correlation |
The LBC method is commonly tuned to match laboratory viscosity data:
// Get current parameters
LBCViscosityMethod lbc = (LBCViscosityMethod)
fluid.getPhase("oil").getPhysicalProperties().getViscosityModel();
// Set all 5 parameters
double[] params = {0.1023, 0.023364, 0.058533, -0.040758, 0.0093324};
fluid.getPhase("oil").getPhysicalProperties().setLbcParameters(params);
// Or tune individual parameters
fluid.getPhase("oil").getPhysicalProperties().setLbcParameter(0, 0.105);
Tuning procedure:
import neqsim.thermo.system.SystemSrkEos;
import neqsim.thermodynamicoperations.ThermodynamicOperations;
// Create and flash fluid
SystemInterface fluid = new SystemSrkEos(350.0, 150.0);
fluid.addComponent("methane", 0.70);
fluid.addComponent("ethane", 0.10);
fluid.addComponent("propane", 0.05);
fluid.addComponent("n-heptane", 0.10);
fluid.addComponent("n-decane", 0.05);
fluid.setMixingRule("classic");
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
// Initialize physical properties
fluid.initPhysicalProperties();
// Get viscosities
double gasVisc = fluid.getPhase("gas").getViscosity("cP");
double oilVisc = fluid.getPhase("oil").getViscosity("cP");
System.out.println("Gas viscosity: " + gasVisc + " cP");
System.out.println("Oil viscosity: " + oilVisc + " cP");
String[] models = {"LBC", "friction theory", "PFCT"};
for (String model : models) {
SystemInterface fluid = createFluid();
ops.TPflash();
fluid.initPhysicalProperties();
fluid.getPhase("oil").getPhysicalProperties().setViscosityModel(model);
fluid.initPhysicalProperties();
double visc = fluid.getPhase("oil").getViscosity("cP");
System.out.println(model + ": " + visc + " cP");
}
SystemInterface baseFluid = createFluid();
baseFluid.initPhysicalProperties();
double[] temps = {300, 320, 340, 360, 380, 400}; // K
for (double T : temps) {
SystemInterface fluid = baseFluid.clone();
fluid.setTemperature(T, "K");
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
fluid.initPhysicalProperties();
double visc = fluid.getPhase("oil").getViscosity("cP");
System.out.println("T=" + (T-273.15) + "°C: " + visc + " cP");
}
For dilute gases, viscosity is calculated from kinetic theory:
$$\eta_0 = \frac{5}{16} \frac{\sqrt{\pi m k_B T}}{\pi \sigma^2 \Omega^{(2,2)*}}$$
where:
Most models use molar fraction-weighted mixing:
$$\eta_{mix} = \exp\left(\sum_i x_i \ln \eta_i\right)$$
or the more rigorous:
$$\eta_{mix} = \frac{\sum_i x_i \sqrt{M_i} \eta_i}{\sum_i x_i \sqrt{M_i}}$$
This guide documents the density correction models available in NeqSim for improving volumetric predictions.
Density predictions from cubic equations of state (SRK, PR) often have systematic errors:
NeqSim provides volume translation and correlation-based methods to improve liquid density predictions.
Basic density access:
fluid.init(3); // Initialize with derivatives
fluid.initPhysicalProperties();
double density = fluid.getPhase(1).getDensity("kg/m3"); // Liquid phase
double molarVolume = fluid.getPhase(1).getMolarVolume(); // m³/mol
Cubic equations of state calculate compressibility factor $Z$:
$$PV = ZnRT$$
Molar volume is then: $$V_m = \frac{ZRT}{P}$$
Density: $$\rho = \frac{PM_w}{ZRT}$$
Issue with cubic EoS: At the critical point, $Z_c^{SRK} = 0.333$ and $Z_c^{PR} = 0.307$, while real hydrocarbons have $Z_c \approx 0.26$. This causes systematic liquid volume overprediction.
The Peneloux correction adds a constant shift to the EoS molar volume:
$$V_{corrected} = V_{EoS} - c$$
where $c$ is the volume shift parameter.
Class: Peneloux
Mixture shift: $$c_{mix} = \sum_i x_i c_i$$
Component shift correlation: $$c_i = 0.40768 \frac{RT_{c,i}}{P_{c,i}} \left( 0.29441 - Z_{RA,i} \right)$$
where $Z_{RA}$ is the Rackett compressibility factor (from COMP database).
Setting shift parameters:
// Enable Peneloux correction (default for SRK)
fluid.setDensityModel("Peneloux");
// Or set component-specific shifts
fluid.getPhase(0).getComponent("methane").setVolumeCorrectionConst(0.0);
fluid.getPhase(1).getComponent("n-heptane").setVolumeCorrectionConst(-0.0105);
Advantages:
Limitations:
NeqSim stores volume correction constants in the COMP database. For heavy hydrocarbons or polar compounds, these may need tuning.
Accessing correction constants:
// Get current volume correction
double vc = fluid.getPhase(1).getComponent("n-decane").getVolumeCorrectionConst();
// Modify correction
fluid.getPhase(1).getComponent("n-decane").setVolumeCorrectionConst(-0.015);
Temperature-dependent shift (Jhaveri-Youngren): Some systems require temperature-dependent corrections:
$$c(T) = c_0 + c_1 (T - T_{ref})$$
This is implemented in specific component models.
The COSTALD (COrreSponding STAtes Liquid Density) correlation predicts saturated liquid volumes.
Class: Costald
Equation: $$V_s = V^* V_R^{(0)} \left[ 1 - \omega_{SRK} V_R^{(\delta)} \right]$$
where:
Reduced volume functions: $$V_R^{(0)} = 1 + a(1-T_r)^{1/3} + b(1-T_r)^{2/3} + c(1-T_r) + d(1-T_r)^{4/3}$$
$$V_R^{(\delta)} = \frac{e + fT_r + gT_r^2 + hT_r^3}{T_r - 1.00001}$$
Constants:
Mixing rules: $$V^_{mix} = \frac{1}{4} \left[ \sum_i x_i V^_i + 3 \left(\sum_i x_i V^{*2/3}_i\right) \left(\sum_i x_i V^{*1/3}_i\right) \right]$$
$$\omega_{mix} = \sum_i x_i \omega_i$$
Usage:
fluid.setDensityModel("Costald");
fluid.initPhysicalProperties();
double liquidDensity = fluid.getPhase(1).getDensity("kg/m3");
Best for:
A simple corresponding states correlation for saturated liquid density.
Equation: $$V_s = \frac{RT_c}{P_c} Z_{RA}^{[1 + (1-T_r)^{2/7}]}$$
where $Z_{RA}$ is the Rackett compressibility factor.
Spencer-Danner modification: Uses optimized $Z_{RA}$ values from experimental data rather than critical compressibility.
For mixtures: $$Z_{RA,mix} = \sum_i x_i Z_{RA,i}$$ $$T_{c,mix} = \sum_i x_i T_{c,i}$$
Usage:
// Access Rackett parameter
double Zra = fluid.getPhase(1).getComponent("n-pentane").getRacketZ();
// Rackett is used internally for volume correction
import neqsim.thermo.system.SystemSrkEos;
import neqsim.thermodynamicoperations.ThermodynamicOperations;
SystemInterface fluid = new SystemSrkEos(300.0, 50.0);
fluid.addComponent("methane", 0.1);
fluid.addComponent("n-pentane", 0.9);
fluid.setMixingRule("classic");
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
// EoS density (no correction)
double densityEoS = fluid.getPhase(1).getDensity("kg/m3");
System.out.println("EoS only: " + densityEoS + " kg/m³");
// With Peneloux correction (default for SRK)
fluid.initPhysicalProperties();
double densityPeneloux = fluid.getPhase(1).getDensity("kg/m3");
System.out.println("Peneloux: " + densityPeneloux + " kg/m³");
// With Costald
fluid.setDensityModel("Costald");
fluid.initPhysicalProperties();
double densityCostald = fluid.getPhase(1).getDensity("kg/m3");
System.out.println("Costald: " + densityCostald + " kg/m³");
// Create fluid with known experimental density
SystemInterface fluid = new SystemSrkEos(293.15, 1.01325);
fluid.addComponent("n-hexane", 1.0);
fluid.setMixingRule("classic");
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
fluid.initPhysicalProperties();
double expDensity = 659.0; // kg/m³ at 20°C
double calcDensity = fluid.getPhase(1).getDensity("kg/m3");
double error = (calcDensity - expDensity) / expDensity * 100;
System.out.println("Initial error: " + error + "%");
// Adjust volume correction to match experimental
double molarMass = fluid.getPhase(1).getMolarMass() * 1000; // kg/kmol
double calcMolarVolume = molarMass / calcDensity; // m³/kmol
double expMolarVolume = molarMass / expDensity; // m³/kmol
double correction = (calcMolarVolume - expMolarVolume) / 1000; // m³/mol
fluid.getPhase(1).getComponent("n-hexane").setVolumeCorrectionConst(correction);
fluid.initPhysicalProperties();
double newDensity = fluid.getPhase(1).getDensity("kg/m3");
System.out.println("Tuned density: " + newDensity + " kg/m³");
SystemInterface fluid = new SystemSrkEos(300.0, 10.0);
fluid.addComponent("n-heptane", 1.0);
fluid.setMixingRule("classic");
double[] temps = {280, 300, 320, 340, 360, 380};
for (double T : temps) {
fluid.setTemperature(T, "K");
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
if (fluid.getPhase(1).getPhaseTypeName().equals("oil")) {
fluid.initPhysicalProperties();
double rho = fluid.getPhase(1).getDensity("kg/m3");
System.out.println("T=" + T + " K: ρ=" + rho + " kg/m³");
}
}
// Compressed liquid density at high pressure
SystemInterface fluid = new SystemSrkEos(300.0, 500.0); // 500 bar
fluid.addComponent("n-decane", 1.0);
fluid.setMixingRule("classic");
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
fluid.initPhysicalProperties();
double rho = fluid.getPhase(0).getDensity("kg/m3");
System.out.println("High-P density: " + rho + " kg/m³");
// For high-pressure liquids, Peneloux may be insufficient
// Consider using PC-SAFT or adjusting correction
| Situation | Recommended Model | Notes |
|---|---|---|
| General hydrocarbons | Peneloux | Default, good accuracy |
| Near saturation | Costald | Better for sat. liquids |
| Polar compounds | PC-SAFT or CPA | Better fundamental basis |
| High pressure | Peneloux with tuning | May need adjustment |
| Critical region | GERG-2008 | If available |
| Quick estimate | EoS only | 5-15% error typical |
| Method | Liquid Density Error | Vapor Density Error |
|---|---|---|
| SRK (no correction) | 5-15% | 1-3% |
| SRK + Peneloux | 1-3% | 1-3% |
| PR (no correction) | 3-10% | 1-3% |
| PR + Peneloux | 1-3% | 1-3% |
| Costald | 1-2% | N/A |
| GERG-2008 | 0.1-0.5% | 0.1-0.5% |
// Set density model for all phases
fluid.setDensityModel("Peneloux"); // or "Costald"
// The model affects initPhysicalProperties() calls
fluid.initPhysicalProperties();
// Mass density
double rhoMass = phase.getDensity("kg/m3");
double rhoMass2 = phase.getDensity("lb/ft3");
// Molar density
double rhoMolar = phase.getDensity("mol/m3");
// Molar volume
double Vm = phase.getMolarVolume(); // m³/mol
// Get/set volume correction constant
double c = component.getVolumeCorrectionConst();
component.setVolumeCorrectionConst(newValue);
// Get Rackett parameter
double Zra = component.getRacketZ();
This guide documents the thermal conductivity calculation methods available in NeqSim for gas, liquid, and multiphase systems.
Thermal conductivity ($\lambda$ or $k$) describes a material's ability to conduct heat. It is essential for:
Units:
Setting a conductivity model:
fluid.initPhysicalProperties();
fluid.getPhase("gas").getPhysicalProperties().setConductivityModel("Chung");
fluid.getPhase("oil").getPhysicalProperties().setConductivityModel("PFCT");
The Pedersen Corresponding States method uses methane as a reference fluid with molecular weight corrections.
Class: PFCTConductivityMethodMod86
Principle: Uses corresponding states with methane as reference:
$$\lambda_{mix} = \lambda_{ref}(T_0, P_0) \cdot \frac{\alpha_{mix}}{\alpha_0}$$
where:
Corresponding state mapping: $$T_0 = T \cdot \frac{T_{c,ref}}{T_{c,mix}} \cdot \frac{\alpha_0}{\alpha_{mix}}$$
$$P_0 = P \cdot \frac{P_{c,ref}}{P_{c,mix}} \cdot \frac{\alpha_0}{\alpha_{mix}}$$
Applicable phases: Gas, Oil
Best for:
Usage:
fluid.getPhase("oil").getPhysicalProperties().setConductivityModel("PFCT");
The Chung method (1988) is a corresponding states correlation based on kinetic theory.
Class: ChungConductivityMethod
Equation (dilute gas): $$\lambda_0 = \frac{7.452 \eta_0 \Psi}{M}$$
where:
The correction factor accounts for:
Dense fluid correction: $$\lambda = \lambda_0 \cdot G_2(T^, \rho^) + B_1 q B_2$$
where $G_2$ and $B$ terms account for density effects.
Applicable phases: Primarily gas phase
Best for:
Usage:
fluid.getPhase("gas").getPhysicalProperties().setConductivityModel("Chung");
Uses component-specific polynomial coefficients from the database.
Class: Conductivity (in liquid package)
Equation: $$\lambda = A + BT + CT^2$$
where A, B, C are component-specific parameters.
Database columns: LIQUIDCONDUCTIVITY1, LIQUIDCONDUCTIVITY2, LIQUIDCONDUCTIVITY3
Mixing rule: $$\lambda_{mix} = \sum_i x_i \lambda_i$$
Applicable phases: Liquid
Best for:
Usage:
fluid.getPhase("oil").getPhysicalProperties().setConductivityModel("polynom");
High-accuracy thermal conductivity for CO₂ based on the Vesovic et al. correlation.
Class: CO2ConductivityMethod
Coverage:
Best for:
Usage:
fluid.getPhase("gas").getPhysicalProperties().setConductivityModel("CO2Model");
| Application | Recommended Model | Notes |
|---|---|---|
| Petroleum mixtures | PFCT | Corresponding states with MW correction |
| Gas processing | Chung | Good for gases |
| Simple liquid mixtures | polynom | Uses database parameters |
| Pure CO₂ | CO2Model | High accuracy |
| Wide P-T range | PFCT | Robust extrapolation |
| Polar systems | Chung | Includes polar corrections |
import neqsim.thermo.system.SystemSrkEos;
import neqsim.thermodynamicoperations.ThermodynamicOperations;
// Create and flash fluid
SystemInterface fluid = new SystemSrkEos(350.0, 50.0);
fluid.addComponent("methane", 0.85);
fluid.addComponent("ethane", 0.10);
fluid.addComponent("propane", 0.05);
fluid.setMixingRule("classic");
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
// Initialize physical properties
fluid.initPhysicalProperties();
// Get thermal conductivity
double gasConductivity = fluid.getPhase("gas").getThermalConductivity("W/mK");
System.out.println("Gas thermal conductivity: " + gasConductivity + " W/(m·K)");
String[] models = {"PFCT", "Chung"};
for (String model : models) {
SystemInterface fluid = createFluid();
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
fluid.initPhysicalProperties();
fluid.getPhase("gas").getPhysicalProperties().setConductivityModel(model);
fluid.initPhysicalProperties();
double k = fluid.getPhase("gas").getThermalConductivity("W/mK");
System.out.println(model + ": " + k + " W/(m·K)");
}
SystemInterface baseFluid = new SystemSrkEos(350.0, 10.0);
baseFluid.addComponent("methane", 1.0);
baseFluid.setMixingRule("classic");
double[] pressures = {10, 50, 100, 150, 200}; // bar
for (double P : pressures) {
SystemInterface fluid = baseFluid.clone();
fluid.setPressure(P, "bar");
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
fluid.initPhysicalProperties();
double k = fluid.getPhase(0).getThermalConductivity("W/mK");
System.out.println("P=" + P + " bar: " + k + " W/(m·K)");
}
SystemInterface fluid = new SystemSrkEos(280.0, 30.0);
fluid.addComponent("methane", 0.5);
fluid.addComponent("n-pentane", 0.5);
fluid.setMixingRule("classic");
fluid.setMultiPhaseCheck(true);
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
fluid.initPhysicalProperties();
if (fluid.hasPhaseType("gas")) {
System.out.println("Gas k: " +
fluid.getPhase("gas").getThermalConductivity("W/mK") + " W/(m·K)");
}
if (fluid.hasPhaseType("oil")) {
System.out.println("Oil k: " +
fluid.getPhase("oil").getThermalConductivity("W/mK") + " W/(m·K)");
}
For dilute gases, thermal conductivity is related to viscosity through:
$$\lambda = \frac{f \cdot \eta \cdot C_v}{M}$$
where:
For mixtures, thermal conductivity is typically calculated using:
Mass fraction weighting: $$\lambda_{mix} = \sum_i w_i \lambda_i$$
Molar weighting with interaction: $$\lambda_{mix} = \sum_i \sum_j \frac{x_i x_j \lambda_{ij}}{\sum_k x_k \phi_{ik}}$$
where $\lambda_{ij}$ is a combining rule and $\phi_{ik}$ is an interaction factor.
Thermal conductivity increases with pressure, particularly in dense fluids:
The PFCT method accounts for this through corresponding states mapping to reference fluid behavior.
This guide documents the diffusion coefficient calculation methods available in NeqSim for gas and liquid systems.
Diffusion coefficients describe the rate of molecular transport due to concentration gradients. They are essential for:
Units:
Setting a diffusivity model:
fluid.initPhysicalProperties();
fluid.getPhase("gas").getPhysicalProperties().setDiffusionCoefficientModel("Wilke Lee");
fluid.getPhase("oil").getPhysicalProperties().setDiffusionCoefficientModel("Siddiqi Lucas");
The diffusion coefficient for species $i$ moving through species $j$ at infinite dilution.
The effective diffusivity of species $i$ in a multicomponent mixture:
$$D_i^{eff} = \frac{1 - x_i}{\sum_{j \neq i} \frac{x_j}{D_{ij}}}$$
The fundamental diffusion coefficients describing molecular interactions, related to Fick diffusion through thermodynamic factors.
The Wilke-Lee method is based on Chapman-Enskog kinetic theory for gases.
Class: WilkeLeeDiffusivity
Equation: $$D_{ij} = \frac{(1.084 - 0.249\sqrt{1/M_i + 1/M_j}) \times 10^{-4} T^{1.5} \sqrt{1/M_i + 1/M_j}}{P \sigma_{ij}^2 \Omega_D}$$
where:
Collision diameter combining rule: $$\sigma_{ij} = \frac{\sigma_i + \sigma_j}{2}$$
Collision integral approximation: $$\Omega_D = \frac{A}{(T^)^B} + \frac{C}{\exp(DT^)} + \frac{E}{\exp(FT^)} + \frac{G}{\exp(HT^)}$$
where $T^* = k_B T / \epsilon_{ij}$.
Applicable phases: Gas
Best for:
Usage:
fluid.getPhase("gas").getPhysicalProperties().setDiffusionCoefficientModel("Wilke Lee");
The Siddiqi-Lucas method is designed for liquid-phase binary diffusion.
Class: SiddiqiLucasMethod
Aqueous systems: $$D_{ij} = 2.98 \times 10^{-7} \frac{T}{\eta_j^{1.026} V_i^{0.5473}}$$
Non-aqueous systems: $$D_{ij} = 9.89 \times 10^{-8} \frac{T}{\eta_j^{0.907} V_i^{0.45} V_j^{-0.265}}$$
where:
Applicable phases: Liquid (aqueous and organic)
Best for:
Usage:
fluid.getPhase("oil").getPhysicalProperties().setDiffusionCoefficientModel("Siddiqi Lucas");
A generalized corresponding states method for both gas and liquid diffusion.
Class: CorrespondingStatesDiffusivity
Principle: Uses reduced temperature and density to correlate diffusion:
$$D^* = D \cdot \frac{\sigma^2}{(M/N_A) \sqrt{k_B T / M}}$$
The reduced diffusivity is correlated against reduced temperature and density.
Applicable phases: Gas, Liquid
Best for:
Usage:
fluid.getPhase("gas").getPhysicalProperties().setDiffusionCoefficientModel("CSP");
Specialized correlations for amine solutions used in gas treating.
Class: AmineDiffusivity
Includes:
Applicable phases: Aqueous amine solutions
Best for:
Usage:
fluid.getPhase("aqueous").getPhysicalProperties()
.setDiffusionCoefficientModel("Alkanol amine");
| Application | Recommended Model | Notes |
|---|---|---|
| Gas phase | Wilke Lee | Based on kinetic theory |
| Aqueous liquids | Siddiqi Lucas | Validated for water |
| Organic liquids | Siddiqi Lucas | Use non-aqueous correlation |
| Wide P-T range | CSP | Corresponding states |
| Amine systems | Alkanol amine | Specialized for gas treating |
| CO₂ in water | CO2water model | Specific correlation |
import neqsim.thermo.system.SystemSrkEos;
import neqsim.thermodynamicoperations.ThermodynamicOperations;
// Create and flash fluid
SystemInterface fluid = new SystemSrkEos(300.0, 10.0);
fluid.addComponent("methane", 0.9);
fluid.addComponent("ethane", 0.05);
fluid.addComponent("CO2", 0.05);
fluid.setMixingRule("classic");
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
// Initialize physical properties
fluid.initPhysicalProperties();
// Get binary diffusion coefficients
double[][] Dij = fluid.getPhase("gas").getPhysicalProperties()
.getDiffusivityCalc().getBinaryDiffusionCoefficients();
// Print diffusion matrix
int n = fluid.getPhase("gas").getNumberOfComponents();
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
System.out.println("D[" + i + "][" + j + "] = " + Dij[i][j] + " m²/s");
}
}
// Get effective diffusion coefficient
double[] Deff = fluid.getPhase("gas").getPhysicalProperties()
.getDiffusivityCalc().getEffectiveDiffusionCoefficient();
for (int i = 0; i < n; i++) {
String name = fluid.getPhase("gas").getComponent(i).getName();
System.out.println("D_eff[" + name + "] = " + Deff[i] + " m²/s");
}
SystemInterface fluid = new SystemSrkEos(300.0, 50.0);
fluid.addComponent("CO2", 0.1);
fluid.addComponent("n-octane", 0.9);
fluid.setMixingRule("classic");
fluid.setMultiPhaseCheck(true);
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
fluid.initPhysicalProperties();
// Compare gas and liquid diffusivities
if (fluid.hasPhaseType("gas")) {
double[][] Dgas = fluid.getPhase("gas").getPhysicalProperties()
.getDiffusivityCalc().getBinaryDiffusionCoefficients();
System.out.println("Gas D_CO2-octane: " + Dgas[0][1] + " m²/s");
}
if (fluid.hasPhaseType("oil")) {
double[][] Dliq = fluid.getPhase("oil").getPhysicalProperties()
.getDiffusivityCalc().getBinaryDiffusionCoefficients();
System.out.println("Liquid D_CO2-octane: " + Dliq[0][1] + " m²/s");
}
// Gas diffusivity is typically 10,000x larger than liquid
SystemInterface fluid = new SystemSrkCPAstatoil(313.15, 1.0);
fluid.addComponent("CO2", 0.1);
fluid.addComponent("water", 0.7);
fluid.addComponent("MDEA", 0.2);
fluid.setMixingRule(10);
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
// Use amine-specific diffusivity model
fluid.initPhysicalProperties("AMINE");
double[][] D = fluid.getPhase("aqueous").getPhysicalProperties()
.getDiffusivityCalc().getBinaryDiffusionCoefficients();
System.out.println("D_CO2 in amine: " + D[0][1] + " m²/s");
Gases: $$D \propto \frac{1}{P}$$
At constant temperature, gas diffusivity is inversely proportional to pressure.
Liquids: Weak pressure dependence; can often be neglected.
Gases: $$D \propto T^{1.5}$$
Liquids: $$D \propto T / \eta$$
Since viscosity decreases with temperature, liquid diffusivity increases.
| Phase | Diffusivity Range |
|---|---|
| Gas (1 bar) | 10⁻⁵ to 10⁻⁴ m²/s |
| Gas (100 bar) | 10⁻⁷ to 10⁻⁶ m²/s |
| Liquid | 10⁻¹⁰ to 10⁻⁹ m²/s |
| Supercritical | 10⁻⁸ to 10⁻⁷ m²/s |
For multicomponent systems, the flux of species $i$ depends on gradients of all components:
$$J_i = -c_t \sum_{j=1}^{n} D_{ij} \nabla x_j$$
NeqSim calculates:
This guide documents the interfacial property calculations available in NeqSim, including surface tension and related phenomena.
Interfacial tension (IFT) describes the energy required to create a unit area of interface between two phases. It is critical for:
Units:
Basic usage:
fluid.initPhysicalProperties();
double sigma = fluid.getInterphaseProperties().getSurfaceTension(0, 1); // N/m
The Parachor method is an empirical correlation relating surface tension to density difference and component parachors.
Class: ParachorSurfaceTension
Equation: $$\sigma^{1/4} = \sum_i P_i \left( \frac{\rho_L x_i}{M_{mix,L}} - \frac{\rho_V y_i}{M_{mix,V}} \right)$$
where:
Parachor values:
PARACHOR columnPARACHOR_CPAApplicable interfaces: Gas-liquid, Gas-aqueous
Best for:
Usage:
fluid.getInterphaseProperties().setInterfacialTensionModel("gas", "oil", "Parachor");
The Gradient Theory is a rigorous thermodynamic approach based on density functional theory.
Classes:
GTSurfaceTension - Full gradient theory (most rigorous)GTSurfaceTensionSimple - Simplified versionGTSurfaceTensionODE - ODE-based solverPhysical basis:
Near an interface, the Helmholtz energy depends on density gradients:
$$A = \int_{-\infty}^{\infty} \left[ a_0(\boldsymbol{n}) + \frac{1}{2}\sum_i\sum_j c_{ij} \frac{dn_i}{dz}\frac{dn_j}{dz} \right] dz$$
where:
Surface tension calculation: $$\sigma = \int_{-\infty}^{\infty} \sum_i\sum_j c_{ij} \frac{dn_i}{dz}\frac{dn_j}{dz} dz$$
Influence parameter correlation: $$c_i = (A_i t_i + B_i) a_i b_i^{2/3}$$
where:
Applicable interfaces: All phase pairs
Best for:
Usage:
// Full gradient theory
fluid.getInterphaseProperties().setInterfacialTensionModel("gas", "oil", "Full Gradient Theory");
// Simplified version (faster)
fluid.getInterphaseProperties().setInterfacialTensionModel("gas", "oil", "Simple Gradient Theory");
A linearized approximation of gradient theory that is computationally efficient.
Class: LGTSurfaceTension
Approximation: Assumes linear density profile between bulk phases:
$$n_i(z) = n_i^L + \frac{n_i^V - n_i^L}{L} z$$
This allows analytical integration:
$$\sigma = \sum_i\sum_j c_{ij} \frac{(n_i^V - n_i^L)(n_j^V - n_j^L)}{L}$$
where $L$ is optimized to minimize Helmholtz energy.
Applicable interfaces: Gas-liquid
Best for:
Usage:
fluid.getInterphaseProperties().setInterfacialTensionModel("gas", "oil", "Linear Gradient Theory");
A correlation specifically designed for liquid-liquid interfaces (oil-water).
Class: FirozabadiRamleyInterfaceTension
Equation: $$\sigma_{ow} = \sigma_o + \sigma_w - 2\sqrt{\sigma_o \sigma_w} \phi$$
where:
Applicable interfaces: Oil-water (liquid-liquid)
Best for:
Usage:
fluid.getInterphaseProperties().setInterfacialTensionModel("oil", "aqueous", "Firozabadi Ramley");
NeqSim automatically selects models based on phase types, but you can override:
// Set interfacial tension model set by number
fluid.getInterphaseProperties().setInterfacialTensionModel(0); // Default set
| Number | Gas-Oil | Gas-Aqueous | Oil-Aqueous |
|---|---|---|---|
| 0 | Parachor | Parachor | Firozabadi-Ramley |
| 1 | Full GT | Simple GT | Simple GT |
| 2 | LGT | LGT | LGT |
| 3 | Parachor | Parachor | Firozabadi-Ramley |
| 4 | Simple GT | Parachor | LGT |
| 5 | Parachor | Parachor | Firozabadi-Ramley |
// Set specific models per interface
fluid.getInterphaseProperties().setInterfacialTensionModel("gas", "oil", "Full Gradient Theory");
fluid.getInterphaseProperties().setInterfacialTensionModel("gas", "aqueous", "Parachor");
fluid.getInterphaseProperties().setInterfacialTensionModel("oil", "aqueous", "Firozabadi Ramley");
Available model names:
"Parachor" or "Weinaug-Katz""Full Gradient Theory""Simple Gradient Theory""Linear Gradient Theory""Firozabadi Ramley"import neqsim.thermo.system.SystemSrkEos;
import neqsim.thermodynamicoperations.ThermodynamicOperations;
// Create and flash fluid
SystemInterface fluid = new SystemSrkEos(300.0, 50.0);
fluid.addComponent("methane", 0.8);
fluid.addComponent("n-decane", 0.2);
fluid.setMixingRule("classic");
fluid.setMultiPhaseCheck(true);
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
// Initialize physical properties
fluid.initPhysicalProperties();
// Get surface tension between phase 0 and 1
double sigma = fluid.getInterphaseProperties().getSurfaceTension(0, 1);
System.out.println("Surface tension: " + sigma * 1000 + " mN/m");
String[] models = {"Parachor", "Full Gradient Theory", "Linear Gradient Theory"};
SystemInterface baseFluid = createTwoPhaseFluid();
ThermodynamicOperations ops = new ThermodynamicOperations(baseFluid);
ops.TPflash();
baseFluid.initPhysicalProperties();
for (String model : models) {
SystemInterface fluid = baseFluid.clone();
fluid.getInterphaseProperties().setInterfacialTensionModel("gas", "oil", model);
fluid.initPhysicalProperties();
double sigma = fluid.getInterphaseProperties().getSurfaceTension(0, 1) * 1000;
System.out.println(model + ": " + sigma + " mN/m");
}
SystemInterface fluid = new SystemSrkEos(350.0, 10.0);
fluid.addComponent("methane", 0.5);
fluid.addComponent("n-pentane", 0.5);
fluid.setMixingRule("classic");
fluid.setMultiPhaseCheck(true);
double[] pressures = {10, 30, 50, 70, 90, 100, 110};
for (double P : pressures) {
fluid.setPressure(P, "bar");
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
if (fluid.getNumberOfPhases() >= 2) {
fluid.initPhysicalProperties();
double sigma = fluid.getInterphaseProperties().getSurfaceTension(0, 1) * 1000;
System.out.println("P=" + P + " bar: σ=" + sigma + " mN/m");
} else {
System.out.println("P=" + P + " bar: Single phase");
}
}
// Surface tension approaches zero at critical point
SystemInterface fluid = new SystemSrkCPAstatoil(300.0, 30.0);
fluid.addComponent("methane", 0.6);
fluid.addComponent("n-heptane", 0.3);
fluid.addComponent("water", 0.1);
fluid.setMixingRule(10);
fluid.setMultiPhaseCheck(true);
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
fluid.initPhysicalProperties();
// Get all interfacial tensions
int nPhases = fluid.getNumberOfPhases();
for (int i = 0; i < nPhases; i++) {
for (int j = i + 1; j < nPhases; j++) {
double sigma = fluid.getInterphaseProperties().getSurfaceTension(i, j);
System.out.println("Phase " + i + " - Phase " + j + ": " +
sigma * 1000 + " mN/m");
}
}
NeqSim also supports adsorption calculations at solid surfaces.
// Initialize adsorption
fluid.getInterphaseProperties().initAdsorption();
// Set adsorbent material
fluid.getInterphaseProperties().setSolidAdsorbentMaterial("ite");
// Calculate adsorption
fluid.getInterphaseProperties().calcAdsorption();
AdsorptionInterface ads = fluid.getInterphaseProperties().getAdsorptionCalc("gas");
// Access adsorption quantities per component
Surface tension is defined as:
$$\sigma = \left( \frac{\partial G}{\partial A} \right)_{T,P,n}$$
where $G$ is Gibbs energy and $A$ is interfacial area.
The pressure difference across a curved interface:
$$\Delta P = \sigma \left( \frac{1}{R_1} + \frac{1}{R_2} \right)$$
where $R_1, R_2$ are the principal radii of curvature.
Surface tension typically decreases with temperature:
$$\sigma = \sigma_0 \left( 1 - T/T_c \right)^n$$
where $n \approx 1.26$ (Guggenheim exponent).
At the critical point: $\sigma \rightarrow 0$.
Surface tension generally decreases with increasing pressure because:
Near the critical point, $\sigma \propto (\rho_L - \rho_V)^{3.9}$.
| Interface | Temperature | Typical IFT |
|---|---|---|
| Methane-Water | 25°C, 100 bar | 50-70 mN/m |
| Crude Oil-Gas | Reservoir | 5-30 mN/m |
| Crude Oil-Water | 25°C | 20-30 mN/m |
| n-Hexane-Air | 25°C | 18 mN/m |
| Water-Air | 25°C | 72 mN/m |
| Near critical | - | 0-1 mN/m |
Scale formation is a critical challenge in oil and gas production, water treatment, and geothermal systems. When water becomes supersaturated with certain minerals, precipitation (scaling) can occur on equipment surfaces, leading to reduced flow, equipment damage, and costly interventions.
NeqSim provides tools to predict scale potential by calculating the saturation ratio (SR) for various mineral salts. This document covers the theory, implementation, usage, and best practices for scale potential calculations.
The saturation ratio (also called saturation index or relative solubility) is the key parameter for assessing scale potential:
$$SR = \frac{IAP}{K_{sp}}$$
Where:
| SR Value | State | Meaning |
|---|---|---|
| SR < 1 | Undersaturated | Salt will dissolve; no scaling risk |
| SR = 1 | Saturated | Equilibrium; at solubility limit |
| SR > 1 | Supersaturated | Precipitation thermodynamically favored |
| SR >> 1 | Highly supersaturated | High scaling risk |
Note: SR > 1 indicates thermodynamic driving force for precipitation, but kinetics determine actual precipitation rate.
For a salt dissociating as:
$$M_{\nu_+}A_{\nu_-} \rightleftharpoons \nu_+ M^{z+} + \nu_- A^{z-}$$
The Ion Activity Product is:
$$IAP = a_{M^{z+}}^{\nu_+} \cdot a_{A^{z-}}^{\nu_-} = (\gamma_+ m_+)^{\nu_+} \cdot (\gamma_- m_-)^{\nu_-}$$
Where:
The solubility product is the equilibrium constant for salt dissolution. In NeqSim, K_sp is calculated from temperature-dependent correlations:
$$\ln(K_{sp}) = \frac{A}{T} + B + C \cdot \ln(T) + D \cdot T + \frac{E}{T^2}$$
Where T is temperature in Kelvin. The coefficients A, B, C, D, E are stored in the database.
| Mineral | Formula | K_sp (25°C) | Conditions |
|---|---|---|---|
| Calcite | CaCO3 | 10^-8.48 | Most common; pressure/CO2 dependent |
| Siderite | FeCO3 | 10^-10.89 | Iron-rich waters, CO2 systems |
| Magnesite | MgCO3 | 10^-7.46 | High Mg waters |
| Strontianite | SrCO3 | 10^-9.27 | Associated with barite |
| Mineral | Formula | K_sp (25°C) | Conditions |
|---|---|---|---|
| Gypsum | CaSO4·2H2O | 10^-4.58 | T < 40°C, lower salinity |
| Anhydrite | CaSO4 | 10^-4.36 | T > 40°C, high salinity |
| Barite | BaSO4 | 10^-9.97 | Seawater mixing; very insoluble |
| Celestite | SrSO4 | 10^-6.63 | Associated with barite |
| Mineral | Formula | K_sp (25°C) | Conditions |
|---|---|---|---|
| Halite | NaCl | 10^+1.58 | Highly soluble; evaporative systems |
| Sylvite | KCl | 10^+0.85 | Very soluble |
| Mineral | Formula | K_sp (25°C) | Conditions |
|---|---|---|---|
| Iron sulfide | FeS | varies | Sour (H2S) systems |
CheckScalePotential (neqsim.thermodynamicoperations.flashops.saturationops.CheckScalePotential)
COMPSALT.csv (src/main/resources/data/COMPSALT.csv)
The COMPSALT.csv file contains:
| Column | Description | Example |
|---|---|---|
| ID | Unique identifier | 5 |
| SaltName | Mineral name | CaSO4_A |
| ion1 | Cation species | Ca++ |
| ion2 | Anion species | SO4-- |
| stoc1 | Cation stoichiometry | 1.0 |
| stoc2 | Anion stoichiometry | 1.0 |
| Kspwater | Coefficient A | -19966.8 |
| Kspwater2 | Coefficient B | 454.86 |
| Kspwater3 | Coefficient C | -69.84 |
| Kspwater4 | Coefficient D | 0.0 |
| Kspwater5 | Coefficient E | 0.0 |
NeqSim currently supports 21 salts:
| Category | Salts |
|---|---|
| Chlorides | NaCl, KCl, HgCl2 |
| Carbonates | CaCO3, FeCO3, MgCO3, BaCO3, SrCO3, Na2CO3, K2CO3 |
| Bicarbonates | NaHCO3, KHCO3, Mg(HCO3)2 |
| Sulfates | CaSO4_A, CaSO4_G, BaSO4, SrSO4 |
| Hydroxides | Mg(OH)2 |
| Sulfides | FeS |
| Complex | Hydromagnesite |
import neqsim.thermo.system.SystemElectrolyteCPAstatoil;
import neqsim.thermo.system.SystemInterface;
import neqsim.thermodynamicoperations.ThermodynamicOperations;
// Create electrolyte system at 25°C, 1 bar
SystemInterface system = new SystemElectrolyteCPAstatoil(298.15, 1.0);
// Add water (1 kg basis)
system.addComponent("water", 1.0, "kg/sec");
// Add ions (molality = mol/kg water)
system.addComponent("Ca++", 0.01); // 10 mmol/kg calcium
system.addComponent("SO4--", 0.01); // 10 mmol/kg sulfate
system.addComponent("Na+", 0.5); // 500 mmol/kg sodium
system.addComponent("Cl-", 0.5); // 500 mmol/kg chloride
// Initialize system
system.chemicalReactionInit();
system.createDatabase(true);
system.setMixingRule(10); // Electrolyte mixing rule
// Run flash calculation
ThermodynamicOperations ops = new ThermodynamicOperations(system);
ops.TPflash();
system.init(1);
// Calculate scale potential
int aqueousPhase = system.getPhaseNumberOfPhase("aqueous");
ops.checkScalePotential(aqueousPhase);
// Get results
String[][] results = ops.getResultTable();
// Print results (skip header row)
System.out.println("Salt\t\tSaturation Ratio");
for (int i = 1; i < results.length && results[i][0] != null && !results[i][0].isEmpty(); i++) {
System.out.println(results[i][0] + "\t\t" + results[i][1]);
}
Salt Saturation Ratio
NaCl 0.00607
CaSO4_A 1.864
CaSO4_G 3.115
From the output above:
// Check scale potential at different temperatures
double[] temperatures = {283.15, 298.15, 323.15, 348.15, 373.15}; // 10-100°C
for (double T : temperatures) {
SystemInterface sys = new SystemElectrolyteCPAstatoil(T, 1.0);
sys.addComponent("water", 1.0, "kg/sec");
sys.addComponent("Ca++", 0.01);
sys.addComponent("SO4--", 0.01);
sys.chemicalReactionInit();
sys.createDatabase(true);
sys.setMixingRule(10);
ThermodynamicOperations ops = new ThermodynamicOperations(sys);
ops.TPflash();
sys.init(1);
int aqPhase = sys.getPhaseNumberOfPhase("aqueous");
ops.checkScalePotential(aqPhase);
// Extract CaSO4 results...
}
// Typical formation water composition
SystemInterface brine = new SystemElectrolyteCPAstatoil(353.15, 100.0); // 80°C, 100 bar
brine.addComponent("water", 1.0, "kg/sec");
brine.addComponent("Na+", 2.5); // 2500 mmol/kg
brine.addComponent("Cl-", 2.8); // 2800 mmol/kg
brine.addComponent("Ca++", 0.025); // 25 mmol/kg
brine.addComponent("Mg++", 0.015); // 15 mmol/kg
brine.addComponent("Ba++", 0.0001); // 0.1 mmol/kg
brine.addComponent("Sr++", 0.002); // 2 mmol/kg
brine.addComponent("SO4--", 0.001); // 1 mmol/kg (low - formation water)
brine.addComponent("HCO3-", 0.005); // 5 mmol/kg
brine.chemicalReactionInit();
brine.createDatabase(true);
brine.setMixingRule(10);
ThermodynamicOperations ops = new ThermodynamicOperations(brine);
ops.TPflash();
brine.init(1);
int aqPhase = brine.getPhaseNumberOfPhase("aqueous");
ops.checkScalePotential(aqPhase);
NaCl uses a polynomial correlation for K_sp instead of the standard ln(K_sp) form:
ksp = -814.18 + 7.4685*T - 2.3262e-2*T² + 3.0536e-5*T³ - 1.4573e-8*T⁴
FeS calculation includes pH correction:
ksp *= [H3O+] // Accounts for H2S dissociation equilibrium
Complex mineral 3MgCO₃·Mg(OH)₂·3H₂O requires special handling with water and hydroxide activities.
When MEG (monoethylene glycol) is present, the algorithm temporarily replaces MEG with water for the calculation to maintain consistent molality basis.
| Concentration | Expected Accuracy | Notes |
|---|---|---|
| Dilute (< 0.1 mol/kg) | ±10-20% | Best accuracy range |
| Moderate (0.1-1.0 mol/kg) | ±20-50% | Good for most applications |
| Concentrated (> 1 mol/kg) | > 50% | Activity coefficients less accurate |
| Temperature | K_sp Accuracy | Notes |
|---|---|---|
| 0-50°C | Good | Well-calibrated correlations |
| 50-100°C | Moderate | Some extrapolation |
| > 100°C | Variable | Validate against literature |
Activity Coefficient Asymmetry
Pressure Effects
Complex Brines
Mixed-Solvent Systems
The K_sp correlations in COMPSALT.csv are derived from:
| Source | Salts | Reference |
|---|---|---|
| WATEQ4F | Most minerals | Nordstrom & Munoz (1990) |
| Langmuir | CaSO4_A, MgCO3 | Langmuir (1997) |
| Plummer & Busenberg | CaCO3 | Geochim. Cosmochim. Acta 46:1011 (1982) |
| NIST | KCl | Standard Reference Database 46 |
| Pitzer | NaCl | Activity Coefficients in Electrolyte Solutions (1991) |
Always specify ion concentrations in molality (mol/kg water) for consistency:
// Correct: explicit molality
system.addComponent("Ca++", 0.01); // 0.01 mol/kg water
// For mass-based input, calculate molality
double CaPpm = 400; // mg/L
double CaMolality = (CaPpm / 1000.0) / 40.08; // Ca molar mass = 40.08
system.addComponent("Ca++", CaMolality);
Total positive charges should equal total negative charges:
// Check charge balance
double positiveCharge = 2*[Ca++] + 2*[Mg++] + [Na+] + [K+];
double negativeCharge = [Cl-] + 2*[SO4--] + [HCO3-] + 2*[CO3--];
// These should be approximately equal
Include all major ions even if not interested in their scales:
// Even if only checking CaSO4, include NaCl for ionic strength
system.addComponent("Na+", 0.5);
system.addComponent("Cl-", 0.5);
system.addComponent("Ca++", 0.01);
system.addComponent("SO4--", 0.01);
Test the model at known saturation conditions:
// At NaCl saturation (6.15 mol/kg), SR should be ~1.0
// At BaSO4 saturation (~1e-5 mol/kg), SR should be ~1.0
SR > 1 means precipitation is thermodynamically possible, not that it will occur immediately:
| Issue | Likely Cause | Solution |
|---|---|---|
Matrix is singular error |
Complex multi-ion system | Simplify to major ions |
| Very high SR (> 10^10) | Incorrect K_sp correlation | Check database values |
| SR always 0 | Ions not found in database | Check ion names (e.g., "Ca++" not "Ca2+") |
| No results returned | Ions not in system | Verify addComponent calls |
| Negative SR | Numerical error | Check activity coefficient calculation |
Use NeqSim ion naming convention:
| Ion | NeqSim Name | Common Alternatives (don't use) |
|---|---|---|
| Calcium | Ca++ |
Ca2+, Ca(2+) |
| Magnesium | Mg++ |
Mg2+, Mg(2+) |
| Sodium | Na+ |
Na(+), Na1+ |
| Barium | Ba++ |
Ba2+, Ba(2+) |
| Sulfate | SO4-- |
SO4(2-), SO42- |
| Carbonate | CO3-- |
CO3(2-), CO32- |
| Bicarbonate | HCO3- |
HCO3(-), HCO31- |
| Chloride | Cl- |
Cl(-), Cl1- |
All K_sp correlations have been verified against literature values:
| Salt | Calculated log₁₀(K_sp) | Literature log₁₀(K_sp) | Error | Source |
|---|---|---|---|---|
| CaSO4_A (Anhydrite) | -4.356 | -4.360 | 0.004 | Langmuir 1997 |
| CaSO4_G (Gypsum) | -4.581 | -4.580 | 0.001 | WATEQ4F |
| SrSO4 (Celestite) | -6.631 | -6.630 | 0.001 | WATEQ4F |
| BaSO4 (Barite) | -9.970 | -9.970 | 0.000 | WATEQ4F |
| KCl (Sylvite) | 0.851 | 0.850 | 0.001 | NIST |
| NaCl (Halite) | 1.582 | 1.580 | 0.002 | Pitzer 1991 |
| MgCO3 (Magnesite) | -7.491 | -7.460 | 0.031 | Langmuir 1997 |
| CaCO3 (Calcite) | -8.531 | -8.480 | 0.051 | Plummer 1982 |
| FeCO3 (Siderite) | -10.89 | -10.89 | 0.000 | WATEQ4F |
| Mg(OH)₂ (Brucite) | -11.16 | -11.16 | 0.000 | WATEQ4F |
All errors are < 0.1 in log₁₀(K_sp), corresponding to < 25% error in K_sp.
BaSO4 was tested at moderate concentrations where the electrolyte model is most accurate:
| Ba²⁺ Molality (mol/kg) | SO₄²⁻ Molality (mol/kg) | Calculated SR | Expected SR | Accuracy |
|---|---|---|---|---|
| 5.0×10⁻⁶ | 5.0×10⁻⁶ | 0.21 | ~0.25 | ✓ Good |
| 1.0×10⁻⁵ | 1.0×10⁻⁵ | 0.85 | ~1.0 | ✓ Good |
| 2.0×10⁻⁵ | 2.0×10⁻⁵ | 3.38 | ~4.0 | ✓ Good |
| 5.0×10⁻⁵ | 5.0×10⁻⁵ | 21.1 | ~25 | ✓ Good |
| 1.0×10⁻⁴ | 1.0×10⁻⁴ | 84.5 | ~100 | ✓ Good |
The SR scales correctly with concentration squared (as expected for a 1:1 salt where SR ∝ [Ba²⁺][SO₄²⁻]).
At moderate ionic strength (< 0.1 mol/kg), activity coefficients are accurate:
| System | Ionic Strength | Calculated γ± | Literature γ± | Error |
|---|---|---|---|---|
| NaCl 0.1 mol/kg | 0.1 | 0.778 | 0.778 | < 1% |
| CaCl₂ 0.01 mol/kg | 0.03 | 0.732 | 0.729 | < 1% |
| BaSO4 at saturation | ~2×10⁻⁵ | 0.92 | ~0.90 | ~2% |
Note: At high ionic strength (> 1 mol/kg), activity coefficients become less accurate due to model limitations.
Several salt correlations were corrected:
| Salt | Previous Error | Status |
|---|---|---|
| CaSO4_A (Anhydrite) | 56 orders of magnitude | ✅ Fixed |
| SrSO4 (Celestite) | 7 orders of magnitude | ✅ Fixed |
| KCl (Sylvite) | 2.6 orders of magnitude | ✅ Fixed |
| MgCO3 (Magnesite) | 2.3 orders of magnitude | ✅ Fixed |
Nordstrom, D.K. and Munoz, J.L. (1990). Geochemical Thermodynamics, 2nd ed. Blackwell Scientific.
Langmuir, D. (1997). Aqueous Environmental Geochemistry. Prentice Hall.
Plummer, L.N. and Busenberg, E. (1982). "The solubilities of calcite, aragonite and vaterite in CO2-H2O solutions." Geochimica et Cosmochimica Acta 46:1011-1040.
Pitzer, K.S. (1991). Activity Coefficients in Electrolyte Solutions, 2nd ed. CRC Press.
NIST Standard Reference Database 46 - Critically Selected Stability Constants of Metal Complexes.
Appelo, C.A.J. and Postma, D. (2005). Geochemistry, Groundwater and Pollution, 2nd ed. Balkema.
This page documents the basic equations implemented in Iapws_if97.
For the saturation line (Region 4) the following equations are used:
Pressure as function of temperature:
[ \ln(p) = 4 \cdot \ln\left(\frac{2 C}{-B + \sqrt{B^2-4 A C}}\right) ]
where
[ A = \theta^2 + 1167.0521452767\,\theta - 724213.16703206\ B = -17.073846940092\,\theta^2 + 12020.82470247\,\theta - 3232555.0322333\ C = 14.91510861353\,\theta^2 - 4823.2657361591\,\theta + 405113.40542057\ \theta = T - \frac{0.23855557567849}{T-650.17534844798} ]
Temperature as function of pressure is obtained by solving the inverse relation.
The specific Gibbs free energy is expressed with dimensionless variables (\pi) and (\tau). For region 1
[ \gamma(\pi,\tau)=\sum n_i (7.1-\pi)^{I_i} (\tau-1.222)^{J_i} ]
while region 2 uses an ideal and residual part
[ \gamma(\pi,\tau)=\ln\pi + \sum n_i^0\tau^{J_i^0} + \sum n_i^r\pi^{I_i^r}(\tau-0.5)^{J_i^r} ]
Thermodynamic properties follow from derivatives of (\gamma):
[ v = \frac{R T}{p}\,\pi\, \gamma_{\pi} \quad\quad h = R T\tau\, \gamma_{\tau} \ s = R(\tau\gamma_{\tau}-\gamma) ]
where (R=0.461526\,\mathrm{kJ\,kg^{-1}\,K^{-1}}).
Use these recipes to configure fluids and run equilibrium calculations with NeqSim. The Java snippets mirror the workflow used in other language bindings.
SystemInterface fluid = new SystemPrEos(313.15, 80.0);
fluid.addComponent("methane", 0.85);
fluid.addComponent("ethane", 0.05);
fluid.addTBPfraction("C7+", 0.10, 0.45, 8.0); // name, moles, density [g/cc], MW
fluid.createDatabase(true); // enable access to component data
fluid.setMixingRule(1); // classical van der Waals mixing rule
fluid.init(0);
Tips:
createDatabase(true) before adding TBP fractions so critical properties and acentric factors are filled automatically.addPlusFraction for simpler heavy-end inputs, or addFluid to merge two existing systems.setMixingRule(1): classical quadratic kij.setMixingRule(2): Huron–Vidal (gamma-phi) coupling.setMixingRule(4): Wong–Sandler (NRTL-based) coupling.setMixingRule(7): Simplified CPA cross-association rules.For lean gas, start with PR and kij from correlations; for rich liquids or polar systems, move to SRK-Twu + Huron–Vidal or CPA-SRK.
Instantiate ThermodynamicOperations with the configured fluid to access flash and envelope tools:
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
fluid.initProperties();
System.out.println("Vapor fraction: " + fluid.getPhaseFraction(0));
Common operations include:
PSflash(pressure, entropy), PHflash(pressure, enthalpy) for process simulators.dewPointTemperature(pressure) and bubblePointPressure(temperature) for PVT lab matches.calcPTphaseEnvelope() and calcPseudocriticalTemperature() for compositional screening.Export an EOS state to JSON or clone fluids when sweeping conditions:
SystemInterface clone = fluid.clone();
clone.setTemperature(280.0);
clone.setPressure(10.0);
new ThermodynamicOperations(clone).TPflash();
display() on the fluid to dump compositions, kij values, and phase properties.calcChemicalEquilibrium() after setting reaction stoichiometry to couple reactions into flashes.getMolarMass() and getZ() against lab PVT data to verify characterization accuracy.This guide documents the INTER table in NeqSim, which contains binary interaction parameters (BIPs) for thermodynamic models including equations of state (EoS), activity coefficient models, and CPA association parameters.
The INTER table is the central repository for all binary interaction parameters in NeqSim. It stores parameters for:
Location: src/main/resources/data/INTER.csv
Database table name: INTER (or INTERTEMP for temporary tables)
The INTER table contains one row per component pair. Each row has parameters for multiple thermodynamic models, allowing the same fluid definition to be used with different mixing rules.
Example entry:
ID,COMP1,COMP2,HVTYPE,KIJSRK,KIJTSRK,KIJTType,KIJPR,KIJTPR,KIJPCSAFT,...
6950,CO2,methane,Classic,0.0973,0,0,0.0973,0,...
| Column | Type | Description |
|---|---|---|
ID |
Integer | Unique row identifier |
COMP1 |
String | First component name (must match COMP table) |
COMP2 |
String | Second component name (must match COMP table) |
Important: Component pairs are symmetric. NeqSim searches for both (COMP1=A, COMP2=B) and (COMP1=B, COMP2=A).
These parameters are used in the classical van der Waals mixing rules for cubic equations of state.
| Column | Type | Description | Used By |
|---|---|---|---|
KIJSRK |
Double | Binary interaction parameter for SRK EoS | SystemSrkEos |
KIJTSRK |
Double | Temperature correction to $k_{ij}$ for SRK | SRK with T-dependent kij |
KIJTType |
Integer | Type of temperature dependence (0, 1, 2) | All EoS |
KIJPR |
Double | Binary interaction parameter for PR EoS | SystemPrEos |
KIJTPR |
Double | Temperature correction to $k_{ij}$ for PR | PR with T-dependent kij |
KIJPCSAFT |
Double | Binary interaction for PC-SAFT EoS | SystemPCSAFT |
Temperature dependence types (KIJTType):
0 - No temperature dependence: $k_{ij}(T) = k_{ij}$1 - Inverse temperature: $k_{ij}(T) = k_{ij} + k_{ij}^T / T$2 - Linear temperature: $k_{ij}(T) = k_{ij} + k_{ij}^T \cdot T$Mathematical role:
The $k_{ij}$ parameter appears in the classical mixing rule for the EoS attractive parameter:
$$a_{mix} = \sum_i \sum_j x_i x_j \sqrt{a_i a_j} (1 - k_{ij})$$
A positive $k_{ij}$ reduces the attractive interactions between unlike molecules.
Huron-Vidal mixing rules combine an EoS with an activity coefficient model.
| Column | Type | Description |
|---|---|---|
HVTYPE |
String | Type of mixing rule ("Classic" or "HV") |
HVALPHA |
Double | NRTL non-randomness parameter ($\alpha$) |
HVGIJ |
Double | NRTL interaction parameter $g_{ij}$ (J/mol) |
HVGJI |
Double | NRTL interaction parameter $g_{ji}$ (J/mol) |
HVGIJT |
Double | Temperature derivative of $g_{ij}$ |
HVGJIT |
Double | Temperature derivative of $g_{ji}$ |
Note: When HVTYPE = "Classic", the Huron-Vidal parameters are not used.
Mathematical formulation:
$$\tau_{ij} = \frac{g_{ij} - g_{jj}}{RT} = \frac{\Delta g_{ij}}{RT}$$
$$G_{ij} = \exp(-\alpha_{ij} \tau_{ij})$$
Wong-Sandler mixing rules provide thermodynamic consistency between high and low pressure limits.
| Column | Type | Description |
|---|---|---|
WSTYPE |
String | Type ("Classic" or "WS") |
KIJWS |
Double | Wong-Sandler interaction parameter |
KIJWSunifac |
Double | WS parameter for UNIFAC integration |
CalcWij |
Integer | Flag for calculated vs fitted W parameters |
W1, W2, W3 |
Double | Second virial coefficient parameters |
WSGIJT |
Double | Temperature derivative for WS $g_{ij}$ |
WSGJIT |
Double | Temperature derivative for WS $g_{ji}$ |
Non-Random Two-Liquid parameters for activity coefficient calculations.
| Column | Type | Description |
|---|---|---|
NRTLALPHA |
Double | Non-randomness parameter $\alpha_{ij}$ (typically 0.2-0.47) |
NRTLGIJ |
Double | Interaction parameter $g_{ij}$ (J/mol) |
NRTLGJI |
Double | Interaction parameter $g_{ji}$ (J/mol) |
NRTL Activity Coefficient:
$$\ln \gamma_i = \frac{\sum_j x_j \tau_{ji} G_{ji}}{\sum_k x_k G_{ki}} + \sum_j \frac{x_j G_{ij}}{\sum_k x_k G_{kj}} \left( \tau_{ij} - \frac{\sum_m x_m \tau_{mj} G_{mj}}{\sum_k x_k G_{kj}} \right)$$
These parameters control cross-association in the CPA (Cubic Plus Association) equation of state.
| Column | Type | Description |
|---|---|---|
cpakij_SRK |
Double | SRK-CPA binary interaction parameter |
cpakijT_SRK |
Double | Temperature correction for CPA $k_{ij}$ |
cpakijx_SRK |
Double | Composition-dependent $k_{ij}$ (asymmetric) |
cpakjix_SRK |
Double | Composition-dependent $k_{ji}$ (asymmetric) |
cpakij_PR |
Double | PR-CPA binary interaction parameter |
cpaAssosiationType |
Integer | Association scheme type (0, 1, 2, ...) |
cpaBetaCross |
Double | Cross-association volume parameter $\beta^{AB}$ |
cpaEpsCross |
Double | Cross-association energy parameter $\epsilon^{AB}$ (K) |
Association scheme types:
0 - No induced association1 - CR1 combining rule (Elliott rule)2 - CR2 combining rule (geometric mean)Cross-association combining rules:
When cpaAssosiationType = 1 (CR1/Elliott rule):
$$\epsilon^{AB}_{ij} = \frac{\epsilon^{AA}_{ii} + \epsilon^{BB}_{jj}}{2}$$
$$\beta^{AB}_{ij} = \sqrt{\beta^{AA}_{ii} \beta^{BB}_{jj}}$$
When explicit values are provided in cpaBetaCross and cpaEpsCross, they override the combining rules.
| Column | Type | Description |
|---|---|---|
GIJVISC |
Double | Viscosity mixing parameter |
KIJWhitsonSoriede |
Double | Soreide-Whitson correlation parameter |
For amine-gas systems with the Desmukh-Mather model.
| Column | Type | Description |
|---|---|---|
aijDesMath |
Double | Desmukh-Mather $a_{ij}$ parameter |
bijDesMath |
Double | Desmukh-Mather $b_{ij}$ parameter |
The INTER table parameters are loaded when you call setMixingRule(). The mixing rule number determines which columns are read:
| Mixing Rule | Number | Parameters Used |
|---|---|---|
| Classic (kij=0) | 1 | None (all kij set to 0) |
| Classic (from database) | 2 | KIJSRK/KIJPR |
| Classic + T-dependent | 3 | KIJSRK, KIJTSRK, KIJTType |
| Huron-Vidal | 4 | HVALPHA, HVGIJ, HVGJI |
| Wong-Sandler | 5 | KIJWS, W1-W3, NRTL params |
| CPA | 7 | cpakij_SRK, cpaBetaCross, cpaEpsCross |
| CPA + T-dependent | 9 | + cpakijT_SRK |
| CPA + composition-dependent | 10 | + cpakijx_SRK, cpakjix_SRK |
Example:
// Classic with database kij
fluid.setMixingRule(2); // or fluid.setMixingRule("classic");
// Uses KIJSRK/KIJPR columns from INTER table
import neqsim.thermo.system.SystemSrkEos;
import neqsim.thermo.phase.PhaseEos;
SystemInterface fluid = new SystemSrkEos(300.0, 50.0);
fluid.addComponent("methane", 0.8);
fluid.addComponent("CO2", 0.2);
fluid.setMixingRule("classic"); // Loads from INTER table
// Get kij value
PhaseEos phase = (PhaseEos) fluid.getPhase(0);
double kij = phase.getMixingRule().getBinaryInteractionParameter(0, 1);
System.out.println("kij(methane-CO2) = " + kij); // 0.0973
// Method 1: Using component names (recommended)
fluid.setBinaryInteractionParameter("methane", "CO2", 0.10);
// Method 2: Using component indices
((PhaseEos) fluid.getPhase(0)).getMixingRule()
.setBinaryInteractionParameter(0, 1, 0.10);
((PhaseEos) fluid.getPhase(1)).getMixingRule()
.setBinaryInteractionParameter(0, 1, 0.10);
// Set temperature coefficient
((PhaseEos) fluid.getPhase(0)).getMixingRule()
.setBinaryInteractionParameterT1(0, 1, -0.001);
// kij(T) = kij + kijT * T (when KIJTType=2)
double[][] kijMatrix = ((PhaseEos) fluid.getPhase(0))
.getMixingRule().getBinaryInteractionParameters();
// Print matrix
for (int i = 0; i < fluid.getNumberOfComponents(); i++) {
for (int j = 0; j < fluid.getNumberOfComponents(); j++) {
System.out.print(kijMatrix[i][j] + " ");
}
System.out.println();
}
SystemInterface fluid = new SystemSrkCPAstatoil(300.0, 50.0);
fluid.addComponent("methane", 0.7);
fluid.addComponent("water", 0.2);
fluid.addComponent("MEG", 0.1);
fluid.setMixingRule(10); // CPA with composition-dependent kij
// Cross-association parameters are loaded automatically:
// - cpaBetaCross for methane-water
// - cpaEpsCross for water-MEG association
To add interaction parameters for a new component pair:
99999,newcomp1,newcomp2,Classic,0.05,0,0,0.05,0,0,0,0,0,0,0,0,0,Classic,0.5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.05
fluid.setBinaryInteractionParameter("newcomp1", "newcomp2", 0.05);
Some mixing rules support asymmetric $k_{ij} \neq k_{ji}$:
// Set kij (component i with j)
((PhaseEos) fluid.getPhase(0)).getMixingRule()
.setBinaryInteractionParameterij(i, j, 0.05);
// Set kji (component j with i)
((PhaseEos) fluid.getPhase(0)).getMixingRule()
.setBinaryInteractionParameterji(i, j, 0.03);
For undefined pseudo-components (TBP fractions), NeqSim uses correlations or default values:
| Component | TBP Fraction | Default $k_{ij}$ |
|---|---|---|
| CO2 | All | 0.10 |
| N2 | All | 0.08 |
| H2O | All | 0.20 |
| MEG | All | 0.20 |
When setCalcEOSInteractionParameters(true) is called, NeqSim calculates $k_{ij}$ from critical volumes:
$$k_{ij} = 1 - \left( \frac{2 \sqrt[3]{V_{c,i} V_{c,j}}}{V_{c,i}^{1/3} + V_{c,j}^{1/3}} \right)^n$$
where $n$ is configurable (default = 6).
| Category | Columns |
|---|---|
| Identification | ID, COMP1, COMP2 |
| SRK EoS | KIJSRK, KIJTSRK |
| PR EoS | KIJPR, KIJTPR |
| PC-SAFT | KIJPCSAFT |
| Temperature Type | KIJTType |
| Huron-Vidal | HVTYPE, HVALPHA, HVGIJ, HVGJI, HVGIJT, HVGJIT |
| Wong-Sandler | WSTYPE, KIJWS, KIJWSunifac, CalcWij, W1, W2, W3, WSGIJT, WSGJIT |
| NRTL | NRTLALPHA, NRTLGIJ, NRTLGJI |
| CPA | cpakij_SRK, cpakijT_SRK, cpakijx_SRK, cpakjix_SRK, cpakij_PR, cpaAssosiationType, cpaBetaCross, cpaEpsCross |
| Physical Properties | GIJVISC |
| Soreide-Whitson | KIJWhitsonSoriede |
| Desmukh-Mather | aijDesMath, bijDesMath |
| Pair | $k_{ij}$ | Notes |
|---|---|---|
| CH4 - C2H6 | 0.003 | Very similar molecules |
| CH4 - CO2 | 0.097 | Significant non-ideality |
| CH4 - H2S | 0.08 | Acid gas |
| CH4 - N2 | 0.032 | Typical |
| CH4 - H2O | 0.45-0.65 | Polar-nonpolar (CPA: ~-0.08) |
| CO2 - H2S | 0.10-0.12 | Acid gas pair |
| CO2 - C3H8 | 0.12-0.14 | |
| H2O - MEG | 0.13 | CPA model |
| Pair | $\beta^{AB}$ | $\epsilon^{AB}$ (K) |
|---|---|---|
| H2O - MEG | 0.055 | 2000-2500 |
| H2O - methanol | 0.039 | 2000-2500 |
| CO2 - H2O | 0.085 | 0 (solvation) |
This document describes the gas hydrate thermodynamic models implemented in NeqSim for predicting hydrate formation, stability, and phase equilibrium.
Gas hydrates (clathrate hydrates) are ice-like crystalline compounds formed when water molecules create cage structures that encapsulate gas molecules (guest molecules) under high pressure and low temperature conditions. NeqSim provides comprehensive models for:
NeqSim supports two common hydrate crystal structures:
| Property | Small Cavity (5¹²) | Large Cavity (5¹²6²) |
|---|---|---|
| Coordination Number | 20 | 24 |
| Cavity Radius (Å) | 3.95 | 4.33 |
| Number per Unit Cell | 2 | 6 |
| Water per Cavity | 1/23 | 3/23 |
Typical Guest Molecules: Methane, ethane, CO₂, H₂S
Unit Cell Formula: 46 H₂O · 8 guest molecules (2 small + 6 large cavities)
| Property | Small Cavity (5¹²) | Large Cavity (5¹²6⁴) |
|---|---|---|
| Coordination Number | 20 | 28 |
| Cavity Radius (Å) | 3.91 | 4.73 |
| Number per Unit Cell | 16 | 8 |
| Water per Cavity | 2/17 | 1/17 |
Typical Guest Molecules: Propane, isobutane, natural gas mixtures
Unit Cell Formula: 136 H₂O · 24 guest molecules (16 small + 8 large cavities)
The algorithm automatically selects the most stable structure based on Gibbs energy minimization. For mixed gases, the structure depends on composition:
// Get the stable hydrate structure (1 = sI, 2 = sII)
int structure = fluid.getPhase(PhaseType.HYDRATE).getComponent("methane").getHydrateStructure();
NeqSim implements the classical van der Waals-Platteeuw (vdWP) statistical thermodynamic model as the foundation for hydrate calculations.
The chemical potential difference between water in hydrate and empty hydrate lattice:
$$\Delta\mu_w^H = -RT \sum_{i=1}^{N_{cav}} \nu_i \ln\left(1 - \sum_{j=1}^{N_g} Y_{ij}\right)$$
where:
The cavity occupancy follows the Langmuir isotherm:
$$Y_{ij} = \frac{C_{ij} f_j}{1 + \sum_{k=1}^{N_g} C_{ik} f_k}$$
where:
The Langmuir constants are calculated from the cell potential:
$$C_{ij}(T) = \frac{4\pi}{k_B T} \int_0^{R_{cell}} \exp\left(-\frac{w(r)}{k_B T}\right) r^2 dr$$
where $w(r)$ is the spherically averaged Kihara cell potential.
Class: ComponentHydrateGF, ComponentHydrateStatoil
The CPA (Cubic Plus Association) hydrate model is the recommended model for systems containing polar components like water, MEG, and methanol. It uses the CPA equation of state for fugacity calculations.
Key Features:
Usage:
SystemInterface fluid = new SystemSrkCPAstatoil(273.15, 100.0);
fluid.addComponent("methane", 0.9);
fluid.addComponent("water", 0.1);
fluid.setMixingRule(10); // CPA mixing rule
fluid.setHydrateCheck(true);
Class: ComponentHydratePVTsim
The default hydrate model based on the PVTsim approach, suitable for hydrocarbon-water systems.
Key Features:
Usage:
SystemInterface fluid = new SystemSrkEos(273.15, 100.0);
fluid.addComponent("methane", 0.9);
fluid.addComponent("water", 0.1);
fluid.setMixingRule("classic");
fluid.setHydrateCheck(true);
Class: ComponentHydrateBallard
Based on the work of Ballard (2002), this model uses an improved approach for Langmuir constant calculation.
Class: ComponentHydrateKluda
Alternative hydrate model with different parameterization for specific applications.
Hydrate-specific parameters are stored in the component database:
| Parameter | Description | Unit |
|---|---|---|
LJdiameterHYDRATE |
Lennard-Jones diameter for hydrate cavity | Å |
LJepsHYDRATE |
Lennard-Jones energy parameter | K |
sphericalCoreRadius |
Kihara spherical core radius | Å |
Components that can occupy hydrate cavities (hydrate formers):
| Component | Structure | Small Cavity | Large Cavity |
|---|---|---|---|
| Methane | sI, sII | ✓ | ✓ |
| Ethane | sI | - | ✓ |
| Propane | sII | - | ✓ |
| i-Butane | sII | - | ✓ |
| CO₂ | sI | ✓ | ✓ |
| H₂S | sI, sII | ✓ | ✓ |
| Nitrogen | sII | ✓ | ✓ |
Check if a component is a hydrate former:
boolean isFormer = fluid.getPhase(0).getComponent("methane").isHydrateFormer();
NeqSim supports thermodynamic hydrate inhibitors that shift the hydrate equilibrium curve:
| Inhibitor | Common Name | Effect |
|---|---|---|
| MEG | Monoethylene glycol | Lowers hydrate temperature |
| TEG | Triethylene glycol | Lowers hydrate temperature |
| methanol | Methanol | Strong temperature depression |
| ethanol | Ethanol | Moderate temperature depression |
| NaCl | Salt | Salinity effect |
// Calculate required MEG concentration for target temperature
SystemInterface fluid = new SystemSrkCPAstatoil(273.15 + 5.0, 80.0);
fluid.addComponent("methane", 0.85);
fluid.addComponent("ethane", 0.05);
fluid.addComponent("water", 0.08);
fluid.addComponent("MEG", 0.02);
fluid.setMixingRule(10);
fluid.setHydrateCheck(true);
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
// Calculate inhibitor concentration needed to prevent hydrate at 5°C
ops.hydrateInhibitorConcentration("MEG", 273.15 + 5.0);
double requiredMEGwt = fluid.getPhase("aqueous").getComponent("MEG").getwtfrac();
import neqsim.thermo.system.SystemSrkCPAstatoil;
import neqsim.thermodynamicoperations.ThermodynamicOperations;
// Create fluid at potential hydrate conditions
SystemInterface fluid = new SystemSrkCPAstatoil(273.15 + 5.0, 100.0);
fluid.addComponent("methane", 0.85);
fluid.addComponent("ethane", 0.08);
fluid.addComponent("propane", 0.04);
fluid.addComponent("water", 0.03);
fluid.setMixingRule(10);
fluid.setHydrateCheck(true); // Enable hydrate phase
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
// Calculate hydrate formation temperature
ops.hydrateFormationTemperature();
System.out.println("Hydrate formation T: " + fluid.getTemperature("C") + " °C");
// Generate hydrate PT curve
double[] pressures = {10, 20, 50, 100, 150, 200}; // bar
System.out.println("P (bar)\tT_hydrate (°C)");
for (double P : pressures) {
fluid.setPressure(P);
fluid.setTemperature(280.0); // Initial guess
ops.hydrateFormationTemperature();
System.out.println(P + "\t" + fluid.getTemperature("C"));
}
// Get cavity occupancy for each guest molecule
PhaseInterface hydratePhase = fluid.getPhase(PhaseType.HYDRATE);
for (int i = 0; i < hydratePhase.getNumberOfComponents(); i++) {
if (hydratePhase.getComponent(i).isHydrateFormer()) {
ComponentHydrate comp = (ComponentHydrate) hydratePhase.getComponent(i);
double smallCavity = comp.calcYKI(0, 0, hydratePhase); // Structure I, small cavity
double largeCavity = comp.calcYKI(0, 1, hydratePhase); // Structure I, large cavity
System.out.println(comp.getName() +
" - Small: " + smallCavity + ", Large: " + largeCavity);
}
}
van der Waals, J.H., Platteeuw, J.C. (1959). "Clathrate Solutions." Advances in Chemical Physics, 2, 1-57.
Sloan, E.D., Koh, C.A. (2008). Clathrate Hydrates of Natural Gases, 3rd ed. CRC Press.
Ballard, A.L., Sloan, E.D. (2002). "The next generation of hydrate prediction: I. Hydrate standard states and incorporation of spectroscopy." Fluid Phase Equilibria, 194-197, 371-383.
Kontogeorgis, G.M., et al. (2006). "Ten Years with the CPA (Cubic-Plus-Association) Equation of State." Industrial & Engineering Chemistry Research, 45, 4855-4868.
Munck, J., Skjold-Jørgensen, S., Rasmussen, P. (1988). "Computations of the formation of gas hydrates." Chemical Engineering Science, 43, 2661-2672.
This document provides comprehensive documentation for hydrate phase equilibrium flash calculations in NeqSim.
NeqSim provides specialized flash calculations for systems containing gas hydrates. These operations extend the standard thermodynamic operations to include hydrate phase equilibrium.
Key Classes:
TPHydrateFlash - TP flash with hydrate phase equilibriumHydrateFormationTemperatureFlash - Calculate hydrate formation temperatureHydrateFormationPressureFlash - Calculate hydrate formation pressureHydrateInhibitorConcentrationFlash - Calculate inhibitor requirementsHydrateEquilibriumLine - Generate hydrate PT curvePerforms a temperature-pressure flash calculation including hydrate phase equilibrium. This is the main method for calculating hydrate phase fraction and composition at given T and P.
Method: ThermodynamicOperations.hydrateTPflash()
Algorithm:
Example:
SystemInterface fluid = new SystemSrkCPAstatoil(273.15 + 5.0, 100.0);
fluid.addComponent("methane", 0.85);
fluid.addComponent("ethane", 0.08);
fluid.addComponent("propane", 0.04);
fluid.addComponent("water", 0.03);
fluid.setMixingRule(10);
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.hydrateTPflash();
// Check results
System.out.println("Number of phases: " + fluid.getNumberOfPhases());
System.out.println("Has hydrate: " + fluid.hasHydratePhase());
if (fluid.hasHydratePhase()) {
System.out.println("Hydrate fraction: " + fluid.getBeta(PhaseType.HYDRATE));
}
fluid.prettyPrint();
Output Phases:
| Phase | Type | Description |
|---|---|---|
| 0 | GAS | Vapor phase with dissolved water |
| 1 | AQUEOUS | Water-rich phase |
| 2+ | HYDRATE | Clathrate hydrate phase |
Specialized flash targeting gas-hydrate equilibrium without aqueous phase. This is useful for systems with trace water where all water can be consumed by hydrate formation.
Method: ThermodynamicOperations.gasHydrateTPflash()
When to Use:
Example:
// Dry gas with trace water
SystemInterface fluid = new SystemSrkCPAstatoil(273.15 - 15.0, 250.0);
fluid.addComponent("methane", 0.9998);
fluid.addComponent("water", 0.0002); // 200 ppm water
fluid.setMixingRule(10);
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.gasHydrateTPflash();
// Result: GAS + HYDRATE phases (no AQUEOUS)
boolean hasAqueous = false;
for (int i = 0; i < fluid.getNumberOfPhases(); i++) {
if (fluid.getPhase(i).getType() == PhaseType.AQUEOUS) {
hasAqueous = true;
}
}
System.out.println("Has aqueous phase: " + hasAqueous); // false
Algorithm:
Calculates the temperature at which hydrate first forms at given pressure.
Methods:
void hydrateFormationTemperature()
void hydrateFormationTemperature(double initialGuess)
void hydrateFormationTemperature(int structure) // 0=ice, 1=sI, 2=sII
Example:
SystemInterface fluid = new SystemSrkCPAstatoil(280.0, 100.0);
fluid.addComponent("methane", 0.9);
fluid.addComponent("ethane", 0.05);
fluid.addComponent("CO2", 0.02);
fluid.addComponent("water", 0.03);
fluid.setMixingRule(10);
fluid.setHydrateCheck(true);
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.hydrateFormationTemperature();
System.out.println("Hydrate formation T: " + fluid.getTemperature("C") + " °C");
System.out.println("At pressure: " + fluid.getPressure("bara") + " bara");
Calculates the pressure at which hydrate first forms at given temperature.
Method:
void hydrateFormationPressure()
void hydrateFormationPressure(int structure)
Example:
fluid.setTemperature(278.15); // 5°C
ops.hydrateFormationPressure();
System.out.println("Hydrate formation P: " + fluid.getPressure("bara") + " bara");
Calculate required inhibitor concentration to prevent hydrate formation.
Methods:
// Calculate inhibitor needed for target temperature
void hydrateInhibitorConcentration(String inhibitor, double targetTemperature)
// Set inhibitor weight fraction and calculate effect
void hydrateInhibitorConcentrationSet(String inhibitor, double wtFraction)
Supported Inhibitors:
"MEG" - Monoethylene glycol"TEG" - Triethylene glycol"methanol" - Methanol"ethanol" - EthanolExample:
SystemInterface fluid = new SystemSrkCPAstatoil(273.15 + 5.0, 100.0);
fluid.addComponent("methane", 0.85);
fluid.addComponent("water", 0.10);
fluid.addComponent("MEG", 0.05);
fluid.setMixingRule(10);
fluid.setHydrateCheck(true);
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
// What MEG concentration prevents hydrate at 5°C?
ops.hydrateInhibitorConcentration("MEG", 273.15 + 5.0);
System.out.println("Required MEG (wt%): " +
fluid.getPhase("aqueous").getComponent("MEG").getwtfrac() * 100);
Generate the complete hydrate equilibrium curve (P-T diagram).
Class: HydrateEquilibriumLine
Example:
SystemInterface fluid = new SystemSrkCPAstatoil(280.0, 50.0);
fluid.addComponent("methane", 0.9);
fluid.addComponent("water", 0.1);
fluid.setMixingRule(10);
fluid.setHydrateCheck(true);
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.calcHydrateEquilibriumLine();
// Get curve data
double[][] curve = ops.getOperation().get2DData();
// curve[0] = temperatures (K)
// curve[1] = pressures (bar)
The most common scenario: gas in equilibrium with water and hydrate.
SystemInterface fluid = new SystemSrkCPAstatoil(273.15 + 2.0, 80.0);
fluid.addComponent("methane", 0.80);
fluid.addComponent("ethane", 0.05);
fluid.addComponent("propane", 0.03);
fluid.addComponent("n-butane", 0.01);
fluid.addComponent("CO2", 0.01);
fluid.addComponent("water", 0.10);
fluid.setMixingRule(10);
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.hydrateTPflash();
// Expected phases: GAS, AQUEOUS, HYDRATE
fluid.prettyPrint();
Four-phase equilibrium with condensate/oil phase.
// Rich gas condensate with water
SystemInterface fluid = new SystemSrkCPAstatoil(273.15 + 4.0, 100.0);
// Gas components
fluid.addComponent("methane", 0.70);
fluid.addComponent("ethane", 0.08);
fluid.addComponent("propane", 0.05);
fluid.addComponent("n-butane", 0.02);
fluid.addComponent("n-pentane", 0.01);
// Oil/condensate components
fluid.addComponent("n-hexane", 0.01);
fluid.addComponent("n-heptane", 0.02);
fluid.addComponent("n-octane", 0.01);
// Water
fluid.addComponent("water", 0.10);
fluid.setMixingRule(10);
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.hydrateTPflash();
// Expected phases: GAS, OIL, AQUEOUS, HYDRATE
System.out.println("Number of phases: " + fluid.getNumberOfPhases());
for (int i = 0; i < fluid.getNumberOfPhases(); i++) {
System.out.println("Phase " + i + ": " + fluid.getPhase(i).getType() +
", beta = " + fluid.getBeta(i));
}
For systems with very low water content where hydrate consumes all water.
// 500 ppm water at extreme conditions
SystemInterface fluid = new SystemSrkCPAstatoil(273.15 - 20.0, 300.0);
fluid.addComponent("methane", 0.9995);
fluid.addComponent("water", 0.0005);
fluid.setMixingRule(10);
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.gasHydrateTPflash();
// Expected phases: GAS, HYDRATE (no AQUEOUS)
boolean hasAqueous = false;
for (int i = 0; i < fluid.getNumberOfPhases(); i++) {
if (fluid.getPhase(i).getType() == PhaseType.AQUEOUS) {
hasAqueous = true;
}
}
System.out.println("Has aqueous: " + hasAqueous); // false
| Method | Description |
|---|---|
hydrateTPflash() |
TP flash with hydrate equilibrium |
hydrateTPflash(boolean checkForSolids) |
TP flash with solid check |
gasHydrateTPflash() |
TP flash targeting gas-hydrate equilibrium |
hydrateFormationTemperature() |
Calculate hydrate formation T |
hydrateFormationTemperature(double guess) |
With initial guess |
hydrateFormationTemperature(int structure) |
For specific structure |
hydrateFormationPressure() |
Calculate hydrate formation P |
hydrateFormationPressure(int structure) |
For specific structure |
hydrateInhibitorConcentration(String, double) |
Calculate inhibitor needed |
hydrateInhibitorConcentrationSet(String, double) |
Set inhibitor and calculate |
calcHydrateEquilibriumLine() |
Generate PT curve |
| Method | Description |
|---|---|
setHydrateCheck(boolean) |
Enable/disable hydrate phase |
hasHydratePhase() |
Check if hydrate exists |
getHydrateFraction() |
Get hydrate mole fraction |
| Method | Description |
|---|---|
isHydrateFormed() |
Check if hydrate formed |
getHydrateFraction() |
Get hydrate beta value |
getStableHydrateStructure() |
Get structure (1 or 2) |
getCavityOccupancy(String, int, int) |
Get cavity occupancy |
setGasHydrateOnlyMode(boolean) |
Enable gas-hydrate mode |
isGasHydrateOnlyMode() |
Check if mode enabled |
import neqsim.thermo.system.SystemInterface;
import neqsim.thermo.system.SystemSrkCPAstatoil;
import neqsim.thermo.phase.PhaseType;
import neqsim.thermodynamicoperations.ThermodynamicOperations;
public class HydrateAnalysis {
public static void main(String[] args) {
// 1. Create production fluid
SystemInterface fluid = new SystemSrkCPAstatoil(273.15 + 5.0, 100.0);
fluid.addComponent("methane", 0.80);
fluid.addComponent("ethane", 0.06);
fluid.addComponent("propane", 0.04);
fluid.addComponent("CO2", 0.02);
fluid.addComponent("water", 0.08);
fluid.setMixingRule(10);
fluid.setHydrateCheck(true);
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
// 2. Calculate hydrate formation temperature
ops.hydrateFormationTemperature();
double Thyd = fluid.getTemperature("C");
System.out.println("Hydrate formation temperature: " + Thyd + " °C");
// 3. At 5°C, check if hydrate exists
fluid.setTemperature(273.15 + 5.0);
ops.hydrateTPflash();
if (fluid.hasHydratePhase()) {
System.out.println("Hydrate forms at 5°C, 100 bar");
// Get phase fractions
for (int i = 0; i < fluid.getNumberOfPhases(); i++) {
System.out.printf("Phase %d (%s): %.4f mol%%\n",
i, fluid.getPhase(i).getType(), fluid.getBeta(i) * 100);
}
}
// 4. Calculate MEG needed to prevent hydrate
fluid.addComponent("MEG", 0.05); // Add MEG
fluid.createDatabase(true);
ops.hydrateInhibitorConcentration("MEG", 273.15 + 5.0);
double megWt = fluid.getPhase("aqueous").getComponent("MEG").getwtfrac();
System.out.println("Required MEG in aqueous phase: " + megWt * 100 + " wt%");
}
}
import neqsim.process.equipment.stream.Stream;
import neqsim.process.measurementdevice.HydrateEquilibriumTemperatureAnalyser;
// Create stream with hydrate checking
SystemInterface gas = new SystemSrkCPAstatoil(280.0, 100.0);
gas.addComponent("methane", 0.9);
gas.addComponent("water", 0.1);
gas.setMixingRule(10);
gas.setHydrateCheck(true);
Stream gasStream = new Stream("Gas Feed", gas);
gasStream.setFlowRate(100.0, "kg/hr");
gasStream.run();
// Add hydrate analyser
HydrateEquilibriumTemperatureAnalyser hydrateAnalyser =
new HydrateEquilibriumTemperatureAnalyser("Hydrate Monitor", gasStream);
hydrateAnalyser.run();
double hydrateT = hydrateAnalyser.getMeasuredValue("C");
System.out.println("Hydrate equilibrium temperature: " + hydrateT + " °C");
For CPA-based hydrate calculations, use mixing rule 10:
fluid.setMixingRule(10);
Before hydrate calculations:
fluid.setHydrateCheck(true);
After hydrate flash, verify beta sum equals 1.0:
double betaSum = 0.0;
for (int i = 0; i < fluid.getNumberOfPhases(); i++) {
betaSum += fluid.getBeta(i);
}
assert Math.abs(betaSum - 1.0) < 1e-6 : "Mass conservation violated";
Always verify expected phases exist:
boolean hasHydrate = fluid.hasHydratePhase();
boolean hasAqueous = false;
for (int i = 0; i < fluid.getNumberOfPhases(); i++) {
if (fluid.getPhase(i).getType() == PhaseType.AQUEOUS) {
hasAqueous = true;
}
}
| Scenario | Method |
|---|---|
| Normal water content (> 1%) | hydrateTPflash() |
| Trace water (< 1%) | gasHydrateTPflash() |
| Find formation T | hydrateFormationTemperature() |
| Find formation P | hydrateFormationPressure() |
fluid.setHydrateCheck(true)prettyPrint() to inspect phase compositionsThis is expected behavior. Use gasHydrateTPflash() for systems with trace water to achieve gas-hydrate equilibrium directly.
The process package provides process equipment, unit operations, controllers, and process system management for building complete flowsheets.
Location: neqsim.process
Purpose:
ProcessSystemThis documentation is organized into the following sections:
| Section | Description |
|---|---|
| equipment/ | Equipment documentation (separators, compressors, etc.) |
| processmodel/ | ProcessSystem and flowsheet management |
| safety/ | Safety systems (PSV, ESD, blowdown) |
| controllers.md | Process controllers and logic |
| Document | Description |
|---|---|
| process_design_guide.md | Complete guide to process design workflow using NeqSim |
| Document | Description |
|---|---|
| mechanical_design_standards.md | Design standards (NORSOK, ASME, API, DNV, etc.) |
| mechanical_design_database.md | Data sources, database schemas, and CSV configuration |
| torg_integration.md | Technical Requirements Documents (TORG) integration |
| field_development_orchestration.md | Complete design workflow orchestration |
| Category | Documentation | Classes |
|---|---|---|
| Streams | streams.md | Stream, EnergyStream, VirtualStream |
| Separators | separators.md | Separator, ThreePhaseSeparator, GasScrubber |
| Heat Exchangers | heat_exchangers.md | Heater, Cooler, HeatExchanger |
| Compressors | compressors.md | Compressor, CompressorChart |
| Pumps | pumps.md | Pump, PumpChart |
| Expanders | expanders.md | Expander, TurboExpanderCompressor |
| Valves | valves.md | ThrottlingValve, SafetyValve, BlowdownValve |
| Distillation | distillation.md | DistillationColumn, SimpleTray |
| Absorbers | absorbers.md | SimpleAbsorber, SimpleTEGAbsorber |
| Ejectors | ejectors.md | Ejector |
| Membranes | membranes.md | MembraneSeparator |
| Flares | flares.md | Flare, FlareStack |
| Electrolyzers | electrolyzers.md | Electrolyzer, CO2Electrolyzer |
| Filters | filters.md | Filter, CharCoalFilter |
| Reactors | reactors.md | GibbsReactor |
| Pipelines | pipelines.md | Pipeline, AdiabaticPipe |
| Tanks | tanks.md | Tank, VesselDepressurization |
| Wells | wells.md | Well equipment |
| Mixers/Splitters | mixers_splitters.md | Mixer, Splitter |
| Utility | util/ | Adjuster, Recycle, Calculator |
process/
├── SimulationBaseClass.java # Base class for simulations
├── SimulationInterface.java # Simulation interface
│
├── equipment/ # Process equipment
│ ├── ProcessEquipmentBaseClass.java
│ ├── ProcessEquipmentInterface.java
│ ├── TwoPortEquipment.java # Equipment with inlet/outlet
│ ├── EquipmentFactory.java # Factory for creating equipment
│ │
│ ├── stream/ # Streams
│ │ ├── Stream.java
│ │ ├── StreamInterface.java
│ │ ├── EnergyStream.java
│ │ └── VirtualStream.java
│ │
│ ├── separator/ # Separators
│ │ ├── Separator.java
│ │ ├── ThreePhaseSeparator.java
│ │ ├── GasScrubber.java
│ │ └── SeparatorInterface.java
│ │
│ ├── heatexchanger/ # Heat transfer
│ │ ├── Heater.java
│ │ ├── Cooler.java
│ │ ├── HeatExchanger.java
│ │ ├── NeqHeater.java
│ │ └── Condenser.java
│ │
│ ├── compressor/ # Compression
│ │ ├── Compressor.java
│ │ ├── CompressorInterface.java
│ │ └── CompressorChartInterface.java
│ │
│ ├── pump/ # Pumps
│ │ ├── Pump.java
│ │ └── PumpInterface.java
│ │
│ ├── expander/ # Expanders
│ │ ├── Expander.java
│ │ └── ExpanderInterface.java
│ │
│ ├── valve/ # Valves
│ │ ├── ThrottlingValve.java
│ │ ├── ValveInterface.java
│ │ └── SafetyValve.java
│ │
│ ├── mixer/ # Mixers
│ │ ├── Mixer.java
│ │ ├── StaticMixer.java
│ │ └── MixerInterface.java
│ │
│ ├── splitter/ # Splitters
│ │ ├── Splitter.java
│ │ └── SplitterInterface.java
│ │
│ ├── distillation/ # Distillation
│ │ ├── DistillationColumn.java
│ │ ├── SimpleTray.java
│ │ ├── Condenser.java
│ │ └── Reboiler.java
│ │
│ ├── reactor/ # Reactors
│ │ ├── Reactor.java
│ │ └── PFReactor.java
│ │
│ ├── absorber/ # Absorption
│ │ ├── Absorber.java
│ │ └── SimpleTEGAbsorber.java
│ │
│ ├── pipeline/ # Pipelines
│ │ ├── Pipeline.java
│ │ └── PipelineInterface.java
│ │
│ ├── well/ # Wells
│ │ ├── SimpleWell.java
│ │ └── WellFlow.java
│ │
│ ├── tank/ # Tanks and vessels
│ │ ├── Tank.java
│ │ └── ProcessVessel.java
│ │
│ ├── filter/ # Filters
│ │ └── Filter.java
│ │
│ ├── membrane/ # Membranes
│ │ └── Membrane.java
│ │
│ ├── ejector/ # Ejectors
│ │ └── Ejector.java
│ │
│ ├── electrolyzer/ # Electrolyzers
│ │ └── PEM_Electrolyzer.java
│ │
│ └── util/ # Utility equipment
│ ├── Adjuster.java
│ ├── Recycle.java
│ ├── Calculator.java
│ ├── Setter.java
│ └── MoleFractionSetter.java
│
├── processmodel/ # Process system
│ ├── ProcessSystem.java
│ ├── ProcessModule.java
│ └── graph/ # Graph-based execution
│ ├── ProcessGraph.java
│ └── ProcessGraphBuilder.java
│
├── controllerdevice/ # Controllers
│ ├── ControllerDevice.java
│ └── PIDController.java
│
├── measurementdevice/ # Measurements
│ ├── MeasurementDevice.java
│ ├── TemperatureMeasurement.java
│ ├── PressureMeasurement.java
│ └── FlowMeasurement.java
│
├── logic/ # Process logic
│ ├── ProcessLogicController.java
│ └── ConditionalLogic.java
│
├── alarm/ # Alarm system
│ └── ProcessAlarmManager.java
│
├── safety/ # Safety systems
│ ├── PSV/
│ ├── ESD/
│ └── Blowdown/
│
├── calibration/ # Equipment calibration
├── conditionmonitor/ # Condition monitoring
├── costestimation/ # Cost estimation
├── mechanicaldesign/ # Mechanical design calculations
│ ├── separator/ # Separator vessel design
│ ├── compressor/ # Compressor design (API 617)
│ ├── valve/ # Valve body, sizing, actuator
│ └── designstandards/ # ASME, API, IEC standards
├── mpc/ # Model predictive control
├── ml/ # Machine learning
└── streaming/ # Data streaming
| Equipment | Documentation | Standards |
|---|---|---|
| Separators | See SeparatorMechanicalDesign | ASME, BS 5500 |
| Compressors | CompressorMechanicalDesign.md | API 617, API 672 |
| Valves | ValveMechanicalDesign.md | IEC 60534, ANSI/ISA-75, ASME B16.34 |
The ProcessSystem class is the container for building and running process flowsheets.
import neqsim.process.processmodel.ProcessSystem;
import neqsim.process.equipment.stream.Stream;
import neqsim.process.equipment.separator.Separator;
import neqsim.process.equipment.valve.ThrottlingValve;
// Create process system
ProcessSystem process = new ProcessSystem("Gas Processing Plant");
// Create feed stream
SystemInterface feed = new SystemSrkEos(300.0, 80.0);
feed.addComponent("methane", 0.85);
feed.addComponent("ethane", 0.08);
feed.addComponent("propane", 0.05);
feed.addComponent("n-butane", 0.02);
feed.setMixingRule("classic");
Stream feedStream = new Stream("Feed", feed);
feedStream.setFlowRate(1000.0, "kg/hr");
// Add equipment to process
process.add(feedStream);
// Letdown valve
ThrottlingValve valve = new ThrottlingValve("Inlet Valve", feedStream);
valve.setOutletPressure(40.0, "bara");
process.add(valve);
// Separator
Separator separator = new Separator("HP Separator", valve.getOutletStream());
process.add(separator);
// Run process (recommended - auto-optimized)
process.runOptimized();
// Get results
System.out.println("Separator gas rate: " +
separator.getGasOutStream().getFlowRate("kg/hr") + " kg/hr");
System.out.println("Separator liquid rate: " +
separator.getLiquidOutStream().getFlowRate("kg/hr") + " kg/hr");
NeqSim provides multiple execution strategies for optimal performance:
| Method | Best For | Speedup |
|---|---|---|
run() |
General use | baseline |
runOptimized() |
Recommended | 28-40% |
runParallel() |
Feed-forward (no recycles) | 40-57% |
runHybrid() |
Complex recycle processes | 38% |
// Recommended - auto-selects best strategy
process.runOptimized();
// Or use specific strategies:
process.run(); // Sequential (default)
process.runParallel(); // Parallel (feed-forward only)
process.runHybrid(); // Hybrid (parallel + iterative)
// Check for recycles
boolean hasRecycles = process.hasRecycleLoops();
// Get detailed execution analysis
System.out.println(process.getExecutionPartitionInfo());
| Method | Description |
|---|---|
add(equipment) |
Add equipment to process |
run() |
Run sequential simulation |
runOptimized() |
Run with auto-optimized strategy |
runParallel() |
Run with parallel execution |
runHybrid() |
Run with hybrid execution |
runTransient(time, dt) |
Run transient simulation |
getUnit(name) |
Get equipment by name |
hasRecycleLoops() |
Check for recycle loops |
getExecutionPartitionInfo() |
Get execution analysis |
copy() |
Clone the process system |
getReport() |
Get process report |
display() |
Display process summary |
// Material stream
Stream gas = new Stream("Natural Gas", fluid);
gas.setFlowRate(5000.0, "Sm3/hr");
gas.setTemperature(25.0, "C");
gas.setPressure(100.0, "bara");
gas.run();
// Energy stream
EnergyStream heat = new EnergyStream("Heating Duty");
heat.setEnergyFlow(1000.0, "kW");
// Two-phase separator
Separator sep2p = new Separator("V-100", inletStream);
sep2p.run();
Stream gas = sep2p.getGasOutStream();
Stream liquid = sep2p.getLiquidOutStream();
// Three-phase separator
ThreePhaseSeparator sep3p = new ThreePhaseSeparator("V-200", inletStream);
sep3p.run();
Stream gas = sep3p.getGasOutStream();
Stream oil = sep3p.getOilOutStream();
Stream water = sep3p.getWaterOutStream();
// Heater (duty specified)
Heater heater = new Heater("E-100", inletStream);
heater.setOutTemperature(80.0, "C");
heater.run();
System.out.println("Duty: " + heater.getDuty() + " W");
// Cooler
Cooler cooler = new Cooler("E-200", inletStream);
cooler.setOutTemperature(30.0, "C");
cooler.run();
// Shell-tube heat exchanger
HeatExchanger hx = new HeatExchanger("E-300", hotStream, coldStream);
hx.setUAvalue(5000.0); // W/K
hx.run();
// Compressor with polytropic efficiency
Compressor comp = new Compressor("K-100", inletStream);
comp.setOutletPressure(80.0, "bara");
comp.setPolytropicEfficiency(0.75);
comp.setUsePolytropicCalc(true);
comp.run();
System.out.println("Power: " + comp.getPower("kW") + " kW");
System.out.println("Outlet T: " + comp.getOutletStream().getTemperature("C") + " °C");
// Throttling valve (Joule-Thomson)
ThrottlingValve valve = new ThrottlingValve("FV-100", inletStream);
valve.setOutletPressure(50.0, "bara");
valve.run();
// Valve with Cv
valve.setCv(100.0, "US");
valve.setPercentValveOpening(50.0);
// Simple distillation column
DistillationColumn column = new DistillationColumn("T-100", 10, true, true);
column.addFeedStream(feedStream, 5);
column.setCondenserTemperature(40.0, "C");
column.setReboilerTemperature(120.0, "C");
column.run();
Stream overhead = column.getGasOutStream();
Stream bottoms = column.getLiquidOutStream();
Adjust a parameter to meet a specification.
// Adjust heater duty to achieve target temperature
Adjuster tempAdjuster = new Adjuster("TC-100");
tempAdjuster.setAdjustedVariable(heater, "duty");
tempAdjuster.setTargetVariable(heater.getOutletStream(), "temperature", 80.0, "C");
process.add(tempAdjuster);
Handle recycle loops in the process.
Recycle recycle = new Recycle("Recycle");
recycle.addStream(recycleStream);
recycle.setOutletStream(recycleInletStream);
recycle.setTolerance(1e-6);
process.add(recycle);
Perform custom calculations.
Calculator calc = new Calculator("MW Calculator");
calc.addInputVariable(stream);
calc.setOutputVariable(heater, "duty");
calc.setExpression("molarMass * 1000");
process.add(calc);
SafetyValve psv = new SafetyValve("PSV-100", vessel);
psv.setSetPressure(120.0, "bara");
psv.setBlowdownPressure(0.1); // 10% blowdown
process.add(psv);
See Safety Simulation Roadmap for detailed safety system documentation.
// Run transient simulation
double simulationTime = 3600.0; // 1 hour
double timeStep = 1.0; // 1 second
process.setTimeStep(timeStep);
for (double t = 0; t < simulationTime; t += timeStep) {
process.runTransient();
// Log data
System.out.println(t + ", " +
separator.getPressure() + ", " +
separator.getGasOutStream().getFlowRate("kg/hr"));
}
// Get JSON report
String jsonReport = process.getReport_json();
// Get tabular report
String[][] table = process.getUnitOperationsAsTable();
// Display to console
process.display();
NeqSim includes foundational infrastructure to support the future of process simulation:
| Capability | Documentation | Description |
|---|---|---|
| Lifecycle Management | lifecycle/ | Model versioning, state export/import, lifecycle tracking |
| Emissions Tracking | sustainability/ | CO2e accounting, regulatory reporting |
| Advisory Systems | advisory/ | Look-ahead predictions with uncertainty |
| ML Integration | ml/ | Surrogate models, physics constraint validation |
| Safety Scenarios | safety/scenario-generation.md | Automatic failure scenario generation |
| Batch Studies | optimization/batch-studies.md | Parallel parameter studies |
See Future Infrastructure Overview for complete documentation.
NeqSim provides a powerful framework for modeling chemical and petroleum process plants. By connecting unit operations (separators, compressors, heat exchangers, valves) with streams, you can build complete process flowsheets for steady-state and dynamic simulation.
NeqSim combines rigorous thermodynamic calculations with flexible process modeling:
| Feature | Description |
|---|---|
| Rigorous Thermodynamics | Equations of state (SRK, PR, CPA, GERG-2008) with accurate phase equilibria |
| Comprehensive Equipment | 50+ unit operation types including specialized oil & gas equipment |
| Dynamic Simulation | Time-stepping for transient analysis, blowdown, and startup/shutdown |
| Control Integration | PID controllers, adjusters, and recycle solvers built-in |
| Safety Systems | PSV, ESD, HIPPS modeling for hazard analysis |
| Extensibility | Java API allows custom equipment and integration with external systems |
| Class | Purpose |
|---|---|
ProcessSystem |
Container for all equipment; manages execution order and convergence |
Stream |
Fluid flow with thermodynamic state (T, P, composition, flow rate) |
ProcessEquipmentInterface |
Base interface for all unit operations |
Recycle |
Handles iterative convergence for recycle loops |
Adjuster |
Adjusts variables to meet specifications |
Calculator |
Custom calculations with lambda expressions |
ProcessEquipmentBaseClass
├── TwoPortEquipment (single inlet/outlet)
│ ├── ThrottlingValve
│ ├── Compressor
│ ├── Pump
│ ├── Heater / Cooler
│ └── AdiabaticPipe
├── Separator (multiple outlets)
│ ├── ThreePhaseSeparator
│ └── GasScrubber
├── Mixer / Splitter
├── DistillationColumn
└── Specialized Equipment
├── Ejector
├── MembraneSeparator
└── Electrolyzer
1. Define fluid (thermodynamic system)
2. Create feed stream(s)
3. Instantiate equipment and connect streams
4. Add all units to ProcessSystem
5. Run simulation (process.run())
6. Retrieve results from streams/equipment
A minimal working example - gas separation at reduced pressure:
import neqsim.process.equipment.stream.Stream;
import neqsim.process.equipment.valve.ThrottlingValve;
import neqsim.process.equipment.separator.Separator;
import neqsim.process.processmodel.ProcessSystem;
import neqsim.thermo.system.SystemSrkEos;
// 1. Define fluid
SystemSrkEos fluid = new SystemSrkEos(298.15, 50.0);
fluid.addComponent("methane", 0.90);
fluid.addComponent("ethane", 0.05);
fluid.addComponent("propane", 0.03);
fluid.addComponent("n-heptane", 0.02);
fluid.setMixingRule("classic");
// 2. Create feed stream
Stream feed = new Stream("Feed", fluid);
feed.setFlowRate(1000.0, "kg/hr");
feed.setTemperature(30.0, "C");
feed.setPressure(50.0, "bara");
// 3. Add equipment
ThrottlingValve valve = new ThrottlingValve("JT Valve", feed);
valve.setOutletPressure(10.0, "bara");
Separator separator = new Separator("HP Separator", valve.getOutletStream());
// 4. Build process system
ProcessSystem process = new ProcessSystem();
process.add(feed);
process.add(valve);
process.add(separator);
// 5. Run simulation
process.run();
// 6. Get results
System.out.println("Gas rate: " + separator.getGasOutStream().getFlowRate("kg/hr") + " kg/hr");
System.out.println("Liquid rate: " + separator.getLiquidOutStream().getFlowRate("kg/hr") + " kg/hr");
System.out.println("Separator temp: " + separator.getTemperature("C") + " °C");
Create a thermodynamic system using an equation of state:
// SRK equation of state
SystemInterface fluid = new SystemSrkEos(298.15, 50.0); // T(K), P(bar)
fluid.addComponent("methane", 90.0); // mole fraction or moles
fluid.addComponent("ethane", 5.0);
fluid.addComponent("propane", 3.0);
fluid.addComponent("n-heptane", 2.0);
fluid.setMixingRule("classic");
// Or use CPA for polar components (water, MEG, methanol)
SystemInterface cpaFluid = new SystemSrkCPAstatoil(298.15, 50.0);
cpaFluid.addComponent("methane", 0.9);
cpaFluid.addComponent("water", 0.05);
cpaFluid.addComponent("MEG", 0.05);
cpaFluid.setMixingRule(10); // CPA mixing rule
Stream feedStream = new Stream("Feed Stream", fluid);
feedStream.setFlowRate(1000.0, "kg/hr"); // or "kmol/hr", "MSm3/day"
feedStream.setTemperature(30.0, "C"); // or "K"
feedStream.setPressure(50.0, "bara"); // or "barg", "Pa"
Most equipment takes an inlet stream in the constructor:
// Valve - reduces pressure
ThrottlingValve valve = new ThrottlingValve("Inlet Valve", feedStream);
valve.setOutletPressure(30.0, "bara");
// Separator - splits phases
Separator separator = new Separator("Test Separator", valve.getOutletStream());
// Compressor - increases pressure
Compressor compressor = new Compressor("Export Compressor", separator.getGasOutStream());
compressor.setOutletPressure(100.0, "bara");
compressor.setIsentropicEfficiency(0.75);
// Heater - adds heat
Heater heater = new Heater("Gas Heater", compressor.getOutletStream());
heater.setOutTemperature(50.0, "C");
ProcessSystem process = new ProcessSystem();
process.add(feedStream);
process.add(valve);
process.add(separator);
process.add(compressor);
process.add(heater);
Note: Equipment names must be unique within a ProcessSystem.
// Steady-state run
process.run();
// For debugging, run with UUID tracking
UUID id = UUID.randomUUID();
process.run(id);
// Stream properties
double gasRate = separator.getGasOutStream().getFlowRate("kg/hr");
double gasTemp = separator.getGasOutStream().getTemperature("C");
double gasPressure = separator.getGasOutStream().getPressure("bara");
// Equipment performance
double compressorPower = compressor.getPower("kW");
double compressorHead = compressor.getPolytropicHead("kJ/kg");
// Composition
double methaneInGas = separator.getGasOutStream()
.getFluid().getComponent("methane").getMoleFraction();
Process modules are pre-configured collections of unit operations designed to perform standard processing tasks. They encapsulate complex logic for reuse.
| Module | Purpose |
|---|---|
GlycolDehydrationlModule |
TEG/MEG dehydration systems |
SeparationTrainModule |
Multi-stage separation |
CompressionModule |
Multi-stage compression with intercooling |
import neqsim.process.processmodel.processmodules.GlycolDehydrationlModule;
// Initialize the module
GlycolDehydrationlModule tegModule = new GlycolDehydrationlModule("TEG Plant");
tegModule.addInputStream("GasFeed", separator.getGasOutStream());
tegModule.addInputStream("TEGFeed", tegFeedStream);
// Configure module parameters
tegModule.setSpecification("water content", 50.0); // ppm target
// Add to process system
process.add(tegModule);
process.run();
Extend ProcessModuleBaseClass to create reusable process blocks:
public class MyCustomModule extends ProcessModuleBaseClass {
public MyCustomModule(String name) {
super(name);
}
@Override
public void initializeModule() {
// Define internal units and connections
Separator sep = new Separator("Internal Sep", getInputStream("feed"));
Compressor comp = new Compressor("Internal Comp", sep.getGasOutStream());
getOperations().add(sep);
getOperations().add(comp);
// Set output streams
setOutputStream("gas", comp.getOutletStream());
setOutputStream("liquid", sep.getLiquidOutStream());
}
}
Handle closed loops with the Recycle class:
Recycle recycle = new Recycle("Recycle Controller");
recycle.addStream(separator.getLiquidOutStream());
recycle.setTolerance(1e-6);
recycle.setMaximumIterations(50);
process.add(recycle);
See Advanced Process Simulation for complete examples.
Automate equipment operation with PID controllers:
ControllerDeviceBaseClass flowController = new ControllerDeviceBaseClass();
flowController.setTransmitter(flowTransmitter);
flowController.setControllerSetPoint(50.0);
flowController.setControllerParameters(0.5, 100.0, 0.0); // Kp, Ti, Td
valve.setController(flowController);
Adjust a variable to meet a target specification:
Adjuster adj = new Adjuster("Pressure Adjuster");
adj.setAdjustedVariable(valve, "opening");
adj.setTargetVariable(separator, "pressure", 30.0, "bara");
process.add(adj);
Run time-stepping simulations for transient analysis:
process.setTimeStep(0.1); // seconds
for (int i = 0; i < 1000; i++) {
process.runTransient();
// Log or record results
}
Use lambda expressions for flexible calculations:
// Calculator with lambda
Calculator calc = new Calculator("Energy Balance");
calc.addInputVariable(inlet);
calc.setOutputVariable(outlet);
calc.setCalculationMethod((inputs, output) -> {
double totalEnthalpy = inputs.stream()
.mapToDouble(e -> ((Stream)e).getThermoSystem().getEnthalpy())
.sum();
// Apply to output...
});
// Adjuster with lambda
adjuster.setTargetValueCalculator(equipment -> {
return ((Stream) equipment).getFlowRate("kg/hr") * 0.1;
});
See Logical Unit Operations for complete functional interface documentation.
| Topic | Documentation |
|---|---|
| Advanced Topics | Advanced Process Simulation |
| Control Logic | Logical Unit Operations |
| Equipment Details | Process Equipment |
| Dynamic Simulation | Process Transient Guide |
| Safety Systems | Integrated Safety Systems |
| Bottleneck Analysis | Bottleneck Analysis |
| Examples | Usage Examples |
For equipment-specific documentation, see the equipment documentation.
This guide covers advanced features of NeqSim's process simulation capabilities, including execution optimization, recycles, control systems, and dynamic simulation.
NeqSim provides multiple execution strategies to optimize simulation performance. The recommended approach is to use runOptimized() which automatically analyzes your process and selects the best strategy.
| Method | Best For | Typical Speedup |
|---|---|---|
run() |
Simple processes | baseline |
runOptimized() |
All processes (recommended) | 28-40% |
runParallel() |
Feed-forward (no recycles) | 40-57% |
runHybrid() |
Complex recycle processes | 38% |
// Recommended - auto-selects best strategy
process.runOptimized();
The method analyzes your process topology and selects the appropriate strategy:
runParallel() for maximum speedrunHybrid() which:
// Check if process has recycles
boolean hasRecycles = process.hasRecycleLoops();
// Get detailed execution partition analysis
System.out.println(process.getExecutionPartitionInfo());
Example output:
=== Execution Partition Analysis ===
Total units: 40
Has recycle loops: true
Parallel levels: 29
Max parallelism: 6
Units in recycle loops: 30
=== Hybrid Execution Strategy ===
Phase 1 (Parallel): 4 levels, 8 units
Phase 2 (Iterative): 25 levels, 32 units
Execution levels:
Level 0 [PARALLEL]: feed TP setter, first stage oil reflux
Level 1 [PARALLEL]: 1st stage separator
--- Recycle Section Start (iterative) ---
Level 4: oil heater second stage [RECYCLE]
...
For complex processes, graph-based execution optimizes unit ordering:
// Enable graph-based execution
process.setUseGraphBasedExecution(true);
process.run();
// Or use runOptimized() which handles this automatically
process.runOptimized();
Run simulations in background threads:
// Run in background
Future<?> task = process.runAsTask();
// Do other work...
// Wait for completion
task.get();
In process simulation, a recycle loop occurs when a downstream stream is fed back to an upstream unit. NeqSim handles this using the Recycle class, which iterates until the properties of the recycled stream converge.
The Recycle unit operation compares the properties (flow rate, composition, temperature, pressure) of the stream from the previous iteration with the current iteration. If the difference is within a specified tolerance, the loop is considered converged.
import neqsim.process.equipment.util.Recycle;
import neqsim.process.equipment.mixer.StaticMixer;
// ... other imports
// 1. Create Feed and Recycle Streams
Stream feed = new Stream("Feed", fluid);
Stream recycleStream = new Stream("Recycle Stream", fluid.clone()); // Initial guess
// 2. Mix Feed and Recycle
StaticMixer mixer = new StaticMixer("Mixer");
mixer.addStream(feed);
mixer.addStream(recycleStream);
// ... Process units (e.g., Compressor, Cooler, Separator) ...
Separator separator = new Separator("Separator", cooler.getOutletStream());
// 3. Define the Recycle Unit
// The input to the Recycle unit is the stream you want to recycle (e.g., liquid from separator)
Recycle recycle = new Recycle("Recycle Controller");
recycle.addStream(separator.getLiquidOutStream());
// 4. Connect Recycle Output back to Mixer
// IMPORTANT: The output of the Recycle unit is what you connect to the upstream mixer
recycleStream = recycle.getOutletStream();
mixer.replaceStream(1, recycleStream); // Or set it up initially if possible
// 5. Add to ProcessSystem
process.add(feed);
process.add(mixer);
// ... add other units ...
process.add(recycle); // Add recycle last or where appropriate in sequence
// 6. Run
process.run();
You can adjust the tolerance and maximum iterations:
recycle.setTolerance(1e-6);
recycle.setMaximumIterations(50);
NeqSim allows you to add PID controllers to automate the operation of equipment, such as valves or compressors, to maintain a specific setpoint (e.g., pressure, flow, level).
import neqsim.process.controllerdevice.ControllerDeviceBaseClass;
import neqsim.process.measurementdevice.VolumeFlowTransmitter;
import neqsim.process.equipment.valve.ThrottlingValve;
// 1. Create the Stream and Valve
Stream stream = new Stream("Stream", fluid);
ThrottlingValve valve = new ThrottlingValve("Control Valve", stream);
// 2. Create a Transmitter
// Measures flow rate of the stream
VolumeFlowTransmitter flowTransmitter = new VolumeFlowTransmitter(stream);
flowTransmitter.setUnit("kg/hr");
flowTransmitter.setMaximumValue(100.0);
flowTransmitter.setMinimumValue(0.0);
// 3. Create the Controller
ControllerDeviceBaseClass flowController = new ControllerDeviceBaseClass();
flowController.setTransmitter(flowTransmitter);
flowController.setReverseActing(true); // Action depends on process physics
flowController.setControllerSetPoint(50.0); // Target Flow
flowController.setControllerParameters(0.5, 100.0, 0.0); // Kp, Ti, Td
// 4. Assign Controller to Valve
valve.setController(flowController);
// 5. Add to ProcessSystem
process.add(stream);
process.add(valve);
process.add(flowTransmitter); // Transmitter must be added to system
// 6. Run
process.run();
NeqSim supports dynamic (transient) simulation, allowing you to model how the process changes over time. This is useful for studying startup/shutdown, control system tuning, and buffer tank sizing.
separator.setCalculateSteadyState(false)).// ... Setup system with valves, separators, controllers ...
// Configure unit for dynamics
separator.setCalculateSteadyState(false); // Enable dynamic level calculation
separator.setInternalDiameter(2.0);
separator.setSeparatorLength(5.0);
// Run steady state first to get initial condition
process.run();
// Dynamic Loop
double timeStep = 10.0; // seconds
process.setTimeStep(timeStep);
for (int i = 0; i < 100; i++) {
process.runTransient();
// Log results
double time = i * timeStep;
double level = separator.getLiquidLevel();
double pressure = separator.getGasOutStream().getPressure();
System.out.println("Time: " + time + " s, Level: " + level + ", Pressure: " + pressure);
}
process.setTimeStep(double seconds): Sets the integration step.process.runTransient(): Advances the simulation by one time step.unit.setCalculateSteadyState(boolean): Toggles between steady-state (mass balance) and dynamic (accumulation) modes for specific equipment.For large simulations, it is often better to split the plant into smaller, manageable ProcessSystem objects (e.g., "Inlet Separation", "Gas Compression", "Oil Stabilization") and then combine them into a single ProcessModel.
ProcessModel manages the execution of sub-systems.runOptimized() for best performance.import neqsim.process.processmodel.ProcessModel;
import neqsim.process.processmodel.ProcessSystem;
// 1. Create Individual Process Systems
ProcessSystem inletSystem = new ProcessSystem();
inletSystem.setName("Inlet Section");
// ... add units to inletSystem ...
ProcessSystem compressionSystem = new ProcessSystem();
compressionSystem.setName("Compression Section");
// ... add units to compressionSystem ...
// 2. Connect Systems
// Typically, a stream from the first system is used as input to the second
Stream gasFromInlet = (Stream) inletSystem.getUnit("Inlet Separator").getGasOutStream();
Compressor compressor = new Compressor("1st Stage Compressor", gasFromInlet);
compressionSystem.add(compressor);
// 3. Create ProcessModel
ProcessModel plantModel = new ProcessModel();
plantModel.add("Inlet", inletSystem);
plantModel.add("Compression", compressionSystem);
// 4. Run the Full Model (uses optimized execution by default)
plantModel.run();
// Or disable optimized execution if needed
plantModel.setUseOptimizedExecution(false);
plantModel.run();
The ProcessModel will execute the added systems in the order they were added. By default, each ProcessSystem uses runOptimized() which auto-selects the best execution strategy (parallel for feed-forward, hybrid for recycle processes).
// Get execution analysis for all ProcessSystems
System.out.println(plantModel.getExecutionPartitionInfo());
Example output:
=== ProcessModel Execution Analysis ===
Total ProcessSystems: 2
Optimized execution: enabled
--- ProcessSystem: Inlet ---
Units: 15
Has recycles: true
Strategy: Hybrid (parallel + iterative)
--- ProcessSystem: Compression ---
Units: 8
Has recycles: false
Strategy: Parallel
To reduce boilerplate when assembling larger flowsheets, reuse the Recycle, controller, and ProcessModel patterns as pre-made building blocks. The following templates can be copied as-is or combined inside a ProcessModel catalog to let automated agents stitch together flowsheets without rewiring every unit manually.
Building blocks: feed stream → choke valve (optional) → inlet cooler → three-phase separator → level/pressure controllers → recycle loop.
Why this helps: captures the standard inlet handling motif (cooling, phase split, level trim) while providing a ready-made recycle loop for gas reprocessing or compressor suction stabilization.
// Streams
Stream feed = new Stream("Feed", feedFluid);
Stream recycleStream = new Stream("Recycle Seed", feedFluid.clone());
// Front-end conditioning
ThrottlingValve choke = new ThrottlingValve("Choke", feed);
Cooler inletCooler = new Cooler("Inlet Cooler", choke.getOutletStream());
Separator inletSep = new Separator("Inlet Separator", inletCooler.getOutletStream());
// Controllers
LevelTransmitter levelTI = new LevelTransmitter(inletSep);
ControllerDeviceBaseClass levelController = new ControllerDeviceBaseClass();
levelController.setTransmitter(levelTI);
levelController.setControllerSetPoint(0.6); // 60% level
levelController.setReverseActing(true);
ThrottlingValve levelValve = new ThrottlingValve("Liquid LV", inletSep.getLiquidOutStream());
levelValve.setController(levelController);
PressureTransmitter pTI = new PressureTransmitter(inletSep);
ControllerDeviceBaseClass pressureController = new ControllerDeviceBaseClass();
pressureController.setTransmitter(pTI);
pressureController.setControllerSetPoint(50.0); // bara
pressureController.setReverseActing(false);
ThrottlingValve pressureValve = new ThrottlingValve("Gas PCV", inletSep.getGasOutStream());
pressureValve.setController(pressureController);
// Recycle
Recycle recycle = new Recycle("Separator Gas Recycle");
recycle.addStream(pressureValve.getOutletStream());
recycle.setTolerance(1e-6);
recycle.setMaximumIterations(50);
// Stitch recycle back to the front-end mixer (or directly to choke/cooler)
StaticMixer frontMixer = new StaticMixer("Front Mixer");
frontMixer.addStream(feed);
frontMixer.addStream(recycle.getOutletStream());
choke.setOutletStream(frontMixer.getOutStream());
// Register in a ProcessSystem
ProcessSystem inletSystem = new ProcessSystem();
inletSystem.add(feed, recycleStream, choke, inletCooler, inletSep, levelTI, levelValve, pTI, pressureValve, recycle, frontMixer);
Composition hints:
inletSep.getGasOutStream() and inletSep.getOilOutStream() as outputs so other templates (e.g., gas compression or stabilizer) can consume them.ProcessModel, add this system first so downstream templates can reference the separator gas as an upstream dependency.Building blocks: gas feed → stage 1 compressor → interstage cooler + separator (optional) → stage 2 compressor → aftercooler → pressure controller or recycle.
Why this helps: standardizes multi-stage compression including thermal conditioning between stages and hooks for surge/recycle control.
// Assume gasFeed is provided by an upstream template (e.g., inlet separator train)
Compressor comp1 = new Compressor("1st Stage", gasFeed);
Cooler intercooler = new Cooler("Interstage Cooler", comp1.getOutletStream());
Separator interSep = new Separator("Interstage Separator", intercooler.getOutletStream());
Compressor comp2 = new Compressor("2nd Stage", interSep.getGasOutStream());
Cooler afterCooler = new Cooler("Aftercooler", comp2.getOutletStream());
// Discharge pressure control via recycle
PressureTransmitter dischargePT = new PressureTransmitter(afterCooler);
ControllerDeviceBaseClass dischargePC = new ControllerDeviceBaseClass();
dischargePC.setTransmitter(dischargePT);
dischargePC.setControllerSetPoint(100.0); // bara
dischargePC.setReverseActing(false);
ThrottlingValve recycleValve = new ThrottlingValve("Discharge Recycle Valve", afterCooler.getOutletStream());
recycleValve.setController(dischargePC);
Recycle dischargeRecycle = new Recycle("Compression Recycle");
dischargeRecycle.addStream(recycleValve.getOutletStream());
dischargeRecycle.setTolerance(1e-7);
dischargeRecycle.setMaximumIterations(75);
// Tie recycle back to first-stage suction
StaticMixer suctionMixer = new StaticMixer("Suction Mixer");
suctionMixer.addStream(gasFeed);
suctionMixer.addStream(dischargeRecycle.getOutletStream());
comp1.setInletStream(suctionMixer.getOutStream());
// Organize as a ProcessSystem for catalog reuse
ProcessSystem compressionSystem = new ProcessSystem();
compressionSystem.add(comp1, intercooler, interSep, comp2, afterCooler, dischargePT, recycleValve, dischargeRecycle, suctionMixer);
Composition hints:
gasFeed to inletSep.getGasOutStream() and merge the ProcessSystem instances using ProcessModel.interSep.getOilOutStream()) for condensate handling templates.setControllerParameters) can be kept in a shared catalog so AI agents can swap them without editing structure.NeqSim provides several "logical" unit operations that do not represent physical equipment but are used to control the simulation, transfer data, or perform calculations. These include Calculator, Adjuster, SetPoint, and Recycle.
The Calculator unit operation allows for custom calculations and data manipulation within a process simulation. It is useful for calculating derived properties or implementing simple control logic. Custom lambdas are the preferred hook for AI-generated logic because they let you keep the same simulator graph while swapping in new behavior at runtime.
Calculator calc = new Calculator("name");calc.addInputVariable(inputUnit);calc.setOutputVariable(outputUnit);setCalculationMethod with a lambda expression.Calculator energyCalc = new Calculator("Energy Calc");
energyCalc.addInputVariable(inletStream);
energyCalc.setOutputVariable(outletStream);
energyCalc.setCalculationMethod((inputs, output) -> {
Stream in = (Stream) inputs.get(0);
Stream out = (Stream) output;
double energy = in.LCV() * in.getFlowRate("Sm3/hr");
// Adjust outlet temperature based on energy
out.setTemperature(300.0 + energy / 1e5, "K");
});
For frequently reused logic you can rely on CalculatorLibrary presets instead of hand-written lambdas. This makes it easier to reference calculations declaratively (e.g., from an AI agent or configuration file):
Calculator presetCalc = new Calculator("dew point targeter");
presetCalc.addInputVariable(feedStream);
presetCalc.setOutputVariable(targetStream);
// Apply by enum or by name
presetCalc.setCalculationMethod(CalculatorLibrary.preset(CalculatorLibrary.Preset.DEW_POINT_TARGETING));
// presetCalc.setCalculationMethod(CalculatorLibrary.byName("dewPointTargeting"));
Available presets:
CalculatorLibrary.dewPointTargeting(double marginKelvin)).The Adjuster is used to vary a parameter in one unit operation (the "adjusted variable") to achieve a specific value in another unit operation (the "target variable"). It is essentially a single-variable solver. Use lambdas for the getters/setters to keep the hook flexible for AI-generated control logic.
You can specify standard properties like "pressure", "temperature", "flow", etc.
Adjuster adjuster = new Adjuster("Pressure Adjuster");
adjuster.setAdjustedVariable(inletStream, "flow", "kg/hr");
adjuster.setTargetVariable(outletStream, "pressure", 50.0, "bara");
You can also define a custom function to calculate the target value from the target equipment. This is useful if the variable you want to control is not a standard property.
adjuster.setTargetValueCalculator((equipment) -> {
Stream s = (Stream) equipment;
// Control based on a custom metric, e.g., Flow * Temperature
return s.getFlowRate("kg/hr") * s.getTemperature("K");
});
You can also define custom logic for how to read and write the adjusted variable. This allows you to manipulate parameters that are not standard properties.
// Define how to read the current value of the adjusted variable
adjuster.setAdjustedValueGetter((equipment) -> {
return ((Stream) equipment).getTemperature("K");
});
// Define how to set the new value of the adjusted variable
adjuster.setAdjustedValueSetter((equipment, val) -> {
((Stream) equipment).setTemperature(val, "K");
});
The SetPoint unit operation sets the value of a variable in a target unit operation equal to the value of a variable in a source unit operation. It is used for feed-forward control or copying values between equipment.
SetPoint setPoint = new SetPoint("Pressure Copy");
setPoint.setSourceVariable(sourceStream, "pressure");
setPoint.setTargetVariable(targetStream, "pressure");
| Equipment Type | Supported Variables |
|---|---|
Stream |
pressure, temperature |
ThrottlingValve |
pressure (outlet) |
Compressor |
pressure (outlet) |
Pump |
pressure (outlet) |
Heater/Cooler |
pressure, temperature |
Use setSourceValueCalculator to define a custom function that calculates the value to set on the target equipment. This provides full flexibility for non-linear relationships, unit conversions, or conditional logic.
| Method | Type | Description |
|---|---|---|
setSourceValueCalculator |
Function<ProcessEquipmentInterface, Double> |
Custom function to compute the value to set |
SetPoint setPoint = new SetPoint("Custom SetPoint");
setPoint.setSourceVariable(sourceStream);
setPoint.setTargetVariable(targetStream, "pressure");
// Set target pressure based on source temperature: P = T / 10.0
setPoint.setSourceValueCalculator((equipment) -> {
Stream s = (Stream) equipment;
return s.getTemperature("K") / 10.0;
});
setPoint.run();
// Target pressure is now 30.0 bara (if source temp = 300 K)
setPoint.setSourceValueCalculator((equipment) -> {
Stream s = (Stream) equipment;
// Set target pressure to be 10% of source pressure
return s.getPressure("bara") * 0.1;
});
// Set compressor outlet pressure based on inlet conditions
SetPoint pressureRatio = new SetPoint("Pressure Ratio Control");
pressureRatio.setSourceVariable(compressorInlet);
pressureRatio.setTargetVariable(compressor, "pressure");
pressureRatio.setSourceValueCalculator((equipment) -> {
Stream inlet = (Stream) equipment;
double inletP = inlet.getPressure("bara");
double inletT = inlet.getTemperature("K");
// Higher inlet temperature = lower pressure ratio
double ratio = 4.0 - (inletT - 300.0) * 0.01;
return inletP * Math.max(ratio, 2.0);
});
| Use Case | Example |
|---|---|
| Non-linear relationships | Pressure = f(temperature, flow) |
| Unit conversions | Convert from source units to target units |
| Computed ratios | Set valve to percentage of max flow |
| Conditional logic | Different values based on operating mode |
| Multi-variable calculations | Value depends on multiple stream properties |
The Recycle unit operation is used to close loops in a process simulation. It compares the inlet and outlet streams of the recycle block and iterates until they converge within a specified tolerance.
Recycle recycle = new Recycle("Recycle");
recycle.addStream(recycleStream);
recycle.setTolerance(1e-6);
This guide describes the complete process design workflow using NeqSim, from initial process simulation through mechanical design and final validation. NeqSim provides an integrated framework for:
┌─────────────────────────────────────────────────────────────────────────────────┐
│ PROCESS DESIGN WORKFLOW │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │
│ │ 1. DEFINE │────▶│ 2. PROCESS │────▶│ 3. MECHANICAL│────▶│ 4. VALIDATE│ │
│ │ SYSTEM │ │ SIMULATION │ │ DESIGN │ │ & REPORT │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └────────────┘ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │
│ │ • Fluid │ │ • Run cases │ │ • Apply │ │ • Check │ │
│ │ composition│ │ • Calculate │ │ standards │ │ compliance│ │
│ │ • Equipment │ │ properties │ │ • Size │ │ • Generate │ │
│ │ • Flowsheet │ │ • Heat/mass │ │ equipment │ │ reports │ │
│ │ • TORG │ │ balance │ │ • Materials │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘
Define the thermodynamic system with appropriate equation of state:
import neqsim.thermo.system.SystemSrkEos;
// Create fluid with SRK equation of state
SystemSrkEos fluid = new SystemSrkEos(280.0, 50.0); // T(K), P(bar)
fluid.addComponent("methane", 0.85);
fluid.addComponent("ethane", 0.08);
fluid.addComponent("propane", 0.04);
fluid.addComponent("n-butane", 0.02);
fluid.addComponent("CO2", 0.01);
fluid.setMixingRule("classic");
fluid.createDatabase(true);
Create equipment and connect into a process system:
import neqsim.process.processmodel.ProcessSystem;
import neqsim.process.equipment.stream.Stream;
import neqsim.process.equipment.separator.Separator;
import neqsim.process.equipment.compressor.Compressor;
ProcessSystem process = new ProcessSystem();
// Feed stream
Stream feed = new Stream("Well Feed", fluid);
feed.setFlowRate(50000.0, "kg/hr");
feed.setTemperature(60.0, "C");
feed.setPressure(50.0, "bara");
process.add(feed);
// HP Separator
Separator hpSeparator = new Separator("HP Separator", feed);
process.add(hpSeparator);
// Export Compressor
Compressor exportCompressor = new Compressor("Export Compressor", hpSeparator.getGasOutStream());
exportCompressor.setOutletPressure(150.0, "bara");
process.add(exportCompressor);
Load the Technical Requirements Document governing design standards:
import neqsim.process.mechanicaldesign.torg.TorgManager;
import neqsim.process.mechanicaldesign.torg.CsvTorgDataSource;
TorgManager torgManager = new TorgManager();
torgManager.addDataSource(new CsvTorgDataSource("project_torg.csv"));
Optional<TechnicalRequirementsDocument> optTorg = torgManager.load("TROLL-WEST-2025");
📖 See: TORG Integration for detailed TORG configuration
// Run the process simulation
process.run();
// Access results
double gasRate = hpSeparator.getGasOutStream().getFlowRate("MSm3/day");
double liquidRate = hpSeparator.getLiquidOutStream().getFlowRate("m3/hr");
double compressorPower = exportCompressor.getPower("MW");
System.out.println("Gas rate: " + gasRate + " MSm3/day");
System.out.println("Liquid rate: " + liquidRate + " m3/hr");
System.out.println("Compressor power: " + compressorPower + " MW");
Evaluate different operating scenarios:
import neqsim.process.mechanicaldesign.designstandards.DesignCase;
// Define cases to evaluate
List<DesignCase> designCases = Arrays.asList(
DesignCase.NORMAL,
DesignCase.MAXIMUM,
DesignCase.MINIMUM,
DesignCase.UPSET
);
Map<DesignCase, Double> separatorPressures = new HashMap<>();
for (DesignCase designCase : designCases) {
// Adjust feed based on case
double loadFactor = designCase.getTypicalLoadFactor();
feed.setFlowRate(50000.0 * loadFactor, "kg/hr");
// Run simulation
process.run();
// Store results
separatorPressures.put(designCase, hpSeparator.getPressure("bara"));
}
📖 See: Field Development Orchestration for design case details
Apply appropriate international standards to equipment:
import neqsim.process.mechanicaldesign.designstandards.StandardType;
import neqsim.process.mechanicaldesign.designstandards.StandardRegistry;
// Apply standards to individual equipment
StandardRegistry.applyStandardToEquipment(hpSeparator, StandardType.NORSOK_P002);
StandardRegistry.applyStandardToEquipment(exportCompressor, StandardType.API_617);
// Or apply TORG to entire system (applies all project standards)
if (optTorg.isPresent()) {
torgManager.apply(optTorg.get(), process);
}
📖 See: Mechanical Design Standards for available standards
import neqsim.process.mechanicaldesign.MechanicalDesign;
// Get mechanical design for separator
MechanicalDesign sepDesign = hpSeparator.getMechanicalDesign();
sepDesign.calcDesign();
// Access design results
double designPressure = sepDesign.getDesignPressure();
double designTemperature = sepDesign.getDesignTemperature();
double wallThickness = sepDesign.getWallThickness();
double weight = sepDesign.getWeightTotal();
String materialGrade = sepDesign.getMaterialDesignStandard().getMaterialGrade();
System.out.println("Design Pressure: " + designPressure + " barg");
System.out.println("Design Temperature: " + designTemperature + " °C");
System.out.println("Wall Thickness: " + wallThickness + " mm");
System.out.println("Weight: " + weight + " kg");
System.out.println("Material: " + materialGrade);
// Calculate mechanical design for all equipment
for (ProcessEquipmentInterface equipment : process.getUnitOperations()) {
MechanicalDesign mechDesign = equipment.getMechanicalDesign();
if (mechDesign != null) {
mechDesign.calcDesign();
}
}
📖 See: Mechanical Design Database for data sources
import neqsim.process.mechanicaldesign.designstandards.DesignValidationResult;
DesignValidationResult validation = new DesignValidationResult();
// Check each equipment
for (ProcessEquipmentInterface equipment : process.getUnitOperations()) {
MechanicalDesign design = equipment.getMechanicalDesign();
if (design != null && design.hasDesignStandard()) {
// Validate against TORG requirements
if (optTorg.isPresent()) {
TechnicalRequirementsDocument torg = optTorg.get();
// Check corrosion allowance
double requiredCA = torg.getSafetyFactors().getCorrosionAllowance();
if (design.getCorrosionAllowance() < requiredCA) {
validation.addWarning(equipment.getName() +
": Corrosion allowance below TORG requirement");
}
}
validation.addInfo(equipment.getName() + " design validated");
}
}
// Check results
if (validation.isValid()) {
System.out.println("All equipment meets design requirements");
} else {
System.out.println("Design issues found:");
for (var msg : validation.getMessages()) {
System.out.println(" " + msg.getSeverity() + ": " + msg.getMessage());
}
}
StringBuilder report = new StringBuilder();
report.append("Process Design Report\n");
report.append("=====================\n\n");
// TORG Information
if (optTorg.isPresent()) {
TechnicalRequirementsDocument torg = optTorg.get();
report.append("Project: ").append(torg.getProjectName()).append("\n");
report.append("TORG Revision: ").append(torg.getRevision()).append("\n");
report.append("Design Life: ").append(torg.getDesignLifeYears()).append(" years\n\n");
}
// Equipment Summary
report.append("Equipment Summary\n");
report.append("-----------------\n");
for (ProcessEquipmentInterface equipment : process.getUnitOperations()) {
MechanicalDesign design = equipment.getMechanicalDesign();
if (design != null) {
report.append("\n").append(equipment.getName()).append(":\n");
report.append(" Design Pressure: ").append(design.getDesignPressure()).append(" barg\n");
report.append(" Design Temperature: ").append(design.getDesignTemperature()).append(" °C\n");
report.append(" Weight: ").append(design.getWeightTotal()).append(" kg\n");
}
}
System.out.println(report);
For complex projects, use the FieldDevelopmentDesignOrchestrator to coordinate the entire workflow:
import neqsim.process.mechanicaldesign.designstandards.FieldDevelopmentDesignOrchestrator;
import neqsim.process.mechanicaldesign.designstandards.DesignPhase;
import neqsim.process.mechanicaldesign.designstandards.DesignCase;
// Create orchestrator
FieldDevelopmentDesignOrchestrator orchestrator =
new FieldDevelopmentDesignOrchestrator(process);
// Configure design phase
orchestrator.setDesignPhase(DesignPhase.FEED); // ±15-20% accuracy
// Add design cases
orchestrator.addDesignCase(DesignCase.NORMAL);
orchestrator.addDesignCase(DesignCase.MAXIMUM);
orchestrator.addDesignCase(DesignCase.MINIMUM);
orchestrator.addDesignCase(DesignCase.UPSET);
orchestrator.addDesignCase(DesignCase.EARLY_LIFE);
orchestrator.addDesignCase(DesignCase.LATE_LIFE);
// Load TORG
orchestrator.loadTorg(torgManager, "TROLL-WEST-2025");
// Run complete workflow
orchestrator.runCompleteDesignWorkflow();
// Get results
DesignValidationResult validation = orchestrator.validateDesign();
String report = orchestrator.generateDesignReport();
System.out.println(report);
📖 See: Field Development Orchestration for complete workflow details
Choose the appropriate design phase based on project stage:
| Phase | Use Case | Accuracy | Full Mechanical Design |
|---|---|---|---|
| SCREENING | Early opportunity evaluation | ±40-50% | No |
| CONCEPT_SELECT | Concept comparison | ±30% | No |
| PRE_FEED | Preliminary engineering | ±25% | No |
| FEED | Front-end engineering | ±15-20% | Yes |
| DETAIL_DESIGN | Detailed engineering | ±10% | Yes |
| AS_BUILT | Verification | ±5% | Yes |
DesignPhase phase = DesignPhase.FEED;
// Check phase requirements
if (phase.requiresFullMechanicalDesign()) {
// Run detailed calculations
runFullMechanicalDesign(process);
} else {
// Use simplified estimates
runQuickEstimates(process);
}
NeqSim supports 30+ international standards:
| Category | Standards |
|---|---|
| Pressure Vessels | ASME VIII Div 1/2, PD 5500, EN 13445 |
| Process Design | NORSOK P-001, NORSOK P-002 |
| Piping | ASME B31.3, NORSOK L-001 |
| Pipelines | DNV-OS-F101, API 5L |
| Compressors | API 617, API 618 |
| Heat Exchangers | TEMA, API 660 |
| Materials | ASTM, NACE MR0175 |
| Safety | API 521, ISO 23251 |
📖 See: Mechanical Design Standards for complete list
Design parameters can be loaded from:
MechanicalDesignDataSource// CSV data source
StandardBasedCsvDataSource csvSource =
new StandardBasedCsvDataSource(StandardType.NORSOK_P002, "norsok_p002.csv");
// Register with registry
StandardRegistry.registerDataSource(StandardType.NORSOK_P002, csvSource);
📖 See: Mechanical Design Database for data configuration
import neqsim.thermo.system.SystemSrkEos;
import neqsim.process.processmodel.ProcessSystem;
import neqsim.process.equipment.stream.Stream;
import neqsim.process.equipment.separator.Separator;
import neqsim.process.equipment.compressor.Compressor;
import neqsim.process.mechanicaldesign.designstandards.*;
import neqsim.process.mechanicaldesign.torg.*;
public class ProcessDesignExample {
public static void main(String[] args) {
// ===== STEP 1: Define System =====
// Create fluid
SystemSrkEos fluid = new SystemSrkEos(280.0, 50.0);
fluid.addComponent("methane", 0.85);
fluid.addComponent("ethane", 0.08);
fluid.addComponent("propane", 0.04);
fluid.addComponent("n-butane", 0.03);
fluid.setMixingRule("classic");
// Build flowsheet
ProcessSystem process = new ProcessSystem();
Stream feed = new Stream("Well Feed", fluid);
feed.setFlowRate(50000.0, "kg/hr");
process.add(feed);
Separator hpSep = new Separator("HP Separator", feed);
process.add(hpSep);
Compressor compressor = new Compressor("Export Compressor", hpSep.getGasOutStream());
compressor.setOutletPressure(150.0, "bara");
process.add(compressor);
// ===== STEP 2: Configure Orchestrator =====
FieldDevelopmentDesignOrchestrator orchestrator =
new FieldDevelopmentDesignOrchestrator(process);
orchestrator.setDesignPhase(DesignPhase.FEED);
orchestrator.addDesignCase(DesignCase.NORMAL);
orchestrator.addDesignCase(DesignCase.MAXIMUM);
orchestrator.addDesignCase(DesignCase.UPSET);
// Load TORG
TorgManager torgManager = new TorgManager();
torgManager.addDataSource(new CsvTorgDataSource("project_torg.csv"));
orchestrator.loadTorg(torgManager, "PROJECT-001");
// ===== STEP 3: Run Workflow =====
orchestrator.runCompleteDesignWorkflow();
// ===== STEP 4: Validate and Report =====
DesignValidationResult validation = orchestrator.validateDesign();
if (validation.isValid()) {
System.out.println("Design PASSED");
System.out.println(orchestrator.generateDesignReport());
} else {
System.out.println("Design FAILED");
for (var msg : validation.getMessagesBySeverity(
DesignValidationResult.Severity.ERROR)) {
System.err.println("ERROR: " + msg.getMessage());
}
}
}
}
| Document | Description |
|---|---|
| Mechanical Design Standards | StandardType enum, StandardRegistry, applying standards |
| Mechanical Design Database | Data sources, schemas, CSV configuration |
| TORG Integration | Technical requirements documents |
| Field Development Orchestration | Design phases, cases, orchestrator |
| Class | Purpose |
|---|---|
ProcessSystem |
Container for process flowsheet |
MechanicalDesign |
Base class for equipment mechanical design |
StandardType |
Enum of supported design standards |
StandardRegistry |
Factory for creating and applying standards |
TechnicalRequirementsDocument |
TORG representation |
TorgManager |
Loads and applies TORG |
FieldDevelopmentDesignOrchestrator |
Workflow coordinator |
DesignPhase |
Project lifecycle phases |
DesignCase |
Operating scenarios |
DesignValidationResult |
Validation messages and results |
neqsim.process.processmodel - ProcessSystem
neqsim.process.equipment - All equipment types
neqsim.process.mechanicaldesign - MechanicalDesign base
neqsim.process.mechanicaldesign.designstandards - Standards framework
neqsim.process.mechanicaldesign.torg - TORG framework
neqsim.process.mechanicaldesign.data - Data sources
This folder contains documentation for process system and flowsheet management in NeqSim.
| Document | Description |
|---|---|
| ProcessSystem | Main process system class and execution strategies |
| ProcessModel | Multi-process coordination and management |
| ProcessModule | Modular process units |
| Graph-Based Simulation | Graph-based execution and optimization |
| PFD Diagram Export | Professional process flow diagram generation |
| Architecture & DEXPI | Diagram architecture and DEXPI integration |
| Process Serialization | Saving and loading process models |
The processmodel package provides the framework for building and executing process simulations:
NeqSim provides multiple execution strategies optimized for different process types:
| Method | Best For | Description |
|---|---|---|
run() |
General use | Sequential execution (or optimized if enabled) |
runOptimized() |
Recommended | Auto-selects best strategy based on topology |
runParallel() |
Feed-forward processes | Maximum parallelism for no-recycle processes |
runHybrid() |
Complex processes | Parallel feed-forward + iterative recycle |
For best performance, enable optimized execution so run() automatically uses the best strategy:
process.setUseOptimizedExecution(true);
process.run(); // Now uses runOptimized() internally
Typical performance improvements:
import neqsim.process.processmodel.ProcessSystem;
import neqsim.process.equipment.stream.Stream;
import neqsim.process.equipment.separator.Separator;
// Create process system
ProcessSystem process = new ProcessSystem();
// Add equipment
process.add(feedStream);
process.add(separator);
process.add(compressor);
// Run simulation (recommended - auto-optimized)
process.runOptimized();
// Get results
process.display();
// Check if process has recycles
boolean hasRecycles = process.hasRecycleLoops();
// Get execution partition analysis
System.out.println(process.getExecutionPartitionInfo());
ProcessSystem and ProcessModel support saving to compressed .neqsim files and JSON state files:
// Save process
process.saveToNeqsim("my_process.neqsim");
// Load process
ProcessSystem loaded = ProcessSystem.loadFromNeqsim("my_process.neqsim");
// Save multi-process model
model.saveToNeqsim("field_model.neqsim");
ProcessModel loaded = ProcessModel.loadFromNeqsim("field_model.neqsim");
For full documentation, see Process Serialization Guide.
Documentation for the ProcessSystem class in NeqSim.
Location: neqsim.process.processmodel.ProcessSystem
The ProcessSystem class is the main container for building and running process flowsheets. It:
import neqsim.process.processmodel.ProcessSystem;
// Create empty process system
ProcessSystem process = new ProcessSystem();
// Create with name
ProcessSystem process = new ProcessSystem("Gas Processing Plant");
// Add equipment in sequence
process.add(feedStream);
process.add(heater);
process.add(separator);
process.add(compressor);
Equipment is typically added in flow order, but the ProcessSystem handles dependencies automatically:
// ProcessSystem resolves dependencies
process.add(stream); // First
process.add(heater); // Uses stream as input
process.add(separator); // Uses heater output
process.add(compressor); // Uses separator gas output
All equipment must have unique names:
Stream stream1 = new Stream("Feed", fluid1);
Stream stream2 = new Stream("Feed", fluid2); // ERROR: Duplicate name!
// Use unique names
Stream stream1 = new Stream("Feed-1", fluid1);
Stream stream2 = new Stream("Feed-2", fluid2);
| Method | Best For | Description |
|---|---|---|
run() |
General use | Sequential execution in insertion order |
runOptimized() |
Recommended | Auto-selects best strategy based on topology |
runParallel() |
Feed-forward processes | Maximum parallelism for no-recycle processes |
runHybrid() |
Complex processes | Parallel feed-forward + iterative recycle |
The runOptimized() method automatically analyzes your process and selects the best execution strategy:
// Recommended - auto-selects best strategy
process.runOptimized();
// With calculation ID for tracking
UUID calcId = UUID.randomUUID();
process.runOptimized(calcId);
How it works:
runParallel() for maximum speedrunHybrid() which runs feed-forward sections in parallel, then iterates on recycle sectionsPerformance gains (typical separation train with 40 units, 3 recycles):
| Mode | Time | Speedup |
|---|---|---|
Regular run() |
464 ms | baseline |
| Graph-based | 336 ms | 28% |
runOptimized() |
286 ms | 38% |
// Basic sequential execution
process.run();
// Run with calculation ID for tracking
UUID calcId = UUID.randomUUID();
process.run(calcId);
For feed-forward processes (no recycles), parallel execution runs independent units simultaneously:
// Run independent units in parallel
try {
process.runParallel();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
Note: runParallel() does not handle recycles or adjusters. Use runOptimized() for processes with recycles.
When multiple units share the same input stream (e.g., a splitter/manifold feeding parallel branches), NeqSim automatically groups them to prevent race conditions.
How it works:
Example - Parallel Pipelines:
// Three pipelines fed by the same manifold
Stream feedStream = new Stream("feed", fluid);
Splitter manifold = new Splitter("manifold", feedStream, 3);
AdiabaticPipe pipe1 = new AdiabaticPipe("pipe1", manifold.getSplitStream(0));
AdiabaticPipe pipe2 = new AdiabaticPipe("pipe2", manifold.getSplitStream(1));
AdiabaticPipe pipe3 = new AdiabaticPipe("pipe3", manifold.getSplitStream(2));
process.add(feedStream);
process.add(manifold);
process.add(pipe1);
process.add(pipe2);
process.add(pipe3);
// Safe: Each pipe has its own split stream (different objects)
// Pipes run in parallel without race conditions
process.runParallel();
Example - Shared Input Stream (handled automatically):
// Two units explicitly sharing the same input stream object
Stream sharedInput = valve.getOutletStream();
Heater heater1 = new Heater("heater1", sharedInput); // Same stream object
Heater heater2 = new Heater("heater2", sharedInput); // Same stream object
// NeqSim detects shared input and runs heater1 and heater2 sequentially
// Other independent units at this level still run in parallel
process.runParallel();
Applies to:
runParallel() - Groups by shared input streamsrunHybrid() - Groups in Phase 1 (feed-forward section)runTransient() - Groups at each time stepFor processes with recycles, hybrid execution combines parallel and iterative strategies:
// Hybrid: parallel feed-forward + iterative recycle
try {
process.runHybrid();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
How hybrid works:
Enable graph-based execution for optimized unit ordering:
// Enable graph-based execution order
process.setUseGraphBasedExecution(true);
process.run();
// Or use runOptimized() which handles this automatically
process.runOptimized();
Transient simulations use graph-based parallel execution for independent branches, applying the same shared-stream grouping as steady-state methods.
// Set time step
double dt = 1.0; // seconds
// Run single transient step
process.runTransient(dt);
// Run for specified duration
double totalTime = 3600.0; // 1 hour
for (double t = 0; t < totalTime; t += dt) {
process.runTransient(dt);
logResults(t);
}
// Run transient with event handling
process.runTransient(dt, (time) -> {
if (time > 600.0) {
// Open blowdown valve after 10 minutes
bdv.setOpen(true);
}
});
// Check if process has recycles
boolean hasRecycles = process.hasRecycleLoops();
// Check if parallel execution would be beneficial
boolean useParallel = process.isParallelExecutionBeneficial();
// Get detailed partition analysis
String partitionInfo = process.getExecutionPartitionInfo();
System.out.println(partitionInfo);
Example output:
=== Execution Partition Analysis ===
Total units: 40
Has recycle loops: true
Parallel levels: 29
Max parallelism: 6
Units in recycle loops: 30
=== Hybrid Execution Strategy ===
Phase 1 (Parallel): 4 levels, 8 units
Phase 2 (Iterative): 25 levels, 32 units
Execution levels:
Level 0 [PARALLEL]: feed TP setter, first stage oil reflux, LP stream temp controller
Level 1 [PARALLEL]: 1st stage separator
Level 2 [PARALLEL]: oil depres valve
Level 3 [PARALLEL]:
--- Recycle Section Start (iterative) ---
Level 4: oil heater second stage [RECYCLE]
Level 5: 2nd stage separator [RECYCLE]
...
// Get parallel partition
ProcessGraph.ParallelPartition partition = process.getParallelPartition();
// Number of execution levels
int levels = partition.getLevelCount();
// Maximum units that can run simultaneously
int maxParallelism = partition.getMaxParallelism();
System.out.println("Execution levels: " + levels);
System.out.println("Max parallelism: " + maxParallelism);
// Get specific equipment
Compressor comp = (Compressor) process.getUnit("K-100");
Separator sep = (Separator) process.getUnit("HP Separator");
Stream stream = (Stream) process.getUnit("Feed");
// Get all compressors
List<CompressorInterface> compressors = process.getUnitsOfType(CompressorInterface.class);
// Get all separators
List<SeparatorInterface> separators = process.getUnitsOfType(SeparatorInterface.class);
// Get all equipment
List<ProcessEquipmentInterface> allUnits = process.getUnitOperations();
for (ProcessEquipmentInterface unit : allUnits) {
System.out.println(unit.getName() + ": " + unit.getClass().getSimpleName());
}
// Display summary to console
process.display();
// Get JSON report
String jsonReport = process.getReport_json();
// Save to file
Files.writeString(Path.of("process_report.json"), jsonReport);
// Get as table
String[][] table = process.getUnitOperationsAsTable();
// Print table
for (String[] row : table) {
System.out.println(String.join("\t", row));
}
// Check overall mass balance
double totalIn = 0.0;
double totalOut = 0.0;
for (ProcessEquipmentInterface unit : process.getUnitOperations()) {
if (unit instanceof StreamInterface) {
StreamInterface stream = (StreamInterface) unit;
if (isInletStream(stream)) {
totalIn += stream.getFlowRate("kg/hr");
} else if (isOutletStream(stream)) {
totalOut += stream.getFlowRate("kg/hr");
}
}
}
double balance = (totalIn - totalOut) / totalIn * 100;
System.out.println("Mass balance closure: " + balance + "%");
// Create copy of process
ProcessSystem processCopy = process.copy();
// Modify copy without affecting original
Heater heater = (Heater) processCopy.getUnit("Heater");
heater.setOutTemperature(100.0, "C");
processCopy.run();
All equipment and streams are deep-copied:
// Original
process.run();
double originalT = ((Stream) process.getUnit("Feed")).getTemperature("C");
// Copy and modify
ProcessSystem copy = process.copy();
((Stream) copy.getUnit("Feed")).setTemperature(50.0, "C");
copy.run();
// Original unchanged
assert originalT == ((Stream) process.getUnit("Feed")).getTemperature("C");
// Use optimized execution (recommended)
process.runOptimized(); // Auto-selects best strategy
// Or manually choose strategy:
// 1. Sequential (default)
process.run();
// 2. Graph-based ordering
process.setUseGraphBasedExecution(true);
process.run();
// 3. Parallel execution (no recycles)
process.runParallel();
// 4. Hybrid execution (recycle processes)
process.runHybrid();
// Run in background thread
Future<?> task = process.runAsTask();
// Do other work...
// Wait for completion
task.get();
// Or check if done
if (task.isDone()) {
System.out.println("Simulation complete");
}
// Set global convergence tolerance
process.setGlobalTolerance(1e-6);
// Set maximum iterations for recycles
process.setMaxRecycleIterations(50);
// Add pre-built module
ProcessModule compressorTrain = new CompressorTrainModule("HP Compression");
process.addModule(compressorTrain);
// Connect to process
compressorTrain.setInletStream(feedGas);
Stream compressed = compressorTrain.getOutletStream();
ProcessSystem provides comprehensive validation to check that all equipment is properly configured before running a simulation. This helps catch configuration errors early and provides actionable error messages.
The simplest way to validate a process before execution:
ProcessSystem process = new ProcessSystem();
process.add(feed);
process.add(separator);
process.add(compressor);
// Quick check - returns true if no CRITICAL errors
if (process.isReadyToRun()) {
process.run();
} else {
System.out.println("Process not ready to run");
ValidationResult result = process.validateSetup();
result.getErrors().forEach(System.out::println);
}
Get a combined ValidationResult for the entire process system:
ValidationResult result = process.validateSetup();
if (!result.isValid()) {
System.out.println("Validation issues found:");
System.out.println(result.getReport());
// Iterate through specific issues
for (ValidationIssue issue : result.getIssues()) {
System.out.println(issue.getSeverity() + ": " + issue.getMessage());
System.out.println(" Fix: " + issue.getRemediation());
}
}
Severity Levels:
| Level | Description |
|---|---|
CRITICAL |
Blocks execution - must be fixed |
MAJOR |
Likely to cause errors during simulation |
MINOR |
May affect accuracy of results |
INFO |
Informational warnings |
Get individual validation results for each piece of equipment:
Map<String, ValidationResult> allResults = process.validateAll();
for (Map.Entry<String, ValidationResult> entry : allResults.entrySet()) {
String equipmentName = entry.getKey();
ValidationResult equipResult = entry.getValue();
if (!equipResult.isValid()) {
System.out.println(equipmentName + " has issues:");
equipResult.getErrors().forEach(e -> System.out.println(" - " + e));
}
}
Each equipment class implements validateSetup() to check equipment-specific requirements:
| Equipment | Validates |
|---|---|
| Stream | Has fluid set, temperature > 0 K |
| Separator | Inlet stream connected |
| Mixer | At least one inlet stream |
| Splitter | Inlet stream connected, split fractions sum to 1.0 |
| Tank | Has fluid or input stream |
| DistillationColumn | Feed streams connected, condenser/reboiler configured |
| Recycle | Inlet and outlet streams connected, tolerance > 0 |
| Adjuster | Target and adjustment variables set, tolerance > 0 |
| TwoPortEquipment | Inlet stream connected |
Example - Individual Equipment Validation:
Separator separator = new Separator("V-100");
// Forgot to set inlet stream
ValidationResult result = separator.validateSetup();
if (!result.isValid()) {
// Will report: "Separator 'V-100' has no inlet stream connected"
System.out.println(result.getReport());
}
For AI agents and automated workflows, validation provides structured feedback:
AIIntegrationHelper helper = AIIntegrationHelper.forProcess(process);
if (helper.isReady()) {
ExecutionResult result = helper.safeRun();
} else {
// Get issues as structured text for AI to parse
String[] issues = helper.getIssuesAsText();
for (String issue : issues) {
// AI can parse and fix these issues
System.out.println(issue);
}
}
See AI Validation Framework for more details on AI integration.
ProcessSystem process = new ProcessSystem("Separator System");
// Create fluid
SystemInterface fluid = new SystemSrkEos(300.0, 50.0);
fluid.addComponent("methane", 0.85);
fluid.addComponent("ethane", 0.08);
fluid.addComponent("propane", 0.04);
fluid.addComponent("n-butane", 0.03);
fluid.setMixingRule("classic");
// Feed stream
Stream feed = new Stream("Feed", fluid);
feed.setFlowRate(100000.0, "kg/hr");
process.add(feed);
// Inlet valve
ThrottlingValve inletValve = new ThrottlingValve("Inlet Valve", feed);
inletValve.setOutletPressure(30.0, "bara");
process.add(inletValve);
// HP Separator
Separator hpSep = new Separator("HP Separator", inletValve.getOutletStream());
process.add(hpSep);
// LP Valve
ThrottlingValve lpValve = new ThrottlingValve("LP Valve", hpSep.getLiquidOutStream());
lpValve.setOutletPressure(5.0, "bara");
process.add(lpValve);
// LP Separator
Separator lpSep = new Separator("LP Separator", lpValve.getOutletStream());
process.add(lpSep);
// Run
process.run();
// Results
System.out.println("HP Gas: " + hpSep.getGasOutStream().getFlowRate("MSm3/day") + " MSm3/day");
System.out.println("LP Gas: " + lpSep.getGasOutStream().getFlowRate("MSm3/day") + " MSm3/day");
System.out.println("Liquid: " + lpSep.getLiquidOutStream().getFlowRate("m3/hr") + " m3/hr");
ProcessSystem process = new ProcessSystem("Compression System");
// Gas feed
Stream gas = new Stream("Gas Feed", gasFluid);
gas.setFlowRate(50000.0, "Sm3/hr");
gas.setTemperature(40.0, "C");
gas.setPressure(5.0, "bara");
process.add(gas);
// First stage compressor
Compressor comp1 = new Compressor("K-101", gas);
comp1.setOutletPressure(15.0, "bara");
comp1.setPolytropicEfficiency(0.78);
process.add(comp1);
// Intercooler
Cooler cooler1 = new Cooler("E-101", comp1.getOutletStream());
cooler1.setOutTemperature(40.0, "C");
process.add(cooler1);
// Second stage compressor
Compressor comp2 = new Compressor("K-102", cooler1.getOutletStream());
comp2.setOutletPressure(45.0, "bara");
comp2.setPolytropicEfficiency(0.78);
process.add(comp2);
// Aftercooler
Cooler cooler2 = new Cooler("E-102", comp2.getOutletStream());
cooler2.setOutTemperature(40.0, "C");
process.add(cooler2);
// Run
process.run();
// Total power
double totalPower = comp1.getPower("kW") + comp2.getPower("kW");
System.out.println("Total compression power: " + totalPower + " kW");
ProcessSystem process = new ProcessSystem("Recycle Process");
// Fresh feed
Stream freshFeed = new Stream("Fresh Feed", freshFluid);
freshFeed.setFlowRate(1000.0, "kg/hr");
process.add(freshFeed);
// Mixer for fresh feed and recycle
Mixer feedMixer = new Mixer("Feed Mixer");
feedMixer.addStream(freshFeed);
process.add(feedMixer);
// Reactor
GibbsReactor reactor = new GibbsReactor("Reactor");
reactor.setInletStream(feedMixer.getOutletStream());
process.add(reactor);
// Product separator
Separator productSep = new Separator("Product Sep", reactor.getOutletStream());
process.add(productSep);
// Product stream
Stream product = productSep.getLiquidOutStream();
// Recycle unreacted gas
Recycle recycle = new Recycle("Gas Recycle");
recycle.addStream(productSep.getGasOutStream());
recycle.setOutletStream(feedMixer);
recycle.setTolerance(1e-5);
process.add(recycle);
// Complete the connection
feedMixer.addStream(recycle.getOutletStream());
// Run (will iterate until recycle converges)
process.run();
System.out.println("Recycle converged: " + recycle.isConverged());
System.out.println("Product rate: " + product.getFlowRate("kg/hr") + " kg/hr");
ProcessSystem supports saving and loading to/from compressed .neqsim files and JSON state files for version control.
// Save to compressed .neqsim file (recommended)
process.saveToNeqsim("my_process.neqsim");
// Load (auto-runs after loading)
ProcessSystem loaded = ProcessSystem.loadFromNeqsim("my_process.neqsim");
// Auto-detect format by extension
process.saveAuto("my_process.neqsim"); // Compressed XStream XML
process.saveAuto("my_process.json"); // JSON state export
// JSON state for version control
ProcessSystemState state = ProcessSystemState.fromProcessSystem(process);
state.setVersion("1.0.0");
state.saveToFile("my_process_v1.0.0.json");
For full documentation on serialization options, see Process Serialization Guide.
Documentation for the ProcessModel class in NeqSim.
Location: neqsim.process.processmodel.ProcessModel
The ProcessModel class manages a collection of ProcessSystem objects that can be run together. It provides:
Use ProcessModel when you need to simulate interconnected process systems or coordinate multiple flowsheets.
import neqsim.process.processmodel.ProcessModel;
// Create empty process model
ProcessModel model = new ProcessModel();
// Create process systems
ProcessSystem gasProcessing = new ProcessSystem("Gas Processing");
gasProcessing.add(feedGas);
gasProcessing.add(separator);
gasProcessing.add(compressor);
ProcessSystem oilProcessing = new ProcessSystem("Oil Processing");
oilProcessing.add(oilFeed);
oilProcessing.add(heater);
oilProcessing.add(stabilizer);
// Add to model
model.add("Gas Processing", gasProcessing);
model.add("Oil Processing", oilProcessing);
// Get specific process
ProcessSystem gas = model.get("Gas Processing");
// Get all processes
Collection<ProcessSystem> allProcesses = model.getAllProcesses();
// Run until convergence or max iterations
model.run();
// Check if converged
if (model.isModelConverged()) {
System.out.println("Model converged in " + model.getLastIterationCount() + " iterations");
}
// Enable step mode
model.setRunStep(true);
// Run one step at a time
model.run(); // Runs one step for each process
// Enable optimized execution (default is true)
model.setUseOptimizedExecution(true);
model.run();
// Each ProcessSystem uses runOptimized() internally
// Run in background thread
Future<?> task = model.runAsTask();
// Do other work...
// Wait for completion
task.get();
// Set individual tolerances
model.setFlowTolerance(1e-5); // Relative flow error
model.setTemperatureTolerance(1e-5); // Relative temperature error
model.setPressureTolerance(1e-5); // Relative pressure error
// Or set all at once
model.setTolerance(1e-5);
// Set maximum iterations
model.setMaxIterations(100);
model.run();
// Check overall convergence
boolean converged = model.isModelConverged();
// Get convergence errors
double flowErr = model.getLastMaxFlowError();
double tempErr = model.getLastMaxTemperatureError();
double pressErr = model.getLastMaxPressureError();
double maxErr = model.getError();
// Get detailed summary
System.out.println(model.getConvergenceSummary());
ProcessModel provides comprehensive validation to check that all contained ProcessSystems are properly configured before running.
// Quick check - returns true if no CRITICAL errors
if (model.isReadyToRun()) {
model.run();
} else {
System.out.println("Model not ready to run");
System.out.println(model.getValidationReport());
}
ValidationResult result = model.validateSetup();
if (!result.isValid()) {
System.out.println("Validation issues found:");
System.out.println(result.getReport());
}
Map<String, ValidationResult> allResults = model.validateAll();
for (Map.Entry<String, ValidationResult> entry : allResults.entrySet()) {
String processName = entry.getKey();
ValidationResult processResult = entry.getValue();
if (!processResult.isValid()) {
System.out.println(processName + " has issues:");
processResult.getErrors().forEach(System.out::println);
}
}
// Get a human-readable validation report
String report = model.getValidationReport();
System.out.println(report);
Example output:
=== ProcessModel Validation Report ===
--- EmptyProcess ---
[CRITICAL] ProcessSystem is empty
Fix: Add at least one process equipment using add()
Summary: 1 issue(s) found (1 critical, 0 major)
Ready to run: NO
Validation Methods Summary:
| Method | Returns | Description |
|---|---|---|
validateSetup() |
ValidationResult |
Combined result for all processes |
validateAll() |
Map<String, ValidationResult> |
Per-process results |
isReadyToRun() |
boolean |
True if no CRITICAL errors |
getValidationReport() |
String |
Formatted human-readable report |
// Get mass balance for all processes
Map<String, Map<String, ProcessSystem.MassBalanceResult>> results =
model.checkMassBalance("kg/hr");
// Get failed mass balance checks
Map<String, Map<String, ProcessSystem.MassBalanceResult>> failed =
model.getFailedMassBalance(0.1); // 0.1% threshold
// Get formatted reports
System.out.println(model.getMassBalanceReport());
System.out.println(model.getFailedMassBalanceReport());
// Create gas processing system
ProcessSystem gasProcess = new ProcessSystem("Gas Train");
Stream gasIn = new Stream("Gas Feed", gasFluid);
Separator scrubber = new Separator("Inlet Scrubber", gasIn);
Compressor comp = new Compressor("Export Compressor", scrubber.getGasOutStream());
gasProcess.add(gasIn);
gasProcess.add(scrubber);
gasProcess.add(comp);
// Create oil processing system
ProcessSystem oilProcess = new ProcessSystem("Oil Train");
Stream oilIn = new Stream("Oil Feed", oilFluid);
Heater heater = new Heater("Oil Heater", oilIn);
Separator stabilizer = new Separator("Stabilizer", heater.getOutletStream());
oilProcess.add(oilIn);
oilProcess.add(heater);
oilProcess.add(stabilizer);
// Create model and add processes
ProcessModel model = new ProcessModel();
model.add("Gas Train", gasProcess);
model.add("Oil Train", oilProcess);
// Validate before running
if (model.isReadyToRun()) {
model.run();
if (model.isModelConverged()) {
System.out.println("Model converged!");
System.out.println(model.getConvergenceSummary());
}
} else {
System.out.println(model.getValidationReport());
}
ProcessModel supports saving and loading to/from compressed .neqsim files and JSON state files for version control.
// Save to compressed .neqsim file
model.saveToNeqsim("field_model.neqsim");
// Load (auto-runs after loading)
ProcessModel loaded = ProcessModel.loadFromNeqsim("field_model.neqsim");
// Auto-detect format by extension
model.saveAuto("field_model.neqsim"); // Compressed
model.saveAuto("field_model.json"); // JSON state
// JSON state export for version control
ProcessModelState state = model.exportState();
state.setVersion("1.0.0");
state.saveToFile("field_model_v1.0.0.json");
For full documentation on serialization options, see Process Serialization Guide.
Documentation for modular process units in NeqSim.
Location: neqsim.process.processmodel.ProcessModule
Process modules encapsulate complex process subsystems into reusable units. Benefits include:
import neqsim.process.processmodel.ProcessModule;
// Create module
ProcessModule module = new ProcessModule("Compression Train");
// Add equipment to module
module.add(scrubber);
module.add(compressor);
module.add(cooler);
// Set inlet/outlet
module.setInletStream(gasIn);
module.setOutletStream(cooler.getOutletStream());
ProcessSystem process = new ProcessSystem();
// Add module to process
process.addModule(module);
// Connect to other equipment
module.setInletStream(feedStream);
Stream compressed = module.getOutletStream();
process.add(downstream equipment);
public interface ModuleInterface {
// Identification
String getName();
void setName(String name);
// Streams
void setInletStream(StreamInterface stream);
StreamInterface getInletStream();
StreamInterface getOutletStream();
// Execution
void run();
void runTransient(double dt);
// Initialization
void initializeModule();
// Equipment access
ProcessEquipmentInterface getUnit(String name);
List<ProcessEquipmentInterface> getUnits();
}
// Multi-stage compression with intercooling
CompressorTrainModule compTrain = new CompressorTrainModule("HP Compression");
compTrain.setNumberOfStages(3);
compTrain.setOutletPressure(150.0, "bara");
compTrain.setIntercoolerTemperature(40.0, "C");
compTrain.setIsentropicEfficiency(0.78);
compTrain.setInletStream(feedGas);
process.addModule(compTrain);
// Multi-stage separation
SeparationTrainModule sepTrain = new SeparationTrainModule("Separation");
sepTrain.addStage(50.0, "bara"); // HP stage
sepTrain.addStage(15.0, "bara"); // MP stage
sepTrain.addStage(3.0, "bara"); // LP stage
sepTrain.setInletStream(wellFluid);
process.addModule(sepTrain);
// Get products
Stream exportGas = sepTrain.getGasOutStream();
Stream exportOil = sepTrain.getOilOutStream();
Stream prodWater = sepTrain.getWaterOutStream();
// TEG dehydration unit
DehydrationModule dehy = new DehydrationModule("Gas Dehy");
dehy.setWaterDewPoint(-20.0, "C");
dehy.setSolventType("TEG");
dehy.setInletStream(wetGas);
process.addModule(dehy);
Stream dryGas = dehy.getOutletStream();
public class MyCustomModule extends ProcessModule {
private Separator inlet Scrubber;
private HeatExchanger heatEx;
private Compressor compressor;
public MyCustomModule(String name) {
super(name);
initializeEquipment();
}
private void initializeEquipment() {
inletScrubber = new Separator("Inlet Scrubber");
add(inletScrubber);
heatEx = new HeatExchanger("Feed/Effluent HX");
add(heatEx);
compressor = new Compressor("Main Compressor");
add(compressor);
}
@Override
public void setInletStream(StreamInterface stream) {
super.setInletStream(stream);
inletScrubber.setInletStream(stream);
}
@Override
public StreamInterface getOutletStream() {
return compressor.getOutletStream();
}
public void setOutletPressure(double pressure, String unit) {
compressor.setOutletPressure(pressure, unit);
}
}
MyCustomModule customModule = new MyCustomModule("My Unit");
customModule.setInletStream(feed);
customModule.setOutletPressure(80.0, "bara");
process.addModule(customModule);
process.run();
public class TwoInletModule extends ProcessModule {
private Mixer inletMixer;
public void setInletStream1(StreamInterface stream) {
inletMixer.addStream(stream);
}
public void setInletStream2(StreamInterface stream) {
inletMixer.addStream(stream);
}
}
public class TwoOutletModule extends ProcessModule {
private Splitter outletSplitter;
public StreamInterface getOutletStream1() {
return outletSplitter.getOutletStream(0);
}
public StreamInterface getOutletStream2() {
return outletSplitter.getOutletStream(1);
}
}
public class LNGTrainModule extends ProcessModule {
private Cooler precooler;
private HeatExchanger mainCryoExchanger;
private Expander jt Expander;
private Separator lngSeparator;
public LNGTrainModule(String name) {
super(name);
precooler = new Cooler("Precooler");
add(precooler);
mainCryoExchanger = new HeatExchanger("MCHE");
add(mainCryoExchanger);
jtExpander = new Expander("JT Expander");
add(jtExpander);
lngSeparator = new Separator("LNG Separator");
add(lngSeparator);
}
@Override
public void setInletStream(StreamInterface stream) {
super.setInletStream(stream);
precooler.setInletStream(stream);
}
public void setLNGTemperature(double temp, String unit) {
// Configure for target LNG temperature
}
public StreamInterface getLNGStream() {
return lngSeparator.getLiquidOutStream();
}
public StreamInterface getBoilOffGas() {
return lngSeparator.getGasOutStream();
}
}
// Usage
LNGTrainModule lngTrain = new LNGTrainModule("LNG Production");
lngTrain.setInletStream(treatedGas);
lngTrain.setLNGTemperature(-162.0, "C");
process.addModule(lngTrain);
process.run();
Stream lng = lngTrain.getLNGStream();
System.out.println("LNG production: " + lng.getFlowRate("tonne/day") + " t/d");
// Complete FPSO processing
ProcessSystem fpso = new ProcessSystem("FPSO Topsides");
// Separation module
SeparationTrainModule separation = new SeparationTrainModule("Separation");
separation.setInletStream(wellFluid);
fpso.addModule(separation);
// Gas compression
CompressorTrainModule gasComp = new CompressorTrainModule("Gas Compression");
gasComp.setInletStream(separation.getGasOutStream());
gasComp.setOutletPressure(200.0, "bara");
fpso.addModule(gasComp);
// Gas dehydration
DehydrationModule dehy = new DehydrationModule("Dehydration");
dehy.setInletStream(gasComp.getOutletStream());
fpso.addModule(dehy);
// Water treatment
WaterTreatmentModule waterTreat = new WaterTreatmentModule("Water Treatment");
waterTreat.setInletStream(separation.getWaterOutStream());
fpso.addModule(waterTreat);
fpso.run();
Documentation for graph-based execution in NeqSim.
Location: neqsim.process.processmodel.graph
Classes:
| Class | Description |
|---|---|
ProcessGraph |
Graph representation of process |
ProcessGraphBuilder |
Builder for constructing graphs |
ProcessNode |
Node representing equipment |
ProcessEdge |
Edge representing stream connection |
Graph-based simulation represents the process as a directed graph where:
Benefits:
The recommended approach is to use runOptimized() which automatically analyzes your process and selects the best strategy:
// Auto-selects best execution strategy based on process topology
process.runOptimized();
// With calculation ID for tracking
UUID calcId = UUID.randomUUID();
process.runOptimized(calcId);
The method inspects the process for:
| Strategy | Method | Best For | When Used by runOptimized() |
|---|---|---|---|
| Sequential | run() or runSequential() |
Recycles, multi-input equipment | Has Recycle units or Mixer/HeatExchanger/etc. |
| Graph-based | setUseGraphBasedExecution(true) |
Complex ordering | Manual configuration only |
| Parallel | runParallel() |
Feed-forward (no recycles) | No recycles, no multi-input, no adjusters |
| Hybrid | runHybrid() |
Processes with adjusters | Has adjusters but no recycles/multi-input |
| Optimized | runOptimized() |
All processes | Auto-selects from above |
Standard execution in insertion order:
process.run();
Uses topological ordering for optimal execution sequence:
// Enable graph-based ordering
process.setUseGraphBasedExecution(true);
process.run();
Executes independent units simultaneously using thread pool:
// For feed-forward processes (no recycles)
try {
process.runParallel();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
How it works:
Combines parallel and iterative execution for processes with recycles:
// For processes with recycles
try {
process.runHybrid();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
How it works:
Automatically selects the best strategy based on process topology:
// Auto-selects best execution strategy
process.runOptimized();
Decision logic (in order of priority):
| Condition | Strategy | Reason |
|---|---|---|
Has Recycle units |
runSequential() |
Recycles require full iterative convergence |
| Has multi-input equipment | runSequential() |
Mixer, Manifold, etc. need correct stream ordering |
Has Adjuster units |
runHybrid() |
Adjusters need iteration but feed-forward can parallelize |
| Feed-forward only | runParallel() |
Maximum speed with no dependencies |
Multi-input equipment includes:
Mixer, ManifoldTurboExpanderCompressor, EjectorHeatExchanger, MultiStreamHeatExchangerFurnaceBurner, FlareStackNote: hasRecycles() checks for explicit Recycle unit operations, not graph-based cycle detection.
// Check if process has Recycle units (requires iterative execution)
boolean hasRecycles = process.hasRecycles();
// Check if process has Adjuster units (requires iteration)
boolean hasAdjusters = process.hasAdjusters();
// Check if process has multi-input equipment (requires sequential)
// Includes: Mixer, Manifold, HeatExchanger, TurboExpanderCompressor, etc.
boolean hasMultiInput = process.hasMultiInputEquipment();
// Check if parallel execution would be beneficial (graph-based)
boolean beneficial = process.isParallelExecutionBeneficial();
// Get detailed partition analysis
System.out.println(process.getExecutionPartitionInfo());
// What will runOptimized() do for my process?
if (process.hasRecycles()) {
System.out.println("Will use: runSequential() - Recycle units detected");
} else if (process.hasMultiInputEquipment()) {
System.out.println("Will use: runSequential() - Multi-input equipment detected");
} else if (process.hasAdjusters()) {
System.out.println("Will use: runHybrid() - Adjusters with parallel feed-forward");
} else {
System.out.println("Will use: runParallel() - Full parallel execution");
}
=== Execution Partition Analysis ===
Total units: 40
Has recycle loops: true
Parallel levels: 29
Max parallelism: 6
Units in recycle loops: 30
- 1st stage compressor
- 2nd stage separator
...
=== Hybrid Execution Strategy ===
Phase 1 (Parallel): 4 levels, 8 units
Phase 2 (Iterative): 25 levels, 32 units
Execution levels:
Level 0 [PARALLEL]: feed TP setter, first stage oil reflux, export oil
Level 1 [PARALLEL]: 1st stage separator
Level 2 [PARALLEL]: oil depres valve
Level 3 [PARALLEL]:
--- Recycle Section Start (iterative) ---
Level 4: oil heater second stage [RECYCLE]
Level 5: 2nd stage separator [RECYCLE]
...
// Get detailed partition info
ProcessGraph.ParallelPartition partition = process.getParallelPartition();
System.out.println("Execution levels: " + partition.getLevelCount());
System.out.println("Max parallelism: " + partition.getMaxParallelism());
// Iterate through levels
for (List<ProcessNode> level : partition.getLevels()) {
System.out.println("Level has " + level.size() + " units");
}
import neqsim.process.processmodel.graph.ProcessGraph;
import neqsim.process.processmodel.graph.ProcessGraphBuilder;
// Build graph from process
ProcessGraphBuilder builder = new ProcessGraphBuilder();
ProcessGraph graph = builder.build(processSystem);
// Execute in topological order
graph.execute();
// Get number of nodes (equipment)
int nodeCount = graph.getNodeCount();
// Get number of edges (connections)
int edgeCount = graph.getEdgeCount();
// Check for cycles (recycles)
boolean hasCycles = graph.hasCycles();
// Build from existing process
ProcessGraphBuilder builder = new ProcessGraphBuilder();
ProcessGraph graph = builder.build(process);
ProcessGraph graph = new ProcessGraph();
// Add nodes
graph.addNode(feed);
graph.addNode(heater);
graph.addNode(separator);
// Add edges (connections)
graph.addEdge(feed, heater);
graph.addEdge(heater, separator);
// Add node with properties
Map<String, Object> props = new HashMap<>();
props.put("criticality", "high");
props.put("maintainPriority", 1);
graph.addNode(compressor, props);
// Get execution order
List<ProcessEquipmentInterface> order = graph.topologicalSort();
for (int i = 0; i < order.size(); i++) {
System.out.println((i+1) + ". " + order.get(i).getName());
}
// Identify recycle loops
List<List<ProcessEquipmentInterface>> cycles = graph.findCycles();
for (List<ProcessEquipmentInterface> cycle : cycles) {
System.out.println("Cycle found:");
for (ProcessEquipmentInterface node : cycle) {
System.out.println(" - " + node.getName());
}
}
// Find longest path (critical path)
List<ProcessEquipmentInterface> criticalPath = graph.findCriticalPath();
System.out.println("Critical path:");
for (ProcessEquipmentInterface node : criticalPath) {
System.out.println(" " + node.getName());
}
// Export for Graphviz visualization
String dot = graph.toDOT();
Files.writeString(Path.of("process_graph.dot"), dot);
// Generate image with Graphviz:
// dot -Tpng process_graph.dot -o process_graph.png
// Export graph structure to JSON
String json = graph.toJSON();
Files.writeString(Path.of("process_graph.json"), json);
ProcessSystem process = new ProcessSystem();
// Feed splitter
Splitter splitter = new Splitter("Feed Splitter", feedStream);
splitter.setSplitRatios(new double[]{0.5, 0.5});
process.add(splitter);
// Parallel compressor trains (can execute simultaneously)
Compressor comp1 = new Compressor("K-101", splitter.getOutletStream(0));
comp1.setOutletPressure(80.0, "bara");
process.add(comp1);
Compressor comp2 = new Compressor("K-102", splitter.getOutletStream(1));
comp2.setOutletPressure(80.0, "bara");
process.add(comp2);
// Merger
Mixer mixer = new Mixer("Discharge Mixer");
mixer.addStream(comp1.getOutletStream());
mixer.addStream(comp2.getOutletStream());
process.add(mixer);
// Build graph
ProcessGraph graph = new ProcessGraphBuilder().build(process);
// Parallel execution - K-101 and K-102 run simultaneously
graph.setExecutionStrategy(ExecutionStrategy.PARALLEL);
graph.execute();
// Build graph from complex process
ProcessGraph graph = new ProcessGraphBuilder().build(process);
// Analyze structure
System.out.println("Process structure:");
System.out.println(" Equipment count: " + graph.getNodeCount());
System.out.println(" Connections: " + graph.getEdgeCount());
System.out.println(" Has recycles: " + graph.hasCycles());
// Identify independent sections
List<Set<ProcessEquipmentInterface>> sections = graph.findConnectedComponents();
System.out.println(" Independent sections: " + sections.size());
// Find potential bottlenecks (high in-degree)
for (ProcessEquipmentInterface node : graph.getNodes()) {
int inDegree = graph.getInDegree(node);
if (inDegree > 2) {
System.out.println(" Potential bottleneck: " + node.getName() +
" (" + inDegree + " inputs)");
}
}
ProcessGraph graph = new ProcessGraphBuilder().build(process);
// Find all recycle streams
List<List<ProcessEquipmentInterface>> cycles = graph.findCycles();
System.out.println("Recycle loops identified:");
for (int i = 0; i < cycles.size(); i++) {
System.out.println("Recycle " + (i+1) + ":");
List<ProcessEquipmentInterface> cycle = cycles.get(i);
for (ProcessEquipmentInterface node : cycle) {
System.out.println(" -> " + node.getName());
}
// Suggest tear stream (node with lowest "impact")
ProcessEquipmentInterface tearStream = graph.suggestTearStream(cycle);
System.out.println(" Suggested tear stream: " + tearStream.getName());
}
// Get execution levels
List<List<ProcessEquipmentInterface>> levels = graph.getExecutionLevels();
// Count parallel opportunities
int parallelOps = 0;
for (List<ProcessEquipmentInterface> level : levels) {
if (level.size() > 1) {
parallelOps += level.size() - 1;
}
}
System.out.println("Parallel execution opportunities: " + parallelOps);
System.out.println("Potential speedup: " +
(double)graph.getNodeCount() / levels.size() + "x");
// Extract subgraph for specific section
Set<ProcessEquipmentInterface> compressionUnits = process.getUnitsOfType(
CompressorInterface.class).stream().collect(Collectors.toSet());
ProcessGraph compressionGraph = graph.extractSubgraph(compressionUnits);
// Analyze compression section separately
compressionGraph.execute();
When combining multiple ProcessSystem instances into a ProcessModel, execution follows a similar pattern:
import neqsim.process.processmodel.ProcessModel;
// Create and populate model
ProcessModel model = new ProcessModel();
model.add("Upstream", upstreamProcess);
model.add("Compression", compressionProcess);
model.add("Export", exportProcess);
// Run until convergence (uses optimized execution internally)
model.run();
// Check convergence
if (model.isModelConverged()) {
System.out.println("Converged in " + model.getLastIterationCount() + " iterations");
}
// Continuous mode (default) - iterates until convergence
model.setRunStep(false);
model.run();
// Step mode - run one iteration at a time
model.setRunStep(true);
model.run(); // One step for each ProcessSystem
model.run(); // Next step...
// Asynchronous execution
Future<?> task = model.runAsTask();
// ... do other work ...
task.get(); // Wait for completion
Each ProcessSystem within a ProcessModel uses runOptimized() by default:
// Enable/disable optimized execution for contained ProcessSystems
model.setUseOptimizedExecution(true); // Default
model.run();
NeqSim can generate professional oil & gas style process flow diagrams (PFDs) that follow industry conventions, comparable to UniSim, Aspen, and HYSYS.
// Create and run your process
ProcessSystem process = new ProcessSystem("Gas Processing Plant");
// ... add equipment ...
process.run();
// Export to DOT format (text)
String dot = process.toDOT();
// Or use the diagram exporter for more control
process.createDiagramExporter()
.setTitle("Gas Processing Plant")
.setDetailLevel(DiagramDetailLevel.STANDARD)
.exportAsSVG(Path.of("diagram.svg"));
The diagram layout follows oil & gas conventions with left-to-right flow:
Streams are automatically colored based on phase composition:
For two-phase separators, outlets are positioned:
For three-phase separators (gas, oil, aqueous), outlets follow gravity:
The diagram system supports all NeqSim equipment types with industry-standard shapes:
| Equipment | Shape | Color |
|---|---|---|
| Separator | Cylinder | Green |
| ThreePhaseSeparator | Cylinder | Green |
| Scrubber | Cylinder | Light Green |
| GasScrubber | Cylinder | Light Green |
| KnockOutDrum | Cylinder | Light Green |
| Flash | Cylinder | Light Green |
| Equipment | Shape | Color |
|---|---|---|
| DistillationColumn | Tall Cylinder | Green |
| Absorber | Rectangle | Light Green |
| Stripper | Rectangle | Light Green |
| WaterStripperColumn | Rectangle | Light Green |
| Equipment | Shape | Symbol |
|---|---|---|
| Compressor | Trapezoid | Standard P&ID trapezoid |
| CompressorModule | Trapezoid | Standard P&ID trapezoid |
| Expander | Inverted Trapezium | Inverted trapezoid |
| TurbineExpander | Inverted Trapezium | Inverted trapezoid |
| Equipment | Shape | Symbol |
|---|---|---|
| Pump | Circle with impeller | Circle on triangle base |
| PumpModule | Circle with impeller | Circle on triangle base |
| ESP (Electrical Submersible Pump) | Circle with impeller | Circle on triangle base |
| Equipment | Shape | Symbol |
|---|---|---|
| HeatExchanger | Circle | Simple circle |
| Cooler | Circle | Simple circle |
| Heater | Circle | Simple circle |
| Condenser | Circle | Simple circle |
| Reboiler | Circle | Simple circle |
| MultiStreamHeatExchanger | Circle | Simple circle |
| DirectContactHeater | Circle | Simple circle |
| Equipment | Shape | Symbol |
|---|---|---|
| ThrottlingValve | Bowtie (▶◀) | Two triangles tip-to-tip |
| ValveMoV | Bowtie (▶◀) | Two triangles tip-to-tip |
| ControlValve | Bowtie (▶◀) | Two triangles tip-to-tip |
| SafetyValve | Bowtie (▶◀) | Two triangles tip-to-tip |
| PressureReliefValve | Bowtie (▶◀) | Two triangles tip-to-tip |
| ESDValve | Bowtie (▶◀) | Two triangles tip-to-tip |
| HIPPSValve | Bowtie (▶◀) | Two triangles tip-to-tip |
| Equipment | Shape | Color |
|---|---|---|
| Reactor | Hexagon | Orange |
| GibbsReactor | Hexagon | Orange |
| EquilibriumReactor | Hexagon | Orange |
| ElectrolyzerCell | Hexagon | Light Blue |
| Equipment | Shape | Color |
|---|---|---|
| Mixer | Triangle | Light Gray |
| Splitter | Inverted Triangle | Light Gray |
| Stream | Ellipse | Light Green |
| Ejector | Pentagon | Light Gray |
| Flare | Diamond | Orange |
| Filter | Rectangle | Tan |
| Membrane | Rectangle | Light Blue |
| Tank | Cylinder | Gray |
| Pipeline | Rectangle | Gray |
| Well | House | Brown |
| GasGenerator/GasTurbine | Octagon | Steel Blue |
| Equipment | Shape | Color |
|---|---|---|
| Recycle | Rectangle (dashed) | Gray |
| Adjuster | Rectangle (dashed) | Gray |
| Calculator | Rectangle (dashed) | Light Blue |
| Controller | Rectangle (dashed) | Light Yellow |
| SetPoint | Rectangle (dashed) | Light Gray |
Four diagram styles are available to match different simulator conventions:
import neqsim.process.processmodel.diagram.DiagramStyle;
ProcessDiagramExporter exporter = new ProcessDiagramExporter(process);
exporter.setDiagramStyle(DiagramStyle.HYSYS);
| Style | Description | Stream Color | Background |
|---|---|---|---|
NEQSIM |
Default NeqSim style with phase-based coloring | Phase-dependent | White |
HYSYS |
HYSYS/UniSim style | Blue (#0066CC) | Light Cyan |
PROII |
PRO/II style | Dark Blue (#003366) | White |
ASPEN_PLUS |
Aspen Plus style | Blue (#0066FF) | Light Gray |
Equipment symbols follow oil & gas PFD conventions:
| Equipment | Symbol | Description |
|---|---|---|
| Valve | ▶◀ | Bowtie - two triangles tip-to-tip |
| Heater/Cooler | ○ | Simple circle |
| Pump | ○ on ▽ | Circle with impeller on triangle |
| Compressor | ⌂ | Trapezoid shape |
| Mixer | ▶ | Right-pointing triangle |
| Splitter | ◀ | Left-pointing triangle |
| Separator | ▭ | Vertical cylinder |
Anti-surge loops and recycle streams are automatically detected and highlighted:
Two display modes for process values:
exporter.setShowStreamValues(true)
.setUseStreamTables(false);
Shows: Stream Name\n25.0°C, 50.0 bar\n1000 kg/hr
exporter.setShowStreamValues(true)
.setUseStreamTables(true);
Generates HTML tables with:
Control equipment (Recycle, Adjuster, Calculator, etc.) can be hidden for cleaner diagrams:
exporter.setShowControlEquipment(false);
Three detail levels are available:
// Quick DOT export
String dot = process.toDOT();
// DOT export with specific detail level
String dot = process.toDOT(DiagramDetailLevel.CONCEPTUAL);
// Full-featured exporter
ProcessDiagramExporter exporter = process.createDiagramExporter();
// Direct SVG/PNG export (requires Graphviz)
process.exportDiagramSVG(Path.of("diagram.svg"));
process.exportDiagramPNG(Path.of("diagram.png"));
ProcessDiagramExporter exporter = new ProcessDiagramExporter(process)
.setTitle("My Process")
.setDiagramStyle(DiagramStyle.HYSYS) // NEQSIM, HYSYS, PROII, ASPEN_PLUS
.setDetailLevel(DiagramDetailLevel.ENGINEERING)
.setVerticalLayout(false) // LR layout (left-to-right flow) - default
.setUseClusters(true) // Group equipment by role
.setShowLegend(true) // Include legend
.setShowStreamValues(true) // Show T/P/F on streams
.setUseStreamTables(false) // Use HTML tables (true) or text (false)
.setHighlightRecycles(true) // Highlight recycle streams
.setShowControlEquipment(true) // Show/hide control equipment
.setShowDexpiMetadata(true); // Show DEXPI line numbers/fluid codes
// Export options
String dot = exporter.toDOT();
exporter.exportDOT(Path.of("diagram.dot"));
exporter.exportSVG(Path.of("diagram.svg")); // Requires Graphviz
exporter.exportPNG(Path.of("diagram.png")); // Requires Graphviz
exporter.exportPDF(Path.of("diagram.pdf")); // Requires Graphviz
The DOT format can be rendered using Graphviz:
# Install Graphviz (if needed)
# Windows: choco install graphviz
# macOS: brew install graphviz
# Linux: apt-get install graphviz
# Render to SVG
dot -Tsvg process.dot -o process.svg
# Render to PNG
dot -Tpng process.dot -o process.png
# Render to PDF
dot -Tpdf process.dot -o process.pdf
// Create fluid
SystemInterface fluid = new SystemSrkEos(298.0, 50.0);
fluid.addComponent("methane", 0.7);
fluid.addComponent("ethane", 0.15);
fluid.addComponent("propane", 0.1);
fluid.addComponent("n-butane", 0.05);
fluid.setMixingRule("classic");
// Build process
ProcessSystem process = new ProcessSystem("Gas Separation");
Stream feed = new Stream("Well Fluid", fluid);
feed.setFlowRate(5000.0, "kg/hr");
feed.setTemperature(60.0, "C");
feed.setPressure(80.0, "bara");
process.add(feed);
Separator hpSep = new Separator("HP Separator", feed);
process.add(hpSep);
Compressor comp = new Compressor("Export Compressor", hpSep.getGasOutStream());
comp.setOutletPressure(120.0, "bara");
process.add(comp);
Cooler cooler = new Cooler("Gas Cooler", comp.getOutletStream());
cooler.setOutTemperature(40.0, "C");
process.add(cooler);
Pump pump = new Pump("Oil Pump", hpSep.getLiquidOutStream());
pump.setOutletPressure(60.0, "bara");
process.add(pump);
process.run();
// Export diagram
process.createDiagramExporter()
.setTitle("Gas Separation Process")
.setDetailLevel(DiagramDetailLevel.ENGINEERING)
.exportSVG(Path.of("gas_separation.svg"));
This generates a professional PFD with:
// Create three-phase fluid (gas, oil, water)
SystemInterface fluid = new SystemSrkEos(298.0, 50.0);
fluid.addComponent("methane", 0.5);
fluid.addComponent("n-heptane", 0.3);
fluid.addComponent("water", 0.2);
fluid.setMixingRule("classic");
fluid.setMultiPhaseCheck(true);
// Build process
ProcessSystem process = new ProcessSystem("Production Separation");
Stream feed = new Stream("Well Fluid", fluid);
feed.setFlowRate(5000.0, "kg/hr");
feed.setTemperature(60.0, "C");
feed.setPressure(80.0, "bara");
process.add(feed);
// Three-phase separator
ThreePhaseSeparator separator = new ThreePhaseSeparator("Production Separator", feed);
process.add(separator);
// Gas compression
Compressor gasCompressor = new Compressor("Gas Compressor", separator.getGasOutStream());
gasCompressor.setOutletPressure(120.0, "bara");
process.add(gasCompressor);
// Oil export pump
Pump oilPump = new Pump("Oil Pump", separator.getOilOutStream());
oilPump.setOutletPressure(60.0, "bara");
process.add(oilPump);
// Produced water handling
Pump waterPump = new Pump("Water Pump", separator.getWaterOutStream());
waterPump.setOutletPressure(10.0, "bara");
process.add(waterPump);
process.run();
// Export diagram
process.createDiagramExporter()
.setTitle("Production Separation")
.setDetailLevel(DiagramDetailLevel.ENGINEERING)
.exportSVG(Path.of("production_separation.svg"));
This generates a PFD where the three-phase separator shows:
Each stream is color-coded by phase type for easy identification.
// Create gas fluid
SystemInterface gas = new SystemSrkEos(298.0, 50.0);
gas.addComponent("methane", 0.9);
gas.addComponent("ethane", 0.1);
gas.setMixingRule("classic");
// Build process with recycle
ProcessSystem process = new ProcessSystem("Compressor Station");
Stream feed = new Stream("Feed Gas", gas);
feed.setFlowRate(1000.0, "kg/hr");
feed.setTemperature(25.0, "C");
feed.setPressure(50.0, "bara");
process.add(feed);
// Mixer for recycle
Mixer suctionMixer = new Mixer("Suction Mixer");
suctionMixer.addStream(feed);
process.add(suctionMixer);
// Main compressor
Compressor compressor = new Compressor("Main Compressor", suctionMixer.getOutletStream());
compressor.setOutletPressure(100.0, "bara");
process.add(compressor);
// Anti-surge splitter
Splitter splitter = new Splitter("Discharge Splitter", compressor.getOutletStream(), 2);
splitter.setSplitFactors(new double[] {0.9, 0.1});
process.add(splitter);
// Anti-surge recycle
Recycle recycle = new Recycle("Anti-Surge Recycle");
recycle.addStream(splitter.getSplitStream(1));
recycle.setOutletStream(suctionMixer.getOutletStream());
process.add(recycle);
process.run();
// Export with recycle highlighting
process.createDiagramExporter()
.setTitle("Compressor Anti-Surge System")
.setDetailLevel(DiagramDetailLevel.ENGINEERING)
.setHighlightRecycles(true) // Purple dashed lines for recycles
.setShowStreamValues(true)
.exportSVG(Path.of("anti_surge.svg"));
This generates a PFD with:
setShowControlEquipment(false))The diagram system integrates with DEXPI (Data Exchange in the Process Industry) for importing P&ID data and generating PFD diagrams from industry-standard data exchange files.
// Import DEXPI XML and create pre-configured diagram exporter
ProcessDiagramExporter exporter = DexpiDiagramBridge.importAndCreateExporter(
Paths.get("plant.xml"));
// DEXPI metadata (line numbers, fluid codes) shown in labels by default
exporter.exportDOT(Paths.get("diagram.dot"));
exporter.exportSVG(Paths.get("diagram.svg")); // Requires Graphviz
// Import DEXPI → run simulation → generate diagram → export enriched DEXPI
ProcessSystem system = DexpiDiagramBridge.roundTrip(
Paths.get("input.xml"), // Input DEXPI P&ID file
Paths.get("diagram.dot"), // Output diagram
Paths.get("output.xml")); // Re-exported DEXPI with simulation results
Equipment imported from DEXPI files displays P&ID reference information:
// Enable/disable DEXPI metadata display
exporter.setShowDexpiMetadata(true);
// Standard exporter with DEXPI features enabled
ProcessDiagramExporter exporter = DexpiDiagramBridge.createExporter(system);
// Detailed exporter with full operating conditions
ProcessDiagramExporter detailed = DexpiDiagramBridge.createDetailedExporter(system);
See DEXPI XML Reader for complete DEXPI import/export documentation.
PFDLayoutPolicy customPolicy = new PFDLayoutPolicy();
// Policy automatically classifies equipment by type
ProcessDiagramExporter exporter = new ProcessDiagramExporter(process, customPolicy);
Visual styles are defined in EquipmentVisualStyle with defaults for all common equipment types. The style includes:
The diagram export system consists of:
Professional PFDs are not drawn — they are computed using rules.
The layout intelligence layer applies engineering conventions:
This approach produces diagrams that are:
neqsim.process
├── equipment/ # Individual unit operations (150+ classes)
│ ├── EquipmentEnum # Canonical equipment type enumeration
│ └── ProcessEquipmentInterface
├── processmodel/
│ ├── ProcessSystem # Main flowsheet container
│ ├── graph/ # Graph representation layer
│ │ ├── ProcessGraph # DAG with cycle detection
│ │ ├── ProcessNode # Equipment nodes
│ │ ├── ProcessEdge # Stream edges
│ │ └── ProcessGraphBuilder
│ ├── diagram/ # PFD visualization layer
│ │ ├── ProcessDiagramExporter
│ │ ├── PFDLayoutPolicy
│ │ ├── EquipmentRole
│ │ ├── DiagramDetailLevel
│ │ └── EquipmentVisualStyle
│ ├── dexpi/ # DEXPI integration layer
│ │ ├── DexpiXmlReader
│ │ ├── DexpiXmlWriter
│ │ ├── DexpiProcessUnit
│ │ ├── DexpiStream
│ │ ├── DexpiMetadata
│ │ └── DexpiRoundTripProfile
│ └── lifecycle/ # Phase/state management
└── thermo/ # Thermodynamic models
The PFD diagram system integrates cleanly at three architectural levels:
| Layer | Integration Point | Relationship |
|---|---|---|
| ProcessSystem | toDOT(), createDiagramExporter() |
Facade methods for convenience |
| ProcessGraph | ProcessGraphBuilder.build() |
Diagram uses graph for topology |
| Equipment | ProcessEquipmentInterface |
Visual styles keyed by class name |
Key Design Decisions:
ProcessGraph, not raw equipment listsSerializable| Principle | Diagram System Compliance |
|---|---|
Equipment extends ProcessEquipmentBaseClass |
✓ Uses interfaces only, no tight coupling |
| Mixing rules before init | ✓ Works on run() output, not during setup |
Cloning with system.clone() |
✓ No shared mutable state |
| Java 8 compatibility | ✓ No var, no List.of(), streams used correctly |
| Serialization support | ✓ All classes serializable |
| Package boundaries | ✓ Located in processmodel.diagram |
NeqSim already has comprehensive DEXPI support:
| Component | Purpose |
|---|---|
DexpiXmlReader |
Import DEXPI P&ID XML → ProcessSystem |
DexpiXmlWriter |
Export ProcessSystem → DEXPI XML |
DexpiProcessUnit |
Lightweight placeholder for imported equipment |
DexpiStream |
Runnable stream with DEXPI metadata |
DexpiMetadata |
Shared constants (tag names, line numbers, etc.) |
DexpiRoundTripProfile |
Validation for round-trip fidelity |
dexpi_equipment_mapping.properties |
DEXPI class → EquipmentEnum mapping |
Current State:
EquipmentVisualStyle uses class names: "Separator", "Compressor", etc.dexpi_equipment_mapping.properties maps DEXPI classes → EquipmentEnumOpportunity: Unify equipment type handling through EquipmentEnum:
// EquipmentVisualStyle could use EquipmentEnum as key
public static EquipmentVisualStyle getStyle(EquipmentEnum type) {
return STYLE_CACHE.get(type);
}
// DexpiProcessUnit already has getMappedEquipment() → EquipmentEnum
DexpiProcessUnit unit = ...;
EquipmentVisualStyle style = EquipmentVisualStyle.getStyle(unit.getMappedEquipment());
Current State:
ProcessDiagramExporter generates Graphviz DOTDexpiXmlWriter generates DEXPI XMLOpportunity: Add DEXPI-aware export options:
public class ProcessDiagramExporter {
// Export to DEXPI XML with embedded layout hints
public void exportDexpiWithLayout(Path path) throws IOException {
// 1. Generate ProcessGraph with layout
// 2. Add layout coordinates as GenericAttributes
// 3. Write via DexpiXmlWriter with coordinates
}
// Import DEXPI and preserve P&ID layout
public static ProcessDiagramExporter fromDexpi(Path dexpiXml) {
ProcessSystem system = DexpiXmlReader.read(dexpiXml, template);
return new ProcessDiagramExporter(system)
.preserveDexpiLayout(true); // Use DEXPI positions if available
}
}
Current State:
DexpiMetadata defines: TAG_NAME, LINE_NUMBER, FLUID_CODE, etc.ProcessDiagramExporter doesn't use theseOpportunity: Enrich diagram labels with DEXPI metadata:
private String buildNodeLabel(ProcessEquipmentInterface equipment) {
StringBuilder label = new StringBuilder(equipment.getName());
if (equipment instanceof DexpiProcessUnit) {
DexpiProcessUnit dexpi = (DexpiProcessUnit) equipment;
if (dexpi.getLineNumber() != null) {
label.append("\\nLine: ").append(dexpi.getLineNumber());
}
if (dexpi.getFluidCode() != null) {
label.append("\\nFluid: ").append(dexpi.getFluidCode());
}
}
return label.toString();
}
Current State:
EquipmentVisualStyle uses Graphviz shapes (circle, rectangle, etc.)Opportunity: Map to ISO 10628 symbol classes:
public enum EquipmentSymbol {
// ISO 10628-2 Section 5: Process equipment
VESSEL(5.1, "cylinder", "#90EE90"),
COLUMN(5.2, "cylinder", "#90EE90"),
HEAT_EXCHANGER(5.3, "rectangle", "#FFD700"),
// ISO 10628-2 Section 6: Piping components
VALVE(6.1, "diamond", "#FFB6C1"),
PUMP(6.2, "circle", "#4169E1"),
COMPRESSOR(6.3, "parallelogram", "#87CEEB");
private final String isoSection;
private final String graphvizShape;
private final String defaultColor;
}
EquipmentVisualStyle to accept EquipmentEnum as primary key// Before
EquipmentVisualStyle style = EquipmentVisualStyle.getStyle("Separator");
// After
EquipmentVisualStyle style = EquipmentVisualStyle.getStyle(EquipmentEnum.Separator);
// or
EquipmentVisualStyle style = EquipmentVisualStyle.getStyle(unit.getMappedEquipment());
DexpiProcessUnit and DexpiStream instancesThe diagram/ package is correctly positioned:
processmodel/ (not equipment/)graph/ (uses graph, doesn't extend it)package neqsim.process.equipment;
/**
* Unified equipment type resolution service.
*/
public final class EquipmentTypeResolver {
/**
* Resolves equipment to canonical EquipmentEnum.
*/
public static EquipmentEnum resolve(ProcessEquipmentInterface equipment) {
if (equipment instanceof DexpiProcessUnit) {
return ((DexpiProcessUnit) equipment).getMappedEquipment();
}
// Fall back to class name mapping
String className = equipment.getClass().getSimpleName();
return EquipmentEnum.valueOf(className);
}
/**
* Resolves DEXPI class name to EquipmentEnum using mapping file.
*/
public static EquipmentEnum resolveFromDexpi(String dexpiClassName) {
// Load from dexpi_equipment_mapping.properties
}
}
package neqsim.process.processmodel.diagram;
/**
* Bridge between DEXPI metadata and PFD visualization.
*/
public class DexpiDiagramBridge {
/**
* Creates diagram exporter optimized for DEXPI-imported processes.
*/
public static ProcessDiagramExporter createExporter(ProcessSystem system) {
return new ProcessDiagramExporter(system)
.setShowDexpiMetadata(true)
.setPreserveDexpiLayout(true);
}
/**
* Exports ProcessSystem to DEXPI XML with embedded layout coordinates.
*/
public static void exportWithLayout(ProcessSystem system, Path output) {
// Calculate layout via ProcessDiagramExporter
// Inject coordinates as GenericAttributes
// Write via DexpiXmlWriter
}
}
The PFD diagram system integrates cleanly:
ProcessGraph for topology (not parallel implementation)processmodel.diagram)ProcessSystem (facade pattern)| Synergy Area | Status | Implementation |
|---|---|---|
| EquipmentEnum unification | ✅ Complete | EquipmentVisualStyle.getStyle(EquipmentEnum) |
| DEXPI metadata in labels | ✅ Complete | appendDexpiMetadata(), setShowDexpiMetadata() |
| DexpiDiagramBridge | ✅ Complete | DexpiDiagramBridge class with round-trip support |
| ISO 10628 symbol mapping | ⏳ Future | Planned for P&ID compliance |
EquipmentVisualStyle.getStyle(EquipmentEnum) - Unified styling via canonical enumEquipmentVisualStyle.getStyleForEquipment(equipment) - Auto-detects DEXPI unitsProcessDiagramExporter.setShowDexpiMetadata(true) - Display line numbers/fluid codesDexpiDiagramBridge.createExporter(system) - Pre-configured DEXPI-aware exporterDexpiDiagramBridge.importAndCreateExporter(path) - One-step DEXPI → diagramDexpiDiagramBridge.roundTrip(input, dotOutput, dexpiOutput) - Full import/simulate/exportComprehensive documentation for process streams in NeqSim.
Location: neqsim.process.equipment.stream
Streams are the fundamental connections between process equipment in NeqSim, carrying material and energy through process flowsheets. They encapsulate thermodynamic fluid systems with flow conditions and provide methods for flash calculations, property retrieval, and gas quality analysis.
ProcessEquipmentBaseClass
└── Stream (implements StreamInterface)
└── NeqStream
ProcessEquipmentBaseClass
└── VirtualStream
java.io.Serializable
└── EnergyStream
| Class | Description | Use Case |
|---|---|---|
Stream |
Standard process stream with full thermodynamic calculations | General material flows |
StreamInterface |
Interface defining stream contract | Type declarations and polymorphism |
NeqStream |
Stream without flash (uses existing phase split) | When phase equilibrium is known |
VirtualStream |
Reference stream with property overrides | Branch flows, what-if scenarios |
EnergyStream |
Heat/work duty carrier | Heat exchanger duties, compressor work |
A Stream contains:
SystemInterface fluid object// IMPORTANT: Stream uses the fluid object directly (not cloned)
SystemInterface fluid = new SystemSrkEos(298.15, 50.0);
fluid.addComponent("methane", 1.0);
// The stream references the same fluid object
Stream stream = new Stream("Feed", fluid);
// To create independent streams, clone explicitly
Stream independent = new Stream("Independent", fluid.clone());
Streams can reference other streams:
// Source stream
Stream source = new Stream("Source", fluid);
source.run();
// Linked stream (shares fluid with source)
Stream linked = new Stream("Linked", source);
// When source changes, linked sees the changes after run()
source.setTemperature(350.0, "K");
source.run();
linked.run(); // Uses updated source properties
import neqsim.thermo.system.SystemSrkEos;
import neqsim.process.equipment.stream.Stream;
// 1. Create with name only (fluid set later)
Stream emptyStream = new Stream("Empty");
// 2. Create from fluid system (uses fluid directly, not cloned)
SystemSrkEos fluid = new SystemSrkEos(298.15, 50.0);
fluid.addComponent("methane", 0.90);
fluid.addComponent("ethane", 0.07);
fluid.addComponent("propane", 0.03);
fluid.setMixingRule("classic");
Stream feedStream = new Stream("Feed", fluid);
// 3. Create from another stream (linked reference)
Stream linkedStream = new Stream("Linked", feedStream);
// Temperature (various units)
feed.setTemperature(300.0, "K"); // Kelvin
feed.setTemperature(25.0, "C"); // Celsius
feed.setTemperature(77.0, "F"); // Fahrenheit
// Pressure (various units)
feed.setPressure(50.0, "bara"); // Bar absolute
feed.setPressure(725.0, "psia"); // PSI absolute
feed.setPressure(5.0, "MPa"); // Megapascal
// Flow rate (various units)
feed.setFlowRate(10000.0, "kg/hr"); // Mass flow
feed.setFlowRate(500.0, "kmol/hr"); // Molar flow
feed.setFlowRate(1000000.0, "Sm3/day"); // Standard volume (gas)
feed.setFlowRate(100.0, "m3/hr"); // Actual volume
// IMPORTANT: Always run() after setting conditions
feed.run();
// Replace entire fluid
feed.setFluid(newFluidSystem);
feed.setThermoSystem(newFluidSystem);
// Set from specific phase of another system
feed.setThermoSystemFromPhase(otherSystem, "gas"); // Gas phase only
feed.setThermoSystemFromPhase(otherSystem, "oil"); // Oil phase only
feed.setThermoSystemFromPhase(otherSystem, "aqueous"); // Water phase only
feed.setThermoSystemFromPhase(otherSystem, "liquid"); // All liquid phases
// Create empty stream from template
feed.setEmptyThermoSystem(templateSystem);
The stream specification controls how flash calculations are performed.
| Specification | Description | When to Use |
|---|---|---|
"TP" |
Temperature-Pressure flash (default) | Standard conditions |
"PH" |
Pressure-Enthalpy flash | After isenthalpic processes |
"dewP" |
Dew point temperature at given P | Condensation studies |
"dewT" |
Dew point pressure at given T | Dew point analysis |
"bubP" |
Bubble point temperature at given P | Evaporation studies |
"bubT" |
Bubble point pressure at given T | Bubble point analysis |
"gas quality" |
Constant phase fraction flash | Fixed vapor fraction |
// Default TP flash
Stream stream = new Stream("Process", fluid);
stream.run(); // Performs TP flash
// Dew point calculation
stream.setSpecification("dewP");
stream.run(); // Calculates dew point temperature at current pressure
// Bubble point calculation
stream.setSpecification("bubP");
stream.run(); // Calculates bubble point temperature at current pressure
// Gas quality specification
stream.setSpecification("gas quality");
stream.setGasQuality(0.5); // 50% vapor fraction
stream.run(); // Calculates temperature for specified vapor fraction
stream.run(); // Ensure stream is calculated
// Temperature
double tempK = stream.getTemperature(); // Kelvin (default)
double tempC = stream.getTemperature("C"); // Celsius
double tempF = stream.getTemperature("F"); // Fahrenheit
// Pressure
double pressPa = stream.getPressure(); // Pascal (default)
double pressBara = stream.getPressure("bara"); // Bar absolute
double pressPsia = stream.getPressure("psia"); // PSI absolute
// Flow rates
double massFlow = stream.getFlowRate("kg/hr");
double molarFlow = stream.getFlowRate("kmol/hr");
double molarRate = stream.getMolarRate(); // Total moles
double volFlow = stream.getFlowRate("m3/hr");
double stdVolFlow = stream.getFlowRate("Sm3/day");
| Unit | Description | Basis |
|---|---|---|
"kg/sec" |
Kilograms per second | Mass |
"kg/min" |
Kilograms per minute | Mass |
"kg/hr" |
Kilograms per hour | Mass |
"kg/day" |
Kilograms per day | Mass |
"kmol/hr" |
Kilomoles per hour | Molar |
"mole/sec" |
Moles per second | Molar |
"mole/min" |
Moles per minute | Molar |
"mole/hr" |
Moles per hour | Molar |
"m3/sec" |
Actual m³/second | Volume |
"m3/min" |
Actual m³/minute | Volume |
"m3/hr" |
Actual m³/hour | Volume |
"Sm3/sec" |
Standard m³/second | Std Volume |
"Sm3/hr" |
Standard m³/hour | Std Volume |
"Sm3/day" |
Standard m³/day | Std Volume |
"MSm3/day" |
Million Sm³/day | Std Volume |
"barrel/day" |
Oil barrels/day | Volume |
// Get fluid object for detailed properties
SystemInterface fluid = stream.getFluid();
// or equivalently:
SystemInterface fluid = stream.getThermoSystem();
// Molecular weight
double mw = fluid.getMolarMass("kg/kmol");
// Enthalpy
double enthalpy = fluid.getEnthalpy("kJ/kg");
// Entropy
double entropy = fluid.getEntropy("kJ/kgK");
// Density
double density = fluid.getDensity("kg/m3");
// Composition
double[] moleFractions = fluid.getMolarComposition();
double methaneFrac = fluid.getComponent("methane").getz();
SystemInterface fluid = stream.getFluid();
// Check for specific phases
boolean hasGas = fluid.hasPhaseType("gas");
boolean hasOil = fluid.hasPhaseType("oil");
boolean hasAqueous = fluid.hasPhaseType("aqueous");
// Number of phases
int numPhases = fluid.getNumberOfPhases();
// Phase mole fractions (beta)
double gasFraction = fluid.getPhase("gas").getBeta(); // Mole basis
if (fluid.hasPhaseType("gas")) {
PhaseInterface gasPhase = fluid.getPhase("gas");
// Phase properties
double gasDensity = gasPhase.getDensity("kg/m3");
double gasViscosity = gasPhase.getViscosity("cP");
double gasMW = gasPhase.getMolarMass("kg/kmol");
double gasZ = gasPhase.getZ(); // Compressibility factor
// Component in phase
double methaneInGas = gasPhase.getComponent("methane").getx();
}
if (fluid.hasPhaseType("oil")) {
PhaseInterface oilPhase = fluid.getPhase("oil");
double oilDensity = oilPhase.getDensity("kg/m3");
double oilViscosity = oilPhase.getViscosity("cP");
}
// From separator outlet
Separator separator = new Separator("Sep", feed);
separator.run();
// Gas outlet
Stream gasOut = new Stream("Gas Out");
gasOut.setThermoSystemFromPhase(separator.getFluid(), "gas");
gasOut.run();
// Oil outlet
Stream oilOut = new Stream("Oil Out");
oilOut.setThermoSystemFromPhase(separator.getFluid(), "oil");
oilOut.run();
// All liquids combined
Stream liquidOut = new Stream("Liquid Out");
liquidOut.setThermoSystemFromPhase(separator.getFluid(), "liquid");
liquidOut.run();
NeqSim provides comprehensive gas quality calculations per ISO 6976 and other standards.
// Gross Calorific Value (Higher Heating Value)
double gcv = stream.GCV(); // kJ/Sm³ at 0°C, 15.55°C combustion
// GCV with specified reference conditions
double gcvCustom = stream.getGCV("volume", 15.0, 15.0); // refT=15°C, combT=15°C
// Net Calorific Value (Lower Heating Value)
double lcv = stream.LCV(); // kJ/Sm³
// Wobbe Index (gas interchangeability measure)
double wi = stream.getWI("volume", 15.0, 15.0); // kJ/Sm³
// Get full ISO 6976 results
Standard_ISO6976 iso = stream.getISO6976("volume", 15.0, 15.0);
iso.calculate();
double gcv = iso.getValue("SuperiorCalorificValue");
double lcv = iso.getValue("InferiorCalorificValue");
double wobbe = iso.getValue("SuperiorWobbeIndex");
double relDensity = iso.getValue("RelativeDensity");
double compressibility = iso.getValue("CompressionFactor");
// Hydrocarbon dew point at specified pressure
double hcDewPoint = stream.getHydrocarbonDewPoint("C", 70.0, "bara");
// Hydrate equilibrium temperature
double hydrateTemp = stream.getHydrateEquilibriumTemperature(); // K
// Solid formation temperature
double freezeTemp = stream.getSolidFormationTemperature("wax");
// Cricondentherm (maximum temperature for two-phase)
double cctTemp = stream.CCT("C"); // Temperature
double cctPres = stream.CCT("bara"); // Pressure at CCT
// Cricondenbar (maximum pressure for two-phase)
double ccbTemp = stream.CCB("C"); // Temperature at CCB
double ccbPres = stream.CCB("bara"); // Pressure
// Phase envelope visualization
stream.phaseEnvelope(); // Opens plot window
// True Vapor Pressure at reference temperature
double tvp = stream.TVP(37.8, "C"); // bara at 100°F
double tvpPsia = stream.getTVP(37.8, "C", "psia");
// Reid Vapor Pressure (ASTM D6377)
double rvp = stream.getRVP(37.8, "C", "psia");
double rvpMethod = stream.getRVP(37.8, "C", "psia", "VPCR4");
VirtualStream creates a modified copy of a reference stream with overridden properties.
import neqsim.process.equipment.stream.VirtualStream;
// Reference stream
Stream mainFlow = new Stream("Main", fluid);
mainFlow.setFlowRate(10000.0, "kg/hr");
mainFlow.run();
// Virtual stream with modified flow
VirtualStream branch = new VirtualStream("Branch", mainFlow);
branch.setFlowRate(2000.0, "kg/hr"); // Override flow
branch.run();
// Virtual stream with modified conditions
VirtualStream heated = new VirtualStream("Heated", mainFlow);
heated.setTemperature(350.0, "K"); // Override temperature
heated.setFlowRate(3000.0, "kg/hr"); // Override flow
heated.run();
// Virtual stream with modified composition
VirtualStream altered = new VirtualStream("Altered", mainFlow);
double[] newComp = {0.95, 0.03, 0.02}; // New mole fractions
altered.setComposition(newComp, "mole");
altered.run();
// Get output stream from virtual
StreamInterface outputStream = altered.getOutletStream();
| Method | Description |
|---|---|
setReferenceStream(stream) |
Set the source stream |
setFlowRate(value, unit) |
Override flow rate |
setTemperature(value, unit) |
Override temperature |
setPressure(value, unit) |
Override pressure |
setComposition(array, unit) |
Override composition |
getOutletStream() |
Get the modified stream |
NeqStream is a specialized stream that skips flash calculations, using the existing phase distribution.
// Standard Stream: performs TP flash
Stream standard = new Stream("Standard", fluid);
standard.run(); // Calculates new phase equilibrium
// NeqStream: uses existing phases, just initializes properties
NeqStream neq = new NeqStream("NeqStream", fluid);
neq.run(); // Skips flash, uses existing x, y, beta
import neqsim.process.equipment.stream.NeqStream;
// After separator has calculated phases
Separator sep = new Separator("Sep", feed);
sep.run();
// Use NeqStream to preserve exact phase split
NeqStream gasStream = new NeqStream("Gas", sep.getGasOutStream());
gasStream.run(); // No reflash, preserves separator results
EnergyStream carries heat or work duty between equipment.
import neqsim.process.equipment.stream.EnergyStream;
// Create energy stream
EnergyStream heatDuty = new EnergyStream("Heater Duty");
heatDuty.setDuty(1000000.0); // Watts
// Get duty
double duty = heatDuty.getDuty(); // Watts
// Heater with energy stream
Heater heater = new Heater("Heater", feed);
heater.setOutletTemperature(350.0, "K");
heater.run();
// Energy stream gets duty from heater
EnergyStream heaterPower = new EnergyStream("Heater Power");
heaterPower.setDuty(heater.getDuty());
// Connect to heat source
HeatExchanger hx = new HeatExchanger("HX");
hx.setEnergyStream(heaterPower);
// Clone with same name (returns copy)
Stream original = new Stream("Feed", fluid);
original.run();
Stream copy = original.clone();
// Clone with new name
Stream namedCopy = original.clone("Feed Copy");
// Clones are independent
copy.setFlowRate(500.0, "kg/hr");
copy.run();
// Original unchanged
Streams cache their last calculated state for optimization:
// Check if recalculation is needed
if (stream.needRecalculation()) {
stream.run(); // Conditions changed, recalculate
}
// Cached values used internally:
// - lastTemperature
// - lastPressure
// - lastFlowRate
// - lastComposition
Streams support dynamic simulation with controller integration.
// Time step in seconds
double dt = 1.0;
UUID calcId = UUID.randomUUID();
// Run transient step
stream.runTransient(dt, calcId);
// Increase simulation time
stream.increaseTime(dt);
// Attach controller
ControllerDeviceInterface controller = new PIDController();
controller.setControllerSetPoint(1000.0); // kg/hr target
stream.setController(controller);
// Transient run adjusts flow via controller
for (int i = 0; i < 100; i++) {
stream.runTransient(1.0, UUID.randomUUID());
}
// Streams below minimum flow are deactivated
if (stream.getFlowRate("kg/hr") < stream.getMinimumFlow()) {
// Stream runs but marks as inactive
stream.isActive(); // Returns false
}
import neqsim.thermo.system.SystemSrkEos;
import neqsim.process.equipment.stream.Stream;
// Natural gas composition
SystemSrkEos gas = new SystemSrkEos(298.15, 70.0);
gas.addComponent("nitrogen", 0.02);
gas.addComponent("CO2", 0.01);
gas.addComponent("methane", 0.85);
gas.addComponent("ethane", 0.06);
gas.addComponent("propane", 0.03);
gas.addComponent("i-butane", 0.01);
gas.addComponent("n-butane", 0.015);
gas.addComponent("i-pentane", 0.005);
gas.setMixingRule("classic");
Stream feed = new Stream("Natural Gas Feed", gas);
feed.setFlowRate(10.0, "MSm3/day"); // 10 million Sm³/day
feed.run();
// Report properties
System.out.println("=== Feed Stream Properties ===");
System.out.println("Temperature: " + feed.getTemperature("C") + " °C");
System.out.println("Pressure: " + feed.getPressure("bara") + " bara");
System.out.println("Mass flow: " + feed.getFlowRate("kg/hr") + " kg/hr");
System.out.println("Molar flow: " + feed.getFlowRate("kmol/hr") + " kmol/hr");
System.out.println("Density: " + feed.getFluid().getDensity("kg/m3") + " kg/m³");
System.out.println("MW: " + feed.getFluid().getMolarMass("kg/kmol") + " kg/kmol");
// Gas quality
System.out.println("\n=== Gas Quality ===");
System.out.println("GCV: " + feed.GCV() / 1000.0 + " MJ/Sm³");
System.out.println("LCV: " + feed.LCV() / 1000.0 + " MJ/Sm³");
System.out.println("Wobbe Index: " + feed.getWI("volume", 15.0, 15.0) / 1000.0 + " MJ/Sm³");
System.out.println("HC Dew Point: " + feed.getHydrocarbonDewPoint("C", 70.0, "bara") + " °C");
// Wellhead mixture
SystemSrkEos wellfluid = new SystemSrkEos(350.0, 150.0);
wellfluid.addComponent("methane", 0.60);
wellfluid.addComponent("ethane", 0.08);
wellfluid.addComponent("propane", 0.05);
wellfluid.addComponent("n-hexane", 0.12);
wellfluid.addComponent("n-decane", 0.10);
wellfluid.addComponent("water", 0.05);
wellfluid.setMixingRule("classic");
Stream wellStream = new Stream("Well Stream", wellfluid);
wellStream.setFlowRate(50000.0, "kg/hr");
wellStream.run();
// Phase analysis
SystemInterface fluid = wellStream.getFluid();
System.out.println("Number of phases: " + fluid.getNumberOfPhases());
if (fluid.hasPhaseType("gas")) {
double gasRate = fluid.getPhase("gas").getBeta()
* wellStream.getFlowRate("kg/hr");
System.out.println("Gas rate: " + gasRate + " kg/hr");
System.out.println("Gas density: " + fluid.getPhase("gas").getDensity("kg/m3") + " kg/m³");
}
if (fluid.hasPhaseType("oil")) {
double oilRate = fluid.getPhase("oil").getBeta()
* wellStream.getFlowRate("kg/hr");
System.out.println("Oil rate: " + oilRate + " kg/hr");
System.out.println("Oil API: " + fluid.getPhase("oil").getPhysicalProperties()
.getValue("API_gravity"));
}
if (fluid.hasPhaseType("aqueous")) {
double waterRate = fluid.getPhase("aqueous").getBeta()
* wellStream.getFlowRate("kg/hr");
System.out.println("Water rate: " + waterRate + " kg/hr");
}
// Main pipeline flow
Stream pipeline = new Stream("Pipeline", gas);
pipeline.setFlowRate(100000.0, "kg/hr");
pipeline.run();
// Customer branches (each takes portion of main flow)
VirtualStream customer1 = new VirtualStream("Customer 1", pipeline);
customer1.setFlowRate(30000.0, "kg/hr");
customer1.run();
VirtualStream customer2 = new VirtualStream("Customer 2", pipeline);
customer2.setFlowRate(25000.0, "kg/hr");
customer2.setTemperature(280.0, "K"); // Heated for customer 2
customer2.run();
VirtualStream customer3 = new VirtualStream("Customer 3", pipeline);
customer3.setFlowRate(45000.0, "kg/hr");
customer3.setPressure(40.0, "bara"); // Reduced pressure
customer3.run();
// Verify mass balance
double totalOut = customer1.getOutletStream().getFlowRate("kg/hr")
+ customer2.getOutletStream().getFlowRate("kg/hr")
+ customer3.getOutletStream().getFlowRate("kg/hr");
System.out.println("Pipeline in: " + pipeline.getFlowRate("kg/hr") + " kg/hr");
System.out.println("Total out: " + totalOut + " kg/hr");
// Gas stream
Stream gasStream = new Stream("Export Gas", gas);
gasStream.setPressure(70.0, "bara");
gasStream.setFlowRate(5000.0, "kmol/hr");
// Calculate dew point temperature
gasStream.setSpecification("dewP");
gasStream.run();
System.out.println("Dew point at 70 bara: " + gasStream.getTemperature("C") + " °C");
// Calculate bubble point
gasStream.setSpecification("bubP");
gasStream.run();
System.out.println("Bubble point at 70 bara: " + gasStream.getTemperature("C") + " °C");
// Return to normal operation
gasStream.setSpecification("TP");
gasStream.setTemperature(25.0, "C");
gasStream.run();
// Feed stream
Stream feed = new Stream("Feed", fluid);
feed.setFlowRate(10000.0, "kg/hr");
feed.run();
// Clone for train A (50%)
Stream trainA = feed.clone("Train A Feed");
trainA.setFlowRate(5000.0, "kg/hr");
trainA.run();
// Clone for train B (50%)
Stream trainB = feed.clone("Train B Feed");
trainB.setFlowRate(5000.0, "kg/hr");
trainB.run();
// Process independently
Heater heaterA = new Heater("Heater A", trainA);
heaterA.setOutletTemperature(400.0, "K");
heaterA.run();
Heater heaterB = new Heater("Heater B", trainB);
heaterB.setOutletTemperature(380.0, "K"); // Different setpoint
heaterB.run();
System.out.println("Train A outlet T: " + heaterA.getOutletStream().getTemperature("C") + " °C");
System.out.println("Train B outlet T: " + heaterB.getOutletStream().getTemperature("C") + " °C");
// Get formatted report
ArrayList<String[]> report = stream.getReport();
for (String[] row : report) {
System.out.println(String.join(" | ", row));
}
// JSON output
String json = stream.toJson();
System.out.println(json);
// Result table
String[][] results = stream.getResultTable();
for (String[] row : results) {
System.out.println(String.join("\t", row));
}
// Display in NeqSim GUI
stream.displayResult();
For single-component systems from other streams, the stream automatically switches to PH flash to handle phase changes correctly:
// Single component from separator
if (stream != null && thermoSystem.getNumberOfComponents() == 1
&& getSpecification().equals("TP")) {
setSpecification("PH"); // Auto-switch for stability
}
Streams track their last state to avoid unnecessary calculations:
// Implementation checks cached values
if (temperature == lastTemperature
&& pressure == lastPressure
&& flowRate == lastFlowRate
&& composition == lastComposition) {
return false; // No recalculation needed
}
Streams are fully serializable for persistence:
// Save process state
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("process.dat"));
out.writeObject(stream);
out.close();
// Restore process state
ObjectInputStream in = new ObjectInputStream(new FileInputStream("process.dat"));
Stream restored = (Stream) in.readObject();
in.close();
// Add streams to process system
ProcessSystem process = new ProcessSystem();
Stream feed = new Stream("Feed", fluid);
feed.setFlowRate(1000.0, "kg/hr");
Heater heater = new Heater("Heater", feed);
heater.setOutletTemperature(350.0, "K");
Separator sep = new Separator("Separator", heater.getOutletStream());
process.add(feed);
process.add(heater);
process.add(sep);
// Run entire process
process.run();
// Access any stream
StreamInterface processedFeed = process.getMeasurementDevice("Feed");
Documentation for stream mixing and splitting equipment in NeqSim.
Location: neqsim.process.equipment.mixer, neqsim.process.equipment.splitter
Classes:
| Class | Description |
|---|---|
Mixer |
Combine multiple streams |
MixerInterface |
Mixer interface |
Splitter |
Split stream into fractions |
SplitterInterface |
Splitter interface |
StaticMixer |
Static mixing element |
Combine multiple streams into one outlet stream.
import neqsim.process.equipment.mixer.Mixer;
Mixer mixer = new Mixer("M-100");
mixer.addStream(stream1);
mixer.addStream(stream2);
mixer.addStream(stream3);
mixer.run();
Stream mixed = mixer.getOutletStream();
The mixer performs mass and energy balance:
$$\dot{m}_{out} = \sum_i \dot{m}_i$$
$$\dot{m}_{out} \cdot h_{out} = \sum_i \dot{m}_i \cdot h_i$$
$$x_{j,out} = \frac{\sum_i \dot{m}_i \cdot x_{j,i}}{\sum_i \dot{m}_i}$$
// Default: outlet pressure = minimum inlet pressure
mixer.run();
// Or specify outlet pressure
mixer.setOutletPressure(20.0, "bara");
mixer.run();
Split a stream into multiple fractions.
import neqsim.process.equipment.splitter.Splitter;
// Split into 2 streams
Splitter splitter = new Splitter("SP-100", inletStream, 2);
splitter.setSplitFactors(new double[]{0.7, 0.3}); // 70% and 30%
splitter.run();
Stream split1 = splitter.getSplitStream(0); // 70%
Stream split2 = splitter.getSplitStream(1); // 30%
// By mass fractions (must sum to 1.0)
splitter.setSplitFactors(new double[]{0.5, 0.3, 0.2});
// By flow rates
splitter.setFlowRates(new double[]{100.0, 60.0, 40.0}, "kg/hr");
All split streams have identical:
Only flow rate differs.
For inline mixing with pressure drop.
import neqsim.process.equipment.mixer.StaticMixer;
StaticMixer staticMixer = new StaticMixer("Static Mixer");
staticMixer.addStream(stream1);
staticMixer.addStream(stream2);
staticMixer.setPressureDrop(0.5, "bara");
staticMixer.run();
import neqsim.thermo.system.SystemSrkEos;
import neqsim.process.equipment.stream.Stream;
import neqsim.process.equipment.mixer.Mixer;
// Stream 1: Rich gas
SystemSrkEos gas1 = new SystemSrkEos(300.0, 50.0);
gas1.addComponent("methane", 0.80);
gas1.addComponent("ethane", 0.15);
gas1.addComponent("propane", 0.05);
gas1.setMixingRule("classic");
Stream stream1 = new Stream("Rich Gas", gas1);
stream1.setFlowRate(5000.0, "kg/hr");
stream1.run();
// Stream 2: Lean gas
SystemSrkEos gas2 = new SystemSrkEos(310.0, 50.0);
gas2.addComponent("methane", 0.95);
gas2.addComponent("ethane", 0.04);
gas2.addComponent("propane", 0.01);
gas2.setMixingRule("classic");
Stream stream2 = new Stream("Lean Gas", gas2);
stream2.setFlowRate(3000.0, "kg/hr");
stream2.run();
// Mix streams
Mixer mixer = new Mixer("M-100");
mixer.addStream(stream1);
mixer.addStream(stream2);
mixer.run();
// Results
Stream mixed = mixer.getOutletStream();
System.out.println("Mixed flow: " + mixed.getFlowRate("kg/hr") + " kg/hr");
System.out.println("Mixed temp: " + mixed.getTemperature("C") + " C");
System.out.println("Methane: " + mixed.getFluid().getMoleFraction("methane"));
// Main process stream
Stream processStream = new Stream("Process", processFluid);
processStream.setFlowRate(10000.0, "kg/hr");
processStream.run();
// Split: 90% product, 10% recycle
Splitter splitter = new Splitter("Recycle Splitter", processStream, 2);
splitter.setSplitFactors(new double[]{0.90, 0.10});
splitter.run();
Stream product = splitter.getSplitStream(0);
Stream recycle = splitter.getSplitStream(1);
System.out.println("Product: " + product.getFlowRate("kg/hr") + " kg/hr");
System.out.println("Recycle: " + recycle.getFlowRate("kg/hr") + " kg/hr");
// Create manifold mixer for 4 wells
Mixer manifold = new Mixer("Production Manifold");
for (int i = 1; i <= 4; i++) {
SystemSrkEos wellFluid = new SystemSrkEos(350.0, 100.0 - i * 5);
wellFluid.addComponent("methane", 0.85);
wellFluid.addComponent("ethane", 0.08);
wellFluid.addComponent("propane", 0.05);
wellFluid.addComponent("water", 0.02);
wellFluid.setMixingRule("classic");
Stream wellStream = new Stream("Well " + i, wellFluid);
wellStream.setFlowRate(1000.0 + i * 200, "Sm3/day");
wellStream.run();
manifold.addStream(wellStream);
}
manifold.run();
System.out.println("Total production: " + manifold.getOutletStream().getFlowRate("Sm3/day") + " Sm3/day");
System.out.println("Manifold pressure: " + manifold.getOutletStream().getPressure("bara") + " bara");
// Gas from separator
Stream gasProduct = separator.getGasOutStream();
// Distribute to 3 customers
Splitter distributor = new Splitter("Gas Distribution", gasProduct, 3);
// Set by flow rates
double[] rates = {5000.0, 3000.0, 2000.0}; // Sm3/hr
distributor.setFlowRates(rates, "Sm3/hr");
distributor.run();
for (int i = 0; i < 3; i++) {
Stream customerStream = distributor.getSplitStream(i);
System.out.println("Customer " + (i+1) + ": " + customerStream.getFlowRate("Sm3/hr") + " Sm3/hr");
}
// Main stream
Stream mainStream = new Stream("Main", fluid);
mainStream.setFlowRate(1000.0, "kg/hr");
mainStream.run();
// Split: 80% through heater, 20% bypass
Splitter bypass = new Splitter("Bypass", mainStream, 2);
bypass.setSplitFactors(new double[]{0.80, 0.20});
bypass.run();
// Heat 80%
Heater heater = new Heater("E-100", bypass.getSplitStream(0));
heater.setOutletTemperature(400.0, "K");
heater.run();
// Remix
Mixer remix = new Mixer("M-100");
remix.addStream(heater.getOutletStream());
remix.addStream(bypass.getSplitStream(1));
remix.run();
System.out.println("Bypass temp control: " + remix.getOutletStream().getTemperature("K") + " K");
This folder contains detailed documentation for all process equipment in NeqSim.
| Equipment | File | Description |
|---|---|---|
| Streams | streams.md | Material and energy streams |
| Mixers & Splitters | mixers_splitters.md | Stream mixing and splitting |
| Equipment | File | Description |
|---|---|---|
| Separators | separators.md | 2-phase and 3-phase separators, scrubbers |
| Distillation | distillation.md | Distillation columns |
| Absorbers | absorbers.md | Absorption/stripping columns |
| Membranes | membranes.md | Membrane separation units |
| Filters | filters.md | Particulate and charcoal filters |
| Equipment | File | Description |
|---|---|---|
| Heat Exchangers | heat_exchangers.md | Heaters, coolers, condensers, reboilers |
| Equipment | File | Description |
|---|---|---|
| Compressors | compressors.md | Gas compression, mechanical losses, seal gas |
| Pumps | pumps.md | Liquid pumping |
| Expanders | expanders.md | Power recovery, turboexpanders |
| Equipment | File | Description |
|---|---|---|
| Valves | valves.md | Throttling valves, chokes, safety valves |
| Equipment | File | Description |
|---|---|---|
| Reactors | reactors.md | CSTR, PFR, equilibrium reactors |
| Electrolyzers | electrolyzers.md | Water and CO₂ electrolysis |
| Equipment | File | Description |
|---|---|---|
| Ejectors | ejectors.md | Steam and gas ejectors |
| Equipment | File | Description |
|---|---|---|
| Flares | flares.md | Flare systems and combustion |
| Equipment | File | Description |
|---|---|---|
| Wells | wells.md | Production wells, chokes |
| Reservoirs | reservoirs.md | Material balance reservoir modeling |
| Subsea Systems | subsea_systems.md | Subsea wells and flowlines |
| Equipment | File | Description |
|---|---|---|
| Pipelines | pipelines.md | Pipe flow, pressure drop |
| Networks | networks.md | Pipeline network modeling |
| Manifolds | manifolds.md | Multi-stream routing |
| Equipment | File | Description |
|---|---|---|
| Differential Pressure | differential_pressure.md | Orifice plates, flow measurement |
| Equipment | File | Description |
|---|---|---|
| Tanks | tanks.md | Storage tanks, LNG boil-off |
| Equipment | File | Description |
|---|---|---|
| Adsorbers | adsorbers.md | CO₂ and gas adsorption |
| Equipment | File | Description |
|---|---|---|
| Power Equipment | power_generation.md | Gas turbines, fuel cells, renewables |
| Equipment | File | Description |
|---|---|---|
| Adjusters | util/adjusters.md | Variable adjustment to meet specs |
| Recycles | util/recycles.md | Recycle stream handling |
| Calculators | util/calculators.md | Custom calculations and setters |
// All equipment follows similar pattern
EquipmentType equipment = new EquipmentType("Name", inletStream);
equipment.setParameter(value);
equipment.run();
Stream outlet = equipment.getOutletStream();
ProcessSystem process = new ProcessSystem();
process.add(stream);
process.add(equipment1);
process.add(equipment2);
process.run();
Compressor comp = (Compressor) process.getUnit("K-100");
All equipment inherits from ProcessEquipmentBaseClass:
| Method | Description |
|---|---|
run() |
Execute calculation |
runTransient() |
Execute transient step |
getName() |
Get equipment name |
getInletStream() |
Get inlet stream |
getOutletStream() |
Get outlet stream |
getPressure() |
Get operating pressure |
getTemperature() |
Get operating temperature |
getMechanicalDesign() |
Get mechanical design object |
needRecalculation() |
Check if recalculation needed |
| Method | Description |
|---|---|
initMechanicalLosses(shaftDiameter) |
Initialize seal gas and bearing loss model |
getSealGasConsumption() |
Get total seal gas consumption (Nm³/hr) |
getBearingLoss() |
Get total bearing power loss (kW) |
getMechanicalEfficiency() |
Get mechanical efficiency (0-1) |
ProcessEquipmentInterface
│
└── ProcessEquipmentBaseClass
│
├── TwoPortEquipment (inlet/outlet pattern)
│ ├── Heater, Cooler
│ ├── Compressor, Pump, Expander
│ ├── ThrottlingValve
│ └── ...
│
├── Separator (multi-outlet)
│ ├── ThreePhaseSeparator
│ ├── GasScrubber
│ └── ...
│
├── Mixer (multi-inlet)
├── Splitter (multi-outlet)
│
└── DistillationColumn
Documentation for separator equipment in NeqSim process simulation.
Location: neqsim.process.equipment.separator
Classes:
Separator - Two-phase gas-liquid separatorThreePhaseSeparator - Three-phase gas-oil-water separatorGasScrubber - Gas scrubbing separatorGasScrubberSimple - Simplified gas scrubberimport neqsim.process.equipment.separator.Separator;
Separator separator = new Separator("V-100", inletStream);
separator.run();
// Get outlet streams
Stream gasOut = separator.getGasOutStream();
Stream liquidOut = separator.getLiquidOutStream();
// Properties
double gasRate = gasOut.getFlowRate("kg/hr");
double liquidRate = liquidOut.getFlowRate("kg/hr");
double liquidLevel = separator.getLiquidLevel();
import neqsim.process.equipment.separator.ThreePhaseSeparator;
ThreePhaseSeparator separator = new ThreePhaseSeparator("V-200", inletStream);
separator.run();
// Get outlet streams
Stream gasOut = separator.getGasOutStream();
Stream oilOut = separator.getOilOutStream();
Stream waterOut = separator.getWaterOutStream();
// Water cut
double waterCut = separator.getWaterCut();
import neqsim.process.equipment.separator.GasScrubber;
GasScrubber scrubber = new GasScrubber("Inlet Scrubber", gasStream);
scrubber.run();
// Dry gas output
Stream dryGas = scrubber.getGasOutStream();
// Condensate removal
Stream condensate = scrubber.getLiquidOutStream();
// Set dimensions
separator.setInternalDiameter(2.0, "m");
separator.setLiquidVolume(10.0, "m3");
// Or specify residence time
separator.setLiquidResidenceTime(120.0, "sec");
separator.setSeparatorType("horizontal");
separator.setLength(10.0, "m");
separator.setInternalDiameter(2.5, "m");
// Enable dynamic mode
separator.setCalculateSteadyState(false);
// Set initial conditions
separator.setLiquidLevel(0.5); // 50% level
// Run transient
for (int i = 0; i < 100; i++) {
separator.runTransient();
double level = separator.getLiquidLevel();
double pressure = separator.getPressure();
}
// Set droplet removal efficiency
separator.setGasCarryUnderFraction(0.001); // 0.1% liquid in gas
separator.setLiquidCarryOverFraction(0.0001); // 0.01% gas in liquid
// HP Separator at 50 bar
Separator hpSep = new Separator("HP Sep", feedStream);
process.add(hpSep);
// Letdown valve
ThrottlingValve lpValve = new ThrottlingValve("LP Valve", hpSep.getLiquidOutStream());
lpValve.setOutletPressure(5.0, "bara");
process.add(lpValve);
// LP Separator at 5 bar
Separator lpSep = new Separator("LP Sep", lpValve.getOutletStream());
process.add(lpSep);
// Run process
process.run();
// Total gas production
double hpGas = hpSep.getGasOutStream().getFlowRate("MSm3/day");
double lpGas = lpSep.getGasOutStream().getFlowRate("MSm3/day");
double totalGas = hpGas + lpGas;
Documentation for distillation column equipment in NeqSim process simulation.
Location: neqsim.process.equipment.distillation
Classes:
DistillationColumn - Main distillation columnSimpleTray - Individual trayCondenser - Column condenserReboiler - Column reboilerimport neqsim.process.equipment.distillation.DistillationColumn;
// Create column with 10 trays, condenser, and reboiler
DistillationColumn column = new DistillationColumn("Deethanizer", 10, true, true);
column.addFeedStream(feedStream, 5); // Feed on tray 5
column.setCondenserTemperature(40.0, "C");
column.setReboilerTemperature(120.0, "C");
column.run();
// Get products
Stream overhead = column.getGasOutStream();
Stream bottoms = column.getLiquidOutStream();
For complex column configurations, use the fluent Builder API:
import neqsim.process.equipment.distillation.DistillationColumn;
import neqsim.process.equipment.distillation.DistillationColumn.SolverType;
// Build column with fluent API
DistillationColumn column = DistillationColumn.builder("Deethanizer")
.numberOfTrays(15)
.withCondenserAndReboiler()
.topPressure(25.0, "bara")
.bottomPressure(26.0, "bara")
.temperatureTolerance(0.001)
.massBalanceTolerance(0.01)
.maxIterations(100)
.solverType(SolverType.INSIDE_OUT)
.internalDiameter(2.5)
.addFeedStream(feedStream, 8)
.build();
column.run();
| Method | Description |
|---|---|
numberOfTrays(int) |
Set number of simple trays (excluding condenser/reboiler) |
withCondenser() |
Add condenser at top |
withReboiler() |
Add reboiler at bottom |
withCondenserAndReboiler() |
Add both |
topPressure(double, String) |
Set top pressure with unit |
bottomPressure(double, String) |
Set bottom pressure with unit |
pressure(double, String) |
Set same pressure top and bottom |
temperatureTolerance(double) |
Convergence tolerance for temperature |
massBalanceTolerance(double) |
Convergence tolerance for mass balance |
tolerance(double) |
Set all tolerances at once |
maxIterations(int) |
Maximum solver iterations |
solverType(SolverType) |
Set solver algorithm |
directSubstitution() |
Use direct substitution solver |
dampedSubstitution() |
Use damped substitution solver |
insideOut() |
Use inside-out solver |
relaxationFactor(double) |
Damping factor for solver |
internalDiameter(double) |
Column internal diameter (meters) |
multiPhaseCheck(boolean) |
Enable/disable multi-phase check |
addFeedStream(Stream, int) |
Add feed stream to specified tray |
build() |
Build the configured column |
// Constructor: (name, numTrays, hasCondenser, hasReboiler)
DistillationColumn column = new DistillationColumn("T-100", 20, true, true);
// Single feed
column.addFeedStream(feed, 10); // Tray 10 from bottom
// Multiple feeds
column.addFeedStream(feed1, 8);
column.addFeedStream(feed2, 12);
// Total condenser
column.setCondenserType("total");
// Partial condenser (vapor overhead)
column.setCondenserType("partial");
// Liquid side draw
column.addSideDraw(7, "liquid", 100.0, "kg/hr");
// Vapor side draw
column.addSideDraw(15, "vapor", 50.0, "kg/hr");
// Condenser temperature
column.setCondenserTemperature(40.0, "C");
// Reboiler temperature
column.setReboilerTemperature(120.0, "C");
// Top pressure
column.setTopPressure(15.0, "bara");
// Bottom pressure (or pressure drop)
column.setBottomPressure(16.0, "bara");
// Or specify pressure drop per tray
column.setPressureDropPerTray(0.05, "bar");
// Reflux ratio
column.setRefluxRatio(3.0);
// Condenser duty
column.setCondenserDuty(-5000000.0); // W (negative = cooling)
// Reboiler duty
column.setReboilerDuty(6000000.0); // W
// Boilup ratio
column.setBoilupRatio(2.5);
// Standard sequential solver
column.setSolverType(DistillationColumn.SolverType.STANDARD);
// Damped solver (more robust)
column.setSolverType(DistillationColumn.SolverType.DAMPED);
// Inside-out solver (fastest for converged cases)
column.setSolverType(DistillationColumn.SolverType.INSIDE_OUT);
// Maximum iterations
column.setMaxIterations(100);
// Tolerance
column.setTolerance(1e-6);
// Damping factor
column.setDampingFactor(0.5);
// Linear temperature profile initialization
column.setInitialTemperatureProfile("linear");
// Custom initialization
double[] initTemps = {120, 115, 110, 105, 100, 95, 90, 85, 80, 75, 70};
column.setInitialTemperatures(initTemps);
column.run();
// Temperature profile
for (int i = 0; i < column.getNumberOfTrays(); i++) {
double T = column.getTray(i).getTemperature("C");
System.out.println("Tray " + i + ": " + T + " °C");
}
// Composition profile
for (int i = 0; i < column.getNumberOfTrays(); i++) {
double[] x = column.getTray(i).getLiquidComposition();
double[] y = column.getTray(i).getVaporComposition();
}
double Qcond = column.getCondenserDuty(); // W
double Qreb = column.getReboilerDuty(); // W
System.out.println("Condenser duty: " + (-Qcond/1e6) + " MW");
System.out.println("Reboiler duty: " + (Qreb/1e6) + " MW");
// Product purities
double overheadPurity = overhead.getFluid().getComponent("ethane").getx();
double bottomsRecovery = 1.0 - (overhead.getFluid().getComponent("propane").getNumberOfmable() /
feedStream.getFluid().getComponent("propane").getNumberOfmable());
// Feed: NGL from gas plant
SystemInterface ngl = new SystemSrkEos(273.15 + 30, 25.0);
ngl.addComponent("methane", 0.02);
ngl.addComponent("ethane", 0.25);
ngl.addComponent("propane", 0.35);
ngl.addComponent("i-butane", 0.10);
ngl.addComponent("n-butane", 0.18);
ngl.addComponent("n-pentane", 0.10);
ngl.setMixingRule("classic");
Stream feed = new Stream("NGL Feed", ngl);
feed.setFlowRate(5000.0, "kg/hr");
ProcessSystem process = new ProcessSystem();
process.add(feed);
// Deethanizer column
DistillationColumn deethanizer = new DistillationColumn("Deethanizer", 25, true, true);
deethanizer.addFeedStream(feed, 12);
deethanizer.setTopPressure(25.0, "bara");
deethanizer.setCondenserTemperature(-10.0, "C");
deethanizer.setReboilerTemperature(100.0, "C");
deethanizer.setSolverType(DistillationColumn.SolverType.INSIDE_OUT);
process.add(deethanizer);
process.run();
// Results
Stream ethaneProduct = deethanizer.getGasOutStream();
Stream c3plusProduct = deethanizer.getLiquidOutStream();
System.out.println("Ethane product:");
System.out.println(" Flow: " + ethaneProduct.getFlowRate("kg/hr") + " kg/hr");
System.out.println(" C2 purity: " +
ethaneProduct.getFluid().getComponent("ethane").getx() * 100 + " mol%");
System.out.println("C3+ product:");
System.out.println(" Flow: " + c3plusProduct.getFlowRate("kg/hr") + " kg/hr");
System.out.println(" C2 content: " +
c3plusProduct.getFluid().getComponent("ethane").getx() * 100 + " mol%");
// Feed from deethanizer bottoms
DistillationColumn depropanizer = new DistillationColumn("Depropanizer", 30, true, true);
depropanizer.addFeedStream(c3plusProduct, 15);
depropanizer.setTopPressure(18.0, "bara");
depropanizer.setCondenserTemperature(45.0, "C");
depropanizer.setReboilerTemperature(110.0, "C");
process.add(depropanizer);
process.run();
Stream propaneProduct = depropanizer.getGasOutStream();
Stream c4plusProduct = depropanizer.getLiquidOutStream();
For absorption without reboiler:
DistillationColumn absorber = new DistillationColumn("Absorber", 10, false, false);
absorber.addFeedStream(gasStream, 1); // Gas at bottom
absorber.addFeedStream(leanSolvent, 10); // Solvent at top
absorber.run();
Stream richSolvent = absorber.getLiquidOutStream();
Stream sweetGas = absorber.getGasOutStream();
For stripping without condenser:
DistillationColumn stripper = new DistillationColumn("Stripper", 8, false, true);
stripper.addFeedStream(richSolvent, 1);
stripper.setReboilerTemperature(120.0, "C");
stripper.run();
Stream acidGas = stripper.getGasOutStream();
Stream leanSolvent = stripper.getLiquidOutStream();
This document describes the mathematical model and solver implementations that power the
DistillationColumn class in NeqSim. The class maps directly to the files
src/main/java/neqsim/process/equipment/distillation/DistillationColumn.java and
DistillationColumnMatrixSolver.java.
Each ideal-equilibrium tray satisfies the familiar MESH relationships:
Total mass balance (tray j)
[ V_{j-1} + L_{j+1} + F_j = V_j + L_j ]
Component balances
[ V_{j-1} y_{i,j-1} + L_{j+1} x_{i,j+1} + F_j z_{i,j} = V_j y_{i,j} + L_j x_{i,j} ]
Phase equilibrium (K-values)
[ y_{i,j} = K_{i,j} x_{i,j}, \qquad K_{i,j} = \frac{\hat f_{i,j}^{\text{vap}}}{\hat f_{i,j}^{\text{liq}}} ]
Energy balance
[ V_{j-1} h_{j-1}^{V} + L_{j+1} h_{j+1}^{L} + F_j h_j^{F} + Q_j = V_j h_j^{V} + L_j h_j^{L} ]
NeqSim evaluates fugacity-based K-values and molar enthalpies through the active
SystemInterface. The matrix solver also uses linearized component balances in
tridiagonal form:
[ A_j l_{i,j-1} + B_j l_{i,j} + C_j l_{i,j+1} = D_{i,j} ]
with stripping factors (S_j = K_{i,j} V_j / L_j) embedded in the diagonal terms.
Temperature updates rely on the log-Newton step derived from (\sum_i y_{i,j}=1):
[ \Delta T_j = -\frac{\ln(\sum_i K_{i,j} x_{i,j}) R T_j^2}{h_j^{V} - h_j^{L}} ]
The code limits (\Delta T_j) to ±5 K and enforces bounds of 50–1000 K for numerical stability.
addFeedStream; unassigned feeds are
auto-placed near matching tray temperatures.init() runs the lowest feed tray, extrapolates temperatures
towards condenser and reboiler, and links neighbouring trays with vapour/liquid streams.prepareColumnForSolve() imposes a linear pressure drop between the
configured bottom and top pressures (or inferred tray values when unspecified).| Solver | Class/Method | Strategy | Notes |
|---|---|---|---|
DIRECT_SUBSTITUTION |
solveSequential() |
Classic two-sweep sequential substitution (liquids down, vapours up) with adaptive relaxation on temperatures and streams. | Converges robustly for well-behaved systems; default choice. |
DAMPED_SUBSTITUTION |
runDamped() |
Same equations as direct substitution but starts with a user-defined fixed relaxation factor before enabling adaptation. | Useful for stiff columns where the default step overshoots. |
INSIDE_OUT |
solveInsideOut() |
Quadrat-structure inside-out method: streams are relaxed against previous iterates while tray properties update using enthalpy-driven temperature corrections. | Balances mass/energy less frequently to reduce cost and supports a polishing phase for tight tolerances. |
BROYDEN (experimental) |
runBroyden() |
Applies a secant correction on tray temperatures, effectively mixing current and previous deltas. | Handy for rapid feasibility studies but less stable than inside-out. |
MATRIX_SOLVER |
DistillationColumnMatrixSolver.solve() |
Builds component flow equations into a TDMA system, blends constant molar overflow (CMO) estimates with sum-rate flows, then updates temperatures via the log-Newton scheme above. | Eliminates explicit stream tearing by solving component balances directly; still refines temperatures iteratively. |
previousGasStreams, previousLiquidStreams).applyRelaxation() mixes flow, temperature, pressure, and composition prior to cloning.feedFlows, vapor/liquid split) per tray.system.init(2) for
enthalpy data and system.init(1) afterwards to refresh K-values.Once any solver converges, the top gas outlet (gasOutStream) and bottom liquid outlet
(liquidOutStream) are cloned from the respective trays. Mass, energy, and iteration statistics
are exposed through getters such as getLastIterationCount(), getLastMassResidual(), and
getLastEnergyResidual().
Documentation for mass transfer columns in NeqSim.
Location: neqsim.process.equipment.absorber
Classes:
| Class | Description |
|---|---|
Absorber |
General absorption column |
SimpleAbsorber |
Simplified absorber model |
WaterStripperColumn |
Water stripping column |
Absorbers transfer components from gas to liquid phase, while strippers transfer from liquid to gas.
import neqsim.process.equipment.absorber.Absorber;
Absorber absorber = new Absorber("Amine Absorber");
absorber.addGasInStream(gasStream);
absorber.addSolventInStream(amineSolution);
absorber.setNumberOfTheoreticalStages(10);
absorber.run();
Stream sweetGas = absorber.getGasOutStream();
Stream richAmine = absorber.getLiquidOutStream();
// Component removal efficiency
absorber.setRemovalEfficiency("CO2", 0.95); // 95% CO2 removal
absorber.setRemovalEfficiency("H2S", 0.99); // 99% H2S removal
absorber.setNumberOfTheoreticalStages(20);
absorber.setStageEfficiency(0.7); // Murphree efficiency
import neqsim.process.equipment.absorber.WaterStripperColumn;
WaterStripperColumn stripper = new WaterStripperColumn("Regenerator");
stripper.setLiquidInStream(richAmine);
stripper.setNumberOfStages(15);
stripper.setReboilerTemperature(120.0, "C");
stripper.run();
Stream leanAmine = stripper.getLiquidOutStream();
Stream acidGas = stripper.getGasOutStream();
Simplified mass transfer model.
import neqsim.process.equipment.absorber.SimpleAbsorber;
SimpleAbsorber absorber = new SimpleAbsorber("CO2 Absorber");
absorber.addGasInStream(feedGas);
absorber.addSolventInStream(solvent);
absorber.setAbsorptionEfficiency(0.90);
absorber.run();
import neqsim.thermo.system.SystemSrkCPAstatoil;
import neqsim.process.equipment.stream.Stream;
import neqsim.process.equipment.absorber.Absorber;
// Sour gas
SystemSrkCPAstatoil sourGas = new SystemSrkCPAstatoil(313.15, 70.0);
sourGas.addComponent("methane", 0.85);
sourGas.addComponent("CO2", 0.10);
sourGas.addComponent("H2S", 0.01);
sourGas.addComponent("water", 0.04);
sourGas.setMixingRule("classic");
Stream gasIn = new Stream("Sour Gas", sourGas);
gasIn.setFlowRate(100000.0, "Sm3/hr");
gasIn.run();
// Lean amine (MDEA solution)
SystemSrkCPAstatoil amine = new SystemSrkCPAstatoil(313.15, 70.0);
amine.addComponent("water", 0.50);
amine.addComponent("MDEA", 0.50);
amine.setMixingRule("classic");
Stream leanAmine = new Stream("Lean Amine", amine);
leanAmine.setFlowRate(50000.0, "kg/hr");
leanAmine.run();
// Absorber
Absorber absorber = new Absorber("Amine Contactor");
absorber.addGasInStream(gasIn);
absorber.addSolventInStream(leanAmine);
absorber.setNumberOfTheoreticalStages(15);
absorber.run();
// Results
Stream sweetGas = absorber.getGasOutStream();
double co2Out = sweetGas.getFluid().getMoleFraction("CO2") * 1e6; // ppm
System.out.println("Sweet gas CO2: " + co2Out + " ppm");
// Wet natural gas
SystemSrkEos wetGas = new SystemSrkEos(303.15, 70.0);
wetGas.addComponent("methane", 0.90);
wetGas.addComponent("ethane", 0.05);
wetGas.addComponent("propane", 0.03);
wetGas.addComponent("water", 0.02);
wetGas.setMixingRule("classic");
Stream gasIn = new Stream("Wet Gas", wetGas);
gasIn.setFlowRate(5000000.0, "Sm3/day");
gasIn.run();
// Lean TEG
SystemSrkEos teg = new SystemSrkEos(313.15, 70.0);
teg.addComponent("TEG", 0.99);
teg.addComponent("water", 0.01);
teg.setMixingRule("classic");
Stream leanTEG = new Stream("Lean TEG", teg);
leanTEG.setFlowRate(1000.0, "kg/hr");
leanTEG.run();
// Contactor
Absorber contactor = new Absorber("TEG Contactor");
contactor.addGasInStream(gasIn);
contactor.addSolventInStream(leanTEG);
contactor.setNumberOfTheoreticalStages(3);
contactor.run();
Stream dryGas = contactor.getGasOutStream();
double waterContent = dryGas.getFluid().getMoleFraction("water") * 1e6;
System.out.println("Dry gas water content: " + waterContent + " ppm");
// Gas with methanol
SystemSrkEos gas = new SystemSrkEos(280.0, 50.0);
gas.addComponent("methane", 0.95);
gas.addComponent("methanol", 0.03);
gas.addComponent("water", 0.02);
gas.setMixingRule("classic");
Stream gasIn = new Stream("Gas", gas);
gasIn.setFlowRate(10000.0, "kg/hr");
gasIn.run();
// Wash water
SystemSrkEos water = new SystemSrkEos(290.0, 50.0);
water.addComponent("water", 1.0);
water.setMixingRule("classic");
Stream washWater = new Stream("Wash Water", water);
washWater.setFlowRate(500.0, "kg/hr");
washWater.run();
// Absorber
SimpleAbsorber waterWash = new SimpleAbsorber("Water Wash");
waterWash.addGasInStream(gasIn);
waterWash.addSolventInStream(washWater);
waterWash.setAbsorptionEfficiency(0.85);
waterWash.run();
Stream cleanGas = waterWash.getGasOutStream();
double meohRemaining = cleanGas.getFluid().getMoleFraction("methanol") * 100;
System.out.println("Methanol in clean gas: " + meohRemaining + " mol%");
This page outlines the basic model implemented in the MembraneSeparator unit. The unit is intended for simple simulations of gas separation membranes or pervaporation modules used in purification and CO2 capture.
For each component $i$ a constant permeate fraction $f_i$ can be specified. The molar amount transferred to the permeate side is $$ N_i^{\text{perm}} = f_i N_i^{\text{feed}} $$ where $N_i^{\text{feed}}$ is the molar amount in the feed stream. Components without a specified fraction use a global default value.
A more rigorous model could employ Fick's law of diffusion through the membrane $$ J_i = P_i \left(p_{i,\text{feed}} - p_{i,\text{perm}}\right) $$ where $P_i$ is the permeability of component $i$ and $p_i$ are partial pressures. The separator can now perform this calculation mode when permeabilities and a membrane area are supplied.
MembraneSeparator mem = new MembraneSeparator("mem", feedStream);
mem.setDefaultPermeateFraction(0.1); // 10 % of each component permeates
mem.setPermeateFraction("CO2", 0.5); // override CO2 fraction
// Alternative using permeability coefficients mem.clearPermeateFractions(); mem.setMembraneArea(5.0); // m^2 mem.setPermeability("CO2", 5e-6); // mol/(m2sPa) mem.setPermeability("methane", 1e-6);
After running the process, the permeate and retentate streams can be obtained via `getPermeateStream()` and `getRetentateStream()`.
## Membrane Equipment
# Membrane Separation Equipment
Documentation for membrane separation equipment in NeqSim process simulation.
## Table of Contents
- [Overview](#overview)
- [MembraneSeparator Class](#membraneseparator-class)
- [Permeation Models](#permeation-models)
- [Configuration](#configuration)
- [Usage Examples](#usage-examples)
---
## Overview
**Location:** `neqsim.process.equipment.membrane`
**Classes:**
| Class | Description |
|-------|-------------|
| `MembraneSeparator` | Generic membrane separation unit |
Membrane separators provide selective separation based on component permeabilities through a membrane material. Applications include:
- CO₂ removal from natural gas
- Hydrogen recovery
- Nitrogen generation
- Dehydration
- Vapor/gas separation
---
## MembraneSeparator Class
### Basic Usage
```java
import neqsim.process.equipment.membrane.MembraneSeparator;
// Create membrane separator
MembraneSeparator membrane = new MembraneSeparator("CO2 Membrane", feedStream);
// Set permeate fractions for each component
membrane.setPermeateFraction("CO2", 0.95); // 95% of CO2 permeates
membrane.setPermeateFraction("methane", 0.05); // 5% of methane permeates
membrane.setPermeateFraction("ethane", 0.03); // 3% of ethane permeates
membrane.run();
// Get output streams
StreamInterface permeate = membrane.getPermeateStream();
StreamInterface retentate = membrane.getRetentateStream();
// With name only
MembraneSeparator membrane = new MembraneSeparator("MEM-100");
membrane.setInletStream(feedStream);
// With name and inlet stream
MembraneSeparator membrane = new MembraneSeparator("MEM-100", feedStream);
The simplest approach specifies what fraction of each component permeates:
// Set permeate fraction (0.0 to 1.0)
membrane.setPermeateFraction("CO2", 0.90);
membrane.setPermeateFraction("H2S", 0.85);
membrane.setPermeateFraction("methane", 0.02);
membrane.setPermeateFraction("ethane", 0.01);
membrane.setPermeateFraction("propane", 0.005);
// Set default for unlisted components
membrane.setDefaultPermeateFraction(0.01);
For more rigorous calculations using permeability coefficients:
// Set membrane area
membrane.setMembraneArea(100.0); // m²
// Set permeability for each component (mol/(m²·s·Pa))
membrane.setPermeability("CO2", 1.0e-9);
membrane.setPermeability("methane", 2.0e-11);
membrane.setPermeability("nitrogen", 5.0e-12);
The selectivity of component A over B is:
$$\alpha_{A/B} = \frac{P_A}{P_B}$$
Where $P_A$ and $P_B$ are the permeabilities of components A and B.
Typical selectivities for polymeric membranes:
| Separation | Selectivity |
|---|---|
| CO₂/CH₄ | 15-50 |
| H₂/CH₄ | 30-100 |
| O₂/N₂ | 4-8 |
| H₂O/CH₄ | >100 |
// Inlet stream conditions affect separation
feedStream.setPressure(50.0, "bara"); // High feed pressure
feedStream.setTemperature(40.0, "C");
// Permeate side typically at lower pressure
// (Pressure difference drives permeation)
The stage cut (θ) is the fraction of feed that permeates:
$$\theta = \frac{\dot{n}_{permeate}}{\dot{n}_{feed}}$$
membrane.run();
double feedFlow = feedStream.getFlowRate("kmol/hr");
double permeateFlow = membrane.getPermeateStream().getFlowRate("kmol/hr");
double stageCut = permeateFlow / feedFlow;
System.out.println("Stage cut: " + (stageCut * 100) + " %");
The permeate contains components that pass through the membrane:
StreamInterface permeate = membrane.getPermeateStream();
// Get permeate composition
double co2InPermeate = permeate.getFluid().getMoleFraction("CO2");
double permeateFlow = permeate.getFlowRate("kmol/hr");
System.out.println("Permeate CO2: " + (co2InPermeate * 100) + " mol%");
The retentate contains components that do not permeate:
StreamInterface retentate = membrane.getRetentateStream();
// Get retentate (product gas) composition
double co2InRetentate = retentate.getFluid().getMoleFraction("CO2");
double ch4InRetentate = retentate.getFluid().getMoleFraction("methane");
System.out.println("Retentate CO2: " + (co2InRetentate * 100) + " mol%");
System.out.println("Retentate CH4: " + (ch4InRetentate * 100) + " mol%");
ProcessSystem process = new ProcessSystem();
// Feed gas with CO2
SystemInterface feedFluid = new SystemSrkEos(310.0, 60.0);
feedFluid.addComponent("methane", 0.85);
feedFluid.addComponent("ethane", 0.05);
feedFluid.addComponent("propane", 0.02);
feedFluid.addComponent("CO2", 0.08);
feedFluid.setMixingRule("classic");
Stream feedGas = new Stream("Feed Gas", feedFluid);
feedGas.setFlowRate(100000.0, "Sm3/day");
process.add(feedGas);
// Membrane unit
MembraneSeparator membrane = new MembraneSeparator("CO2 Membrane", feedGas);
membrane.setPermeateFraction("CO2", 0.90);
membrane.setPermeateFraction("methane", 0.03);
membrane.setPermeateFraction("ethane", 0.02);
membrane.setPermeateFraction("propane", 0.01);
process.add(membrane);
// Run
process.run();
// Check CO2 spec
double productCO2 = membrane.getRetentateStream().getFluid().getMoleFraction("CO2");
System.out.println("Product gas CO2: " + (productCO2 * 100) + " mol%");
// Methane recovery
double feedCH4 = feedGas.getFlowRate("Sm3/day") * 0.85;
double productCH4 = membrane.getRetentateStream().getFlowRate("Sm3/day") *
membrane.getRetentateStream().getFluid().getMoleFraction("methane");
double recovery = productCH4 / feedCH4 * 100;
System.out.println("Methane recovery: " + recovery + " %");
For deep CO₂ removal, multiple stages may be required:
// First stage membrane
MembraneSeparator stage1 = new MembraneSeparator("Stage 1", feedGas);
stage1.setPermeateFraction("CO2", 0.80);
stage1.setPermeateFraction("methane", 0.05);
process.add(stage1);
// Second stage on retentate
MembraneSeparator stage2 = new MembraneSeparator("Stage 2", stage1.getRetentateStream());
stage2.setPermeateFraction("CO2", 0.80);
stage2.setPermeateFraction("methane", 0.05);
process.add(stage2);
// Recycle permeate from stage 2 to stage 1 feed
Mixer mixer = new Mixer("Feed Mixer");
mixer.addStream(feedGas);
mixer.addStream(stage2.getPermeateStream());
process.add(mixer);
// Connect mixer to stage 1
stage1.setInletStream(mixer.getOutletStream());
// Add recycle
Recycle recycle = new Recycle("Membrane Recycle");
recycle.addStream(stage2.getPermeateStream());
recycle.setOutletStream(mixer);
process.add(recycle);
process.run();
// Refinery off-gas
SystemInterface offgas = new SystemSrkEos(320.0, 30.0);
offgas.addComponent("hydrogen", 0.40);
offgas.addComponent("methane", 0.35);
offgas.addComponent("ethane", 0.15);
offgas.addComponent("propane", 0.10);
offgas.setMixingRule("classic");
Stream feed = new Stream("Off-gas", offgas);
feed.setFlowRate(5000.0, "Sm3/hr");
// H2 selective membrane
MembraneSeparator h2Membrane = new MembraneSeparator("H2 Membrane", feed);
h2Membrane.setPermeateFraction("hydrogen", 0.95);
h2Membrane.setPermeateFraction("methane", 0.08);
h2Membrane.setPermeateFraction("ethane", 0.02);
h2Membrane.setPermeateFraction("propane", 0.01);
h2Membrane.run();
// H2 purity in permeate
double h2Purity = h2Membrane.getPermeateStream().getFluid().getMoleFraction("hydrogen");
System.out.println("H2 purity: " + (h2Purity * 100) + " mol%");
// Compress permeate for recycle or further processing
Compressor permeateComp = new Compressor("Permeate Comp", membrane.getPermeateStream());
permeateComp.setOutletPressure(feedPressure, "bara");
permeateComp.setIsentropicEfficiency(0.75);
// Cool membrane feed to improve selectivity
Cooler feedCooler = new Cooler("Membrane Feed Cooler", feedGas);
feedCooler.setOutTemperature(30.0, "C");
membrane.setInletStream(feedCooler.getOutletStream());
| Type | Applications | Selectivity |
|---|---|---|
| Cellulose acetate | CO₂/CH₄ | 15-25 |
| Polyimide | CO₂/CH₄, H₂ | 20-50 |
| Polysulfone | O₂/N₂ | 5-6 |
| PDMS | VOC removal | varies |
Documentation for filter equipment in NeqSim process simulation.
Location: neqsim.process.equipment.filter
Classes:
| Class | Description |
|---|---|
Filter |
Generic filter unit |
CharCoalFilter |
Activated charcoal filter |
Filters are used to remove specific components or contaminants from process streams. Applications include:
import neqsim.process.equipment.filter.Filter;
// Create filter on gas stream
Filter filter = new Filter("Particulate Filter", gasStream);
filter.run();
// Get outlet stream
StreamInterface cleanGas = filter.getOutletStream();
Activated charcoal filter for removing specific components.
import neqsim.process.equipment.filter.CharCoalFilter;
// Create charcoal filter
CharCoalFilter charFilter = new CharCoalFilter("Mercury Filter", gasStream);
charFilter.setRemovalEfficiency("mercury", 0.99); // 99% removal
charFilter.run();
// Get treated stream
StreamInterface treatedGas = charFilter.getOutletStream();
// Set removal efficiency for specific components
charFilter.setRemovalEfficiency("mercury", 0.99);
charFilter.setRemovalEfficiency("H2S", 0.95);
charFilter.setRemovalEfficiency("benzene", 0.90);
ProcessSystem process = new ProcessSystem();
// Raw gas feed
Stream rawGas = new Stream("Raw Gas", gasFluid);
rawGas.setFlowRate(100000.0, "Sm3/day");
process.add(rawGas);
// Particulate filter
Filter particleFilter = new Filter("Inlet Filter", rawGas);
process.add(particleFilter);
// Mercury removal
CharCoalFilter hgFilter = new CharCoalFilter("Hg Guard Bed",
particleFilter.getOutletStream());
hgFilter.setRemovalEfficiency("mercury", 0.999);
process.add(hgFilter);
// Run
process.run();
// Upstream of cryogenic section
CharCoalFilter mercuryRemoval = new CharCoalFilter("Mercury Removal", feed);
mercuryRemoval.setRemovalEfficiency("mercury", 0.9999); // Critical for aluminum equipment
mercuryRemoval.run();
double outletMercury = mercuryRemoval.getOutletStream()
.getFluid().getComponent("mercury").getFlowRate("g/hr");
System.out.println("Outlet mercury: " + outletMercury + " g/hr");
Documentation for compression equipment in NeqSim process simulation.
📖 Detailed Curve Documentation: For comprehensive information on compressor curves, including multi-speed vs single-speed handling, surge curves, and stone wall curves, see Compressor Curves and Performance Maps.
Location: neqsim.process.equipment.compressor
Classes:
Compressor - General compressorCompressorInterface - Compressor interfaceCompressorChartInterface - Performance map interfaceimport neqsim.process.equipment.compressor.Compressor;
Compressor compressor = new Compressor("K-100", inletStream);
compressor.setOutletPressure(80.0, "bara");
compressor.setIsentropicEfficiency(0.75);
compressor.run();
// Results
double power = compressor.getPower("kW");
double outletT = compressor.getOutletStream().getTemperature("C");
double polytropicHead = compressor.getPolytropicHead("kJ/kg");
System.out.println("Power: " + power + " kW");
System.out.println("Outlet temperature: " + outletT + " °C");
compressor.setIsentropicEfficiency(0.75); // 75%
compressor.setUsePolytropicCalc(false);
compressor.run();
double isentropicHead = compressor.getIsentropicHead("kJ/kg");
double isentropicPower = compressor.getPower("kW");
More accurate for real gas behavior.
compressor.setPolytropicEfficiency(0.80); // 80%
compressor.setUsePolytropicCalc(true);
compressor.run();
double polytropicHead = compressor.getPolytropicHead("kJ/kg");
double polytropicExponent = compressor.getPolytropicExponent();
compressor.setPower(5000.0, "kW"); // Specify power
compressor.setIsentropicEfficiency(0.75);
compressor.run();
double outletP = compressor.getOutletStream().getPressure("bara");
$$\eta_{is} = \frac{H_{is}}{H_{actual}} = \frac{T_{2s} - T_1}{T_2 - T_1}$$
$$\eta_p = \frac{n-1}{n} \cdot \frac{k}{k-1}$$
Where:
NeqSim supports detailed compressor performance maps with multiple speed curves. For comprehensive documentation, see Compressor Curves and Performance Maps.
// Define speed curves
double[] speeds = {8000, 9000, 10000, 11000}; // RPM
// For each speed: arrays of flow, head, efficiency
double[][] flows = { {flow1_curve1, flow2_curve1}, {flow1_curve2, flow2_curve2}, ... };
double[][] heads = { {head1_curve1, head2_curve1}, {head1_curve2, head2_curve2}, ... };
double[][] efficiencies = { {eff1_curve1, eff2_curve1}, {eff1_curve2, eff2_curve2}, ... };
CompressorChartInterface chart = compressor.getCompressorChart();
chart.setCurves(chartConditions, speeds, flows, heads, flows, efficiencies);
chart.setHeadUnit("kJ/kg");
compressor.setSpeed(10000); // Operating speed
compressor.setSpeed(10000); // RPM
compressor.run();
double actualFlow = compressor.getActualFlow("m3/hr");
double head = compressor.getPolytropicHead("kJ/kg");
double efficiency = compressor.getPolytropicEfficiency();
| Compressor Type | Surge/Stone Wall | Setting Method |
|---|---|---|
| Multi-speed (≥2 speeds) | Curves (interpolated) | Multiple flow/head points |
| Single-speed (1 speed) | Single points (constant) | Single flow/head point |
// Distance to surge (positive = above surge, safe)
double distanceToSurge = compressor.getDistanceToSurge();
// Distance to stone wall (positive = below choke, safe)
double distanceToStoneWall = compressor.getDistanceToStoneWall();
// Check if in surge
boolean isSurge = compressor.getCompressorChart().getSurgeCurve().isSurge(head, flow);
// Get surge flow rate
double surgeFlow = compressor.getSurgeFlowRate();
// For single-speed compressors, surge is a single point
double[] surgeFlow = {5607.45}; // Single value
double[] surgeHead = {150.0}; // Single value
compressor.getCompressorChart().getSurgeCurve().setCurve(chartConditions, surgeFlow, surgeHead);
// Stone wall is also a single point
double[] stoneWallFlow = {9758.49};
double[] stoneWallHead = {112.65};
compressor.getCompressorChart().getStoneWallCurve().setCurve(chartConditions, stoneWallFlow, stoneWallHead);
📖 See Also: Compressor Curves and Performance Maps for detailed documentation on curve setup, interpolation methods, and Python examples.
ProcessSystem process = new ProcessSystem();
// Stage 1: 20 -> 50 bar
Compressor stage1 = new Compressor("K-100A", inletStream);
stage1.setOutletPressure(50.0, "bara");
stage1.setPolytropicEfficiency(0.78);
process.add(stage1);
// Intercooler
Cooler intercooler = new Cooler("E-100", stage1.getOutletStream());
intercooler.setOutTemperature(40.0, "C");
process.add(intercooler);
// Stage 2: 50 -> 120 bar
Compressor stage2 = new Compressor("K-100B", intercooler.getOutletStream());
stage2.setOutletPressure(120.0, "bara");
stage2.setPolytropicEfficiency(0.78);
process.add(stage2);
// Aftercooler
Cooler aftercooler = new Cooler("E-101", stage2.getOutletStream());
aftercooler.setOutTemperature(40.0, "C");
process.add(aftercooler);
process.run();
// Total power
double totalPower = stage1.getPower("kW") + stage2.getPower("kW");
System.out.println("Total compression power: " + totalPower + " kW");
For minimum work with equal stage ratios:
$$r_{stage} = \left(\frac{P_{out}}{P_{in}}\right)^{1/n}$$
double overallRatio = 120.0 / 20.0; // 6:1
int numStages = 2;
double stageRatio = Math.pow(overallRatio, 1.0 / numStages); // √6 ≈ 2.45
// Recycle valve for antisurge
Splitter recycle = new Splitter("Antisurge Recycle", stage2.getOutletStream());
recycle.setSplitFactors(new double[]{0.9, 0.1}); // 10% recycle
// Mix with inlet
Mixer mixer = new Mixer("Inlet Mixer");
mixer.addStream(inletStream);
mixer.addStream(recycle.getSplitStream(1));
// Account for compressibility
double Z1 = compressor.getInletStream().getZ();
double Z2 = compressor.getOutletStream().getZ();
double Zavg = (Z1 + Z2) / 2;
// Include mechanical losses
compressor.setMechanicalEfficiency(0.98);
double shaftPower = compressor.getShaftPower("kW");
double gasHorsepower = compressor.getGasHorsepower("hp");
// Export gas at 100 MSm³/day
SystemInterface gas = new SystemSrkEos(288.15, 30.0);
gas.addComponent("methane", 0.92);
gas.addComponent("ethane", 0.04);
gas.addComponent("propane", 0.02);
gas.addComponent("CO2", 0.01);
gas.addComponent("nitrogen", 0.01);
gas.setMixingRule("classic");
Stream inlet = new Stream("Inlet Gas", gas);
inlet.setFlowRate(100.0, "MSm3/day");
inlet.setTemperature(30.0, "C");
inlet.setPressure(30.0, "bara");
ProcessSystem process = new ProcessSystem();
process.add(inlet);
// 3-stage compression to 200 bar
double[] stagePressures = {55, 105, 200};
Stream currentStream = inlet;
for (int i = 0; i < 3; i++) {
Compressor comp = new Compressor("K-10" + (i+1), currentStream);
comp.setOutletPressure(stagePressures[i], "bara");
comp.setPolytropicEfficiency(0.78);
comp.setUsePolytropicCalc(true);
process.add(comp);
Cooler cooler = new Cooler("E-10" + (i+1), comp.getOutletStream());
cooler.setOutTemperature(40.0, "C");
process.add(cooler);
currentStream = cooler.getOutletStream();
}
process.run();
// Report
System.out.println("\n=== Compression Summary ===");
double totalPower = 0;
for (int i = 1; i <= 3; i++) {
Compressor c = (Compressor) process.getUnit("K-10" + i);
totalPower += c.getPower("MW");
System.out.printf("Stage %d: %.2f bar -> %.2f bar, %.2f MW%n",
i, c.getInletStream().getPressure("bara"),
c.getOutletStream().getPressure("bara"),
c.getPower("MW"));
}
System.out.println("Total power: " + totalPower + " MW");
When manufacturer performance data is not available, NeqSim can automatically generate realistic compressor curves using predefined templates.
// 1. Create and run compressor to establish design point
Compressor comp = new Compressor("K-100", inletStream);
comp.setOutletPressure(100.0, "bara");
comp.setPolytropicEfficiency(0.78);
comp.setSpeed(10000);
comp.run();
// 2. Generate curves from template
CompressorChartGenerator generator = new CompressorChartGenerator(comp);
CompressorChartInterface chart = generator.generateFromTemplate("PIPELINE", 5);
// 3. Apply and use
comp.setCompressorChart(chart);
comp.run();
| Category | Templates |
|---|---|
| Basic | CENTRIFUGAL_STANDARD, CENTRIFUGAL_HIGH_FLOW, CENTRIFUGAL_HIGH_HEAD |
| Application | PIPELINE, EXPORT, INJECTION, GAS_LIFT, REFRIGERATION, BOOSTER |
| Type | SINGLE_STAGE, MULTISTAGE_INLINE, INTEGRALLY_GEARED, OVERHUNG |
| Use Case | Recommended Template |
|---|---|
| Gas transmission | PIPELINE |
| Offshore export | EXPORT |
| Gas injection/EOR | INJECTION |
| Artificial lift | GAS_LIFT |
| LNG/refrigeration | REFRIGERATION |
| General purpose | CENTRIFUGAL_STANDARD |
📖 Detailed Documentation: See Compressor Curves - Automatic Generation for complete API reference, advanced corrections, and examples.
NeqSim supports modeling of compressor mechanical losses and seal gas consumption per API 617 (bearings) and API 692 (dry gas seals).
The CompressorMechanicalLosses class models:
| Component | Standard | Description |
|---|---|---|
| Dry Gas Seals | API 692 | Primary/secondary leakage, buffer gas, separation gas |
| Bearings | API 617 | Radial and thrust bearing friction losses |
| Lube Oil System | API 614 | Oil flow requirements and cooler duty |
// Create and run compressor
Compressor compressor = new Compressor("K-100", inletStream);
compressor.setOutletPressure(100.0, "bara");
compressor.setSpeed(10000);
compressor.run();
// Initialize mechanical losses with shaft diameter (mm)
compressor.initMechanicalLosses(120.0);
// Get results
double sealGas = compressor.getSealGasConsumption(); // Nm³/hr total
double bearingLoss = compressor.getBearingLoss(); // kW
double mechEfficiency = compressor.getMechanicalEfficiency(); // 0-1
System.out.println("Seal gas consumption: " + sealGas + " Nm³/hr");
System.out.println("Bearing power loss: " + bearingLoss + " kW");
System.out.println("Mechanical efficiency: " + (mechEfficiency * 100) + "%");
CompressorMechanicalLosses losses = compressor.getMechanicalLosses();
// Available seal types
losses.setSealType(CompressorMechanicalLosses.SealType.DRY_GAS_TANDEM); // Most common
losses.setSealType(CompressorMechanicalLosses.SealType.DRY_GAS_DOUBLE); // Lower leakage
losses.setSealType(CompressorMechanicalLosses.SealType.DRY_GAS_SINGLE); // Higher leakage
losses.setSealType(CompressorMechanicalLosses.SealType.LABYRINTH); // Non-contacting
losses.setSealType(CompressorMechanicalLosses.SealType.OIL_FILM); // Legacy
| Flow Type | Description | Typical Range |
|---|---|---|
| Primary Leakage | Gas escaping past primary seal | 0.5-5 Nm³/hr per seal |
| Secondary Leakage | Gas past secondary seal (tandem) | 10-30% of primary |
| Buffer Gas | Clean gas between seals | 2-10 Nm³/hr per seal |
| Separation Gas | N₂ to prevent oil mist ingress | 1-5 Nm³/hr per seal |
// Individual seal gas flows
double primaryLeak = losses.calculatePrimarySealLeakage(); // Nm³/hr
double secondaryLeak = losses.calculateSecondarySealLeakage(); // Nm³/hr
double bufferGas = losses.calculateBufferGasFlow(); // Nm³/hr
double separationGas = losses.calculateSeparationGasFlow(); // Nm³/hr
// Total consumption
double total = losses.getTotalSealGasConsumption(); // Nm³/hr
// Available bearing types
losses.setBearingType(CompressorMechanicalLosses.BearingType.TILTING_PAD); // Standard
losses.setBearingType(CompressorMechanicalLosses.BearingType.PLAIN_SLEEVE); // Higher loss
losses.setBearingType(CompressorMechanicalLosses.BearingType.MAGNETIC_ACTIVE); // Very low loss
losses.setBearingType(CompressorMechanicalLosses.BearingType.GAS_FOIL); // Oil-free
// Bearing losses
double radialLoss = losses.calculateRadialBearingLoss(); // kW
double thrustLoss = losses.calculateThrustBearingLoss(); // kW
double totalBearingLoss = losses.getTotalBearingLoss(); // kW
// Lube oil system
losses.setLubeOilInletTemp(40.0); // °C
losses.setLubeOilOutletTemp(55.0); // °C
double oilFlow = losses.calculateLubeOilFlowRate(); // L/min
double coolerDuty = losses.calculateLubeOilCoolerDuty(); // kW
CompressorMechanicalLosses losses = compressor.initMechanicalLosses(100.0);
// Seal configuration
losses.setSealType(CompressorMechanicalLosses.SealType.DRY_GAS_TANDEM);
losses.setNumberOfSeals(2); // Typically 2 for single-shaft
losses.setSealGasSupplyPressure(105.0); // bara
losses.setSealGasSupplyTemperature(40.0); // °C
// Bearing configuration
losses.setBearingType(CompressorMechanicalLosses.BearingType.TILTING_PAD);
losses.setNumberOfRadialBearings(2);
// Compressor updates operating conditions automatically
compressor.updateMechanicalLosses();
// Natural gas compressor with mechanical losses
SystemInterface gas = new SystemSrkEos(298.0, 50.0);
gas.addComponent("methane", 0.9);
gas.addComponent("ethane", 0.1);
gas.setMixingRule("classic");
Stream inlet = new Stream("inlet", gas);
inlet.setFlowRate(5000.0, "kg/hr");
inlet.run();
// Create and configure compressor
Compressor comp = new Compressor("HP Compressor", inlet);
comp.setOutletPressure(150.0, "bara");
comp.setPolytropicEfficiency(0.78);
comp.setSpeed(12000);
comp.run();
// Configure mechanical losses
CompressorMechanicalLosses losses = comp.initMechanicalLosses(120.0);
losses.setSealType(CompressorMechanicalLosses.SealType.DRY_GAS_TANDEM);
losses.setBearingType(CompressorMechanicalLosses.BearingType.TILTING_PAD);
// Print summary
System.out.println("=== Compressor Results ===");
System.out.println("Power: " + comp.getPower("kW") + " kW");
System.out.println("Polytropic head: " + comp.getPolytropicHead("kJ/kg") + " kJ/kg");
System.out.println();
System.out.println("=== Mechanical Losses ===");
System.out.println("Seal gas consumption: " + comp.getSealGasConsumption() + " Nm³/hr");
System.out.println(" - Primary leakage: " + losses.calculatePrimarySealLeakage() + " Nm³/hr");
System.out.println(" - Buffer gas: " + losses.calculateBufferGasFlow() + " Nm³/hr");
System.out.println("Bearing loss: " + comp.getBearingLoss() + " kW");
System.out.println("Mechanical efficiency: " + (comp.getMechanicalEfficiency() * 100) + "%");
System.out.println();
System.out.println("=== Lube Oil System ===");
System.out.println("Oil flow: " + losses.calculateLubeOilFlowRate() + " L/min");
System.out.println("Cooler duty: " + losses.calculateLubeOilCoolerDuty() + " kW");
from jpype import JImplements, JOverride
from neqsim.thermo import fluid
from neqsim.process import compressor, stream
# Create gas and stream
gas = fluid('srk')
gas.addComponent('methane', 0.9)
gas.addComponent('ethane', 0.1)
gas.setMixingRule('classic')
inlet = stream(gas)
inlet.setFlowRate(5000.0, 'kg/hr')
inlet.setTemperature(25.0, 'C')
inlet.setPressure(50.0, 'bara')
inlet.run()
# Create compressor
comp = compressor(inlet)
comp.setOutletPressure(150.0)
comp.setPolytropicEfficiency(0.78)
comp.setSpeed(12000)
comp.run()
# Initialize mechanical losses (120mm shaft)
losses = comp.initMechanicalLosses(120.0)
losses.setSealType(losses.SealType.DRY_GAS_TANDEM)
# Get results
print(f"Seal gas consumption: {comp.getSealGasConsumption():.2f} Nm³/hr")
print(f"Bearing power loss: {comp.getBearingLoss():.2f} kW")
print(f"Mechanical efficiency: {comp.getMechanicalEfficiency()*100:.1f}%")
Detailed documentation for compressor performance curves in NeqSim, including multi-speed and single-speed compressor handling, automatic curve generation, and predefined templates.
NeqSim supports comprehensive compressor performance modeling through compressor charts (performance maps). The key classes are:
| Class | Description |
|---|---|
CompressorChart |
Standard compressor chart with polynomial interpolation |
CompressorChartKhader2015 |
Advanced chart with Khader 2015 method and fan law scaling |
CompressorChartMWInterpolation |
Multi-map chart with MW interpolation between maps |
CompressorChartGenerator |
Automatic curve generation from templates |
CompressorCurveTemplate |
Predefined curve templates (12 available) |
SafeSplineSurgeCurve |
Spline-based surge curve with safe extrapolation |
SafeSplineStoneWallCurve |
Spline-based stone wall (choke) curve |
CompressorCurve |
Individual speed curve (flow, head, efficiency) |
CompressorMechanicalLosses |
Seal gas and bearing loss calculations (API 692/617) |
Head
↑
│ ╭──────────╮
│ ╱ Stone ╲
│ ╱ Wall ╲
Surge │ ╱ (Choke) ╲
Curve │ ╱ ╲
│ ╱ ╲
│ ╱ Operating ╲
│ ╱ Envelope ╲
│╱ ╲
└─────────────────────────────→ Flow
↑ ↑
Minimum Flow Maximum Flow
(Surge Point) (Stone Wall Point)
Compressor curves are typically measured at specific reference conditions (temperature, pressure, gas composition). When the actual operating fluid has a different molecular weight or composition, the curves must be corrected.
Compressor performance maps are generated at specific reference conditions:
When the actual operating fluid differs from the reference:
NeqSim implements the Khader 2015 method in CompressorChartKhader2015 which automatically corrects curves for varying gas composition using dimensionless similarity parameters.
The method normalizes compressor map data using sound speed-based similarity:
| Parameter | Dimensionless Form | Description |
|---|---|---|
| Corrected Head | $H_{corr} = H / c_s^2$ | Head normalized by sound speed squared |
| Corrected Flow | $Q_{corr} = Q / (c_s \cdot D^2)$ | Flow normalized by sound speed and diameter |
| Machine Mach Number | $Ma = N \cdot D / c_s$ | Speed normalized by sound speed |
Where:
This approach ensures that when molecular weight changes, the operating envelope automatically adjusts.
import neqsim.process.equipment.compressor.CompressorChartKhader2015;
// Create fluid (this is the ACTUAL operating fluid)
SystemInterface operatingFluid = new SystemSrkEos(298.15, 50.0);
operatingFluid.addComponent("methane", 0.85);
operatingFluid.addComponent("ethane", 0.10);
operatingFluid.addComponent("propane", 0.05);
operatingFluid.setMixingRule("classic");
// Create stream
Stream stream = new Stream("inlet", operatingFluid);
stream.setFlowRate(5000.0, "Am3/hr");
stream.run();
// Create compressor with Khader 2015 chart
Compressor compressor = new Compressor("K-100", stream);
double impellerDiameter = 0.3; // meters
// Create the Khader2015 chart - it takes the stream to get the actual fluid
CompressorChartKhader2015 chart = new CompressorChartKhader2015(stream, impellerDiameter);
// Chart conditions: [temperature (°C), pressure (bara), density (kg/m³), MW (g/mol)]
// The MW (4th element) is used to create a reference fluid for the curves
double[] chartConditions = new double[] {25.0, 50.0, 50.0, 20.0};
// Set curves at reference conditions
double[] speed = new double[] {10000, 11000, 12000};
double[][] flow = new double[][] {
{3500, 4000, 4500, 5000, 5500},
{3800, 4300, 4800, 5300, 5800},
{4000, 4500, 5000, 5500, 6000}
};
double[][] head = new double[][] {
{110, 105, 98, 88, 75},
{128, 122, 114, 103, 90},
{148, 141, 132, 120, 105}
};
double[][] polyEff = new double[][] {
{77, 80, 81, 79, 74},
{76, 79, 80, 78, 73},
{75, 78, 79, 77, 72}
};
chart.setCurves(chartConditions, speed, flow, head, flow, polyEff);
chart.setHeadUnit("kJ/kg");
// Apply chart to compressor
compressor.setCompressorChart(chart);
compressor.setSpeed(11000);
compressor.run();
// The chart automatically corrects for the actual fluid's molecular weight!
System.out.println("Polytropic head: " + compressor.getPolytropicHead("kJ/kg"));
System.out.println("Polytropic efficiency: " + compressor.getPolytropicEfficiency());
The chartConditions array specifies the reference conditions:
| Index | Value | Unit | Description |
|---|---|---|---|
| 0 | Temperature | °C | Reference temperature |
| 1 | Pressure | bara | Reference pressure |
| 2 | Density | kg/m³ | Reference density (optional) |
| 3 | Molecular Weight | g/mol | Reference molecular weight |
When the 4th element (molecular weight) is provided, the chart creates a reference fluid that matches this MW by blending methane, ethane, and propane in appropriate proportions.
For more control, you can provide your own reference fluid:
// Create reference fluid (the fluid the curves were measured with)
SystemInterface referenceFluid = new SystemSrkEos(298.15, 50.0);
referenceFluid.addComponent("methane", 0.90);
referenceFluid.addComponent("ethane", 0.07);
referenceFluid.addComponent("propane", 0.03);
referenceFluid.setMixingRule("classic");
// Create chart with both operating and reference fluids
CompressorChartKhader2015 chart = new CompressorChartKhader2015(
operatingFluid, // Actual fluid
referenceFluid, // Reference fluid (curves measured with this)
impellerDiameter
);
chart.setCurves(chartConditions, speed, flow, head, flow, polyEff);
If the fluid composition changes during simulation, regenerate the real curves:
// Fluid composition has changed...
operatingFluid.addComponent("CO2", 0.02); // Added CO2
operatingFluid.init(0);
// Regenerate curves for new fluid
chart.generateRealCurvesForFluid();
// Run compressor with updated curves
compressor.run();
| Feature | CompressorChart | CompressorChartKhader2015 |
|---|---|---|
| MW Correction | Manual | Automatic |
| Method | Polynomial interpolation | Sound speed scaling |
| Impeller diameter | Not required | Required |
| Fluid dependency | None | Uses stream fluid |
| Best for | Fixed composition | Variable composition |
When you have compressor performance maps measured at multiple discrete molecular weights, use CompressorChartMWInterpolation to interpolate between maps based on the actual operating MW.
The simplest way to use multi-MW charts is to let the compressor automatically detect the gas MW:
// 1. Create chart and add maps at different MW values
CompressorChartMWInterpolation chart = new CompressorChartMWInterpolation();
chart.setHeadUnit("kJ/kg");
chart.setAutoGenerateSurgeCurves(true);
chart.setAutoGenerateStoneWallCurves(true);
chart.addMapAtMW(18.0, chartConditions, speed, flow18, head18, polyEff18);
chart.addMapAtMW(22.0, chartConditions, speed, flow22, head22, polyEff22);
// 2. Set chart on compressor
Compressor comp = new Compressor("K-100", inletStream);
comp.setCompressorChart(chart);
comp.setOutletPressure(60.0);
comp.setSpeed(11000);
// 3. Run - MW is automatically detected from inlet stream
comp.run();
// The chart now uses the actual gas MW from the inlet stream
System.out.println("Operating MW: " + chart.getOperatingMW() + " g/mol");
Key features:
run()setOperatingMW() is called automaticallyUse this approach when:
inletStream.getFluid().getMolarMass()import neqsim.process.equipment.compressor.CompressorChartMWInterpolation;
// Create MW interpolation chart
CompressorChartMWInterpolation chart = new CompressorChartMWInterpolation();
chart.setHeadUnit("kJ/kg");
double[] chartConditions = new double[] {25.0, 50.0, 50.0, 20.0};
// Map at MW = 18 g/mol (lighter gas - higher head capacity)
double[] speed18 = {10000, 11000, 12000};
double[][] flow18 = {
{3000, 3500, 4000, 4500, 5000},
{3300, 3800, 4300, 4800, 5300},
{3600, 4100, 4600, 5100, 5600}
};
double[][] head18 = {
{120, 115, 108, 98, 85},
{138, 132, 124, 113, 98},
{158, 151, 142, 130, 113}
};
double[][] polyEff18 = {
{75, 78, 80, 78, 73},
{74, 77, 79, 77, 72},
{73, 76, 78, 76, 71}
};
chart.addMapAtMW(18.0, chartConditions, speed18, flow18, head18, polyEff18);
// Map at MW = 22 g/mol (heavier gas - lower head capacity)
double[] speed22 = {10000, 11000, 12000};
double[][] flow22 = {
{2800, 3300, 3800, 4300, 4800},
{3100, 3600, 4100, 4600, 5100},
{3400, 3900, 4400, 4900, 5400}
};
double[][] head22 = {
{100, 96, 90, 82, 71},
{115, 110, 103, 94, 82},
{132, 126, 118, 108, 94}
};
double[][] polyEff22 = {
{73, 76, 78, 76, 71},
{72, 75, 77, 75, 70},
{71, 74, 76, 74, 69}
};
chart.addMapAtMW(22.0, chartConditions, speed22, flow22, head22, polyEff22);
// Set current operating MW (e.g., from actual fluid composition)
double actualMW = 20.0; // Midpoint - will interpolate 50/50 between maps
chart.setOperatingMW(actualMW);
// Get interpolated values
double flow = 3500.0; // m³/hr
double speed = 10000; // RPM
double polytropicHead = chart.getPolytropicHead(flow, speed);
double efficiency = chart.getPolytropicEfficiency(flow, speed);
System.out.println("Operating MW: " + actualMW + " g/mol");
System.out.println("Interpolated polytropic head: " + polytropicHead + " kJ/kg");
System.out.println("Interpolated efficiency: " + efficiency + " %");
// Apply to compressor
Compressor comp = new Compressor("K-100", stream);
comp.setCompressorChart(chart);
comp.setSpeed(10000);
comp.run();
You can add any number of MW maps. The chart will interpolate between the two nearest:
// Add a third map at MW = 20 g/mol for better accuracy
chart.addMapAtMW(20.0, chartConditions, speed20, flow20, head20, polyEff20);
// Maps are automatically sorted by MW
// Interpolation at MW=19 will use maps at MW=18 and MW=20
// Interpolation at MW=21 will use maps at MW=20 and MW=22
By default, the compressor automatically updates the chart with its inlet stream during each run() call. The chart then uses the actual molecular weight from the inlet stream's fluid. This means you don't need to manually call setOperatingMW() - it's automatically updated each time the compressor runs:
// Create MW interpolation chart
CompressorChartMWInterpolation chart = new CompressorChartMWInterpolation();
chart.setHeadUnit("kJ/kg");
// Add MW maps
chart.addMapAtMW(18.0, chartConditions, speed18, flow18, head18, polyEff18);
chart.addMapAtMW(22.0, chartConditions, speed22, flow22, head22, polyEff22);
// Set the chart on the compressor
Compressor comp = new Compressor("K-100", inletStream);
comp.setCompressorChart(chart);
comp.setOutletPressure(60.0);
// When the compressor runs, it automatically:
// 1. Sets the inlet stream on the chart
// 2. The chart uses the stream's MW for interpolation
comp.run();
// Check the operating MW that was used
System.out.println("Operating MW: " + chart.getOperatingMW() + " g/mol");
// If the gas composition changes, just run again
// The MW will be automatically updated
process.run(); // Chart uses new MW automatically
You can also manually set the inlet stream on the chart if needed:
chart.setInletStream(compressorInletStream);
// MW is automatically updated when calling chart methods
double head = chart.getPolytropicHead(flow, speed); // Uses stream's current MW
If you want to manually control the operating MW:
// Disable auto MW detection
chart.setUseActualMW(false);
// Now you must set MW manually
chart.setOperatingMW(20.0);
By default, when the operating MW is outside the range of available maps, the chart uses the boundary map:
| Operating MW | Default Behavior |
|---|---|
| Below lowest map MW | Uses lowest MW map (no extrapolation) |
| Between maps | Linear interpolation |
| Above highest map MW | Uses highest MW map (no extrapolation) |
Enable extrapolation to linearly extend beyond the MW range:
// Enable extrapolation outside MW range
chart.setAllowExtrapolation(true);
// Now values are extrapolated when MW is outside the map range
chart.setOperatingMW(16.0); // Below lowest map (18.0)
double head = chart.getPolytropicHead(flow, speed); // Extrapolated value
chart.setOperatingMW(26.0); // Above highest map (22.0)
double eff = chart.getPolytropicEfficiency(flow, speed); // Extrapolated value
Caution: Extrapolation can produce unrealistic values if the MW is too far outside the measured range. Use with appropriate limits.
Generate surge and stone wall curves for all maps:
// Generate curves for all MW maps
chart.generateAllSurgeCurves();
chart.generateAllStoneWallCurves();
// Or set curves manually for each MW
chart.setSurgeCurveAtMW(18.0, chartConditions, surgeFlow18, surgeHead18);
chart.setSurgeCurveAtMW(22.0, chartConditions, surgeFlow22, surgeHead22);
chart.setStoneWallCurveAtMW(18.0, chartConditions, stoneWallFlow18, stoneWallHead18);
chart.setStoneWallCurveAtMW(22.0, chartConditions, stoneWallFlow22, stoneWallHead22);
Enable auto-generation before adding maps:
CompressorChartMWInterpolation chart = new CompressorChartMWInterpolation();
chart.setHeadUnit("kJ/kg");
// Enable auto-generation of surge and stone wall curves
chart.setAutoGenerateSurgeCurves(true);
chart.setAutoGenerateStoneWallCurves(true);
// Surge and stone wall are now automatically generated when maps are added
chart.addMapAtMW(18.0, chartConditions, speed18, flow18, head18, polyEff18);
chart.addMapAtMW(22.0, chartConditions, speed22, flow22, head22, polyEff22);
Check operating limits using interpolated curves:
chart.setOperatingMW(20.0);
double head = 100.0; // kJ/kg
double flow = 3500.0; // m³/hr
// Get interpolated surge flow at a given head
double surgeFlow = chart.getSurgeFlow(head);
double stoneWallFlow = chart.getStoneWallFlow(head);
// Check if operating point is in surge or choked
boolean inSurge = chart.isSurge(head, flow);
boolean isChoked = chart.isStoneWall(head, flow);
// Get distance to operating limits
double distanceToSurge = chart.getDistanceToSurge(head, flow); // Positive = above surge
double distanceToStoneWall = chart.getDistanceToStoneWall(head, flow); // Positive = below stone wall
System.out.println("Surge flow: " + surgeFlow + " m³/hr");
System.out.println("Stone wall flow: " + stoneWallFlow + " m³/hr");
System.out.println("In surge: " + inSurge);
System.out.println("Is choked: " + isChoked);
System.out.println("Distance to surge: " + (distanceToSurge * 100) + "%");
System.out.println("Distance to stone wall: " + (distanceToStoneWall * 100) + "%");
For convenience, you can omit chartConditions - default conditions will be generated automatically based on the MW:
// Simplest form: (MW, speed[], flow[][], head[][], efficiency[][])
chart.addMapAtMW(18.0, speeds, flow18, head18, polyEff18);
chart.addMapAtMW(22.0, speeds, flow22, head22, polyEff22);
// With separate flow arrays: (MW, speed[], flow[][], head[][], flowEff[][], efficiency[][])
chart.addMapAtMW(20.0, speeds, flowHead, heads, flowEff, effs);
Complete multi-speed example:
{% raw %}
CompressorChartMWInterpolation chart = new CompressorChartMWInterpolation();
chart.setHeadUnit("kJ/kg");
chart.setAutoGenerateSurgeCurves(true);
chart.setAutoGenerateStoneWallCurves(true);
double[] speeds = {10000, 11000, 12000}; // Multiple speeds (RPM)
// Map at MW = 18 g/mol
double[][] flow18 = {{3000, 3500, 4000, 4500, 5000},
{3300, 3800, 4300, 4800, 5300},
{3600, 4100, 4600, 5100, 5600}};
double[][] head18 = {{120, 115, 108, 98, 85},
{138, 132, 124, 113, 98},
{158, 151, 142, 130, 113}};
double[][] eff18 = {{75, 78, 80, 78, 73},
{74, 77, 79, 77, 72},
{73, 76, 78, 76, 71}};
chart.addMapAtMW(18.0, speeds, flow18, head18, eff18); // No chartConditions needed!
// Map at MW = 22 g/mol
double[][] flow22 = {{2800, 3300, 3800, 4300, 4800},
{3100, 3600, 4100, 4600, 5100},
{3400, 3900, 4400, 4900, 5400}};
double[][] head22 = {{100, 96, 90, 82, 71},
{115, 110, 103, 94, 82},
{132, 126, 118, 108, 94}};
double[][] eff22 = {{73, 76, 78, 76, 71},
{72, 75, 77, 75, 70},
{71, 74, 76, 74, 69}};
chart.addMapAtMW(22.0, speeds, flow22, head22, eff22);
// Use with compressor
Compressor comp = new Compressor("K-100", inletStream);
comp.setCompressorChart(chart);
comp.setSpeed(11000);
comp.run();
{% endraw %}
For single-speed compressors, you can use simplified method signatures with 1D arrays:
// Simplest form: (MW, speed, flow[], head[], efficiency[])
chart.addMapAtMW(18.0, 10000, flow, head, polyEff);
chart.addMapAtMW(22.0, 10000, flow22, head22, polyEff22);
// With separate flow arrays for efficiency: (MW, speed, flow[], head[], flowEff[], efficiency[])
chart.addMapAtMW(20.0, 10000, flowHead, head, flowEff, polyEff);
Complete single-speed example:
CompressorChartMWInterpolation chart = new CompressorChartMWInterpolation();
chart.setHeadUnit("kJ/kg");
chart.setAutoGenerateSurgeCurves(true);
chart.setAutoGenerateStoneWallCurves(true);
double speed = 10000; // Single speed (RPM)
// Map at MW = 18 g/mol
double[] flow18 = {3000, 3500, 4000, 4500, 5000};
double[] head18 = {120, 115, 108, 98, 85};
double[] eff18 = {75, 78, 80, 78, 73};
chart.addMapAtMW(18.0, speed, flow18, head18, eff18);
// Map at MW = 22 g/mol
double[] flow22 = {2800, 3300, 3800, 4300, 4800};
double[] head22 = {100, 96, 90, 82, 71};
double[] eff22 = {73, 76, 78, 76, 71};
chart.addMapAtMW(22.0, speed, flow22, head22, eff22);
// Use with compressor
Compressor comp = new Compressor("K-100", inletStream);
comp.setCompressorChart(chart);
comp.setSpeed(speed);
comp.run();
When efficiency is measured at different flow points than head:
{% raw %}
double[] speeds = {10000, 11000};
// Flow and head arrays
double[][] flowHead = {{3000, 3500, 4000, 4500}, {3300, 3800, 4300, 4800}};
double[][] heads = {{120, 115, 108, 98}, {138, 132, 124, 113}};
// Efficiency measured at different flow points
double[][] flowEff = {{3100, 3600, 4100}, {3400, 3900, 4400}};
double[][] effs = {{76, 79, 77}, {75, 78, 76}};
// Use the 6-argument version of addMapAtMW
chart.addMapAtMW(20.0, chartConditions, speeds, flowHead, heads, flowEff, effs);
{% endraw %}
from jpype import JClass
CompressorChartMWInterpolation = JClass(
'neqsim.process.equipment.compressor.CompressorChartMWInterpolation'
)
# Create multi-map chart
chart = CompressorChartMWInterpolation()
chart.setHeadUnit("kJ/kg")
chart_conditions = [25.0, 50.0, 50.0, 20.0]
# Add map at MW = 18 g/mol
speed_18 = [10000, 11000, 12000]
flow_18 = [[3000, 3500, 4000], [3300, 3800, 4300], [3600, 4100, 4600]]
head_18 = [[120, 115, 108], [138, 132, 124], [158, 151, 142]]
eff_18 = [[75, 78, 80], [74, 77, 79], [73, 76, 78]]
chart.addMapAtMW(18.0, chart_conditions, speed_18, flow_18, head_18, eff_18)
# Add map at MW = 22 g/mol
speed_22 = [10000, 11000, 12000]
flow_22 = [[2800, 3300, 3800], [3100, 3600, 4100], [3400, 3900, 4400]]
head_22 = [[100, 96, 90], [115, 110, 103], [132, 126, 118]]
eff_22 = [[73, 76, 78], [72, 75, 77], [71, 74, 76]]
chart.addMapAtMW(22.0, chart_conditions, speed_22, flow_22, head_22, eff_22)
# Set operating MW and get interpolated values
chart.setOperatingMW(20.0)
head = chart.getPolytropicHead(3500.0, 10000)
eff = chart.getPolytropicEfficiency(3500.0, 10000)
print(f"Interpolated head at MW=20: {head:.2f} kJ/kg")
print(f"Interpolated efficiency at MW=20: {eff:.1f} %")
This example shows a full workflow where the compressor automatically uses the actual gas MW:
from jpype import JClass
from neqsim.thermo import fluid
from neqsim.process import stream, compressor, runProcess
# Import MW interpolation chart
CompressorChartMWInterpolation = JClass(
'neqsim.process.equipment.compressor.CompressorChartMWInterpolation'
)
# Create gas with specific composition
gas = fluid("srk")
gas.addComponent("methane", 0.85)
gas.addComponent("ethane", 0.10)
gas.addComponent("propane", 0.05)
gas.setTemperature(25.0, "C")
gas.setPressure(30.0, "bara")
gas.setTotalFlowRate(5000.0, "Am3/hr")
gas.setMixingRule("classic")
# Create inlet stream
inlet_stream = stream(gas)
inlet_stream.run()
print(f"Inlet gas MW: {inlet_stream.getFluid().getMolarMass('kg/mol') * 1000:.2f} g/mol")
# Create MW interpolation chart with maps at MW = 18 and 22 g/mol
chart = CompressorChartMWInterpolation()
chart.setHeadUnit("kJ/kg")
chart_conditions = [25.0, 50.0, 50.0, 20.0]
# Map at MW = 18 g/mol (lighter gas)
speed = [10000, 11000, 12000]
flow_18 = [[3000, 3500, 4000, 4500, 5000],
[3300, 3800, 4300, 4800, 5300],
[3600, 4100, 4600, 5100, 5600]]
head_18 = [[120, 115, 108, 98, 85],
[138, 132, 124, 113, 98],
[158, 151, 142, 130, 113]]
eff_18 = [[75, 78, 80, 78, 73],
[74, 77, 79, 77, 72],
[73, 76, 78, 76, 71]]
# Map at MW = 22 g/mol (heavier gas)
flow_22 = [[2800, 3300, 3800, 4300, 4800],
[3100, 3600, 4100, 4600, 5100],
[3400, 3900, 4400, 4900, 5400]]
head_22 = [[100, 96, 90, 82, 71],
[115, 110, 103, 94, 82],
[132, 126, 118, 108, 94]]
eff_22 = [[73, 76, 78, 76, 71],
[72, 75, 77, 75, 70],
[71, 74, 76, 74, 69]]
# Add maps and enable auto-generation of surge/stone wall curves
chart.setAutoGenerateSurgeCurves(True)
chart.setAutoGenerateStoneWallCurves(True)
chart.addMapAtMW(18.0, chart_conditions, speed, flow_18, head_18, eff_18)
chart.addMapAtMW(22.0, chart_conditions, speed, flow_22, head_22, eff_22)
# Create compressor and set the chart
comp = compressor(inlet_stream)
comp.setOutletPressure(60.0, "bara")
comp.setSpeed(11000)
comp.setCompressorChart(chart)
# Run - MW is automatically detected from inlet stream
comp.run()
print(f"\nAfter compressor run:")
print(f"Chart operating MW: {chart.getOperatingMW():.2f} g/mol")
print(f"Polytropic head: {comp.getPolytropicHead('kJ/kg'):.2f} kJ/kg")
print(f"Polytropic efficiency: {comp.getPolytropicEfficiency() * 100:.1f}%")
print(f"Outlet pressure: {comp.getOutletStream().getPressure('bara'):.1f} bara")
print(f"Power: {comp.getPower('kW'):.1f} kW")
# Check surge/stone wall margins
print(f"\nOperating margins:")
print(f"Distance to surge: {comp.getDistanceToSurge() * 100:.1f}%")
print(f"Distance to stone wall: {comp.getDistanceToStoneWall() * 100:.1f}%")
# Change gas composition and run again
gas.addComponent("CO2", 0.03)
gas.init(0)
inlet_stream.run()
comp.run()
print(f"\nAfter adding CO2:")
print(f"New gas MW: {inlet_stream.getFluid().getMolarMass('kg/mol') * 1000:.2f} g/mol")
print(f"Chart operating MW: {chart.getOperatingMW():.2f} g/mol")
print(f"Polytropic head: {comp.getPolytropicHead('kJ/kg'):.2f} kJ/kg")
# Enable extrapolation outside the MW range
chart.setAllowExtrapolation(True)
# Now values can be extrapolated for MW outside 18-22 range
# For example, with a very light gas (MW = 16 g/mol)
# or a heavy gas (MW = 26 g/mol)
# Disable automatic MW detection from stream
chart.setUseActualMW(False)
# Now you must manually set the MW
chart.setOperatingMW(20.0)
| Feature | CompressorChartMWInterpolation | CompressorChartKhader2015 |
|---|---|---|
| Input data | Multiple measured maps | Single reference map |
| Correction method | Linear interpolation | Sound speed scaling |
| Accuracy | Higher (measured data) | Theoretical approximation |
| Data requirements | Maps at multiple MW | One map + impeller diameter |
| Best for | Validated multi-MW data | When only one map available |
Multi-speed (variable speed) compressors have performance curves at multiple rotational speeds. NeqSim interpolates between these curves to determine performance at any operating speed.
// Chart conditions: [temperature (°C), pressure (bara), density (kg/m³), MW (g/mol)]
double[] chartConditions = new double[] {25.0, 50.0, 50.0, 20.0};
// Multiple speeds (RPM)
double[] speed = new double[] {8000, 9000, 10000, 11000, 12000};
// Flow arrays for each speed (m³/hr at chart conditions)
double[][] flow = new double[][] {
{3000, 3500, 4000, 4500, 5000}, // Speed 8000 RPM
{3200, 3700, 4200, 4700, 5200}, // Speed 9000 RPM
{3500, 4000, 4500, 5000, 5500}, // Speed 10000 RPM
{3800, 4300, 4800, 5300, 5800}, // Speed 11000 RPM
{4000, 4500, 5000, 5500, 6000} // Speed 12000 RPM
};
// Head arrays for each speed (kJ/kg)
double[][] head = new double[][] {
{80, 78, 74, 68, 60}, // Speed 8000 RPM
{95, 92, 88, 82, 72}, // Speed 9000 RPM
{110, 106, 101, 94, 85}, // Speed 10000 RPM
{128, 123, 117, 109, 99}, // Speed 11000 RPM
{145, 140, 133, 124, 112} // Speed 12000 RPM
};
// Polytropic efficiency arrays for each speed (%)
double[][] polyEff = new double[][] {
{75, 78, 80, 79, 75},
{76, 79, 81, 80, 76},
{77, 80, 82, 81, 77},
{76, 79, 81, 80, 76},
{75, 78, 80, 79, 75}
};
// Set curves on compressor
compressor.getCompressorChart().setCurves(chartConditions, speed, flow, head, flow, polyEff);
compressor.getCompressorChart().setHeadUnit("kJ/kg");
compressor.setSpeed(10000); // Operating speed
For multi-speed compressors with 2 or more speed curves, NeqSim automatically generates surge and stone wall curves:
// Automatic generation from chart data
compressor.getCompressorChart().generateSurgeCurve();
compressor.getCompressorChart().generateStoneWallCurve();
The surge curve is created by connecting the minimum flow points from each speed curve. The stone wall curve is created by connecting the maximum flow points from each speed curve.
Single-speed (fixed speed) compressors operate at a constant rotational speed. For these compressors:
// Chart conditions
double[] chartConditions = new double[] {25.0, 50.0, 50.0, 20.0};
// Single speed
double[] speed = new double[] {10250};
// Single speed curve
double[][] flow = new double[][] {
{5607, 6008, 6480, 7112, 7800, 8180, 8509, 8750, 9007, 9758}
};
double[][] head = new double[][] {
{150.0, 149.5, 148.8, 148.0, 146.1, 144.8, 143.0, 140.7, 137.3, 112.6}
};
double[][] polyEff = new double[][] {
{78, 79, 80, 80.5, 80, 79.5, 79, 78, 77, 70}
};
compressor.setSpeed(10250);
compressor.getCompressorChart().setCurves(chartConditions, speed, flow, head, flow, polyEff);
compressor.getCompressorChart().setHeadUnit("kJ/kg");
For single-speed compressors, surge and stone wall are single points, not curves. NeqSim supports setting these directly:
// Single-point surge (minimum flow point on the curve)
double[] surgeFlow = new double[] {5607.45}; // Single value
double[] surgeHead = new double[] {150.0}; // Corresponding head
compressor.getCompressorChart().getSurgeCurve().setCurve(chartConditions, surgeFlow, surgeHead);
// Single-point stone wall (maximum flow point on the curve)
double[] stoneWallFlow = new double[] {9758.49}; // Single value
double[] stoneWallHead = new double[] {112.65}; // Corresponding head
compressor.getCompressorChart().getStoneWallCurve().setCurve(chartConditions, stoneWallFlow, stoneWallHead);
Key Differences from Multi-Speed:
| Aspect | Multi-Speed | Single-Speed |
|---|---|---|
| Surge/Stone Wall | Curves (interpolated) | Single points (constant) |
| Speed Adjustment | Can vary speed to avoid limits | Fixed speed |
| Control Strategy | Speed + recycle control | Recycle control only |
| Curve Points Required | ≥2 points per curve | 1 point (single point) |
The surge curve defines the minimum stable flow at each head value. Operating below this flow causes unstable, potentially damaging oscillations.
NeqSim uses SafeSplineSurgeCurve which provides:
// Multi-speed: Multiple flow/head points
double[] surgeFlow = {4512.7, 4862.5, 5237.8, 5642.9, 6221.8, 6888.9, 7109.8, 7598.9};
double[] surgeHead = {61.9, 71.4, 81.6, 92.5, 103.5, 114.9, 118.6, 126.7};
compressor.getCompressorChart().getSurgeCurve().setCurve(chartConditions, surgeFlow, surgeHead);
// Single-speed: Single point
double[] singleSurgeFlow = {5607.45};
double[] singleSurgeHead = {150.0};
compressor.getCompressorChart().getSurgeCurve().setCurve(chartConditions, singleSurgeFlow, singleSurgeHead);
// Check if operating point is in surge
boolean inSurge = compressor.getCompressorChart().getSurgeCurve().isSurge(head, flow);
// Get surge flow at current head
double surgeFlow = compressor.getSurgeFlowRate();
// Get margin above surge
double surgeMargin = compressor.getSurgeFlowRateMargin(); // m³/hr above surge
The stone wall (choke) curve defines the maximum flow at each head value. At this limit, the gas velocity approaches sonic conditions and flow cannot increase further.
NeqSim uses SafeSplineStoneWallCurve which provides:
// Multi-speed: Multiple flow/head points
double[] stoneWallFlow = {6500, 7200, 8000, 8800, 9600};
double[] stoneWallHead = {55, 70, 88, 108, 130};
compressor.getCompressorChart().getStoneWallCurve().setCurve(chartConditions, stoneWallFlow, stoneWallHead);
// Single-speed: Single point
double[] singleStoneWallFlow = {9758.49};
double[] singleStoneWallHead = {112.65};
compressor.getCompressorChart().getStoneWallCurve().setCurve(chartConditions, singleStoneWallFlow, singleStoneWallHead);
// Check if operating point is at stone wall
boolean atStoneWall = compressor.isStoneWall();
// Check with explicit flow/head
boolean atStoneWall = compressor.getCompressorChart().getStoneWallCurve().isStoneWall(head, flow);
NeqSim provides methods to calculate the margin from operating limits:
// Returns ratio: (current flow / surge flow) - 1
// Positive = above surge, Negative = in surge
double distanceToSurge = compressor.getDistanceToSurge();
// Example: 0.25 means operating 25% above surge flow
System.out.println("Operating " + (distanceToSurge * 100) + "% above surge");
// Returns ratio: (stone wall flow / current flow) - 1
// Positive = below stone wall, Zero/Negative = at/beyond choke
double distanceToStoneWall = compressor.getDistanceToStoneWall();
// Example: 0.40 means stone wall is 40% above current flow
System.out.println("Stone wall is " + (distanceToStoneWall * 100) + "% above current flow");
For multi-speed compressors (curve is active):
Distance to Surge = (Operating Flow / Surge Flow at Current Head) - 1
Distance to Stone Wall = (Stone Wall Flow at Current Head / Operating Flow) - 1
For single-speed compressors (single-point curve):
Distance to Surge = (Operating Flow / Single Surge Flow Point) - 1
Distance to Stone Wall = (Single Stone Wall Flow Point / Operating Flow) - 1
When you need to determine the compressor speed required to achieve a specific operating point (flow and head), NeqSim provides a robust algorithm that works both within the defined curve range and with extrapolation beyond it.
getSpeed() and getSpeedValue() Methods// Get speed as integer (legacy method)
int speed = chart.getSpeed(flow, head);
// Get speed as double for full precision
double preciseSpeed = chart.getSpeedValue(flow, head);
The speed calculation uses a hybrid approach for robustness:
Fan-law Initial Guess: Uses the relationship $H \propto N^2$ to estimate: $$N_{guess} = N_{ref} \times \sqrt{\frac{H_{target}}{H_{ref}}}$$
Damped Newton-Raphson Iteration: Fast convergence with safeguards:
Bounds Protection: Prevents divergence:
Bisection Fallback: Guaranteed convergence if Newton-Raphson fails:
The compressor chart and compressor classes provide methods to check if the calculated speed is within the defined curve range:
// Using the compressor chart directly
CompressorChartInterface chart = compressor.getCompressorChart();
double speed = chart.getSpeedValue(flow, head);
// Check speed limits on chart
boolean tooHigh = chart.isHigherThanMaxSpeed(speed);
boolean tooLow = chart.isLowerThanMinSpeed(speed);
boolean inRange = chart.isSpeedWithinRange(speed);
// Get ratios (useful for warnings)
double ratioToMax = chart.getRatioToMaxSpeed(speed); // >1.0 means above max
double ratioToMin = chart.getRatioToMinSpeed(speed); // <1.0 means below min
// Using the compressor (uses current operating speed)
compressor.run();
boolean currentSpeedTooHigh = compressor.isHigherThanMaxSpeed();
boolean currentSpeedTooLow = compressor.isLowerThanMinSpeed();
double currentRatioToMax = compressor.getRatioToMaxSpeed();
// Or check a specific speed
boolean testSpeedTooHigh = compressor.isHigherThanMaxSpeed(12000);
When the calculated speed falls outside the defined performance curves, the algorithm uses fan-law extrapolation:
This allows reasonable estimates for speeds up to 50% beyond the curve boundaries.
// Set up compressor
Compressor comp = new Compressor("K-100", inlet);
comp.setOutletPressure(120.0, "bara");
comp.setCompressorChart(chart);
// Run and check speed limits
comp.run();
// Get the calculated speed
double speed = comp.getSpeed();
// Check if operating within design envelope
if (comp.isHigherThanMaxSpeed()) {
double ratio = comp.getRatioToMaxSpeed();
System.out.println("WARNING: Speed " + ratio * 100 + "% of max curve speed");
System.out.println("Compressor may be undersized for this duty");
} else if (comp.isLowerThanMinSpeed()) {
double ratio = comp.getRatioToMinSpeed();
System.out.println("WARNING: Speed " + ratio * 100 + "% of min curve speed");
System.out.println("Compressor may be oversized - turndown issues");
} else {
System.out.println("Operating within design envelope");
}
# Get calculated speed
speed = chart.getSpeedValue(flow, head)
# Check speed limits
if chart.isHigherThanMaxSpeed(speed):
ratio = chart.getRatioToMaxSpeed(speed)
print(f"Speed {speed:.0f} RPM is {ratio:.1%} of max curve speed")
elif chart.isLowerThanMinSpeed(speed):
ratio = chart.getRatioToMinSpeed(speed)
print(f"Speed {speed:.0f} RPM is {ratio:.1%} of min curve speed")
else:
print(f"Speed {speed:.0f} RPM is within curve range")
# Using compressor methods with current operating speed
comp.run()
if comp.isHigherThanMaxSpeed():
print(f"Current speed exceeds max by {(comp.getRatioToMaxSpeed() - 1) * 100:.1f}%")
The anti-surge system adds a safety margin to the surge limit:
// Default surge control factor is 1.05 (5% safety margin)
compressor.getAntiSurge().setSurgeControlFactor(1.10); // 10% margin
// Safe minimum flow = Surge Flow × Surge Control Factor
double safeMinFlow = compressor.getSurgeFlowRate() * compressor.getAntiSurge().getSurgeControlFactor();
AntiSurge antiSurge = compressor.getAntiSurge();
antiSurge.setActive(true);
antiSurge.setSurgeControlFactor(1.10); // 10% margin above surge
// Check current status
boolean isSurging = antiSurge.isSurge();
double controlFactor = antiSurge.getSurgeControlFactor();
| Method | Description |
|---|---|
getSpeed(flow, head) |
Calculate speed for given flow and head (returns int) |
getSpeedValue(flow, head) |
Calculate speed for given flow and head (returns double for precision) |
getMinSpeedCurve() |
Get minimum speed from defined curves (RPM) |
getMaxSpeedCurve() |
Get maximum speed from defined curves (RPM) |
isHigherThanMaxSpeed(speed) |
Check if speed exceeds maximum curve speed |
isLowerThanMinSpeed(speed) |
Check if speed is below minimum curve speed |
isSpeedWithinRange(speed) |
Check if speed is within [min, max] range |
getRatioToMaxSpeed(speed) |
Get ratio speed/maxSpeed (>1.0 means above max) |
getRatioToMinSpeed(speed) |
Get ratio speed/minSpeed (<1.0 means below min) |
| Method | Description |
|---|---|
getPolytropicHead(flow, speed) |
Get head at given flow and speed |
getPolytropicEfficiency(flow, speed) |
Get efficiency at given flow and speed |
getFlow(head, speed, guessFlow) |
Get flow for given head and speed |
| Method | Description |
|---|---|
getSurgeCurve() |
Get the surge curve object |
getStoneWallCurve() |
Get the stone wall curve object |
generateSurgeCurve() |
Auto-generate surge curve from performance curves |
generateStoneWallCurve() |
Auto-generate stone wall curve from performance curves |
getSurgeFlowAtSpeed(speed) |
Get surge flow at given speed |
getSurgeHeadAtSpeed(speed) |
Get surge head at given speed |
getStoneWallFlowAtSpeed(speed) |
Get stone wall flow at given speed |
getStoneWallHeadAtSpeed(speed) |
Get stone wall head at given speed |
| Method | Description |
|---|---|
setCurve(chartConditions, flow, head) |
Set surge curve points (1 or more points) |
getSurgeFlow(head) |
Get surge flow at given head |
getSurgeHead(flow) |
Get surge head at given flow |
isSurge(head, flow) |
Check if point is in surge |
isActive() |
Check if curve is active |
isSinglePointSurge() |
Check if single-point surge (single-speed) |
getSingleSurgeFlow() |
Get single-point surge flow value |
getSingleSurgeHead() |
Get single-point surge head value |
| Method | Description |
|---|---|
setCurve(chartConditions, flow, head) |
Set stone wall curve points (1 or more points) |
getStoneWallFlow(head) |
Get stone wall flow at given head |
getStoneWallHead(flow) |
Get stone wall head at given flow |
isStoneWall(head, flow) |
Check if point is at stone wall |
isActive() |
Check if curve is active |
isSinglePointStoneWall() |
Check if single-point (single-speed) |
getSingleStoneWallFlow() |
Get single-point stone wall flow value |
getSingleStoneWallHead() |
Get single-point stone wall head value |
| Method | Description |
|---|---|
addMapAtMW(mw, chartConditions, speed[], flow[][], head[][], polyEff[][]) |
Add multi-speed map with chart conditions |
addMapAtMW(mw, chartConditions, speed[], flow[][], head[][], flowPolyEff[][], polyEff[][]) |
Add multi-speed map with chart conditions and separate flow arrays |
addMapAtMW(mw, speed[], flow[][], head[][], polyEff[][]) |
Add multi-speed map (default conditions) |
addMapAtMW(mw, speed[], flow[][], head[][], flowPolyEff[][], polyEff[][]) |
Add multi-speed map with separate flow arrays (default conditions) |
addMapAtMW(mw, speed, flow[], head[], polyEff[]) |
Add single-speed map (default conditions) |
addMapAtMW(mw, speed, flow[], head[], flowPolyEff[], polyEff[]) |
Add single-speed map with separate flow arrays |
setOperatingMW(mw) |
Set current operating molecular weight |
getOperatingMW() |
Get current operating molecular weight |
setInletStream(stream) |
Set inlet stream for auto MW detection |
getInletStream() |
Get the inlet stream |
setUseActualMW(enabled) |
Enable/disable auto MW from inlet stream (default: true) |
isUseActualMW() |
Check if auto MW is enabled |
setAllowExtrapolation(enabled) |
Enable/disable extrapolation outside MW range |
isAllowExtrapolation() |
Check if extrapolation is enabled |
getNumberOfMaps() |
Get number of MW maps defined |
getMapMolecularWeights() |
Get list of MW values |
getChartAtMW(mw) |
Get chart at specific MW |
setAutoGenerateSurgeCurves(enabled) |
Enable auto-generation of surge curves |
setAutoGenerateStoneWallCurves(enabled) |
Enable auto-generation of stone wall curves |
generateAllSurgeCurves() |
Generate surge curves for all maps |
generateAllStoneWallCurves() |
Generate stone wall curves for all maps |
setSurgeCurveAtMW(mw, chartConditions, flow, head) |
Set surge curve for specific MW |
setStoneWallCurveAtMW(mw, chartConditions, flow, head) |
Set stone wall curve for specific MW |
getSurgeFlow(head) |
Get interpolated surge flow at head |
getStoneWallFlow(head) |
Get interpolated stone wall flow at head |
getSurgeFlowAtSpeed(speed) |
Get interpolated surge flow at speed |
getStoneWallFlowAtSpeed(speed) |
Get interpolated stone wall flow at speed |
getSurgeHeadAtSpeed(speed) |
Get interpolated surge head at speed |
getStoneWallHeadAtSpeed(speed) |
Get interpolated stone wall head at speed |
isSurge(head, flow) |
Check if point is in surge (interpolated) |
isStoneWall(head, flow) |
Check if point is at stone wall (interpolated) |
getDistanceToSurge(head, flow) |
Get distance to surge (interpolated) |
getDistanceToStoneWall(head, flow) |
Get distance to stone wall (interpolated) |
setInterpolationEnabled(enabled) |
Enable/disable MW interpolation |
| Method | Description |
|---|---|
getDistanceToSurge() |
Ratio margin above surge |
getDistanceToStoneWall() |
Ratio margin below stone wall |
getSurgeFlowRate() |
Surge flow at current head (m³/hr) |
getSurgeFlowRateMargin() |
Flow margin above surge (m³/hr) |
isSurge(flow, head) |
Check if in surge |
isStoneWall() |
Check if at stone wall |
isHigherThanMaxSpeed() |
Check if current speed exceeds max curve speed |
isHigherThanMaxSpeed(speed) |
Check if given speed exceeds max curve speed |
isLowerThanMinSpeed() |
Check if current speed is below min curve speed |
isLowerThanMinSpeed(speed) |
Check if given speed is below min curve speed |
isSpeedWithinRange() |
Check if current speed is within curve range |
isSpeedWithinRange(speed) |
Check if given speed is within curve range |
getRatioToMaxSpeed() |
Get ratio of current speed to max speed |
getRatioToMaxSpeed(speed) |
Get ratio of given speed to max speed |
getRatioToMinSpeed() |
Get ratio of current speed to min speed |
getRatioToMinSpeed(speed) |
Get ratio of given speed to min speed |
import jpype
import jpype.imports
jpype.startJVM(classpath=['path/to/neqsim.jar'])
from neqsim.thermo.system import SystemSrkEos
from neqsim.process.equipment.stream import Stream
from neqsim.process.equipment.compressor import Compressor
# Create fluid
fluid = SystemSrkEos(298.15, 50.0)
fluid.addComponent("methane", 0.9)
fluid.addComponent("ethane", 0.1)
fluid.setMixingRule("classic")
fluid.setTotalFlowRate(5000.0, "Am3/hr")
# Create stream and compressor
stream = Stream("inlet", fluid)
stream.run()
compressor = Compressor("K-100", stream)
compressor.setUsePolytropicCalc(True)
compressor.setOutletPressure(100.0)
# Set multi-speed curves
chartConditions = [25.0, 50.0, 50.0, 20.0]
speed = [8000, 10000, 12000]
flow = [[3000, 4000, 5000], [3500, 4500, 5500], [4000, 5000, 6000]]
head = [[80, 70, 55], [100, 88, 70], [120, 105, 85]]
polyEff = [[78, 80, 76], [79, 81, 77], [78, 80, 76]]
compressor.getCompressorChart().setCurves(chartConditions, speed, flow, head, flow, polyEff)
compressor.getCompressorChart().setHeadUnit("kJ/kg")
compressor.setSpeed(10000)
# Generate surge and stone wall curves automatically
compressor.getCompressorChart().generateSurgeCurve()
compressor.getCompressorChart().generateStoneWallCurve()
compressor.run()
# Check operating margins
print(f"Distance to surge: {compressor.getDistanceToSurge() * 100:.1f}%")
print(f"Distance to stone wall: {compressor.getDistanceToStoneWall() * 100:.1f}%")
# Create compressor with single speed
compressor = Compressor("K-100", stream)
compressor.setUsePolytropicCalc(True)
compressor.setOutletPressure(100.0)
# Set single-speed curve
chartConditions = [25.0, 50.0, 50.0, 20.0]
speed = [10250]
flow = [[5607, 6480, 7800, 8750, 9758]]
head = [[150.0, 148.8, 146.1, 140.7, 112.6]]
polyEff = [[78, 80, 80, 78, 70]]
compressor.getCompressorChart().setCurves(chartConditions, speed, flow, head, flow, polyEff)
compressor.getCompressorChart().setHeadUnit("kJ/kg")
compressor.setSpeed(10250)
# Set single-point surge and stone wall
compressor.getCompressorChart().getSurgeCurve().setCurve(
chartConditions,
[5607.45], # Single surge flow point
[150.0] # Single surge head point
)
compressor.getCompressorChart().getStoneWallCurve().setCurve(
chartConditions,
[9758.49], # Single stone wall flow point
[112.65] # Single stone wall head point
)
compressor.run()
# Verify single-point curves are active
surge_curve = compressor.getCompressorChart().getSurgeCurve()
print(f"Surge curve active: {surge_curve.isActive()}")
print(f"Is single-point surge: {surge_curve.isSinglePointSurge()}")
print(f"Surge flow: {surge_curve.getSingleSurgeFlow()}")
# Check operating margins
print(f"Distance to surge: {compressor.getDistanceToSurge() * 100:.1f}%")
print(f"Distance to stone wall: {compressor.getDistanceToStoneWall() * 100:.1f}%")
from neqsim.thermo import SystemSrkEos
from neqsim.process import Stream, Compressor
# Import the Khader2015 chart class
from jpype import JClass
CompressorChartKhader2015 = JClass('neqsim.process.equipment.compressor.CompressorChartKhader2015')
# Create the actual operating fluid (composition different from reference)
operating_fluid = SystemSrkEos(298.15, 50.0)
operating_fluid.addComponent("methane", 0.75) # Different composition
operating_fluid.addComponent("ethane", 0.15)
operating_fluid.addComponent("propane", 0.08)
operating_fluid.addComponent("n-butane", 0.02)
operating_fluid.setMixingRule("classic")
operating_fluid.setTotalFlowRate(5000.0, "Am3/hr")
# Create stream
stream = Stream("inlet", operating_fluid)
stream.run()
# Create compressor
compressor = Compressor("K-100", stream)
compressor.setUsePolytropicCalc(True)
compressor.setOutletPressure(100.0)
# Create Khader2015 chart with impeller diameter
impeller_diameter = 0.3 # meters
chart = CompressorChartKhader2015(stream, impeller_diameter)
# Chart conditions: [temp °C, pres bara, density kg/m³, MW g/mol]
# MW = 20.0 g/mol is the reference molecular weight
chart_conditions = [25.0, 50.0, 50.0, 20.0]
# Reference curves (measured at MW = 20 g/mol)
speed = [10000, 11000, 12000]
flow = [[3500, 4000, 4500, 5000, 5500],
[3800, 4300, 4800, 5300, 5800],
[4000, 4500, 5000, 5500, 6000]]
head = [[110, 105, 98, 88, 75],
[128, 122, 114, 103, 90],
[148, 141, 132, 120, 105]]
poly_eff = [[77, 80, 81, 79, 74],
[76, 79, 80, 78, 73],
[75, 78, 79, 77, 72]]
chart.setCurves(chart_conditions, speed, flow, head, flow, poly_eff)
chart.setHeadUnit("kJ/kg")
# Apply chart - curves are automatically corrected for the operating fluid's MW
compressor.setCompressorChart(chart)
compressor.setSpeed(11000)
compressor.run()
print(f"Operating fluid MW: {operating_fluid.getMolarMass('kg/mol') * 1000:.1f} g/mol")
print(f"Reference fluid MW: {chart_conditions[3]:.1f} g/mol")
print(f"Polytropic head: {compressor.getPolytropicHead('kJ/kg'):.2f} kJ/kg")
print(f"Polytropic efficiency: {compressor.getPolytropicEfficiency() * 100:.1f}%")
# If fluid composition changes, regenerate curves
operating_fluid.addComponent("CO2", 0.05)
operating_fluid.init(0)
chart.generateRealCurvesForFluid()
compressor.run()
print(f"\nAfter adding CO2:")
print(f"New fluid MW: {operating_fluid.getMolarMass('kg/mol') * 1000:.1f} g/mol")
print(f"Polytropic head: {compressor.getPolytropicHead('kJ/kg'):.2f} kJ/kg")
When you don't have manufacturer performance data, NeqSim can automatically generate realistic compressor curves using the CompressorChartGenerator class and predefined curve templates.
// Create and run compressor to establish design point
Compressor compressor = new Compressor("K-100", inletStream);
compressor.setOutletPressure(100.0, "bara");
compressor.setPolytropicEfficiency(0.78);
compressor.setSpeed(10000);
compressor.run();
// Generate curves automatically
CompressorChartGenerator generator = new CompressorChartGenerator(compressor);
CompressorChartInterface chart = generator.generateFromTemplate("PIPELINE", 5);
// Apply and use
compressor.setCompressorChart(chart);
compressor.run();
| Use Case | Benefit |
|---|---|
| Early design studies | Estimate performance before vendor data available |
| Sensitivity analysis | Quickly evaluate different compressor configurations |
| Education/training | Realistic curves without proprietary data |
| Default behavior | Reasonable performance when no map is provided |
NeqSim provides 12 predefined templates organized into three categories, each representing typical compressor characteristics for different applications.
┌─────────────────────────────────────────────────────────────────────┐
│ COMPRESSOR CURVE TEMPLATES │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────┐ │
│ │ BASIC (3) │ │ APPLICATION (6) │ │ TYPE (4) │ │
│ │ │ │ │ │ │ │
│ │ • STANDARD │ │ • PIPELINE │ │ • SINGLE │ │
│ │ • HIGH_FLOW │ │ • EXPORT │ │ _STAGE │ │
│ │ • HIGH_HEAD │ │ • INJECTION │ │ • MULTI │ │
│ │ │ │ • GAS_LIFT │ │ STAGE │ │
│ │ │ │ • REFRIGERATION │ │ • INTEGRAL │ │
│ │ │ │ • BOOSTER │ │ _GEARED │ │
│ │ │ │ │ │ • OVERHUNG │ │
│ └─────────────────────┘ └─────────────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
Generic centrifugal compressor characteristics for general use.
| Template | Peak η | Flow Range | Head | Best For |
|---|---|---|---|---|
CENTRIFUGAL_STANDARD |
~78% | Medium | Medium | General purpose, default choice |
CENTRIFUGAL_HIGH_FLOW |
~78% | Wide | Lower | High throughput, low pressure ratio |
CENTRIFUGAL_HIGH_HEAD |
~78% | Narrow | High | High pressure ratio, multiple stages |
Optimized for specific oil & gas applications.
| Template | Peak η | Typical Use | Key Characteristics |
|---|---|---|---|
PIPELINE |
82-85% | Gas transmission | High capacity, flat curves, wide turndown (~40%) |
EXPORT |
~80% | Offshore gas export | High pressure, stable operation, 6-8 stages |
INJECTION |
~77% | Gas injection/EOR | Very high pressure ratio, lower capacity |
GAS_LIFT |
~75% | Artificial lift | Wide surge margin (~35%), liquid tolerant |
REFRIGERATION |
~78% | LNG/process cooling | Wide operating range, part-load efficiency |
BOOSTER |
~76% | Process plant | Moderate pressure ratio (2-4), balanced design |
Based on mechanical design characteristics.
| Template | Peak η | Pressure Ratio | Design Features |
|---|---|---|---|
SINGLE_STAGE |
~75% | 1.5-2.5 | Simple, wide flow range, cost-effective |
MULTISTAGE_INLINE |
~78% | 5-15 | Barrel type, 4-8 stages, O&G standard |
INTEGRALLY_GEARED |
82% | Flexible | Multiple pinions, air separation, optimized |
OVERHUNG |
~74% | Low-medium | Cantilever, simple maintenance |
┌─────────────────────────┐
│ What is the application?│
└───────────┬─────────────┘
│
┌──────────────┬───────────┼───────────┬──────────────┐
▼ ▼ ▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌─────────┐ ┌─────────┐ ┌────────────┐
│Gas Pipeline│ │ Offshore │ │ EOR / │ │Gas Lift │ │Refriger. │
│Transmission│ │ Export │ │Injection│ │ │ │ / LNG │
└─────┬──────┘ └─────┬──────┘ └────┬────┘ └────┬────┘ └─────┬──────┘
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
PIPELINE EXPORT INJECTION GAS_LIFT REFRIGERATION
┌─────────────────────────┐
│ What type of machine? │
└───────────┬─────────────┘
│
┌──────────────┬───────────┴───────────┬──────────────┐
▼ ▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐
│ Simple │ │ Barrel │ │ Integrally │ │ Overhung │
│Single Stage│ │ Multistage │ │ Geared │ │ Cantilever │
└─────┬──────┘ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘
│ │ │ │
▼ ▼ ▼ ▼
SINGLE_STAGE MULTISTAGE_INLINE INTEGRALLY_GEARED OVERHUNG
| Your Requirement | Recommended Template |
|---|---|
| Default / don't know | CENTRIFUGAL_STANDARD |
| Large capacity, moderate PR | PIPELINE |
| High discharge pressure | EXPORT or INJECTION |
| Variable inlet conditions | GAS_LIFT |
| Process cooling/LNG | REFRIGERATION |
| Simple, low PR | SINGLE_STAGE |
| High PR, compact | MULTISTAGE_INLINE |
| Highest efficiency | INTEGRALLY_GEARED |
| Small duty, easy maintenance | OVERHUNG |
// Create generator from compressor
CompressorChartGenerator generator = new CompressorChartGenerator(compressor);
// Option 1: Generate from template (recommended)
CompressorChartInterface chart = generator.generateFromTemplate("PIPELINE", 5);
// Option 2: Generate "normal curves" from operating point
CompressorChartInterface chart = generator.generateCompressorChart("normal curves", 5);
// Option 3: Single speed curve
CompressorChartInterface chart = generator.generateCompressorChart("normal curves");
The generator supports three output chart types:
generator.setChartType("interpolate and extrapolate"); // Default, most flexible
generator.setChartType("interpolate"); // No extrapolation
generator.setChartType("simple"); // Basic fan law scaling
| Chart Type | Class | Use Case |
|---|---|---|
interpolate and extrapolate |
CompressorChartAlternativeMapLookupExtrapolate |
Production, wide operating range |
interpolate |
CompressorChartAlternativeMapLookup |
Stay within measured envelope |
simple |
CompressorChart |
Basic calculations, teaching |
// Using number of speeds (auto-distributed)
CompressorChartInterface chart = generator.generateFromTemplate("EXPORT", 5);
// → Generates 5 curves from 70% to 100% of design speed
// Using specific speed values
double[] speeds = {7000, 8000, 9000, 10000, 10500};
CompressorChartInterface chart = generator.generateCompressorChart("normal curves", speeds);
Enable industry-standard corrections for more accurate off-design performance:
CompressorChartGenerator generator = new CompressorChartGenerator(compressor);
// Individual corrections
generator.setUseReynoldsCorrection(true); // Efficiency correction for Re
generator.setUseMachCorrection(true); // Choke flow limitation
generator.setUseMultistageSurgeCorrection(true); // Surge line shift at low speeds
generator.setNumberOfStages(6);
generator.setImpellerDiameter(0.35);
// Or enable all at once
generator.enableAdvancedCorrections(6); // 6 stages
CompressorChartInterface chart = generator.generateFromTemplate("MULTISTAGE_INLINE", 5);
| Correction | Effect | When Important |
|---|---|---|
| Reynolds | Adjusts η at low Re (high viscosity, low speed) | Heavy gases, low-speed operation |
| Mach | Limits stonewall flow based on sonic velocity | Light gases (H₂), high speed |
| Multistage Surge | Shifts surge to higher flow at reduced speed | Variable speed, >3 stages |
// Get all templates
String[] all = CompressorCurveTemplate.getAvailableTemplates();
// → ["CENTRIFUGAL_STANDARD", "CENTRIFUGAL_HIGH_FLOW", ..., "OVERHUNG"]
// Get by category
String[] basic = CompressorCurveTemplate.getTemplatesByCategory("basic");
// → ["CENTRIFUGAL_STANDARD", "CENTRIFUGAL_HIGH_FLOW", "CENTRIFUGAL_HIGH_HEAD"]
String[] application = CompressorCurveTemplate.getTemplatesByCategory("application");
// → ["PIPELINE", "EXPORT", "INJECTION", "GAS_LIFT", "REFRIGERATION", "BOOSTER"]
String[] type = CompressorCurveTemplate.getTemplatesByCategory("type");
// → ["SINGLE_STAGE", "MULTISTAGE_INLINE", "INTEGRALLY_GEARED", "OVERHUNG"]
The getTemplate() method is flexible with naming:
// All of these return the same template:
CompressorCurveTemplate.getTemplate("GAS_LIFT");
CompressorCurveTemplate.getTemplate("gas-lift");
CompressorCurveTemplate.getTemplate("gas lift");
CompressorCurveTemplate.getTemplate("gaslift");
// Abbreviations work too:
CompressorCurveTemplate.getTemplate("igc"); // → INTEGRALLY_GEARED
CompressorCurveTemplate.getTemplate("barrel"); // → MULTISTAGE_INLINE
CompressorCurveTemplate.getTemplate("LNG"); // → REFRIGERATION
// Get template object
CompressorCurveTemplate template = CompressorCurveTemplate.PIPELINE;
// Get unscaled original chart
CompressorChartInterface originalChart = template.getOriginalChart();
// Scale to specific speed
CompressorChartInterface scaledChart = template.scaleToSpeed(8000);
// Scale to design point
CompressorChartInterface chart = template.scaleToDesignPoint(
10000, // designSpeed (RPM)
5000, // designFlow (m³/hr)
85.0, // designHead (kJ/kg)
5 // numberOfSpeeds
);
// Get template metadata
String name = template.getName();
double refSpeed = template.getReferenceSpeed();
double[] speedRatios = template.getSpeedRatios();
import neqsim.process.equipment.compressor.*;
import neqsim.process.equipment.stream.Stream;
import neqsim.thermo.system.SystemSrkEos;
public class CompressorCurveGenerationExample {
public static void main(String[] args) {
// 1. Create fluid and inlet stream
SystemSrkEos gas = new SystemSrkEos(298.15, 50.0);
gas.addComponent("methane", 0.85);
gas.addComponent("ethane", 0.10);
gas.addComponent("propane", 0.05);
gas.setMixingRule("classic");
Stream inlet = new Stream("inlet", gas);
inlet.setFlowRate(15000.0, "kg/hr");
inlet.setTemperature(25.0, "C");
inlet.setPressure(40.0, "bara");
inlet.run();
// 2. Create compressor at design point
Compressor comp = new Compressor("K-100", inlet);
comp.setOutletPressure(120.0, "bara");
comp.setUsePolytropicCalc(true);
comp.setPolytropicEfficiency(0.78);
comp.setSpeed(9500);
comp.run();
System.out.println("=== Design Point ===");
System.out.println("Flow: " + String.format("%.1f", inlet.getFlowRate("m3/hr")) + " m³/hr");
System.out.println("Head: " + String.format("%.1f", comp.getPolytropicFluidHead()) + " kJ/kg");
System.out.println("Power: " + String.format("%.1f", comp.getPower("kW")) + " kW");
// 3. Generate curves using EXPORT template (offshore gas export)
CompressorChartGenerator generator = new CompressorChartGenerator(comp);
generator.setChartType("interpolate and extrapolate");
generator.enableAdvancedCorrections(6); // 6-stage compressor
CompressorChartInterface chart = generator.generateFromTemplate("EXPORT", 5);
// 4. Apply chart and verify
comp.setCompressorChart(chart);
comp.run();
System.out.println("\n=== With Generated Chart ===");
System.out.println("Speeds available: " + chart.getSpeeds().length);
System.out.println("Efficiency from chart: " +
String.format("%.1f", comp.getPolytropicEfficiency() * 100) + "%");
System.out.println("Distance to surge: " +
String.format("%.1f", comp.getDistanceToSurge() * 100) + "%");
// 5. Test at different operating point
inlet.setFlowRate(12000.0, "kg/hr");
inlet.run();
comp.run();
System.out.println("\n=== Turndown Operation ===");
System.out.println("Flow: " + String.format("%.1f", inlet.getFlowRate("m3/hr")) + " m³/hr");
System.out.println("Efficiency: " +
String.format("%.1f", comp.getPolytropicEfficiency() * 100) + "%");
System.out.println("Distance to surge: " +
String.format("%.1f", comp.getDistanceToSurge() * 100) + "%");
}
}
from neqsim import jNeqSim
from neqsim.process import stream, compressor
# Import Java classes
SystemSrkEos = jNeqSim.thermo.system.SystemSrkEos
Stream = jNeqSim.process.equipment.stream.Stream
Compressor = jNeqSim.process.equipment.compressor.Compressor
CompressorChartGenerator = jNeqSim.process.equipment.compressor.CompressorChartGenerator
CompressorCurveTemplate = jNeqSim.process.equipment.compressor.CompressorCurveTemplate
# Create fluid
gas = SystemSrkEos(298.15, 50.0)
gas.addComponent("methane", 0.90)
gas.addComponent("ethane", 0.07)
gas.addComponent("propane", 0.03)
gas.setMixingRule("classic")
# Create stream
inlet = Stream("inlet", gas)
inlet.setFlowRate(20000.0, "kg/hr")
inlet.setTemperature(30.0, "C")
inlet.setPressure(45.0, "bara")
inlet.run()
# Create compressor
comp = Compressor("K-100", inlet)
comp.setOutletPressure(150.0, "bara")
comp.setUsePolytropicCalc(True)
comp.setPolytropicEfficiency(0.77)
comp.setSpeed(10000)
comp.run()
# List available templates
print("Available templates:")
for cat in ["basic", "application", "type"]:
templates = CompressorCurveTemplate.getTemplatesByCategory(cat)
print(f" {cat}: {list(templates)}")
# Generate chart using INJECTION template (high pressure application)
generator = CompressorChartGenerator(comp)
generator.setChartType("interpolate and extrapolate")
generator.enableAdvancedCorrections(8) # 8-stage injection compressor
chart = generator.generateFromTemplate("INJECTION", 5)
comp.setCompressorChart(chart)
comp.run()
print(f"\nDesign efficiency: {comp.getPolytropicEfficiency() * 100:.1f}%")
print(f"Design head: {comp.getPolytropicFluidHead():.1f} kJ/kg")
print(f"Power: {comp.getPower('MW'):.2f} MW")
print(f"Surge margin: {comp.getDistanceToSurge() * 100:.1f}%")
Application: Natural gas transmission (30-50 MW class)
Reference Speed: 5500 RPM (large direct-drive or gear-driven)
Peak Efficiency: 85%
Turndown: ~40%
Curve Characteristics:
Head
↑
79 ──┼────╮
│ ╲ Flat curve
65 ──┼──────╲── for pipeline
│ ╲ stability
53 ──┼────────╲─
│ ╲
└──────────┴────→ Flow
30k 70k m³/hr
Application: Gas injection/EOR (very high pressure)
Reference Speed: 11000 RPM
Peak Efficiency: 77%
Pressure Ratio: 50-200 (overall, with intercooling)
Curve Characteristics:
Head
↑
245 ──┼──╮
│ ╲ Steep curve
165 ──┼────╲── (high head
│ ╲ per stage)
130 ──┼──────╲
│ ╲
└────────┴────→ Flow
2.5k 6k m³/hr
(lower capacity)
Application: Air separation, process air
Reference Speed: 20000 RPM (pinion speed)
Peak Efficiency: 82% (highest of all templates)
Design Features:
- Bull gear drives multiple pinions
- Each pinion has optimized impeller
- Intercooling between stages
┌─────────────────────────┐
│ BULL GEAR │
│ ╭───────╮ │
│ ╱ ● ● ╲ │ ● = Pinion with impeller
│ │ ● ● │ │
│ ╲ ● ● ╱ │
│ ╰───────╯ │
└─────────────────────────┘
NeqSim now provides comprehensive dynamic simulation capabilities for compressors, including state machines, event-driven control, driver modeling, and startup/shutdown sequences.
The dynamic simulation features enable realistic transient simulations including:
The CompressorState enum defines the possible operating states:
| State | Description | Can Start? | Is Operational? |
|---|---|---|---|
STOPPED |
Compressor is not running | Yes | No |
STARTING |
Startup sequence in progress | No | No |
RUNNING |
Normal operation | No | Yes |
SURGE_PROTECTION |
Near or in surge, recycle active | No | Yes |
SPEED_LIMITED |
At max/min speed limit | No | Yes |
SHUTDOWN |
Shutdown sequence in progress | No | No |
DEPRESSURIZING |
Pressure settling after shutdown | No | No |
TRIPPED |
Emergency shutdown, requires acknowledgment | No | No |
STANDBY |
Ready to start after trip acknowledgment | Yes | No |
// Create compressor with dynamic features
Compressor comp = new Compressor("K-100", inletStream);
comp.setOutletPressure(100.0, "bara");
comp.setSpeed(10000);
comp.run();
// Enable dynamic simulation features
comp.enableOperatingHistory();
comp.setRotationalInertia(15.0); // kg⋅m² combined rotor inertia
comp.setMaxAccelerationRate(100.0); // RPM/s
comp.setMaxDecelerationRate(200.0); // RPM/s
// Set surge margin thresholds
comp.setSurgeWarningThreshold(0.15); // 15% margin triggers warning
comp.setSurgeCriticalThreshold(0.05); // 5% margin triggers critical alarm
// Configure anti-surge controller
AntiSurge antiSurge = comp.getAntiSurge();
antiSurge.setControlStrategy(AntiSurge.ControlStrategy.PID);
antiSurge.setPIDParameters(2.0, 0.5, 0.1);
antiSurge.setValveResponseTime(2.0); // seconds
// Start compressor with startup profile
comp.startCompressor(10000); // Target speed
Listen for compressor events during dynamic simulation:
// Create event listener
CompressorEventListener listener = new CompressorEventListener() {
@Override
public void onSurgeApproach(Compressor compressor, double surgeMargin, boolean isCritical) {
if (isCritical) {
System.out.println("CRITICAL: Surge margin only " + surgeMargin * 100 + "%");
} else {
System.out.println("WARNING: Approaching surge, margin = " + surgeMargin * 100 + "%");
}
}
@Override
public void onSurgeOccurred(Compressor compressor, double surgeMargin) {
System.out.println("ALARM: Compressor in surge!");
}
@Override
public void onSpeedLimitExceeded(Compressor compressor, double currentSpeed, double ratio) {
System.out.println("Speed limit exceeded: " + currentSpeed + " RPM (" + ratio * 100 + "% of max)");
}
@Override
public void onSpeedBelowMinimum(Compressor compressor, double currentSpeed, double ratio) {
System.out.println("Speed below minimum: " + currentSpeed + " RPM");
}
@Override
public void onPowerLimitExceeded(Compressor compressor, double currentPower, double maxPower) {
System.out.println("Power limit exceeded: " + currentPower + " kW > " + maxPower + " kW");
}
@Override
public void onStateChange(Compressor compressor, CompressorState oldState, CompressorState newState) {
System.out.println("State change: " + oldState + " -> " + newState);
}
@Override
public void onStoneWallApproach(Compressor compressor, double stoneWallMargin) {
System.out.println("Approaching stone wall, margin = " + stoneWallMargin * 100 + "%");
}
@Override
public void onStartupComplete(Compressor compressor) {
System.out.println("Startup complete!");
}
@Override
public void onShutdownComplete(Compressor compressor) {
System.out.println("Shutdown complete!");
}
};
// Register listener
comp.addEventListener(listener);
Model driver (motor, turbine) characteristics:
// Create driver model
CompressorDriver driver = new CompressorDriver(DriverType.GAS_TURBINE, 5000); // 5000 kW gas turbine
driver.setMaxPower(5500); // 110% overload capacity
driver.setInertia(25.0); // kg⋅m² combined inertia
driver.setMaxAcceleration(80.0); // RPM/s
driver.setMaxDeceleration(150.0); // RPM/s
// For gas turbines: ambient temperature derating
driver.setAmbientTemperature(308.15); // 35°C
driver.setTemperatureDerateFactor(0.005); // 0.5% power reduction per °C above ISO
// For VFD motors: efficiency vs speed curve
CompressorDriver vfdDriver = new CompressorDriver(DriverType.VFD_MOTOR, 3000);
vfdDriver.setVfdEfficiencyCoefficients(0.90, 0.05, -0.02); // η = a + b*(N/Nrated) + c*(N/Nrated)²
// Apply driver to compressor
comp.setDriver(driver);
| Driver Type | Typical Efficiency | Response Time | Characteristics |
|---|---|---|---|
ELECTRIC_MOTOR |
95% | 1 s | Fixed speed, constant torque |
VFD_MOTOR |
93% | 5 s | Variable speed, high efficiency |
GAS_TURBINE |
35% | 30 s | Variable speed, ambient temp dependent |
STEAM_TURBINE |
40% | 20 s | Variable speed, steam supply dependent |
RECIPROCATING_ENGINE |
42% | 15 s | High efficiency, limited speed range |
EXPANDER_DRIVE |
85% | 5 s | Direct drive from process expander |
Define sequenced startup and shutdown procedures:
// Create startup profile
StartupProfile startup = new StartupProfile();
startup.setMinimumIdleSpeed(3000); // RPM
startup.setIdleHoldTime(60.0); // seconds at idle
startup.setWarmupRampRate(50.0); // RPM/s during warmup
startup.setNormalRampRate(100.0); // RPM/s during normal ramp
startup.setRequireAntisurgeOpen(true);
startup.setAntisurgeOpeningDuration(10.0); // seconds before start
// Or use predefined profiles
StartupProfile fastStartup = StartupProfile.createFastProfile(10000); // Emergency restart
StartupProfile slowStartup = StartupProfile.createSlowProfile(10000, 3000); // Cold start
comp.setStartupProfile(startup);
// Create shutdown profile
ShutdownProfile shutdown = new ShutdownProfile(ShutdownProfile.ShutdownType.NORMAL, 10000);
shutdown.setNormalRampRate(100.0); // RPM/s
shutdown.setIdleRundownTime(30.0); // seconds at idle before stop
shutdown.setOpenAntisurgeOnShutdown(true);
comp.setShutdownProfile(shutdown);
// Start/stop compressor
comp.startCompressor(10000); // Start with target speed
comp.stopCompressor(); // Normal shutdown
comp.emergencyShutdown(); // Emergency shutdown (trips compressor)
The enhanced AntiSurge class supports multiple control strategies:
| Strategy | Description | Best For |
|---|---|---|
ON_OFF |
Simple on/off based on surge line | Simple systems, teaching |
PROPORTIONAL |
Linear valve opening based on margin | Most applications |
PID |
Full PID control with anti-windup | Precise control, variable load |
PREDICTIVE |
Uses rate-of-change prediction | Rapid load changes |
DUAL_LOOP |
Separate surge and capacity loops | Complex systems |
AntiSurge antiSurge = comp.getAntiSurge();
// Select control strategy
antiSurge.setControlStrategy(AntiSurge.ControlStrategy.PID);
// Configure PID
antiSurge.setPIDParameters(2.0, 0.5, 0.1); // Kp, Ki, Kd
antiSurge.setPIDSetpoint(0.10); // 10% surge margin setpoint
// Configure predictive control
antiSurge.setControlStrategy(AntiSurge.ControlStrategy.PREDICTIVE);
antiSurge.setPredictiveHorizon(5.0); // 5 second look-ahead
// Valve dynamics
antiSurge.setValveResponseTime(2.0); // First-order time constant
antiSurge.setValveRateLimit(0.5); // Max 50% per second
antiSurge.setMinimumRecycleFlow(500.0); // Minimum flow m³/hr
antiSurge.setMaximumRecycleFlow(3000.0); // Maximum flow m³/hr
// Surge cycle trip protection
antiSurge.setMaxSurgeCyclesBeforeTrip(3); // Trip after 3 surge cycles
Update compressor state during transient simulation:
double dt = 0.1; // 100 ms time step
double simTime = 0.0;
// Startup compressor
comp.startCompressor(10000);
while (simTime < 600.0) { // 10 minute simulation
// Update inlet conditions (from upstream process)
inletStream.run();
// Run compressor
comp.run();
// Update dynamic state (handles startup/shutdown, checks limits)
comp.updateDynamicState(dt);
// Update anti-surge controller
double surgeMargin = comp.getDistanceToSurge();
comp.getAntiSurge().updateController(surgeMargin, dt);
// Record to history
comp.recordOperatingPoint(simTime);
simTime += dt;
}
// Export operating history
comp.getOperatingHistory().exportToCSV("compressor_history.csv");
System.out.println(comp.getOperatingHistory().generateSummary());
Track and analyze operating history:
// Enable history tracking
comp.enableOperatingHistory();
// ... run simulation ...
// Get history summary
CompressorOperatingHistory history = comp.getOperatingHistory();
System.out.println("Total points recorded: " + history.getPointCount());
System.out.println("Surge events: " + history.getSurgeEventCount());
System.out.println("Time in surge: " + history.getTimeInSurge() + " s");
System.out.println("Minimum surge margin: " + history.getMinimumSurgeMargin() * 100 + "%");
System.out.println("Average efficiency: " + history.getAverageEfficiency() * 100 + "%");
// Get peak values
CompressorOperatingHistory.OperatingPoint peakPower = history.getPeakPower();
System.out.println("Peak power: " + peakPower.getPower() + " kW at t=" + peakPower.getTime() + " s");
// Export to CSV for plotting
history.exportToCSV("compressor_history.csv");
// Full summary report
System.out.println(history.generateSummary());
Model compressor performance degradation over time:
// Set degradation factor (1.0 = new, <1.0 = degraded)
comp.setDegradationFactor(0.95); // 5% degradation
// Set fouling factor (head reduction)
comp.setFoulingFactor(0.03); // 3% head reduction due to fouling
// Track operating hours
comp.setOperatingHours(25000); // Initial operating hours
comp.addOperatingHours(100); // Add 100 hours
// Get effective performance
double effectiveHead = comp.getEffectivePolytropicHead(); // Accounts for degradation
double effectiveEff = comp.getEffectivePolytropicEfficiency(); // Accounts for degradation
Automatically calculate speed from operating point:
// Enable auto-speed mode
comp.setAutoSpeedMode(true);
// During simulation, speed is calculated from flow and head
comp.run(); // Speed automatically adjusted based on chart
from neqsim import jNeqSim
from jpype import JClass
# Import classes
Compressor = jNeqSim.process.equipment.compressor.Compressor
CompressorState = JClass('neqsim.process.equipment.compressor.CompressorState')
CompressorDriver = JClass('neqsim.process.equipment.compressor.CompressorDriver')
DriverType = JClass('neqsim.process.equipment.compressor.DriverType')
AntiSurge = jNeqSim.process.equipment.compressor.AntiSurge
StartupProfile = JClass('neqsim.process.equipment.compressor.StartupProfile')
ShutdownProfile = JClass('neqsim.process.equipment.compressor.ShutdownProfile')
# Create compressor (assuming inlet stream exists)
comp = Compressor("K-100", inlet_stream)
comp.setOutletPressure(100.0, "bara")
comp.setSpeed(10000)
comp.run()
# Configure dynamic features
comp.enableOperatingHistory()
comp.setRotationalInertia(15.0)
comp.setSurgeWarningThreshold(0.15)
comp.setSurgeCriticalThreshold(0.05)
# Set up gas turbine driver
driver = CompressorDriver(DriverType.GAS_TURBINE, 5000)
driver.setAmbientTemperature(308.15) # 35°C
comp.setDriver(driver)
# Configure anti-surge
anti_surge = comp.getAntiSurge()
anti_surge.setControlStrategy(AntiSurge.ControlStrategy.PID)
anti_surge.setPIDParameters(2.0, 0.5, 0.1)
# Run dynamic simulation
dt = 0.1 # 100 ms
sim_time = 0.0
comp.startCompressor(10000)
while sim_time < 300.0:
inlet_stream.run()
comp.run()
comp.updateDynamicState(dt)
# Print state periodically
if int(sim_time) % 10 == 0 and sim_time == int(sim_time):
print(f"t={sim_time:.0f}s: State={comp.getOperatingState()}, "
f"Speed={comp.getSpeed():.0f} RPM, "
f"Surge margin={comp.getDistanceToSurge()*100:.1f}%")
sim_time += dt
# Export results
comp.getOperatingHistory().exportToCSV("compressor_dynamic.csv")
print(str(comp.getOperatingHistory().generateSummary()))
This document describes the mechanical design calculations for centrifugal compressors in NeqSim, implemented in the CompressorMechanicalDesign class.
The mechanical design module provides sizing and design calculations for centrifugal compressors based on API 617 (Axial and Centrifugal Compressors) and industry practice. The calculations enable:
| Standard | Description |
|---|---|
| API 617 | Axial and Centrifugal Compressors and Expander-compressors |
| API 672 | Packaged, Integrally Geared Centrifugal Air Compressors |
| API 692 | Dry Gas Sealing Systems |
| API 614 | Lubrication, Shaft-Sealing and Oil-Control Systems |
The number of compression stages is determined by the total polytropic head and the maximum allowable head per stage:
numberOfStages = ceil(totalPolytropicHead / maxHeadPerStage)
Design Limit: Maximum head per stage = 30 kJ/kg (typical for process gas centrifugal compressors)
The actual head per stage is then:
headPerStage = totalPolytropicHead / numberOfStages
The impeller tip speed is derived from the head requirement using the work coefficient:
tipSpeed = sqrt(headPerStage [J/kg] / workCoefficient)
Where:
workCoefficient = 0.50 (typical for backward-curved impellers, range 0.4-0.6)Design Limit: Maximum tip speed = 350 m/s (material limit for steel impellers)
From the tip speed and rotational speed:
impellerDiameter [mm] = (tipSpeed × 60) / (π × speedRPM) × 1000
The design verifies the flow coefficient is within acceptable range (0.01-0.15):
flowCoefficient = volumeFlow [m³/s] / (D² × U)
Shaft diameter is calculated from torque requirements and allowable shear stress:
torque [Nm] = power [kW] × 1000 × 60 / (2π × speedRPM)
shaftDiameter [mm] = ((16 × torque) / (π × allowableShear))^(1/3) × 1000 × safetyFactor
Where:
allowableShear = 50 MPa (typical for alloy steel shafts)safetyFactor = 1.5Driver power includes margins per API 617:
| Shaft Power | Driver Margin |
|---|---|
| < 150 kW | 25% |
| 150-750 kW | 15% |
| > 750 kW | 10% |
driverPower = (shaftPower + mechanicalLosses) × driverMargin
designPressure = dischargePressure × 1.10 (10% margin)
designTemperature = dischargeTemperature + 30°C
| Design Pressure | Casing Type |
|---|---|
| > 100 bara | Barrel |
| 40-100 bara | Horizontally Split |
| < 40 bara | Vertically Split |
maxContinuousSpeed = operatingSpeed × 1.05
tripSpeed = maxContinuousSpeed × 1.05
The first lateral critical speed is estimated using simplified Rayleigh-Ritz formulation based on shaft geometry.
API 617 Requirement: Separation margin from critical speed ≥ 15%
bearingSpan = numberOfStages × (impellerDiameter × 0.8) + impellerDiameter
impellerWeight = numberOfStages × 0.5 × (impellerDiameter/100)^2.5
shaftWeight = bearingSpan/1000 × 7850 × π × (shaftDiameter/2000)²
rotorWeight = impellerWeight + shaftWeight
casingThickness = max(10mm, designPressure × impellerDiameter / (2 × 150))
casingWeight = π × casingOD × casingLength × casingThickness × 7850 × 1.2
For barrel-type casing, add 30% additional weight.
| Component | Estimation Method |
|---|---|
| Casing | As calculated above |
| Bundle (rotor + internals) | rotorWeight + stage internals |
| Seal system | 100 × (shaftDiameter/100) kg |
| Lube oil system | 200 + driverPower × 0.1 kg |
| Baseplate | casingWeight × 0.3 |
| Piping | emptyVesselWeight × 0.2 |
| Electrical | driverPower × 0.5 kg |
| Structural steel | emptyVesselWeight × 0.15 |
moduleLength = compressorLength + driverLength + couplingSpace + auxiliarySpace
moduleWidth = casingOD + 3.0m (access each side)
moduleHeight = casingOD + 2.0m (piping and lifting)
Minimum dimensions: 4m × 3m × 3m
The mechanical design integrates with CompressorMechanicalLosses for:
When setDesign() is called, the mechanical losses model is automatically initialized with the calculated shaft diameter.
// Create and run compressor
SystemInterface gas = new SystemSrkEos(300.0, 10.0);
gas.addComponent("methane", 1.0);
gas.setMixingRule(2);
Stream inlet = new Stream("inlet", gas);
inlet.setFlowRate(10000.0, "kg/hr");
Compressor comp = new Compressor("export compressor", inlet);
comp.setOutletPressure(40.0);
comp.setPolytropicEfficiency(0.76);
comp.setSpeed(8000);
ProcessSystem ps = new ProcessSystem();
ps.add(inlet);
ps.add(comp);
ps.run();
// Calculate mechanical design
comp.getMechanicalDesign().calcDesign();
// Access design results
int stages = comp.getMechanicalDesign().getNumberOfStages();
double impellerD = comp.getMechanicalDesign().getImpellerDiameter(); // mm
double driverPower = comp.getMechanicalDesign().getDriverPower(); // kW
double totalWeight = comp.getMechanicalDesign().getWeightTotal(); // kg
// Apply design (initializes mechanical losses)
comp.getMechanicalDesign().setDesign();
// Get seal gas consumption
double sealGas = comp.getSealGasConsumption(); // Nm³/hr
| Parameter | Method | Unit |
|---|---|---|
| Number of stages | getNumberOfStages() |
- |
| Head per stage | getHeadPerStage() |
kJ/kg |
| Impeller diameter | getImpellerDiameter() |
mm |
| Tip speed | getTipSpeed() |
m/s |
| Shaft diameter | getShaftDiameter() |
mm |
| Bearing span | getBearingSpan() |
mm |
| Design pressure | getDesignPressure() |
bara |
| Design temperature | getDesignTemperature() |
°C |
| Casing type | getCasingType() |
enum |
| Driver power | getDriverPower() |
kW |
| Max continuous speed | getMaxContinuousSpeed() |
rpm |
| Trip speed | getTripSpeed() |
rpm |
| First critical speed | getFirstCriticalSpeed() |
rpm |
| Casing weight | getCasingWeight() |
kg |
| Bundle weight | getBundleWeight() |
kg |
| Total skid weight | getWeightTotal() |
kg |
| Module dimensions | getModuleLength/Width/Height() |
m |
Compressor - Main compressor process equipment classCompressorMechanicalLosses - Seal gas and bearing loss calculationsCompressorChart - Performance curve handlingCompressorCostEstimate - Cost estimation based on mechanical designDocumentation for liquid pumping equipment in NeqSim.
Location: neqsim.process.equipment.pump
Classes:
| Class | Description |
|---|---|
Pump |
Centrifugal or positive displacement pump |
PumpInterface |
Pump interface |
import neqsim.process.equipment.pump.Pump;
// Create pump on liquid stream
Pump pump = new Pump("P-100", liquidStream);
pump.setOutletPressure(50.0, "bara");
pump.run();
// Results
double power = pump.getPower("kW");
double head = pump.getHead("m");
double efficiency = pump.getIsentropicEfficiency();
// By outlet pressure
pump.setOutletPressure(50.0, "bara");
// By pressure rise
pump.setPressureRise(30.0, "bara");
// By head
pump.setHead(300.0, "m");
// Set pump efficiency
pump.setIsentropicEfficiency(0.75); // 75%
// Calculate power
pump.run();
double power = pump.getPower("kW");
double isentropicPower = pump.getIsentropicPower("kW");
double actualPower = pump.getActualPower("kW");
// Efficiency = Isentropic Power / Actual Power
The pump power is calculated as:
$$P = \frac{\dot{m} \cdot \Delta h_{isentropic}}{\eta_{isentropic}}$$
Where:
$$H = \frac{\Delta P}{\rho \cdot g}$$
Where:
// Define head vs flow curve points
double[] flowRates = {0, 50, 100, 150, 200}; // m³/hr
double[] heads = {350, 340, 310, 260, 180}; // m
double[] efficiencies = {0, 0.65, 0.80, 0.75, 0.60};
pump.setHeadCurve(flowRates, heads, "m3/hr", "m");
pump.setEfficiencyCurve(flowRates, efficiencies, "m3/hr");
pump.run();
// Get operating point
double flowRate = pump.getInletStream().getFlowRate("m3/hr");
double actualHead = pump.getHead("m");
double actualEff = pump.getIsentropicEfficiency();
// NPSH available from process conditions
double npshAvailable = pump.getNPSHAvailable("m");
// NPSH required (from pump curve)
double[] flows = {50, 100, 150, 200};
double[] npshReq = {1.5, 2.0, 3.0, 5.0};
pump.setNPSHRequiredCurve(flows, npshReq, "m3/hr", "m");
double npshRequired = pump.getNPSHRequired("m");
// Check cavitation margin
double margin = npshAvailable - npshRequired;
if (margin < 1.0) {
System.out.println("Warning: Low NPSH margin");
}
$$NPSH_A = \frac{P_{suction}}{\rho g} + \frac{v^2}{2g} - \frac{P_{vapor}}{\rho g}$$
import neqsim.thermo.system.SystemSrkEos;
import neqsim.process.equipment.stream.Stream;
import neqsim.process.equipment.pump.Pump;
// Create liquid stream
SystemSrkEos fluid = new SystemSrkEos(298.15, 5.0);
fluid.addComponent("n-heptane", 1.0);
fluid.setMixingRule("classic");
Stream feed = new Stream("Feed", fluid);
feed.setFlowRate(100.0, "m3/hr");
feed.run();
// Pump
Pump pump = new Pump("P-100", feed);
pump.setOutletPressure(30.0, "bara");
pump.setIsentropicEfficiency(0.75);
pump.run();
// Results
System.out.println("Flow rate: " + pump.getInletStream().getFlowRate("m3/hr") + " m³/hr");
System.out.println("Head: " + pump.getHead("m") + " m");
System.out.println("Power: " + pump.getPower("kW") + " kW");
System.out.println("Efficiency: " + pump.getIsentropicEfficiency() * 100 + " %");
// Define pump curves
double[] flows = {0, 25, 50, 75, 100, 125, 150};
double[] heads = {400, 395, 380, 355, 320, 270, 200};
double[] effs = {0, 0.55, 0.70, 0.78, 0.80, 0.75, 0.65};
Pump pump = new Pump("P-100", liquidStream);
pump.setHeadCurve(flows, heads, "m3/hr", "m");
pump.setEfficiencyCurve(flows, effs, "m3/hr");
pump.run();
// Operating point found on curves
System.out.println("Operating flow: " + pump.getInletStream().getFlowRate("m3/hr"));
System.out.println("Operating head: " + pump.getHead("m"));
System.out.println("Operating efficiency: " + pump.getIsentropicEfficiency());
// Inlet conditions
SystemSrkEos crude = new SystemSrkEos(340.0, 3.0);
crude.addComponent("methane", 0.01);
crude.addComponent("n-pentane", 0.20);
crude.addComponent("n-heptane", 0.50);
crude.addComponent("n-decane", 0.29);
crude.setMixingRule("classic");
Stream feed = new Stream("Crude Feed", crude);
feed.setFlowRate(500.0, "m3/hr");
feed.run();
// First stage pump
Pump pump1 = new Pump("P-100A", feed);
pump1.setOutletPressure(20.0, "bara");
pump1.setIsentropicEfficiency(0.78);
pump1.run();
// Second stage pump
Pump pump2 = new Pump("P-100B", pump1.getOutletStream());
pump2.setOutletPressure(50.0, "bara");
pump2.setIsentropicEfficiency(0.75);
pump2.run();
// Total power
double totalPower = pump1.getPower("kW") + pump2.getPower("kW");
System.out.println("Total pump power: " + totalPower + " kW");
SystemInterface fluid = new SystemSrkEos(298.15, 1.0);
fluid.addComponent("water", 1.0);
fluid.setTotalFlowRate(100.0, "m3/hr");
Stream feed = new Stream("Feed", fluid);
feed.setTemperature(20.0, "C");
feed.setPressure(1.0, "bara");
Pump pump = new Pump("Pump1", feed);
pump.setOutletPressure(10.0, "bara");
pump.setIsentropicEfficiency(0.75); // 75% efficiency
pump.run();
double power = pump.getPower("kW");
double outletTemp = pump.getOutletStream().getTemperature("C");
// Define pump performance at different speeds
double[] speed = new double[] {1000.0, 1500.0, 2000.0};
// Flow rates in m³/hr for each speed
double[][] flow = new double[][] {
{10.0, 20.0, 30.0, 40.0, 50.0, 60.0},
{15.0, 30.0, 45.0, 60.0, 75.0, 90.0},
{20.0, 40.0, 60.0, 80.0, 100.0, 120.0}
};
// Head in meters for each speed and flow
double[][] head = new double[][] {
{120.0, 118.0, 115.0, 110.0, 103.0, 94.0},
{270.0, 265.5, 258.8, 247.5, 231.8, 211.5},
{480.0, 472.0, 460.0, 440.0, 412.0, 376.0}
};
// Efficiency in % for each speed and flow
double[][] efficiency = new double[][] {
{65.0, 72.0, 78.0, 82.0, 80.0, 74.0},
{66.0, 73.0, 79.0, 83.0, 81.0, 75.0},
{67.0, 74.0, 80.0, 84.0, 82.0, 76.0}
};
pump.getPumpChart().setCurves(new double[]{}, speed, flow, head, efficiency);
pump.getPumpChart().setHeadUnit("meter"); // or "kJ/kg"
pump.setSpeed(1500.0); // Set operating speed in rpm
Meters (most common):
pump.getPumpChart().setHeadUnit("meter");
// Head represents height of fluid column
// ΔP = ρ × g × H
Specific Energy (kJ/kg):
pump.getPumpChart().setHeadUnit("kJ/kg");
// Head represents specific energy
// ΔP = E × ρ
pump.setCheckNPSH(true);
pump.setNPSHMargin(1.3); // Recommended: 1.1-1.5
pump.run();
// Check for cavitation risk
if (pump.isCavitating()) {
double npsha = pump.getNPSHAvailable();
double npshr = pump.getNPSHRequired();
System.out.println("Warning: NPSHa = " + npsha + " m, NPSHr = " + npshr + " m");
// Take corrective action: increase suction pressure or decrease temperature
}
double npsha = pump.getNPSHAvailable();
double npshr = pump.getNPSHRequired();
if (npsha < 1.3 * npshr) {
// Insufficient NPSH - risk of cavitation
// Solutions:
// 1. Increase suction pressure
// 2. Decrease fluid temperature
// 3. Reduce pump speed
// 4. Select different pump
}
double flow = feed.getFlowRate("m3/hr");
double speed = pump.getSpeed();
String status = pump.getPumpChart().getOperatingStatus(flow, speed);
switch (status) {
case "OPTIMAL":
// Operating near best efficiency point
break;
case "NORMAL":
// Operating within acceptable range
break;
case "LOW_EFFICIENCY":
// Operating far from BEP - inefficient
// Consider adjusting speed or selecting different pump
break;
case "SURGE":
// Flow too low - risk of instability and damage
// Increase flow or reduce speed immediately
break;
case "STONEWALL":
// Flow too high - maximum capacity reached
// Reduce flow or increase speed
break;
}
double bepFlow = pump.getPumpChart().getBestEfficiencyFlowRate();
double bepHead = pump.getPumpChart().getHead(bepFlow, speed);
double bepEfficiency = pump.getPumpChart().getEfficiency(bepFlow, speed);
System.out.println("Best efficiency: " + bepEfficiency + "% at " + bepFlow + " m³/hr");
double ns = pump.getPumpChart().getSpecificSpeed();
if (ns < 1000) {
System.out.println("Radial flow (centrifugal) pump");
} else if (ns < 4000) {
System.out.println("Mixed flow pump");
} else {
System.out.println("Axial flow pump");
}
// Affinity laws: Q ∝ N, H ∝ N², P ∝ N³
double baseSpeed = 1500.0;
double baseFlow = 50.0; // m³/hr
double baseHead = pump.getPumpChart().getHead(baseFlow, baseSpeed);
// To increase head by 44% (factor of 1.44 = 1.2²):
double newSpeed = baseSpeed * 1.2;
double newFlow = baseFlow * 1.2;
double newHead = baseHead * 1.44;
pump.setSpeed(newSpeed);
// Efficiency stays approximately constant at same reduced flow
pump.setMinimumFlow(0.05); // kg/sec
// When flow drops below minimum, pump idles with no pressure rise
// In practice, add minimum flow recirculation loop
Stream stage1Out = new Stream("Stage 1 Out");
Pump stage1 = new Pump("Stage 1", feed);
stage1.setOutletPressure(5.0, "bara");
stage1.setOutStream(stage1Out);
Pump stage2 = new Pump("Stage 2", stage1Out);
stage2.setOutletPressure(10.0, "bara");
// Total head = stage1 head + stage2 head
// Default: Simple fan law interpolation
pump.setPumpChartType("fan law");
// Alternative: Map lookup with extrapolation
pump.setPumpChartType("interpolate and extrapolate");
double rho = feed.getThermoSystem().getDensity("kg/m3");
double Q = feed.getFlowRate("m3/s");
double H = pump.getPumpChart().getHead(feed.getFlowRate("m3/hr"), pump.getSpeed());
double g = 9.81; // m/s²
double hydraulicPower = rho * g * Q * H; // Watts
double efficiency = pump.getIsentropicEfficiency() / 100.0; // Convert % to decimal
double shaftPower = hydraulicPower / efficiency;
double powerKW = pump.getPower("kW");
double hoursPerYear = 8760;
double costPerKWh = 0.10; // $/kWh
double annualEnergyCost = powerKW * hoursPerYear * costPerKWh;
System.out.println("Annual energy cost: $" + annualEnergyCost);
// Create fluid system
SystemInterface water = new SystemSrkEos(298.15, 1.5);
water.addComponent("water", 1.0);
water.setTemperature(25.0, "C");
water.setPressure(1.5, "bara");
water.setTotalFlowRate(75.0, "m3/hr");
Stream feed = new Stream("Pump Feed", water);
feed.run();
// Create pump with curve
Pump pump = new Pump("Booster Pump", feed);
double[] speed = new double[] {1450.0};
double[] flowPoints = {30, 50, 70, 90, 110, 130};
double[] headPoints = {45, 44, 42, 38, 32, 24};
double[] effPoints = {68, 76, 82, 84, 80, 70};
double[][] flow = new double[][] {flowPoints};
double[][] head = new double[][] {headPoints};
double[][] eff = new double[][] {effPoints};
pump.getPumpChart().setCurves(new double[]{}, speed, flow, head, eff);
pump.getPumpChart().setHeadUnit("meter");
pump.setSpeed(1450.0);
pump.setCheckNPSH(true);
pump.setNPSHMargin(1.3);
// Run simulation
pump.run();
// Check results
System.out.println("Outlet pressure: " + pump.getOutletPressure() + " bara");
System.out.println("Power: " + pump.getPower("kW") + " kW");
System.out.println("Outlet temp: " + pump.getOutletStream().getTemperature("C") + " °C");
System.out.println("NPSHa: " + pump.getNPSHAvailable() + " m");
System.out.println("Status: " + pump.getPumpChart().getOperatingStatus(75.0, 1450.0));
if (pump.isCavitating()) {
System.out.println("WARNING: Cavitation risk!");
}
This example demonstrates a realistic pump configuration where a suction line connects an upstream separator to the pump. The suction piping introduces pressure losses and static head changes that directly affect the NPSH available at the pump inlet. Properly modeling the suction line is critical for accurate cavitation assessment.
In real installations, the pump does not receive fluid directly at separator conditions. The suction system introduces:
These effects reduce the pressure at the pump suction flange relative to the source, directly impacting NPSHa. Ignoring suction system effects can lead to:
import neqsim
# Get the oil outlet stream from an upstream separator
# (This would typically come from a configured process system)
pump_feed = oseberg_process.get('main process').getUnit('3RD stage separator').getOilOutStream()
# --- Separator Outlet Valve ---
# Model the isolation/control valve at the separator oil outlet
# Cv sizing: For a 6" valve (DN150) with full port, typical Cv ≈ 400-500
# For a 4" valve (DN100), typical Cv ≈ 150-200
separatorValve = neqsim.process.equipment.valve.ThrottlingValve("SeparatorOutletValve", pump_feed)
separatorValve.setCv(350) # Valve Cv (flow coefficient in US gpm/psi^0.5)
separatorValve.setIsCalcOutPressure(True)
separatorValve.setPercentValveOpening(80) # 80% open - allows for control margin
separatorValve.run()
# --- Suction Line Configuration ---
# Model the piping between separator valve and pump using Beggs & Brill correlation
# This accounts for friction losses and elevation effects
suctionLine = neqsim.process.equipment.pipeline.PipeBeggsAndBrills("SuctionLine", separatorValve.getOutletStream())
suctionLine.setLength(20.0) # Pipe length in meters
suctionLine.setDiameter(0.2) # Internal diameter in meters (200 mm)
suctionLine.setPipeWallRoughness(1.0e-5) # Internal roughness in meters (~smooth pipe)
suctionLine.setElevation(-20) # Pump is 20 m below separator (positive static head)
suctionLine.run()
# --- Pump Configuration ---
# Create the pump taking suction from the pipe outlet
pump1 = neqsim.process.equipment.pump.Pump('oil pump', suctionLine.getOutStream())
pump1.setOutletPressure(60.0, 'bara') # Required discharge pressure
pump1.setCheckNPSH(True) # Enable cavitation monitoring
pump1.setNPSHMargin(1.3) # Require NPSHa >= 1.3 × NPSHr
# --- Pump Performance Curves ---
# Define pump characteristic curves at the operating speed
# These are typically from manufacturer datasheets
speed = [3259] # Pump speed in RPM
flow = [[1, 50, 70, 130]] # Flow points in m³/hr
head = [[250, 240, 230, 180]] # Head in meters at each flow
eff = [[5, 40, 50, 52]] # Efficiency in % at each flow
npsh = [[2.0, 4.3, 6.0, 8.0]] # NPSHr curve in meters
pump1.getPumpChart().setCurves([], speed, flow, head, eff)
pump1.getPumpChart().setNPSHCurve(npsh)
pump1.getPumpChart().setHeadUnit("meter")
pump1.setSpeed(3259)
pump1.run()
# --- Results Analysis ---
print("=== Pump & Suction System Results ===")
print(f"Flow rate (m3/hr): {pump_feed.getFlowRate('idSm3/hr')}")
print(f"Separator outlet pressure (bara): {pump_feed.getPressure('bara')}")
print(f"Valve outlet pressure (bara): {separatorValve.getOutletPressure()}")
print(f"Valve pressure drop (bar): {separatorValve.getDeltaP()}")
print(f"Pump inlet pressure (bara): {pump1.getInletPressure()}")
print(f"Pump outlet pressure (bara): {pump1.getOutletPressure()}")
print(f"Pump NPSHa (meter): {pump1.getNPSHAvailable()}")
print(f"Pump NPSHr (meter): {pump1.getNPSHRequired()}")
print(f"Pump power (kW): {pump1.getPower('kW')}")
print(f"Cavitation risk: {'YES' if pump1.isCavitating() else 'NO'}")
| Parameter | Purpose |
|---|---|
setCv(350) |
Valve flow coefficient - determines pressure drop for given flow |
setPercentValveOpening(80) |
Valve position (0-100%); partially open for control margin |
setLength(20.0) |
Total equivalent length of suction piping including fittings |
setDiameter(0.2) |
Internal pipe diameter - larger diameter reduces friction loss |
setPipeWallRoughness(1.0e-5) |
Surface roughness; affects friction factor |
setElevation(-20) |
Negative elevation means pump is below source (increases NPSHa) |
setCheckNPSH(True) |
Enables automatic cavitation detection |
setNPSHMargin(1.3) |
Safety factor; typical values 1.1–1.5 |
setNPSHCurve(npsh) |
Required NPSH as function of flow from pump datasheet |
Separator outlet pressure vs. Pump inlet pressure: The difference shows the pressure drop across the suction line. If the pump inlet pressure is much lower than expected, consider increasing pipe diameter or reducing length.
NPSHa vs. NPSHr: NPSHa must exceed NPSHr by the specified margin. If isCavitating() returns True, consider:
Static head contribution: With a -20 m elevation (pump below separator), the static head adds approximately 20 m × ρ × g to the suction pressure, which is beneficial for NPSHa.
Suction pipe sizing: Velocity in suction lines should typically be 1–2 m/s for liquids to minimize friction losses while avoiding sedimentation.
Elevation effects: Locating the pump below the liquid source is the most reliable way to ensure adequate NPSHa.
Temperature sensitivity: Hot liquids have higher vapor pressure, reducing NPSHa. Consider subcooling or elevated suction pressure for near-boiling liquids.
Transient conditions: During startup or upset conditions, flow rates may exceed design, increasing NPSHr while simultaneously increasing suction line losses—always check NPSHa at maximum expected flow.
NeqSim provides comprehensive centrifugal pump simulation through the Pump and PumpChart classes. The implementation supports:
Also see pump usage guide.
The affinity laws relate pump performance at different speeds:
| Parameter | Relationship |
|---|---|
| Flow | Q₂/Q₁ = N₂/N₁ |
| Head | H₂/H₁ = (N₂/N₁)² |
| Power | P₂/P₁ = (N₂/N₁)³ |
| NPSH | NPSH₂/NPSH₁ = (N₂/N₁)² |
P_hydraulic = ρ·g·Q·H = Q·ΔP
P_shaft = P_hydraulic / η
NPSHₐ = (P_suction - P_vapor) / (ρ·g) + v²/(2g)
Cavitation occurs when NPSHₐ < NPSHᵣ. A safety margin of 1.3× is typically required.
Pump curves measured with water require correction for other fluids:
H_actual = H_chart × (ρ_chart / ρ_actual)
Pump (PumpInterface)
├── PumpChart (PumpChartInterface)
│ ├── PumpCurve (individual speed curves)
│ └── PumpChartAlternativeMapLookupExtrapolate (alternative implementation)
└── PumpMechanicalDesign
| Class | Purpose |
|---|---|
Pump |
Main pump equipment model |
PumpChart |
Performance curve management |
PumpChartInterface |
Interface for pump chart implementations |
// Create fluid and stream
SystemInterface fluid = new SystemSrkEos(298.15, 2.0);
fluid.addComponent("water", 1.0);
Stream feedStream = new Stream("Feed", fluid);
feedStream.run();
// Create pump with outlet pressure
Pump pump = new Pump("MainPump", feedStream);
pump.setOutletPressure(10.0, "bara");
pump.setIsentropicEfficiency(0.75);
pump.run();
System.out.println("Power: " + pump.getPower("kW") + " kW");
System.out.println("Outlet T: " + pump.getOutletTemperature() + " K");
// Define pump curves at multiple speeds
double[] speed = {1000.0, 1500.0};
double[][] flow = {
{10, 20, 30, 40, 50}, // m³/hr at 1000 rpm
{15, 30, 45, 60, 75} // m³/hr at 1500 rpm
};
double[][] head = {
{120, 115, 108, 98, 85}, // meters at 1000 rpm
{270, 259, 243, 220, 191} // meters at 1500 rpm
};
double[][] efficiency = {
{65, 75, 82, 80, 72}, // % at 1000 rpm
{67, 77, 84, 82, 74} // % at 1500 rpm
};
// chartConditions: [refMW, refTemp, refPressure, refZ, refDensity]
double[] chartConditions = {18.0, 298.15, 1.0, 1.0, 998.0};
Pump pump = new Pump("ChartPump", feedStream);
pump.getPumpChart().setCurves(chartConditions, speed, flow, head, efficiency);
pump.getPumpChart().setHeadUnit("meter");
pump.setSpeed(1200.0);
pump.run();
When pumping fluids with different density than the chart test fluid:
// Option 1: Set via chartConditions (5th element)
double[] chartConditions = {18.0, 298.15, 1.0, 1.0, 998.0}; // 998 kg/m³ reference
pump.getPumpChart().setCurves(chartConditions, speed, flow, head, efficiency);
// Option 2: Set directly
pump.getPumpChart().setReferenceDensity(998.0);
// Check if correction is active
if (pump.getPumpChart().hasDensityCorrection()) {
double correctedHead = pump.getPumpChart().getCorrectedHead(flow, speed, actualDensity);
}
// Enable NPSH checking
pump.setCheckNPSH(true);
pump.setNPSHMargin(1.3); // Safety factor
// Check cavitation risk
double npshAvailable = pump.getNPSHAvailable();
double npshRequired = pump.getNPSHRequired();
boolean cavitating = pump.isCavitating();
// Set NPSH curve from manufacturer data
double[][] npshCurve = {
{2.0, 2.5, 3.2, 4.0, 5.2}, // NPSHr at 1000 rpm
{4.5, 5.6, 7.2, 9.0, 11.7} // NPSHr at 1500 rpm
};
pump.getPumpChart().setNPSHCurve(npshCurve);
// Check operating status
String status = pump.getPumpChart().getOperatingStatus(flowRate, speed);
// Returns: "OPTIMAL", "NORMAL", "LOW_EFFICIENCY", "SURGE", or "STONEWALL"
// Check specific conditions
boolean surging = pump.getPumpChart().checkSurge2(flowRate, speed);
boolean stonewall = pump.getPumpChart().checkStoneWall(flowRate, speed);
// Get best efficiency point
double bepFlow = pump.getPumpChart().getBestEfficiencyFlowRate();
double specificSpeed = pump.getPumpChart().getSpecificSpeed();
This example demonstrates a realistic pump system with:
import neqsim
# Get feed stream from upstream process (e.g., oil from separator)
pump_feed = oseberg_process.get('main process').getUnit('3RD stage separator').getOilOutStream()
# === Separator Outlet Control Valve ===
# Controls flow from separator to pump suction
separatorValve = neqsim.process.equipment.valve.ThrottlingValve("SeparatorOutletValve", pump_feed)
separatorValve.setCv(200) # Valve Cv (flow coefficient in US gpm/psi^0.5)
separatorValve.setPercentValveOpening(80) # 80% open - allows for control margin
separatorValve.setIsCalcOutPressure(True) # Calculate outlet pressure from Cv
separatorValve.run()
# === Suction Pipeline ===
# Models pressure drop and elevation effects on NPSH
suctionLine = neqsim.process.equipment.pipeline.PipeBeggsAndBrills("SuctionLine", separatorValve.getOutletStream())
suctionLine.setLength(20.0) # Pipe length in meters
suctionLine.setDiameter(0.2) # Pipe inner diameter in meters
suctionLine.setPipeWallRoughness(1.0e-5) # Internal roughness in meters
suctionLine.setElevation(-20) # Negative = pump is 20m below separator (adds static head)
suctionLine.run()
# === Centrifugal Pump ===
pump1 = neqsim.process.equipment.pump.Pump('oil pump', suctionLine.getOutStream())
pump1.setOutletPressure(60.0, 'bara') # Target discharge pressure
# Enable NPSH monitoring with 1.3x safety margin
pump1.setCheckNPSH(True)
pump1.setNPSHMargin(1.3)
# Define manufacturer pump curves (single speed)
speed = [3259] # rpm
flow = [[1, 50, 70, 130]] # m³/hr
head = [[250, 240, 230, 180]] # meters
eff = [[5, 40, 50, 52]] # efficiency %
# NPSHr curve (meters) - must match flow array dimensions
npsh = [[2.0, 4.3, 6.0, 8.0]]
# Configure pump chart
pump1.getPumpChart().setCurves([], speed, flow, head, eff)
pump1.getPumpChart().setNPSHCurve(npsh)
pump1.getPumpChart().setHeadUnit("meter")
pump1.setSpeed(3259)
pump1.run()
# === Results ===
print("=== Pump & Suction Line Results ===")
print(f"Flow rate (m3/hr): {pump_feed.getFlowRate('idSm3/hr'):.2f}")
print(f"Separator outlet pressure (bara): {pump_feed.getPressure('bara'):.2f}")
print(f"Pump inlet pressure (bara): {pump1.getInletPressure():.2f}")
print(f"Pump outlet pressure (bara): {pump1.getOutletPressure():.2f}")
print(f"Pump head (m): {pump1.getPumpChart().getHead(pump_feed.getFlowRate('m3/hr'), 3259):.1f}")
print(f"Pump efficiency (%): {pump1.getPumpChart().getEfficiency(pump_feed.getFlowRate('m3/hr'), 3259):.1f}")
print(f"Pump NPSHa (meter): {pump1.getNPSHAvailable():.2f}")
print(f"Pump NPSHr (meter): {pump1.getNPSHRequired():.2f}")
print(f"Pump power (kW): {pump1.getPower('kW'):.1f}")
print(f"Cavitation risk: {'YES - INCREASE SUCTION PRESSURE' if pump1.isCavitating() else 'NO'}")
Suction System Design: The suction line elevation affects NPSHₐ. Negative elevation (pump below source) adds static head, improving NPSH margin.
Control Valve Sizing: The Cv value determines pressure drop at the given flow. Use setIsCalcOutPressure(True) to calculate outlet pressure from Cv.
NPSH Monitoring: Enable with setCheckNPSH(True). The pump calculates:
Pump Curves: The setCurves() method accepts:
[] for chartConditions (or include reference density as 5th element)NPSH Curve: Must be set separately via setNPSHCurve() with same dimensions as flow array.
| Method | Description |
|---|---|
setOutletPressure(double, String) |
Set target outlet pressure |
setIsentropicEfficiency(double) |
Set pump efficiency (0-1) |
setSpeed(double) |
Set pump speed in rpm |
getPower() |
Get shaft power in watts |
getPower(String) |
Get power in specified unit |
getNPSHAvailable() |
Calculate available NPSH in meters |
getNPSHRequired() |
Get required NPSH from chart or estimate |
isCavitating() |
Check if pump is at cavitation risk |
setCheckNPSH(boolean) |
Enable/disable NPSH monitoring |
getPumpChart() |
Get the pump chart object |
| Method | Description |
|---|---|
setCurves(double[], double[], double[][], double[][], double[][]) |
Set complete pump curves |
setHeadUnit(String) |
Set head unit: "meter" or "kJ/kg" |
setNPSHCurve(double[][]) |
Set NPSH required curves |
setReferenceDensity(double) |
Set reference density for correction |
| Method | Description |
|---|---|
getHead(double, double) |
Get head at flow and speed |
getCorrectedHead(double, double, double) |
Get density-corrected head |
getEfficiency(double, double) |
Get efficiency at flow and speed |
getNPSHRequired(double, double) |
Get NPSH required at flow and speed |
getSpeed(double, double) |
Calculate speed for given flow and head |
| Method | Description |
|---|---|
getOperatingStatus(double, double) |
Get operating status string |
checkSurge2(double, double) |
Check if in surge condition |
checkStoneWall(double, double) |
Check if at stonewall |
getBestEfficiencyFlowRate() |
Get flow at BEP |
getSpecificSpeed() |
Calculate pump specific speed |
hasDensityCorrection() |
Check if density correction is active |
hasNPSHCurve() |
Check if NPSH curve is available |
The chartConditions array passed to setCurves() contains reference conditions:
| Index | Parameter | Unit | Description |
|---|---|---|---|
| 0 | refMW | kg/kmol | Reference molecular weight |
| 1 | refTemperature | K | Reference temperature |
| 2 | refPressure | bara | Reference pressure |
| 3 | refZ | - | Reference compressibility |
| 4 | refDensity | kg/m³ | Reference fluid density (optional) |
Note: Element [4] is optional for backward compatibility. If omitted, no density correction is applied.
Pump performance is significantly affected by fluid viscosity. Curves measured with water or light oil require correction when pumping viscous fluids like heavy crude oil.
NeqSim implements the Hydraulic Institute viscosity correction method for centrifugal pumps. The correction uses the B parameter:
B = 26.6 × ν^0.5 × H^0.0625 / (Q^0.375 × N^0.25)
Where:
| Parameter | Factor | Description |
|---|---|---|
| Flow | Cq | Q_viscous = Q_water × Cq |
| Head | Ch | H_viscous = H_water × Ch |
| Efficiency | Cη | η_viscous = η_water × Cη |
Valid range: 4 - 4000 cSt (below 4 cSt, water properties assumed)
// Create pump with chart
Pump pump = new Pump("ViscousPump", feedStream);
pump.getPumpChart().setCurves(chartConditions, speed, flow, head, efficiency);
// Enable viscosity correction
pump.getPumpChart().setReferenceViscosity(1.0); // Chart measured with water (1 cSt)
pump.getPumpChart().setUseViscosityCorrection(true); // Enable correction
// Set pump parameters
pump.getPumpChart().setReferenceFlow(100.0); // BEP flow (m³/hr)
pump.getPumpChart().setReferenceHead(100.0); // BEP head (meters)
pump.getPumpChart().setReferenceSpeed(1500.0); // Reference speed (rpm)
pump.run();
// Check applied corrections
System.out.println("Flow correction factor Cq: " + pump.getPumpChart().getFlowCorrectionFactor());
System.out.println("Head correction factor Ch: " + pump.getPumpChart().getHeadCorrectionFactor());
System.out.println("Efficiency correction Cη: " + pump.getPumpChart().getEfficiencyCorrectionFactor());
import neqsim
from neqsim.process import stream, pump
# Create stream with viscous oil
oil = neqsim.thermo.system.SystemSrkEos(323.15, 5.0)
oil.addComponent("nC20", 1.0) # Heavy hydrocarbon
oil.setMixingRule("classic")
feed = stream.stream("ViscousOilFeed", oil)
feed.setFlowRate(100.0, "kg/hr")
feed.run()
# Create pump with viscosity correction
viscous_pump = pump.pump("OilBooster", feed)
viscous_pump.getPumpChart().setReferenceViscosity(1.0)
viscous_pump.getPumpChart().setUseViscosityCorrection(True)
viscous_pump.setOutletPressure(10.0, "bara")
viscous_pump.run()
print(f"Actual viscosity: {feed.getFluid().getKinematicViscosity('cSt'):.1f} cSt")
print(f"Head correction: {viscous_pump.getPumpChart().getHeadCorrectionFactor():.3f}")
print(f"Efficiency correction: {viscous_pump.getPumpChart().getEfficiencyCorrectionFactor():.3f}")
The ESPPump class extends Pump for handling multiphase gas-liquid flows commonly encountered in oil well production.
Head degradation follows a quadratic relationship:
f = 1 - A × GVF - B × GVF²
Where default coefficients are: A = 0.5, B = 2.0
| Condition | Default Threshold | Description |
|---|---|---|
| Surging | GVF > 15% | Unstable operation begins |
| Gas Lock | GVF > 30% | Pump loses prime, flow stops |
// Create multiphase stream (gas + liquid)
SystemInterface fluid = new SystemSrkEos(323.15, 30.0);
fluid.addComponent("methane", 0.05); // 5% gas
fluid.addComponent("n-heptane", 0.95); // 95% liquid
fluid.setMixingRule("classic");
fluid.setMultiPhaseCheck(true);
Stream wellStream = new Stream("WellProduction", fluid);
wellStream.setFlowRate(1000.0, "kg/hr");
wellStream.run();
// Create ESP pump
ESPPump esp = new ESPPump("ESP-1", wellStream);
esp.setNumberOfStages(100); // 100-stage pump
esp.setHeadPerStage(10.0); // 10 m head per stage
// Configure GVF handling
esp.setMaxGVF(0.30); // 30% max GVF before gas lock
esp.setSurgingGVF(0.15); // 15% - surging onset
esp.setHasGasSeparator(true); // Include rotary gas separator
esp.setGasSeparatorEfficiency(0.60); // 60% gas separation
esp.run();
// Check operating status
System.out.println("GVF at inlet: " + (esp.getGasVoidFraction() * 100) + "%");
System.out.println("Head degradation: " + (1 - esp.getHeadDegradationFactor()) * 100 + "% loss");
System.out.println("Surging: " + esp.isSurging());
System.out.println("Gas locked: " + esp.isGasLocked());
System.out.println("Pressure boost: " + (esp.getOutletPressure() - esp.getInletPressure()) + " bara");
import neqsim
from neqsim.thermo.system import SystemSrkEos
from neqsim.process.equipment.pump import ESPPump
# Create multiphase well fluid
well_fluid = SystemSrkEos(353.15, 25.0)
well_fluid.addComponent("methane", 0.08)
well_fluid.addComponent("n-heptane", 0.92)
well_fluid.setMixingRule("classic")
well_fluid.setMultiPhaseCheck(True)
well_stream = neqsim.process.stream.stream("WellStream", well_fluid)
well_stream.setFlowRate(2000.0, "kg/hr")
well_stream.run()
# Create and configure ESP
esp = ESPPump("ESP-1", well_stream)
esp.setNumberOfStages(80)
esp.setHeadPerStage(12.0)
esp.setMaxGVF(0.25)
esp.setHasGasSeparator(True)
esp.setGasSeparatorEfficiency(0.70)
esp.run()
# Monitor performance
print(f"Inlet GVF: {esp.getGasVoidFraction()*100:.1f}%")
print(f"Head degradation factor: {esp.getHeadDegradationFactor():.3f}")
print(f"Effective head: {esp.calculateTotalHead():.1f} m")
print(f"Is surging: {esp.isSurging()}")
| Method | Description |
|---|---|
setNumberOfStages(int) |
Set number of impeller stages |
setHeadPerStage(double) |
Set head per stage (meters) |
setMaxGVF(double) |
Set gas lock threshold (0-1) |
setSurgingGVF(double) |
Set surging onset threshold (0-1) |
setHasGasSeparator(boolean) |
Enable rotary gas separator |
setGasSeparatorEfficiency(double) |
Set separator efficiency (0-1) |
getGasVoidFraction() |
Get calculated inlet GVF |
getHeadDegradationFactor() |
Get head degradation (0-1) |
isSurging() |
Check if pump is surging |
isGasLocked() |
Check if pump has lost prime |
calculateTotalHead() |
Get total developed head |
| Unit | Description | Pressure Calculation |
|---|---|---|
"meter" |
Meters of fluid | ΔP = ρ·g·H |
"kJ/kg" |
Specific energy | ΔP = E·ρ·1000 |
The pump implementation includes comprehensive tests:
| Test Class | Tests | Coverage |
|---|---|---|
PumpTest |
3 | Basic pump operations |
PumpChartTest |
3 | Curve interpolation |
PumpAffinityLawTest |
6 | Affinity law scaling |
PumpNPSHTest |
8 | Cavitation detection |
PumpNPSHCurveTest |
12 | NPSH curve handling |
PumpDensityCorrectionTest |
7 | Density correction |
PumpViscosityCorrectionTest |
12 | HI viscosity correction method |
ESPPumpTest |
12 | ESP multiphase handling |
Total: 63 tests
Documentation for expansion equipment in NeqSim.
Location: neqsim.process.equipment.expander
Classes:
| Class | Description |
|---|---|
Expander |
General gas expander |
TurboExpander |
Turboexpander with shaft coupling |
ExpanderCompressorModule |
Compander unit |
Gas expanders are used for:
import neqsim.process.equipment.expander.Expander;
// Create expander
Expander expander = new Expander("EX-100", gasStream);
expander.setOutletPressure(10.0, "bara");
expander.setIsentropicEfficiency(0.85);
expander.run();
// Results
double power = expander.getPower("kW");
double outletTemp = expander.getOutletTemperature("C");
// By outlet pressure
expander.setOutletPressure(10.0, "bara");
// By pressure ratio
expander.setPressureRatio(5.0);
// By outlet temperature
expander.setOutletTemperature(-50.0, "C");
For direct shaft coupling to compressor.
import neqsim.process.equipment.expander.TurboExpander;
TurboExpander turboExpander = new TurboExpander("TEX-100", gasStream);
turboExpander.setOutletPressure(10.0, "bara");
turboExpander.setIsentropicEfficiency(0.85);
turboExpander.run();
// Couple expander to compressor
turboExpander.setCoupledCompressor(compressor);
// Power balance
turboExpander.run();
compressor.run();
double expanderPower = turboExpander.getPower("kW");
double compressorPower = compressor.getPower("kW");
double netPower = expanderPower - compressorPower;
$$W_{isentropic} = \dot{m} \cdot (h_1 - h_{2s})$$
Where:
$$W_{actual} = \eta_{isentropic} \cdot W_{isentropic}$$
// Get temperatures
double T_in = expander.getInletTemperature("C");
double T_out = expander.getOutletTemperature("C");
double deltaT = T_in - T_out;
// Compare to JT expansion (throttling)
ThrottlingValve valve = new ThrottlingValve("JT", gasStream);
valve.setOutletPressure(10.0, "bara");
valve.run();
double T_out_JT = valve.getOutletTemperature("C");
double deltaT_JT = T_in - T_out_JT;
System.out.println("Expander cooling: " + deltaT + " C");
System.out.println("JT cooling: " + deltaT_JT + " C");
Combined expander-compressor on single shaft.
import neqsim.process.equipment.expander.ExpanderCompressorModule;
ExpanderCompressorModule compander = new ExpanderCompressorModule("Compander");
compander.setExpanderInletStream(hotGas);
compander.setCompressorInletStream(coldGas);
compander.setExpanderOutletPressure(10.0, "bara");
compander.setCompressorOutletPressure(40.0, "bara");
compander.setIsentropicEfficiency(0.85);
compander.run();
double netPower = compander.getNetPower("kW"); // Can be positive or negative
import neqsim.thermo.system.SystemSrkEos;
import neqsim.process.equipment.stream.Stream;
import neqsim.process.equipment.expander.Expander;
// High pressure gas
SystemSrkEos gas = new SystemSrkEos(320.0, 80.0);
gas.addComponent("methane", 0.90);
gas.addComponent("ethane", 0.07);
gas.addComponent("propane", 0.03);
gas.setMixingRule("classic");
Stream feed = new Stream("HP Gas", gas);
feed.setFlowRate(50000.0, "kg/hr");
feed.run();
// Expander
Expander expander = new Expander("EX-100", feed);
expander.setOutletPressure(20.0, "bara");
expander.setIsentropicEfficiency(0.85);
expander.run();
System.out.println("Inlet: " + feed.getTemperature("C") + " C, " + feed.getPressure("bara") + " bara");
System.out.println("Outlet: " + expander.getOutletTemperature("C") + " C, " + expander.getOutletPressure("bara") + " bara");
System.out.println("Power generated: " + expander.getPower("kW") + " kW");
// Rich gas feed
SystemSrkEos richGas = new SystemSrkEos(300.0, 70.0);
richGas.addComponent("nitrogen", 0.02);
richGas.addComponent("methane", 0.75);
richGas.addComponent("ethane", 0.10);
richGas.addComponent("propane", 0.08);
richGas.addComponent("n-butane", 0.05);
richGas.setMixingRule("classic");
Stream feed = new Stream("Rich Gas", richGas);
feed.setFlowRate(100000.0, "Sm3/day");
feed.run();
// Pre-cooling
Cooler precooler = new Cooler("Pre-cooler", feed);
precooler.setOutletTemperature(280.0, "K");
precooler.run();
// Turboexpander
TurboExpander expander = new TurboExpander("TEX-100", precooler.getOutletStream());
expander.setOutletPressure(25.0, "bara");
expander.setIsentropicEfficiency(0.82);
expander.run();
// Cold separator
Separator coldSep = new Separator("Cold Sep", expander.getOutletStream());
coldSep.run();
// Results
System.out.println("Expander outlet: " + expander.getOutletTemperature("C") + " C");
System.out.println("Power: " + expander.getPower("kW") + " kW");
System.out.println("NGL recovered: " + coldSep.getLiquidOutStream().getFlowRate("m3/hr") + " m³/hr");
// Same inlet conditions
SystemSrkEos gas = new SystemSrkEos(300.0, 60.0);
gas.addComponent("methane", 0.85);
gas.addComponent("ethane", 0.10);
gas.addComponent("propane", 0.05);
gas.setMixingRule("classic");
Stream feed1 = new Stream("Feed 1", gas);
feed1.setFlowRate(10000.0, "kg/hr");
feed1.run();
Stream feed2 = new Stream("Feed 2", gas.clone());
feed2.setFlowRate(10000.0, "kg/hr");
feed2.run();
// JT valve
ThrottlingValve valve = new ThrottlingValve("JT Valve", feed1);
valve.setOutletPressure(15.0, "bara");
valve.run();
// Expander
Expander expander = new Expander("Expander", feed2);
expander.setOutletPressure(15.0, "bara");
expander.setIsentropicEfficiency(0.85);
expander.run();
System.out.println("JT Valve outlet: " + valve.getOutletTemperature("C") + " C");
System.out.println("Expander outlet: " + expander.getOutletTemperature("C") + " C");
System.out.println("Extra cooling: " + (valve.getOutletTemperature("C") - expander.getOutletTemperature("C")) + " C");
System.out.println("Power recovered: " + expander.getPower("kW") + " kW");
This document summarizes the mathematical basis of the coupled expander/compressor model, how reference curves are applied (and can be replaced), and provides a usage walkthrough for configuring and running the unit in a process simulation.
The expander and compressor share a shaft speed that is iteratively adjusted until the expander power balances the compressor power plus bearing losses using a Newton-Raphson iteration. The key computational steps are described below.
The isentropic enthalpy drop across the expander is calculated from an isentropic flash at the target outlet pressure:
$$ \Delta h_s = (h_{in} - h_{out,s}) \times 1000 \quad \text{[J/kg]} $$
where $h_{in}$ is the inlet enthalpy and $h_{out,s}$ is the isentropic outlet enthalpy.
The tip speed $U$ and spouting (jet) velocity $C$ are computed as:
$$ U = \frac{\pi \cdot D \cdot N}{60} $$
$$ C = \sqrt{2 \cdot \Delta h_s} $$
The velocity ratio is then:
$$ u_c = \frac{U}{C \cdot u_{c,design}} $$
An efficiency correction factor is evaluated from the UC reference curve based on this ratio.
The actual expander isentropic efficiency is calculated by applying correction factors:
$$ \eta_s = \eta_{s,design} \cdot f_{UC}(u_c) \cdot f_{Q/N}\left(\frac{Q/N}{(Q/N)_{design}}\right) $$
where:
The expander shaft power is:
$$ W_{expander} = \dot{m} \cdot \Delta h_s \cdot \eta_s $$
where $\dot{m}$ is the mass flow rate.
The compressor polytropic head and efficiency are corrected for off-design operation:
$$ H_p = H_{p,design} \cdot f_{head}\left(\frac{Q/N}{(Q/N)_{design}}\right) \cdot \left(\frac{N}{N_{design}}\right)^2 $$
$$ \eta_p = \eta_{p,design} \cdot f_{\eta}\left(\frac{Q/N}{(Q/N)_{design}}\right) $$
where:
The compressor shaft power is:
$$ W_{comp} = \frac{\dot{m} \cdot H_p}{\eta_p} $$
The Newton-Raphson iteration solves for the shaft speed $N$ that satisfies:
$$ f(N) = W_{expander} - \left(W_{comp} + W_{bearing}\right) = 0 $$
where the bearing losses are modeled as a quadratic function of speed:
$$ W_{bearing} = k \cdot N^2 $$
The iteration continues until the power mismatch is negligible or iteration limits are reached. The final speed is applied to compute outlet stream properties.
Three types of reference curves tune performance away from the design point:
A constrained parabola through the peak at $(u_c = 1, \eta = 1)$:
$$ f_{UC}(u_c) = a \cdot u_c^2 + b \cdot u_c + c $$
The curve can be replaced with setUCcurve(ucValues, efficiencyValues) if alternate test data are available.
A monotonic cubic Hermite spline built from paired Q/N and efficiency arrays via setQNEfficiencycurve:
$$ f_{\eta}\left(\frac{Q/N}{(Q/N)_{design}}\right) = \text{spline interpolation} $$
Values are extrapolated linearly outside the provided range, allowing off-map operation while preserving trend continuity.
A similar cubic Hermite spline created with setQNHeadcurve that scales polytropic head at off-design flows:
$$ f_{head}\left(\frac{Q/N}{(Q/N)_{design}}\right) = \text{spline interpolation} $$
Like the efficiency spline, it preserves monotonicity and extrapolates linearly beyond the data range.
Note: Curve coefficients are stored on the equipment instance and can be replaced at runtime to test alternative reference maps or updated dynamically from external performance monitoring tools.
Clone feeds for the expander and compressor outputs when instantiating the equipment.
Provide impeller diameter, design speed, efficiencies, design Q/N, and optional expander design Q/N if expander flow corrections are needed. The defaults mirror the embedded design values but can be overridden through the available setters.
If site-specific head or efficiency curves exist, call setUCcurve, setQNEfficiencycurve, and setQNHeadcurve with measured points before running the unit.
Call run(UUID id) (or the no-argument overload) to iterate speed matching and populate result fields and outlet streams. Retrieve shaft powers with getPowerExpander(unit) and getPowerCompressor(unit) or inspect efficiencies, head, and Q/N ratios through the getters.
A realistic setup that mirrors common plant data collection and map-updating workflows:
TurboExpanderCompressor turboExpanderComp = new TurboExpanderCompressor(
"TurboExpanderCompressor", jt_tex_splitter.getSplitStream(0));
turboExpanderComp.setUCcurve(
new double[] {0.9964751359624449, 0.7590835113213541, 0.984295619176559, 0.8827799803397821,
0.9552460269880922, 1.0},
new double[] {0.984090909090909, 0.796590909090909, 0.9931818181818183, 0.9363636363636364,
0.9943181818181818, 1.0});
turboExpanderComp.setQNEfficiencycurve(
new double[] {0.5, 0.7, 0.85, 1.0, 1.2, 1.4, 1.6},
new double[] {0.88, 0.91, 0.95, 1.0, 0.97, 0.85, 0.6});
turboExpanderComp.setQNHeadcurve(
new double[] {0.5, 0.8, 1.0, 1.2, 1.4, 1.6},
new double[] {1.1, 1.05, 1.0, 0.9, 0.7, 0.4});
turboExpanderComp.setImpellerDiameter(0.424);
turboExpanderComp.setDesignSpeed(6850.0);
turboExpanderComp.setExpanderDesignIsentropicEfficiency(0.88);
turboExpanderComp.setDesignUC(0.7);
turboExpanderComp.setDesignQn(0.03328);
turboExpanderComp.setExpanderOutPressure(inp.expander_out_pressure);
turboExpanderComp.setCompressorDesignPolytropicEfficiency(0.81);
turboExpanderComp.setCompressorDesignPolytropicHead(20.47);
turboExpanderComp.setMaximumIGVArea(1.637e4);
// Run the coupled model and retrieve power with unit conversion
turboExpanderComp.run();
double expanderPowerMW = turboExpanderComp.getPowerExpander("MW");
double compressorPowerMW = turboExpanderComp.getPowerCompressor("MW");
| Parameter | Description |
|---|---|
setUCcurve(ucValues, effValues) |
Normalizes the velocity ratio $u_c = \frac{U}{C \cdot u_{c,design}}$ to an efficiency multiplier via a constrained parabola fitted to the supplied points |
| Parameter | Description |
|---|---|
setQNEfficiencycurve(qnValues, effValues) |
Cubic Hermite spline that scales expander and compressor efficiencies against flow coefficient deviations $Q/N$ |
setQNHeadcurve(qnValues, headValues) |
Spline used to scale the compressor polytropic head for off-design flows before applying the $(N/N_{design})^2$ speed law |
| Parameter | Description |
|---|---|
setImpellerDiameter(D) |
Impeller diameter [m] — sets the peripheral velocity $U$ at design, anchoring UC corrections |
setDesignSpeed(N) |
Design rotational speed [rpm] — anchor for Newton iteration speed matching |
setExpanderDesignIsentropicEfficiency(η) |
Base isentropic efficiency multiplied by curve correction factors |
setCompressorDesignPolytropicEfficiency(η) |
Base polytropic efficiency for the compressor |
setCompressorDesignPolytropicHead(Hp) |
Design polytropic head [kJ/kg] |
setDesignUC(uc) |
Design velocity ratio for the expander |
setDesignQn(qn) |
Reference flow coefficient $(Q/N)_{design}$ for the compressor |
setDesignExpanderQn(qn) |
Reference flow coefficient for the expander (optional) |
| Parameter | Description |
|---|---|
setExpanderOutPressure(P) |
Target outlet pressure for the isentropic flash that produces $\Delta h_s$ |
| Parameter | Description |
|---|---|
setMaximumIGVArea(A) |
Maximum inlet guide vane throat area [mm²] |
setIgvAreaIncreaseFactor(f) |
Optional factor to expand available IGV area |
Tip: The same update paths can be invoked during runtime if monitoring identifies drift in the reference maps; supplying new curve points and re-running will propagate the new performance predictions.
The Inlet Guide Vane (IGV) opening is computed from the last stage enthalpy drop, mass flow, and volumetric flow each time run() completes.
The helper evaluateIGV performs the following:
$$ v_{nozzle} = \sqrt{\Delta h_{stage}} $$
$$ A_{required} = \frac{\dot{V}}{v_{nozzle}} $$
$$ \text{IGV}_{opening} = \min\left(\frac{A_{required}}{A_{throat}}, 1.0\right) $$
If the required area exceeds the installed IGV area, an optional enlargement factor (setIgvAreaIncreaseFactor) increases the available area:
$$ A_{available} = A_{max} \cdot f_{increase} $$
| Method | Description |
|---|---|
calcIGVOpening() |
Returns the calculated IGV opening fraction (0–1) |
calcIGVOpenArea() |
Returns the actual open area [mm²] |
getCurrentIGVArea() |
Returns the current IGV throat area [mm²] |
Documentation for ejector equipment in NeqSim process simulation.
Location: neqsim.process.equipment.ejector
Classes:
| Class | Description |
|---|---|
Ejector |
Steam/gas ejector for compression |
EjectorDesignResult |
Design calculation results |
Ejectors use the kinetic energy of a high-pressure motive stream to entrain and compress a low-pressure suction stream. Common applications include:
import neqsim.process.equipment.ejector.Ejector;
// Create ejector with motive and suction streams
Ejector ejector = new Ejector("Ejector-100", motiveStream, suctionStream);
ejector.setDischargePressure(5.0); // bara
ejector.setEfficiencyIsentropic(0.75); // Nozzle efficiency
ejector.setDiffuserEfficiency(0.80); // Diffuser efficiency
ejector.run();
// Get mixed stream
StreamInterface mixedStream = ejector.getMixedStream();
double dischargeT = mixedStream.getTemperature("C");
double dischargeP = mixedStream.getPressure("bara");
// High-pressure motive stream (e.g., HP steam or gas)
SystemInterface motiveFluid = new SystemSrkEos(250.0, 10.0);
motiveFluid.addComponent("water", 1.0);
motiveFluid.setMixingRule("classic");
Stream motiveStream = new Stream("Motive Steam", motiveFluid);
motiveStream.setFlowRate(1000.0, "kg/hr");
motiveStream.run();
// Low-pressure suction stream
SystemInterface suctionFluid = new SystemSrkEos(300.0, 1.5);
suctionFluid.addComponent("methane", 0.95);
suctionFluid.addComponent("ethane", 0.05);
suctionFluid.setMixingRule("classic");
Stream suctionStream = new Stream("Suction Gas", suctionFluid);
suctionStream.setFlowRate(500.0, "kg/hr");
suctionStream.run();
An ejector consists of four main sections:
The ejector performs isentropic expansion and compression:
$$\eta_{nozzle} = \frac{h_1 - h_2}{h_1 - h_{2s}}$$
$$\eta_{diffuser} = \frac{h_{4s} - h_3}{h_4 - h_3}$$
Where:
The entrainment ratio (ER) is defined as:
$$ER = \frac{\dot{m}_{suction}}{\dot{m}_{motive}}$$
// Nozzle isentropic efficiency (typically 0.7-0.9)
ejector.setEfficiencyIsentropic(0.75);
// Diffuser efficiency (typically 0.7-0.85)
ejector.setDiffuserEfficiency(0.80);
// Set target discharge pressure
ejector.setDischargePressure(5.0); // bara
// Optional: Override default suction and diffuser velocities
ejector.setDesignSuctionVelocity(30.0); // m/s
ejector.setDesignDiffuserOutletVelocity(20.0); // m/s
// Optional: Set connection pipe lengths for pressure drop
ejector.setSuctionConnectionLength(2.0); // m
ejector.setDischargeConnectionLength(3.0); // m
// Get mechanical design parameters
EjectorMechanicalDesign mechDesign = ejector.getMechanicalDesign();
// Calculate sizing
mechDesign.calcDesign();
// Get geometry
double throatDiameter = mechDesign.getThroatDiameter(); // m
double nozzleLength = mechDesign.getNozzleLength(); // m
double mixingLength = mechDesign.getMixingLength(); // m
double diffuserLength = mechDesign.getDiffuserLength(); // m
ProcessSystem process = new ProcessSystem();
// High-pressure motive gas from compressor discharge
Stream motiveGas = new Stream("HP Gas", hpGasFluid);
motiveGas.setFlowRate(2000.0, "kg/hr");
motiveGas.setTemperature(60.0, "C");
motiveGas.setPressure(40.0, "bara");
process.add(motiveGas);
// Low-pressure flare header gas
Stream flareGas = new Stream("Flare Gas", flareGasFluid);
flareGas.setFlowRate(500.0, "kg/hr");
flareGas.setTemperature(40.0, "C");
flareGas.setPressure(1.2, "bara");
process.add(flareGas);
// Ejector to recover flare gas
Ejector fgr = new Ejector("FGR Ejector", motiveGas, flareGas);
fgr.setDischargePressure(8.0); // bara
fgr.setEfficiencyIsentropic(0.75);
fgr.setDiffuserEfficiency(0.80);
process.add(fgr);
// Run process
process.run();
// Results
double entrainmentRatio = flareGas.getFlowRate("kg/hr") / motiveGas.getFlowRate("kg/hr");
System.out.println("Entrainment ratio: " + entrainmentRatio);
System.out.println("Discharge pressure: " + fgr.getMixedStream().getPressure("bara") + " bara");
// HP steam as motive fluid
Stream hpSteam = new Stream("HP Steam", steamFluid);
hpSteam.setFlowRate(1500.0, "kg/hr");
hpSteam.setTemperature(200.0, "C");
hpSteam.setPressure(10.0, "bara");
// Vacuum overhead vapor
Stream vacuumVapor = new Stream("Vacuum Vapor", vaporFluid);
vacuumVapor.setFlowRate(300.0, "kg/hr");
vacuumVapor.setTemperature(50.0, "C");
vacuumVapor.setPressure(0.1, "bara");
// First stage ejector
Ejector ejector1 = new Ejector("1st Stage", hpSteam, vacuumVapor);
ejector1.setDischargePressure(0.5); // bara
ejector1.run();
// Intercondenser
Cooler intercondenser = new Cooler("Intercondenser", ejector1.getMixedStream());
intercondenser.setOutTemperature(40.0, "C");
// Second stage ejector
Ejector ejector2 = new Ejector("2nd Stage", hpSteam2, intercondenser.getOutletStream());
ejector2.setDischargePressure(1.1); // bara
ejector2.run();
For a given motive pressure and geometry, ejector performance follows characteristic curves:
// Calculate performance at different suction pressures
double[] suctionPressures = {0.5, 1.0, 1.5, 2.0}; // bara
for (double Ps : suctionPressures) {
suctionStream.setPressure(Ps, "bara");
suctionStream.run();
ejector.run();
double compressionRatio = ejector.getMixedStream().getPressure("bara") / Ps;
System.out.println("Suction P: " + Ps + " bara, CR: " + compressionRatio);
}
Documentation for heat transfer equipment in NeqSim process simulation.
Location: neqsim.process.equipment.heatexchanger
Classes:
Heater - Simple heater (duty or outlet T specified)Cooler - Simple cooler (duty or outlet T specified)HeatExchanger - Shell-tube heat exchangerNeqHeater - Non-equilibrium heaterCondenser - Overhead condenserimport neqsim.process.equipment.heatexchanger.Heater;
// Specify outlet temperature
Heater heater = new Heater("E-100", inletStream);
heater.setOutTemperature(80.0, "C");
heater.run();
double duty = heater.getDuty(); // W
System.out.println("Heating duty: " + duty/1000.0 + " kW");
// Or specify duty
Heater heater2 = new Heater("E-101", inletStream);
heater2.setEnergyInput(500000.0); // 500 kW
heater2.run();
import neqsim.process.equipment.heatexchanger.Cooler;
Cooler cooler = new Cooler("E-200", hotStream);
cooler.setOutTemperature(30.0, "C");
cooler.run();
double duty = cooler.getDuty(); // Negative for cooling
System.out.println("Cooling duty: " + (-duty/1000.0) + " kW");
Two-stream heat exchanger with hot and cold sides.
import neqsim.process.equipment.heatexchanger.HeatExchanger;
HeatExchanger hx = new HeatExchanger("E-300", hotStream, coldStream);
// Specify UA value
hx.setUAvalue(10000.0); // W/K
// Or specify approach temperature
hx.setApproachTemperature(10.0, "C");
hx.run();
// Results
double hotOut = hx.getOutStream(0).getTemperature("C");
double coldOut = hx.getOutStream(1).getTemperature("C");
double duty = hx.getDuty();
double LMTD = hx.getLMTD();
import neqsim.process.equipment.heatexchanger.MultiStreamHeatExchanger;
MultiStreamHeatExchanger mshx = new MultiStreamHeatExchanger("LNG-100");
mshx.addStream(stream1, "hot");
mshx.addStream(stream2, "hot");
mshx.addStream(stream3, "cold");
mshx.setUAvalue(50000.0);
mshx.run();
// Outlet temperature
heater.setOutTemperature(100.0, "C");
// Temperature change
heater.setdT(50.0, "C"); // ΔT = 50°C
// Fixed duty
heater.setEnergyInput(1000000.0); // 1 MW
// Duty from energy stream
EnergyStream energyIn = new EnergyStream("Heat Source");
energyIn.setEnergyFlow(500.0, "kW");
heater.setEnergyStream(energyIn);
// For heat exchangers
hx.setUAvalue(5000.0); // W/K
// Calculate UA from geometry
double U = 500.0; // Overall HTC, W/(m²·K)
double A = 100.0; // Area, m²
hx.setUAvalue(U * A);
import neqsim.process.equipment.heatexchanger.Condenser;
Condenser condenser = new Condenser("Overhead Condenser", vaporStream);
// Total condensation
condenser.setOutTemperature(40.0, "C");
condenser.run();
// Partial condensation
condenser.setDewPointTemperature(true); // Operate at dew point
condenser.run();
// Sub-cooling
condenser.setSubCooling(5.0, "C"); // 5°C subcooling
heater.setCalculateSteadyState(false);
// Set thermal mass
heater.setThermalMass(10000.0); // J/K
// Transient response
for (double t = 0; t < 3600; t += 1.0) {
heater.runTransient();
double Tout = heater.getOutletStream().getTemperature("C");
}
ProcessSystem process = new ProcessSystem();
// Hot gas inlet
Stream hotGas = new Stream("Hot Gas", gasFluid);
hotGas.setFlowRate(50000.0, "kg/hr");
hotGas.setTemperature(150.0, "C");
hotGas.setPressure(80.0, "bara");
process.add(hotGas);
// Air cooler
Cooler airCooler = new Cooler("Air Cooler", hotGas);
airCooler.setOutTemperature(60.0, "C");
process.add(airCooler);
// Trim cooler (seawater)
Cooler trimCooler = new Cooler("Trim Cooler", airCooler.getOutletStream());
trimCooler.setOutTemperature(25.0, "C");
process.add(trimCooler);
// Separator for condensate
Separator separator = new Separator("Inlet Sep", trimCooler.getOutletStream());
process.add(separator);
process.run();
// Total cooling duty
double airDuty = -airCooler.getDuty() / 1e6; // MW
double trimDuty = -trimCooler.getDuty() / 1e6; // MW
System.out.println("Air cooler duty: " + airDuty + " MW");
System.out.println("Trim cooler duty: " + trimDuty + " MW");
HeatExchanger hx = new HeatExchanger("E-400", hotIn, coldIn);
hx.setUAvalue(ua);
hx.run();
double LMTD = hx.getLMTD();
double duty = hx.getDuty();
double UA = duty / LMTD;
double area = UA / overallHTC;
double NTU = hx.getNTU();
double effectiveness = hx.getEffectiveness();
The AirCooler is a simple process unit for estimating the amount of cooling air needed when a process stream is cooled by ambient air. The calculation makes use of the humid air utility in NeqSim to evaluate the enthalpy rise of the air between the inlet and outlet temperature.
For a given inlet temperature, outlet temperature and relative humidity of the air, the mass flow of dry air is obtained from
[ \dot m_{air} = \frac{Q}{h_{out}-h_{in}} ]
where Q is the heat removed from the process stream in watt and h_{in} and h_{out} are the specific humid–air enthalpies in kJ per kg dry air. The volumetric flow is calculated from the ideal–gas relation at the inlet conditions.
Basic usage:
AirCooler cooler = new AirCooler("air cooler", stream);
cooler.setOutTemperature(40.0, "C");
cooler.setAirInletTemperature(20.0, "C");
cooler.setAirOutletTemperature(30.0, "C");
cooler.setRelativeHumidity(0.5);
The WaterCooler equipment cools water streams using the dedicated water physical
property package. It also estimates the required cooling water flow rate using
the IAPWS IF97 steam tables. Both the inlet and outlet process streams are
forced to use PhysicalPropertyModel.WATER.
SystemInterface water = new SystemSrkEos(298.15, 1.0);
water.addComponent("water", 1.0);
water.setPhysicalPropertyModel(PhysicalPropertyModel.WATER);
Stream feed = new Stream("water feed", water);
feed.setTemperature(40.0, "C");
WaterCooler cooler = new WaterCooler("cooler", feed);
cooler.setOutTemperature(20.0, "C");
cooler.setWaterInletTemperature(25.0, "C");
cooler.setWaterOutletTemperature(35.0, "C");
cooler.setWaterPressure(1.0, "bara");
// After running the process system the calculated cooling water flow can be obtained
double cwFlow = cooler.getCoolingWaterFlowRate("kg/hr");
The SteamHeater heats process streams while forcing the water property package. It estimates the required steam flow rate using the IAPWS IF97 steam tables.
SystemInterface water = new SystemSrkEos(298.15, 1.0);
water.addComponent("water", 1.0);
water.setPhysicalPropertyModel(PhysicalPropertyModel.WATER);
Stream feed = new Stream("water feed", water);
feed.setTemperature(25.0, "C");
SteamHeater heater = new SteamHeater("heater", feed);
heater.setOutTemperature(80.0, "C");
heater.setSteamInletTemperature(180.0, "C");
heater.setSteamOutletTemperature(100.0, "C");
heater.setSteamPressure(2.0, "bara");
// After running the process system the calculated steam flow can be obtained
double steamFlow = heater.getSteamFlowRate("kg/hr");
The HeatExchangerMechanicalDesign class provides sizing estimates for shell-and-tube, plate-and-frame, air cooler, and double-pipe exchangers. It can be attached to a full two-stream HeatExchanger or to single-stream Heater and Cooler units that supply an auxiliary utility specification. The mechanical design routine evaluates the candidate exchanger types, computes the required UA and approach temperatures, and selects a preferred configuration based on area, weight, or pressure-drop criteria.
initMechanicalDesign) before requesting the sizing results.For a HeatExchanger, the design routine uses the duty, stream inlet/outlet temperatures, and (if provided) UA or thermal effectiveness to compute the log-mean temperature difference (LMTD) and overall heat-transfer requirements. The selected exchanger geometry is exposed through HeatExchangerSizingResult.
HeatExchanger exchanger = new HeatExchanger("hx", hotStream, coldStream);
exchanger.run();
exchanger.initMechanicalDesign();
HeatExchangerMechanicalDesign design = exchanger.getMechanicalDesign();
design.calcDesign();
HeatExchangerSizingResult result = design.getSelectedSizingResult();
System.out.println(result.getType() + " area = " + result.getRequiredArea());
A Heater or Cooler needs a UtilityStreamSpecification so the mechanical design can derive the utility-side temperatures or heat capacity rate.
Heater heater = new Heater("heater", feed);
heater.setOutTemperature(80.0, "C");
UtilityStreamSpecification utility = new UtilityStreamSpecification();
utility.setSupplyTemperature(180.0, "C");
utility.setReturnTemperature(160.0, "C");
utility.setOverallHeatTransferCoefficient(650.0);
heater.setUtilitySpecification(utility);
heater.run();
heater.initMechanicalDesign();
HeatExchangerMechanicalDesign design = heater.getMechanicalDesign();
design.calcDesign();
The specification supports setting:
m*Cp) to back-calculate the return temperature from duty.You can also configure the utility through convenience setters such as setUtilitySupplyTemperature, setUtilityReturnTemperature, setUtilityHeatCapacityRate, and setUtilityApproachTemperature.
All evaluated designs are available through getSizingResults(). Use the selection helpers to control the preferred configuration:
design.setCandidateTypes(
HeatExchangerType.SHELL_AND_TUBE,
HeatExchangerType.PLATE_AND_FRAME);
design.setSelectionCriterion(SelectionCriterion.MIN_WEIGHT);
design.calcDesign();
setManualSelection forces a specific exchanger type when benchmarking alternatives.setSelectionCriterion controls the automatic choice (area, weight, or pressure drop).getSizingSummary() returns a short formatted overview suitable for logs or reports.UtilityStreamSpecification (JavaDoc) for the full list of utility parameters.HeaterCoolerMechanicalDesignTest illustrate heater and cooler sizing workflows.Documentation for valve equipment in NeqSim process simulation.
Location: neqsim.process.equipment.valve
Classes:
ThrottlingValve - Joule-Thomson throttling valve (control valve)ValveInterface - Valve interfaceSafetyValve - Pressure relief valveSafetyReliefValve - Full PSV with API sizingBlowdownValve - Emergency blowdown valveControlValve - Specialized control valveMechanical Design: neqsim.process.mechanicaldesign.valve
ValveMechanicalDesign - Body sizing, weight, actuator calculationsControlValveSizing - IEC 60534 Cv/Kv calculationsControlValveSizing_IEC_60534 - Full IEC implementationControlValveSizing_simple - Production choke sizingimport neqsim.process.equipment.valve.ThrottlingValve;
ThrottlingValve valve = new ThrottlingValve("FV-100", inletStream);
valve.setOutletPressure(30.0, "bara");
valve.run();
// Joule-Thomson cooling
double Tin = inletStream.getTemperature("C");
double Tout = valve.getOutletStream().getTemperature("C");
double deltaT = Tout - Tin;
System.out.println("Temperature change: " + deltaT + " °C");
Throttling is isenthalpic (constant enthalpy):
double H_in = inletStream.getEnthalpy("J/mol");
double H_out = valve.getOutletStream().getEnthalpy("J/mol");
// H_in ≈ H_out (within numerical precision)
// Set Cv
valve.setCv(150.0, "US"); // US gallons/min at 1 psi ΔP
// Or
valve.setCv(150.0, "SI"); // m³/hr at 1 bar ΔP
// Get Cv at current conditions
double Cv = valve.getCv("US");
// Set valve position
valve.setPercentValveOpening(50.0); // 50% open
// Cv varies with opening (inherent characteristic)
valve.setValveCharacteristic("linear");
// or
valve.setValveCharacteristic("equal_percentage");
// or
valve.setValveCharacteristic("quick_opening");
// Given Cv and flow, calculate ΔP
valve.setCv(100.0, "US");
valve.setPercentValveOpening(75.0);
valve.run();
double Pin = inletStream.getPressure("bara");
double Pout = valve.getOutletStream().getPressure("bara");
double deltaP = Pin - Pout;
Control valves have inherent flow characteristics that define how Cv varies with valve opening.
| Characteristic | Description | Best Application |
|---|---|---|
| Linear | Flow proportional to opening | Constant ΔP systems, bypass valves |
| Equal Percentage | Equal opening increments = equal % flow change | Variable ΔP, most process control |
| Quick Opening | Large flow change at small openings | On/off service, safety applications |
| Modified Parabolic | Compromise between linear and equal % | General purpose |
import neqsim.process.mechanicaldesign.valve.ValveMechanicalDesign;
ValveMechanicalDesign mechDesign = (ValveMechanicalDesign) valve.getMechanicalDesign();
mechDesign.setValveCharacterization("equal percentage");
│
100% │ ●─── Quick Opening
│ ●───●
Flow │ ●───● ●── Linear
(Cv) │ ●───● ●
│ ●───● ●───── Equal Percentage
│● ●───●
0% └────────────────────
0% Opening 100%
NeqSim supports multiple valve sizing standards for different applications.
| Standard | Code | Description |
|---|---|---|
| Default | default |
Simplified IEC 60534 for gas, standard for liquid |
| IEC 60534 | IEC 60534 |
Full IEC 60534-2-1 implementation |
| Extended | IEC 60534 full |
IEC 60534 with all correction factors |
| Prod Choke | prod choke |
Production choke with discharge coefficient |
ValveMechanicalDesign mechDesign = (ValveMechanicalDesign) valve.getMechanicalDesign();
mechDesign.setValveSizingStandard("IEC 60534");
For compressible fluids (gas/vapor):
$$K_v = \frac{Q}{N_9 \cdot P_1 \cdot Y} \sqrt{\frac{M \cdot T \cdot Z}{x}}$$
Where:
Flow becomes choked when: $$x \geq F_\gamma \cdot x_T$$
Where:
Complete mechanical design calculations are available for valve body sizing, weight estimation, and actuator requirements.
ValveMechanicalDesign mechDesign = (ValveMechanicalDesign) valve.getMechanicalDesign();
mechDesign.calcDesign();
| Property | Method | Unit |
|---|---|---|
| ANSI Pressure Class | getAnsiPressureClass() |
- |
| Nominal Size | getNominalSizeInches() |
inches |
| Face-to-Face | getFaceToFace() |
mm |
| Body Wall Thickness | getBodyWallThickness() |
mm |
| Design Pressure | getDesignPressure() |
bara |
| Design Temperature | getDesignTemperature() |
°C |
| Actuator Thrust | getRequiredActuatorThrust() |
N |
| Actuator Weight | getActuatorWeight() |
kg |
| Total Weight | getWeightTotal() |
kg |
// Create and run valve
ThrottlingValve valve = new ThrottlingValve("PCV-101", gasStream);
valve.setOutletPressure(60.0, "bara");
valve.run();
// Calculate mechanical design
ValveMechanicalDesign mechDesign = (ValveMechanicalDesign) valve.getMechanicalDesign();
mechDesign.calcDesign();
// Print results
System.out.println("=== VALVE MECHANICAL DESIGN ===");
System.out.println("Cv: " + valve.getCv());
System.out.println("ANSI Class: " + mechDesign.getAnsiPressureClass());
System.out.println("Size: " + mechDesign.getNominalSizeInches() + " inches");
System.out.println("Face-to-Face: " + mechDesign.getFaceToFace() + " mm");
System.out.println("Wall Thickness: " + mechDesign.getBodyWallThickness() + " mm");
System.out.println("Total Weight: " + mechDesign.getWeightTotal() + " kg");
System.out.println("Actuator Thrust: " + mechDesign.getRequiredActuatorThrust() + " N");
See Valve Mechanical Design for complete documentation including:
For high pressure drops, flow becomes critical (choked).
// Check if flow is critical
boolean isCritical = valve.isCriticalFlow();
// Critical flow factor
double Cf = valve.getCriticalFlowFactor();
import neqsim.process.equipment.valve.SafetyValve;
SafetyValve psv = new SafetyValve("PSV-100", vessel);
psv.setSetPressure(100.0, "barg"); // Set pressure
psv.setBlowdownPressure(10.0, "%"); // 10% blowdown
psv.run();
// Check if valve is open
boolean isOpen = psv.isOpen();
double relievingFlow = psv.getRelievingFlow("kg/hr");
// Required relieving capacity
psv.setRequiredCapacity(10000.0, "kg/hr");
// Get required orifice area
double area = psv.getRequiredOrificeArea("cm2");
// Select API orifice
String orifice = psv.selectAPIorifice(); // e.g., "J", "K", "L"
For wellhead and production applications:
import neqsim.process.equipment.valve.ChokeValve;
ChokeValve choke = new ChokeValve("Wellhead Choke", wellStream);
choke.setOutletPressure(50.0, "bara");
choke.run();
// Or specify bean size
choke.setBeanSize(32, "64ths"); // 32/64" = 0.5"
choke.run();
double Pout = choke.getOutletStream().getPressure("bara");
// Valve dynamics
valve.setCalculateSteadyState(false);
// Valve stroke time
valve.setStrokeTime(10.0); // seconds for 0-100%
// Step change
valve.setPercentValveOpening(80.0);
for (double t = 0; t < 30; t += 0.1) {
valve.runTransient();
double opening = valve.getActualOpening(); // Lags setpoint
}
ProcessSystem process = new ProcessSystem();
// HP gas inlet
Stream hpGas = new Stream("HP Gas", gasFluid);
hpGas.setFlowRate(50000.0, "kg/hr");
hpGas.setTemperature(50.0, "C");
hpGas.setPressure(100.0, "bara");
process.add(hpGas);
// Stage 1: 100 -> 50 bar
ThrottlingValve pv1 = new ThrottlingValve("PV-100", hpGas);
pv1.setOutletPressure(50.0, "bara");
pv1.setCv(200.0, "US");
process.add(pv1);
// Heater (compensate JT cooling)
Heater heater = new Heater("E-100", pv1.getOutletStream());
heater.setOutTemperature(40.0, "C");
process.add(heater);
// Stage 2: 50 -> 10 bar
ThrottlingValve pv2 = new ThrottlingValve("PV-101", heater.getOutletStream());
pv2.setOutletPressure(10.0, "bara");
pv2.setCv(300.0, "US");
process.add(pv2);
process.run();
// JT effects
System.out.println("After PV-100: " + pv1.getOutletStream().getTemperature("C") + " °C");
System.out.println("After E-100: " + heater.getOutletStream().getTemperature("C") + " °C");
System.out.println("After PV-101: " + pv2.getOutletStream().getTemperature("C") + " °C");
For ideal gases, $\mu_{JT} = 0$. For real gases:
$$\mu_{JT} = \left(\frac{\partial T}{\partial P}\right)_H = \frac{1}{C_p}\left[T\left(\frac{\partial V}{\partial T}\right)_P - V\right]$$
// Get JT coefficient
double muJT = inletStream.getJouleThomsonCoefficient(); // K/bar
This document describes the mechanical design calculations for control valves in NeqSim, implemented in the ValveMechanicalDesign class.
The valve mechanical design module provides sizing and design calculations for control valves based on IEC 60534, ANSI/ISA-75, and ASME B16.34 standards. The calculations enable:
| Standard | Description |
|---|---|
| IEC 60534 | Industrial-process control valves |
| ANSI/ISA-75.01 | Flow Equations for Sizing Control Valves |
| ANSI/ISA-75.08 | Face-to-Face Dimensions for Flanged Globe-Style Control Valve Bodies |
| ASME B16.34 | Valves - Flanged, Threaded, and Welding End |
| API 6D | Pipeline and Piping Valves |
The pressure class is automatically selected based on the design pressure:
| Design Pressure | ANSI Class |
|---|---|
| ≤ 19.6 bara | Class 150 |
| ≤ 51.1 bara | Class 300 |
| ≤ 102.1 bara | Class 600 |
| ≤ 153.2 bara | Class 900 |
| ≤ 255.3 bara | Class 1500 |
| ≤ 425.5 bara | Class 2500 |
// Design pressure with 10% margin
designPressure = operatingPressure × 1.10
The nominal valve size is calculated from the Cv coefficient using the ISA correlation for globe valves:
Cv ≈ 10 × d²
Where d is the nominal pipe size in inches. Rearranging:
d = sqrt(Cv / 10)
The calculated size is then rounded to the nearest standard pipe size:
Face-to-face dimensions are per ANSI/ISA-75.08 for globe-style control valves:
| Nominal Size (in) | Face-to-Face (mm) |
|---|---|
| ≤ 1.0 | 108 |
| 1.5 | 117 |
| 2.0 | 152 |
| 3.0 | 203 |
| 4.0 | 241 |
| 6.0 | 292 |
| 8.0 | 356 |
| 10.0 | 432 |
| 12.0 | 495 |
| > 12.0 | 508 + (size - 12) × 30 |
Adjustment for Pressure Class:
Wall thickness is calculated using the ASME B16.34 pressure vessel formula:
t = (P × R) / (S × E - 0.6 × P) + CA
Where:
P = design pressure (MPa)R = inner radius (mm)S = allowable stress = 138 MPa (carbon steel at ambient)E = joint efficiency = 1.0 (forged body)CA = corrosion allowanceMinimum: 3.0 mm wall thickness
The required actuator thrust is calculated from:
Fluid Force: Force to overcome pressure across the seat
F_fluid = P_design × A_seat
Packing Friction: Typically 15% of fluid force
F_packing = 0.15 × F_fluid
Seat Load: For tight shutoff (Class IV/V)
F_seat = π × d_seat × 7 N/mm
Total Thrust:
F_total = (F_fluid + F_packing + F_seat) × 1.25
Valve weight is estimated using empirical correlations:
W_body = 2.5 × (size_inches)^2.5 × (class / 150)^0.5
W_trim = 0.3 × W_body
W_actuator = 0.015 × F_thrust + 5.0 kg (minimum 10 kg)
W_total = W_body + W_trim + W_actuator
import neqsim.process.equipment.valve.ThrottlingValve;
import neqsim.process.mechanicaldesign.valve.ValveMechanicalDesign;
// Create and run valve
ThrottlingValve valve = new ThrottlingValve("PCV-101", inletStream);
valve.setOutletPressure(60.0, "bara");
valve.run();
// Get mechanical design
ValveMechanicalDesign mechDesign = (ValveMechanicalDesign) valve.getMechanicalDesign();
mechDesign.calcDesign();
// Access results
System.out.println("ANSI Class: " + mechDesign.getAnsiPressureClass());
System.out.println("Nominal Size: " + mechDesign.getNominalSizeInches() + " inches");
System.out.println("Face-to-Face: " + mechDesign.getFaceToFace() + " mm");
System.out.println("Body Wall: " + mechDesign.getBodyWallThickness() + " mm");
System.out.println("Actuator Thrust: " + mechDesign.getRequiredActuatorThrust() + " N");
System.out.println("Total Weight: " + mechDesign.getWeightTotal() + " kg");
ValveMechanicalDesign Class| Method | Return | Description |
|---|---|---|
calcDesign() |
void | Performs all mechanical design calculations |
getAnsiPressureClass() |
int | Returns ANSI class (150, 300, 600, 900, 1500, 2500) |
getNominalSizeInches() |
double | Returns nominal valve size in inches |
getFaceToFace() |
double | Returns face-to-face dimension in mm |
getBodyWallThickness() |
double | Returns body wall thickness in mm |
getRequiredActuatorThrust() |
double | Returns required actuator thrust in N |
getActuatorWeight() |
double | Returns estimated actuator weight in kg |
getDesignPressure() |
double | Returns design pressure in bara |
getDesignTemperature() |
double | Returns design temperature in °C |
getWeightTotal() |
double | Returns total valve weight in kg |
NeqSim supports multiple valve sizing standards that can be selected via setValveSizingStandard():
| Standard | Description | Best For |
|---|---|---|
default |
IEC 60534-based calculation | General control valves |
IEC 60534 |
Full IEC 60534-2-1 implementation | Engineering calculations |
IEC 60534 full |
Extended IEC 60534 with all factors | Detailed sizing studies |
prod choke |
Production choke sizing with Cd | Wellhead chokes |
ValveMechanicalDesign mechDesign = (ValveMechanicalDesign) valve.getMechanicalDesign();
mechDesign.setValveSizingStandard("IEC 60534");
Available valve characteristics for flow control:
| Characteristic | Formula | Application |
|---|---|---|
| Linear | Cv/Cv₁₀₀ = opening/100 |
Constant ΔP systems |
| Equal Percentage | Cv/Cv₁₀₀ = R^((opening/100)-1) |
Variable ΔP, process control |
| Quick Opening | Cv/Cv₁₀₀ = sqrt(opening/100) |
On/off, safety applications |
| Modified Parabolic | Cv/Cv₁₀₀ = opening²/10000 |
Compromise between linear/EQ% |
ValveMechanicalDesign mechDesign = (ValveMechanicalDesign) valve.getMechanicalDesign();
mechDesign.setValveCharacterization("equal percentage");
For compressible fluids, the Kv (or Cv) is calculated using:
$$K_v = \frac{Q}{N_9 \cdot P_1 \cdot Y} \sqrt{\frac{M \cdot T \cdot Z}{x}}$$
Where:
When $x \geq F_\gamma \cdot x_T$, flow becomes choked and:
$$Y = \frac{2}{3}$$ $$x_{effective} = F_\gamma \cdot x_T$$
This page documents the equations implemented in the Orifice equipment for
computing flow through differential pressure meters. All variables are in SI
units.
The discharge coefficient $C$ is calculated with the Reader–Harris/Gallagher correlation as implemented in ISO 5167:
$$ C = 0.5961 + 0.0261\beta^2 - 0.216\beta^8 + 0.000521\left(\frac{10^6\beta}{Re_D}\right)^{0.7} +(0.0188 + 0.0063A)\beta^{3.5}\left(\frac{10^6}{Re_D}\right)^{0.3} +(0.043 + 0.080e^{-10L_1}-0.123e^{-7L_1})(1-0.11A)\frac{\beta^4}{1-\beta^4} -0.031(M_2' -0.8M_2'^{1.1})\beta^{1.3} $$
The expansibility factor is $$ \epsilon = 1 - (0.351 +0.256\beta^4 +0.93\beta^8)\left[1-\left(\frac{P_2}{P_1}\right)^{1/\kappa}\right] $$
The mass flow rate is obtained iteratively from $$ m = \left(\tfrac{\pi D^2\beta^2}{4}\right) C \epsilon \frac{\sqrt{2\rho(P_1-P_2)}}{\sqrt{1-\beta^4}}. $$
This document describes the Venturi flow meter calculation methods implemented in NeqSim for computing mass flow rates from differential pressure measurements, and vice versa.
NeqSim implements Venturi flow calculations primarily in the DifferentialPressureFlowCalculator class, which is a utility for calculating mass flow rates from various differential pressure devices using NeqSim thermodynamic properties. The calculator supports both:
Location: DifferentialPressureFlowCalculator.java
The calculator supports multiple differential pressure device types:
| Flow Type | Default Discharge Coefficient |
|---|---|
| Venturi | 0.985 |
| Orifice | Calculated (Reader-Harris/Gallagher) |
| V-Cone | 0.82 |
| Nozzle | Calculated |
| DallTube | Calculated |
| Annubar | Calculated |
| Simplified | User-provided Cv |
| Perrys-Orifice | Subsonic: 0.62, Sonic: 0.75-0.84 |
The Venturi flow calculation uses the compressible flow equation with an expansibility (expansion) factor:
$$ \dot{m} = \frac{C}{\sqrt{1 - \beta^4}} \cdot \varepsilon \cdot \frac{\pi d^2}{4} \cdot \sqrt{2 \rho \Delta P} $$
Where:
The expansibility factor accounts for compressibility effects in gas flow and is calculated using an isentropic expansion model:
$$ \varepsilon = \sqrt{\frac{\kappa \cdot \tau^{2/\kappa}}{\kappa - 1} \cdot \frac{1 - \beta^4}{1 - \beta^4 \tau^{2/\kappa}} \cdot \frac{1 - \tau^{(\kappa-1)/\kappa}}{1 - \tau}} $$
Where:
private static double[] calcVenturi(double[] dp, double[] p, double[] rho, double[] kappa,
double D, double d, double C) {
double beta = d / D;
double beta4 = Math.pow(beta, 4.0);
double betaTerm = Math.sqrt(Math.max(1.0 - beta4, 1e-30));
double[] massFlow = new double[dp.length];
for (int i = 0; i < dp.length; i++) {
double tau = p[i] / (p[i] + dp[i]);
double k = kappa[i];
double tau2k = Math.pow(tau, 2.0 / k);
// Expansibility factor calculation
double numerator = k * tau2k / (k - 1.0) * (1.0 - beta4)
/ (1.0 - beta4 * tau2k) * (1.0 - Math.pow(tau, (k - 1.0) / k)) / (1.0 - tau);
double eps = Math.sqrt(Math.max(numerator, 0.0));
// Mass flow calculation
double rootTerm = Math.sqrt(Math.max(dp[i] * rho[i] * 2.0, 0.0));
double value = C / betaTerm * eps * Math.PI / 4.0 * d * d * rootTerm;
massFlow[i] = tau == 1.0 ? 0.0 : value * 3600.0; // Convert to kg/h
}
return massFlow;
}
To calculate the differential pressure from a known mass flow rate, we rearrange the Venturi equation:
$$ \Delta P = \frac{1}{2\rho} \left( \frac{\dot{m} \cdot \sqrt{1 - \beta^4}}{C \cdot \varepsilon \cdot A} \right)^2 $$
Where:
Since the expansibility factor $\varepsilon$ depends on the differential pressure (through the pressure ratio $\tau$), an iterative solution is required.
Initial estimate (assuming incompressible flow, $\varepsilon = 1$): $$ \Delta P_0 = \frac{1}{2\rho} \left( \frac{\dot{m} \cdot \sqrt{1 - \beta^4}}{C \cdot A} \right)^2 $$
Iterate until convergence:
public static double calculateDpFromFlowVenturi(double massFlowKgPerHour, double pressureBara,
double density, double kappa, double pipeDiameterMm, double throatDiameterMm,
double dischargeCoefficient) {
double D = pipeDiameterMm / 1000.0;
double d = throatDiameterMm / 1000.0;
double C = dischargeCoefficient;
double massFlowKgPerSec = massFlowKgPerHour / 3600.0;
double beta = d / D;
double beta4 = Math.pow(beta, 4.0);
double betaTerm = Math.sqrt(Math.max(1.0 - beta4, 1e-30));
// Initial estimate (incompressible)
double A = Math.PI / 4.0 * d * d;
double dpInitial = Math.pow(massFlowKgPerSec * betaTerm / (C * A), 2) / (2.0 * density);
// Iterate to account for expansibility factor
double dpPa = dpInitial;
double pPa = pressureBara * 1.0e5;
for (int iter = 0; iter < 100; iter++) {
double tau = pPa / (pPa + dpPa);
double tau2k = Math.pow(tau, 2.0 / kappa);
double numerator = kappa * tau2k / (kappa - 1.0) * (1.0 - beta4)
/ (1.0 - beta4 * tau2k) * (1.0 - Math.pow(tau, (kappa - 1.0) / kappa)) / (1.0 - tau);
double eps = Math.sqrt(Math.max(numerator, 1e-30));
double dpNew = Math.pow(massFlowKgPerSec * betaTerm / (C * eps * A), 2) / (2.0 * density);
if (Math.abs(dpNew - dpPa) < 0.01) {
dpPa = dpNew;
break;
}
dpPa = dpNew;
}
return dpPa / 100.0; // Convert Pa to mbar
}
The calculator requires the following inputs:
| Index | Parameter | Unit |
|---|---|---|
| 0 | Pipe diameter (D) | mm |
| 1 | Throat diameter (d) | mm |
| 2 | Discharge coefficient (optional) | - |
| Parameter | Unit |
|---|---|
| Pressure | barg |
| Temperature | °C |
| Differential Pressure | mbar |
NeqSim uses the SRK (Soave-Redlich-Kwong) equation of state to calculate the required thermodynamic properties:
The FlowCalculationResult class provides:
| Output | Unit |
|---|---|
| Mass flow rate | kg/h |
| Volumetric flow rate (actual) | m³/h |
| Standard volumetric flow | MSm³/day |
| Molecular weight | g/mol |
import neqsim.process.equipment.diffpressure.DifferentialPressureFlowCalculator;
import neqsim.process.equipment.diffpressure.DifferentialPressureFlowCalculator.FlowCalculationResult;
import java.util.Arrays;
import java.util.List;
// Operating conditions
double[] pressureBarg = {50.0}; // 50 barg
double[] temperatureC = {25.0}; // 25°C
double[] dpMbar = {200.0}; // 200 mbar differential pressure
// Venturi geometry: D=300mm, d=200mm, Cd=0.985
double[] flowData = {300.0, 200.0, 0.985};
// Gas composition
List<String> components = Arrays.asList("methane", "ethane", "propane");
double[] fractions = {0.85, 0.10, 0.05};
// Calculate flow
FlowCalculationResult result = DifferentialPressureFlowCalculator.calculate(
pressureBarg, temperatureC, dpMbar, "Venturi", flowData,
components, fractions, true);
double massFlowKgH = result.getMassFlowKgPerHour()[0];
double stdFlowMSm3Day = result.getStandardFlowMSm3PerDay()[0];
import neqsim.process.equipment.diffpressure.DifferentialPressureFlowCalculator;
import java.util.Arrays;
import java.util.List;
// Known mass flow rate
double massFlowKgPerHour = 50000.0; // 50,000 kg/h
// Operating conditions
double pressureBarg = 50.0; // 50 barg
double temperatureC = 25.0; // 25°C
// Venturi geometry: D=300mm, d=200mm, Cd=0.985
double[] flowData = {300.0, 200.0, 0.985};
// Gas composition
List<String> components = Arrays.asList("methane", "ethane", "propane");
double[] fractions = {0.85, 0.10, 0.05};
// Calculate differential pressure
double dpMbar = DifferentialPressureFlowCalculator.calculateDpFromFlow(
massFlowKgPerHour, pressureBarg, temperatureC, "Venturi", flowData,
components, fractions, true);
System.out.println("Differential pressure: " + dpMbar + " mbar");
// If you already have fluid properties calculated
double massFlowKgPerHour = 50000.0;
double pressureBara = 51.0125; // bara
double density = 42.5; // kg/m³
double kappa = 1.28; // isentropic exponent
double pipeDiameterMm = 300.0; // mm
double throatDiameterMm = 200.0; // mm
double Cd = 0.985; // discharge coefficient
double dpMbar = DifferentialPressureFlowCalculator.calculateDpFromFlowVenturi(
massFlowKgPerHour, pressureBara, density, kappa,
pipeDiameterMm, throatDiameterMm, Cd);
System.out.println("Differential pressure: " + dpMbar + " mbar");
Uses the Reader-Harris/Gallagher correlation (ISO 5167) for discharge coefficient with iterative solution:
$$ C = 0.5961 + 0.0261\beta^2 - 0.216\beta^8 + 0.000521\left(\frac{10^6\beta}{Re_D}\right)^{0.7} + \ldots $$
Expansibility factor: $$ \varepsilon = 1 - (0.351 + 0.256\beta^4 + 0.93\beta^8)\left[1 - \left(\frac{P_2}{P_1}\right)^{1/\kappa}\right] $$
Uses a similar approach to Venturi but with a different discharge coefficient correlation: $$ C = 0.99 - 0.2262\beta^{4.1} - (0.00175\beta^2 - 0.0033\beta^{4.15})\left(\frac{10^7}{Re_D}\right)^{1.15} $$
Uses a modified beta ratio based on cone geometry: $$ \beta_{V-Cone} = \sqrt{1 - \frac{d_{cone}^2}{D^2}} $$
Expansibility factor: $$ \varepsilon = 1 - (0.649 + 0.696\beta^4)\frac{\Delta P}{\kappa \cdot P} $$
The implementations are based on:
Documentation for liquid storage tanks in NeqSim.
Location: neqsim.process.equipment.tank
Classes:
| Class | Description |
|---|---|
Tank |
Basic storage tank |
LNGTank |
LNG storage tank with boil-off |
import neqsim.process.equipment.tank.Tank;
Tank tank = new Tank("T-100", liquidStream);
tank.setVolume(1000.0, "m3");
tank.setLiquidLevel(0.5); // 50% full
tank.run();
// Atmospheric tank
tank.setPressure(1.013, "bara");
// Pressurized storage
tank.setPressure(5.0, "bara");
// Initial conditions
tank.setLiquidLevel(0.3); // 30% full
tank.run();
// Simulate filling
for (double t = 0; t < 3600; t += 60) {
tank.setInletStream(inletStream);
tank.runTransient();
double level = tank.getLiquidLevel();
System.out.println("Time: " + t + " s, Level: " + level * 100 + " %");
}
$$\frac{dV_{liq}}{dt} = \dot{Q}_{in} - \dot{Q}_{out}$$
$$L = \frac{V_{liq}}{V_{tank}}$$
For cryogenic storage (LNG, LPG).
import neqsim.process.equipment.tank.LNGTank;
LNGTank lngTank = new LNGTank("LNG Storage", lngStream);
lngTank.setVolume(160000.0, "m3");
lngTank.setHeatInput(500.0, "kW"); // Heat leak
lngTank.run();
// Boil-off rate
double bogRate = lngTank.getBoilOffGasRate("kg/hr");
Stream bog = lngTank.getBoilOffGasStream();
$$\dot{m}_{BOG} = \frac{\dot{Q}_{heat}}{\Delta H_{vap}}$$
Where:
import neqsim.thermo.system.SystemSrkEos;
import neqsim.process.equipment.stream.Stream;
import neqsim.process.equipment.tank.Tank;
// Crude oil
SystemSrkEos oil = new SystemSrkEos(298.15, 1.013);
oil.addComponent("n-heptane", 0.50);
oil.addComponent("n-decane", 0.50);
oil.setMixingRule("classic");
Stream oilStream = new Stream("Crude", oil);
oilStream.setFlowRate(100.0, "m3/hr");
oilStream.run();
// Storage tank
Tank storage = new Tank("Crude Storage", oilStream);
storage.setVolume(10000.0, "m3");
storage.setLiquidLevel(0.6);
storage.run();
double inventory = storage.getLiquidVolume("m3");
System.out.println("Inventory: " + inventory + " m³");
// LNG composition
SystemSrkEos lng = new SystemSrkEos(112.0, 1.013); // -161°C
lng.addComponent("nitrogen", 0.01);
lng.addComponent("methane", 0.92);
lng.addComponent("ethane", 0.05);
lng.addComponent("propane", 0.02);
lng.setMixingRule("classic");
Stream lngIn = new Stream("LNG In", lng);
lngIn.setFlowRate(1000.0, "m3/hr");
lngIn.run();
// LNG tank
LNGTank tank = new LNGTank("LNG Tank", lngIn);
tank.setVolume(160000.0, "m3"); // 160,000 m³ tank
tank.setHeatInput(300.0, "kW"); // Heat leak
tank.run();
// Results
System.out.println("BOG rate: " + tank.getBoilOffGasRate("kg/hr") + " kg/hr");
System.out.println("BOG temp: " + tank.getBoilOffGasStream().getTemperature("C") + " °C");
System.out.println("BOG rate %: " + tank.getBoilOffRate() * 100 + " %/day");
ProcessSystem process = new ProcessSystem();
// Feed stream
Stream feed = new Stream("Feed", oilFluid);
feed.setFlowRate(100.0, "m3/hr");
process.add(feed);
// Storage tank
Tank tank = new Tank("T-100", feed);
tank.setVolume(5000.0, "m3");
tank.setLiquidLevel(0.5);
process.add(tank);
// Outlet with level control
ThrottlingValve outlet = new ThrottlingValve("LV-100", tank.getOutletStream());
outlet.setOutletPressure(1.0, "bara");
process.add(outlet);
// Level controller
PIDController lc = new PIDController("LC-100");
lc.setMeasuredVariable(tank, "liquidLevel");
lc.setControlledVariable(outlet, "opening");
lc.setSetPoint(0.5);
lc.setKp(5.0);
lc.setKi(0.1);
process.add(lc);
// Run transient
for (double t = 0; t < 7200; t += 60) {
// Disturb inlet at t=1800
if (Math.abs(t - 1800) < 30) {
feed.setFlowRate(150.0, "m3/hr");
}
process.runTransient();
System.out.printf("%.0f, %.3f, %.1f%n",
t, tank.getLiquidLevel(), outlet.getOpening() * 100);
}
Documentation for chemical reactor equipment in NeqSim.
Location: neqsim.process.equipment.reactor
Classes:
| Class | Description |
|---|---|
Reactor |
Base reactor class |
CSTRReactor |
Continuous stirred tank reactor |
PFRReactor |
Plug flow reactor |
EquilibriumReactor |
Chemical equilibrium reactor |
GibbsReactor |
Gibbs energy minimization reactor |
| Reactor | When to Use |
|---|---|
| CSTR | Liquid-phase reactions, good mixing |
| PFR | Gas-phase reactions, no back-mixing |
| Equilibrium | Fast reactions at equilibrium |
| Gibbs | Complex equilibrium without specifying reactions |
Continuous Stirred Tank Reactor with perfect mixing.
import neqsim.process.equipment.reactor.CSTRReactor;
CSTRReactor cstr = new CSTRReactor("R-100", feedStream);
cstr.setVolume(10.0, "m3");
cstr.setTemperature(400.0, "K");
cstr.run();
// Define reaction: A + B → C
cstr.addReaction("component_A", -1); // reactant
cstr.addReaction("component_B", -1); // reactant
cstr.addReaction("component_C", 1); // product
// Reaction rate constant
cstr.setRateConstant(0.1, "1/s");
cstr.run();
$$\tau = \frac{V}{\dot{Q}}$$
Where:
Plug Flow Reactor with no back-mixing.
import neqsim.process.equipment.reactor.PFRReactor;
PFRReactor pfr = new PFRReactor("R-100", feedStream);
pfr.setLength(10.0, "m");
pfr.setDiameter(0.5, "m");
pfr.run();
// Set reaction kinetics
pfr.setReaction(reaction);
pfr.setNumberOfReactorSegments(100);
pfr.run();
For reactions at chemical equilibrium.
import neqsim.process.equipment.reactor.EquilibriumReactor;
EquilibriumReactor eqReactor = new EquilibriumReactor("R-100", feedStream);
eqReactor.setTemperature(500.0, "K");
eqReactor.setPressure(10.0, "bara");
eqReactor.run();
// Water-gas shift: CO + H2O ⇌ CO2 + H2
eqReactor.addReaction("CO", -1);
eqReactor.addReaction("H2O", -1);
eqReactor.addReaction("CO2", 1);
eqReactor.addReaction("H2", 1);
// Equilibrium constant
eqReactor.setEquilibriumConstant(Keq);
Minimize Gibbs free energy to find equilibrium composition.
import neqsim.process.equipment.reactor.GibbsReactor;
GibbsReactor gibbs = new GibbsReactor("R-100", feedStream);
gibbs.setTemperature(1000.0, "K");
gibbs.setPressure(10.0, "bara");
gibbs.run();
// Get equilibrium composition
Stream outlet = gibbs.getOutletStream();
// Specify which elements to balance
gibbs.setElementBalanceCheck(true);
// Specify inert components
gibbs.setInertComponent("N2", true);
import neqsim.thermo.system.SystemSrkEos;
import neqsim.process.equipment.stream.Stream;
import neqsim.process.equipment.reactor.CSTRReactor;
// Feed with reactants
SystemSrkEos feed = new SystemSrkEos(350.0, 5.0);
feed.addComponent("methanol", 0.5);
feed.addComponent("water", 0.5);
feed.setMixingRule("classic");
Stream feedStream = new Stream("Feed", feed);
feedStream.setFlowRate(1000.0, "kg/hr");
feedStream.run();
// Reactor
CSTRReactor reactor = new CSTRReactor("R-100", feedStream);
reactor.setVolume(5.0, "m3");
reactor.run();
double residenceTime = reactor.getResidenceTime("min");
System.out.println("Residence time: " + residenceTime + " min");
// SMR: CH4 + H2O ⇌ CO + 3H2
SystemSrkEos feed = new SystemSrkEos(700.0, 20.0);
feed.addComponent("methane", 1.0);
feed.addComponent("water", 3.0); // Steam to carbon ratio = 3
feed.setMixingRule("classic");
// Add possible products
feed.addComponent("CO", 0.0);
feed.addComponent("CO2", 0.0);
feed.addComponent("hydrogen", 0.0);
Stream feedStream = new Stream("SMR Feed", feed);
feedStream.setFlowRate(100.0, "kmol/hr");
feedStream.run();
// Gibbs reactor for equilibrium
GibbsReactor smr = new GibbsReactor("SMR Reactor", feedStream);
smr.setTemperature(1100.0, "K");
smr.setPressure(20.0, "bara");
smr.run();
// Results
Stream product = smr.getOutletStream();
System.out.println("H2 mole fraction: " + product.getFluid().getMoleFraction("hydrogen"));
System.out.println("CO mole fraction: " + product.getFluid().getMoleFraction("CO"));
System.out.println("CH4 conversion: " +
(1 - product.getFluid().getMoleFraction("methane") /
feedStream.getFluid().getMoleFraction("methane")) * 100 + " %");
// N2 + 3H2 ⇌ 2NH3
SystemSrkEos synthGas = new SystemSrkEos(700.0, 200.0);
synthGas.addComponent("nitrogen", 1.0);
synthGas.addComponent("hydrogen", 3.0);
synthGas.addComponent("ammonia", 0.0);
synthGas.setMixingRule("classic");
Stream feed = new Stream("Syngas", synthGas);
feed.setFlowRate(1000.0, "kmol/hr");
feed.run();
EquilibriumReactor ammoniaReactor = new EquilibriumReactor("Ammonia Reactor", feed);
ammoniaReactor.setTemperature(700.0, "K");
ammoniaReactor.setPressure(200.0, "bara");
ammoniaReactor.run();
double nh3Prod = ammoniaReactor.getOutletStream().getFluid().getMoleFraction("ammonia");
System.out.println("Ammonia mole fraction: " + nh3Prod);
The Gibbs Reactor is a chemical equilibrium reactor that computes outlet compositions by minimizing the total Gibbs free energy of the system. It is used for modeling chemical reactions at thermodynamic equilibrium.
The GibbsReactor class performs chemical equilibrium calculations using Gibbs free energy minimization with Lagrange multipliers. The reactor automatically determines the equilibrium composition based on:
The reactor minimizes the objective function:
$$G = \sum_i n_i \left( \mu_i^0 + RT \ln(\phi_i y_i P) \right) - \sum_j \lambda_j \left( \sum_i a_{ij} n_i - b_j \right)$$
Where:
The Newton-Raphson method iteratively solves for compositions and Lagrange multipliers until convergence.
import neqsim.process.equipment.reactor.GibbsReactor;
import neqsim.process.equipment.stream.Stream;
import neqsim.thermo.system.SystemSrkEos;
// Create inlet stream
SystemSrkEos system = new SystemSrkEos(298.15, 10.0);
system.addComponent("methane", 1.0, "mol/sec");
system.addComponent("oxygen", 2.0, "mol/sec");
system.addComponent("CO2", 0.0, "mol/sec");
system.addComponent("water", 0.0, "mol/sec");
system.setMixingRule(2);
Stream inlet = new Stream("inlet", system);
inlet.run();
// Create and configure reactor
GibbsReactor reactor = new GibbsReactor("combustion reactor", inlet);
reactor.setEnergyMode(GibbsReactor.EnergyMode.ISOTHERMAL);
reactor.setMaxIterations(5000);
reactor.setConvergenceTolerance(1e-6);
reactor.setDampingComposition(0.01);
reactor.run();
// Get results
Stream outlet = (Stream) reactor.getOutletStream();
System.out.println("Outlet temperature: " + outlet.getTemperature("C") + " °C");
System.out.println("Conversion completed: " + reactor.hasConverged());
// Isothermal: temperature remains constant
reactor.setEnergyMode(GibbsReactor.EnergyMode.ISOTHERMAL);
// Adiabatic: temperature changes based on reaction enthalpy
reactor.setEnergyMode(GibbsReactor.EnergyMode.ADIABATIC);
// Using string (case-insensitive)
reactor.setEnergyMode("adiabatic");
| Parameter | Method | Default | Description |
|---|---|---|---|
| Max Iterations | setMaxIterations(int) |
5000 | Maximum Newton-Raphson iterations |
| Convergence Tolerance | setConvergenceTolerance(double) |
1e-3 | Convergence criterion for delta norm |
| Damping Factor | setDampingComposition(double) |
0.05 | Step size for composition updates |
reactor.setMaxIterations(10000);
reactor.setConvergenceTolerance(1e-8);
reactor.setDampingComposition(0.001); // Smaller = more stable, slower
Mark components that should not participate in reactions:
// By name
reactor.setComponentAsInert("nitrogen");
reactor.setComponentAsInert("argon");
// By index
reactor.setComponentAsInert(0);
// Use only components present in inlet stream (default)
reactor.setUseAllDatabaseSpecies(false);
// Add all species from Gibbs database (for product prediction)
reactor.setUseAllDatabaseSpecies(true);
if (reactor.hasConverged()) {
System.out.println("Solution converged in " + reactor.getActualIterations() + " iterations");
} else {
System.out.println("Failed to converge. Final error: " + reactor.getFinalConvergenceError());
}
// Enthalpy of reaction (kJ)
double deltaH = reactor.getEnthalpyOfReactions();
// Temperature change in adiabatic mode (K)
double deltaT = reactor.getTemperatureChange();
// Reactor power (W, kW, or MW)
double powerW = reactor.getPower("W");
double powerKW = reactor.getPower("kW");
// Check mass balance closure
double massError = reactor.getMassBalanceError(); // Percentage error
boolean balanced = reactor.getMassBalanceConverged(); // True if error < 0.1%
// Element-wise balance
double[] elementIn = reactor.getElementMoleBalanceIn();
double[] elementOut = reactor.getElementMoleBalanceOut();
double[] elementDiff = reactor.getElementMoleBalanceDiff();
String[] elementNames = reactor.getElementNames(); // ["O", "N", "C", "H", "S", "Ar", "Z"]
List<Double> inletMoles = reactor.getInletMoles();
List<Double> outletMoles = reactor.getOutletMoles();
For CO2/acid gas systems, use GibbsReactorCO2 which provides pre-configured reaction pathways:
import neqsim.process.equipment.reactor.GibbsReactorCO2;
GibbsReactorCO2 acidGasReactor = new GibbsReactorCO2("acid gas reactor", inlet);
acidGasReactor.run();
Important Limitations of GibbsReactorCO2:
setDampingComposition(0.001) or smallersetMaxIterations(20000)If mass balance doesn't close:
For stiff systems:
reactor.setDampingComposition(0.0001); // Very small steps
reactor.setMaxIterations(50000); // Allow more iterations
reactor.setConvergenceTolerance(1e-4); // Relax tolerance slightly
The reactor uses thermodynamic data from CSV files in src/main/resources/data/GibbsReactDatabase/:
GibbsReactDatabase.csv - Component properties (elements, heat capacity, formation enthalpies)DatabaseGibbsFreeEnergyCoeff.csv - Polynomial coefficients for Gibbs energy calculationsThe reactor tracks mass balance for: O, N, C, H, S, Ar, Z (charge)
Custom components can be added to the database files following the existing format. Each component requires:
Documentation for electrolyzer equipment in NeqSim process simulation.
Location: neqsim.process.equipment.electrolyzer
Classes:
| Class | Description |
|---|---|
Electrolyzer |
Base electrolyzer class |
CO2Electrolyzer |
CO₂ electrolysis unit |
Electrolyzers convert electrical energy into chemical energy through electrochemical reactions. Key applications:
import neqsim.process.equipment.electrolyzer.Electrolyzer;
// Create electrolyzer with water feed
Electrolyzer electrolyzer = new Electrolyzer("PEM Electrolyzer", waterStream);
electrolyzer.setPower(1e6); // 1 MW
electrolyzer.setEfficiency(0.70); // 70% efficiency
electrolyzer.run();
// Get hydrogen production
StreamInterface h2Stream = electrolyzer.getHydrogenStream();
double h2Rate = h2Stream.getFlowRate("kg/hr");
System.out.println("H2 production: " + h2Rate + " kg/hr");
The CO2Electrolyzer converts CO₂ to valuable products through electrolysis.
import neqsim.process.equipment.electrolyzer.CO2Electrolyzer;
// Create CO2 electrolyzer
CO2Electrolyzer co2Elec = new CO2Electrolyzer("CO2 Electrolyzer", co2Stream);
co2Elec.setPower(500e3); // 500 kW
co2Elec.run();
// Get products
StreamInterface products = co2Elec.getOutletStream();
CO₂ electrolysis can produce various products depending on the catalyst:
The electrical power required for electrolysis:
$$P = \frac{\Delta G}{\eta_{elec}}$$
Where:
// Set efficiency (energy efficiency)
electrolyzer.setEfficiency(0.75); // 75%
// Get actual power consumption
double power = electrolyzer.getPower(); // W
The fraction of electrical current that drives the desired reaction:
$$\eta_F = \frac{n \times F \times \dot{n}_{product}}{I}$$
Where:
$$2H_2O \rightarrow 2H_2 + O_2$$
Theoretical minimum: 39.4 kWh/kg H₂ (based on HHV)
Typical actual consumption:
| Technology | Energy (kWh/kg H₂) |
|---|---|
| Alkaline | 50-55 |
| PEM | 50-60 |
| SOEC | 35-45 |
// Water feed
SystemInterface waterFluid = new SystemSrkEos(298.15, 1.0);
waterFluid.addComponent("water", 1.0);
waterFluid.setMixingRule("classic");
Stream waterFeed = new Stream("Water Feed", waterFluid);
waterFeed.setFlowRate(1000.0, "kg/hr");
waterFeed.run();
// PEM Electrolyzer
Electrolyzer pemElec = new Electrolyzer("PEM Stack", waterFeed);
pemElec.setPower(10e6); // 10 MW
pemElec.setEfficiency(0.70); // 70%
pemElec.run();
// Hydrogen output
double h2Production = pemElec.getHydrogenStream().getFlowRate("kg/hr");
double specificEnergy = pemElec.getPower() / 1000 / h2Production; // kWh/kg
System.out.println("H2 production: " + h2Production + " kg/hr");
System.out.println("Specific energy: " + specificEnergy + " kWh/kg H2");
ProcessSystem process = new ProcessSystem();
// Deionized water feed
SystemInterface diWater = new SystemSrkEos(298.15, 1.0);
diWater.addComponent("water", 1.0);
Stream waterFeed = new Stream("DI Water", diWater);
waterFeed.setFlowRate(2000.0, "kg/hr");
process.add(waterFeed);
// Electrolyzer stack
Electrolyzer electrolyzer = new Electrolyzer("H2 Electrolyzer", waterFeed);
electrolyzer.setPower(20e6); // 20 MW from renewable source
electrolyzer.setEfficiency(0.72);
process.add(electrolyzer);
// Hydrogen purification (PSA or membrane)
MembraneSeparator h2Purifier = new MembraneSeparator("H2 Purifier",
electrolyzer.getHydrogenStream());
h2Purifier.setPermeateFraction("hydrogen", 0.999);
h2Purifier.setPermeateFraction("water", 0.01);
process.add(h2Purifier);
// Compression for storage
Compressor h2Comp = new Compressor("H2 Compressor", h2Purifier.getPermeateStream());
h2Comp.setOutletPressure(350.0, "bara");
h2Comp.setPolytropicEfficiency(0.80);
process.add(h2Comp);
// Run process
process.run();
// Results
double h2Output = h2Comp.getOutletStream().getFlowRate("kg/hr");
double h2Purity = h2Comp.getOutletStream().getFluid().getMoleFraction("hydrogen") * 100;
System.out.println("H2 output: " + h2Output + " kg/hr");
System.out.println("H2 purity: " + h2Purity + " %");
// CO2 capture stream
Stream co2Stream = new Stream("Captured CO2", co2Fluid);
co2Stream.setFlowRate(1000.0, "kg/hr");
// CO2 electrolyzer producing syngas
CO2Electrolyzer co2Elec = new CO2Electrolyzer("CO2 Electrolyzer", co2Stream);
co2Elec.setPower(5e6); // 5 MW
co2Elec.run();
// Additional hydrogen from water electrolysis
Electrolyzer h2Elec = new Electrolyzer("H2 Electrolyzer", waterStream);
h2Elec.setPower(15e6); // 15 MW
h2Elec.run();
// Mix syngas and H2 for methanol synthesis
Mixer syngasMixer = new Mixer("Syngas Mixer");
syngasMixer.addStream(co2Elec.getOutletStream());
syngasMixer.addStream(h2Elec.getHydrogenStream());
// Methanol reactor (downstream)
// ...
// Variable renewable power input
double[] powerProfile = loadRenewablePowerProfile(); // hourly MW
// Electrolyzer with variable power
Electrolyzer electrolyzer = new Electrolyzer("Variable Electrolyzer", waterFeed);
electrolyzer.setEfficiency(0.70);
double totalH2 = 0.0;
for (int hour = 0; hour < 24; hour++) {
double power = powerProfile[hour] * 1e6; // Convert MW to W
if (power > 0) {
electrolyzer.setPower(power);
electrolyzer.run();
double h2Rate = electrolyzer.getHydrogenStream().getFlowRate("kg/hr");
totalH2 += h2Rate;
System.out.println("Hour " + hour + ": Power=" + power/1e6 +
" MW, H2=" + h2Rate + " kg/hr");
}
}
System.out.println("Total H2 production: " + totalH2 + " kg");
// Set operating conditions
electrolyzer.setTemperature(80.0, "C"); // PEM typical
electrolyzer.setPressure(30.0, "bara"); // Pressurized operation
// High-temperature electrolysis (SOEC)
electrolyzer.setTemperature(800.0, "C");
electrolyzer.setPressure(1.0, "bara");
// Set number of cells
electrolyzer.setNumberOfCells(100);
// Set cell voltage
electrolyzer.setCellVoltage(1.8); // V
// Calculate current
double current = electrolyzer.getCurrent(); // A
The snippet below illustrates how to couple the new CO2Electrolyzer with a CO₂-rich feed, a
power supply, and a downstream separator in a ProcessSystem.
import neqsim.process.equipment.electrolyzer.CO2Electrolyzer;
import neqsim.process.equipment.separator.Separator;
import neqsim.process.equipment.stream.Stream;
import neqsim.process.equipment.stream.StreamInterface;
import neqsim.process.equipment.battery.BatteryStorage;
import neqsim.process.processmodel.ProcessSystem;
import neqsim.thermo.Fluid;
import neqsim.thermo.system.SystemInterface;
SystemInterface feedFluid = new Fluid().create2(
new String[] {"CO2", "water"},
new double[] {0.95, 0.05},
"mole/sec");
feedFluid.setTemperature(298.15);
feedFluid.setPressure(20.0);
Stream feedStream = new Stream("CO2 feed", feedFluid);
CO2Electrolyzer electrolyzer = new CO2Electrolyzer("CO2 electrolyzer", feedStream);
electrolyzer.setCO2Conversion(0.55);
electrolyzer.setGasProductSelectivity("CO", 0.7);
electrolyzer.setGasProductSelectivity("H2", 0.3);
electrolyzer.setProductFaradaicEfficiency("CO", 0.9);
electrolyzer.setElectronsPerMoleProduct("H2", 2.0);
// Downstream separation polishing the gas product
Separator syngasPolisher = new Separator("syngas polisher");
syngasPolisher.setInletStream((StreamInterface) electrolyzer.getGasProductStream());
ProcessSystem process = new ProcessSystem("CO2 to fuels");
process.add(feedStream);
process.add(electrolyzer);
process.add(syngasPolisher);
process.run();
double powerDraw = electrolyzer.getEnergyStream().getDuty();
BatteryStorage battery = new BatteryStorage("renewable battery", 5.0e8);
battery.discharge(powerDraw, 1.0 / 3600.0);
System.out.println("Electrolyzer power demand: " + powerDraw + " W");
The gas product stream carries the vapor-phase synthesis gas, the liquid product stream (accessible
through getLiquidProductStream()) contains soluble products such as formate or methanol, and the
EnergyStream keeps track of the instantaneous electrical duty demanded from the battery.
Documentation for flare equipment in NeqSim process simulation.
Location: neqsim.process.equipment.flare
Classes:
| Class | Description |
|---|---|
Flare |
Main flare combustion unit |
FlareStack |
Flare stack with dispersion |
FlareCapacityDTO |
Capacity check results |
FlarePerformanceDTO |
Performance metrics |
Flares are safety devices that combust hydrocarbon gases that cannot be recovered. They are used for:
import neqsim.process.equipment.flare.Flare;
// Create flare with inlet stream
Flare flare = new Flare("HP Flare", inletStream);
flare.run();
// Get results
double heatRelease = flare.getHeatDuty(); // W
double co2Emission = flare.getCO2Emission(); // kg/s
System.out.println("Heat release: " + heatRelease/1e6 + " MW");
System.out.println("CO2 emission: " + co2Emission * 3600 + " kg/hr");
// With name only
Flare flare = new Flare("Flare");
flare.setInletStream(flareGas);
// With name and inlet stream
Flare flare = new Flare("Flare", flareGas);
The flare calculates heat release based on the Lower Calorific Value (LCV):
$$Q = LCV \times \dot{V}_{Sm3}$$
// Get heat release rate
double heatDuty = flare.getHeatDuty(); // W
// Convert to MW
double heatMW = heatDuty / 1e6;
System.out.println("Heat release: " + heatMW + " MW");
CO₂ emissions are calculated from the carbon content of the flared gas:
$$\dot{m}_{CO_2} = \sum_i (x_i \times \dot{n}_{total} \times n_{C,i} \times M_{CO_2})$$
Where:
// Get CO2 emission rate
double co2Rate = flare.getCO2Emission(); // kg/s
// Convert to tonnes per hour
double co2TonHr = co2Rate * 3.6;
System.out.println("CO2 emissions: " + co2TonHr + " t/hr");
// Set radiant fraction (fraction of heat released as radiation)
flare.setRadiantFraction(0.20); // 20%
// Set flame height for radiation calculations
flare.setFlameHeight(40.0); // m
// Get radiation at ground level
double radiation = flare.getRadiationAtDistance(100.0); // W/m² at 100m
// Set tip diameter for velocity calculations
flare.setTipDiameter(0.5); // m
// Get tip velocity
double tipVelocity = flare.getTipVelocity(); // m/s
// Set design capacity limits
flare.setDesignHeatDutyCapacity(100e6); // 100 MW
flare.setDesignMassFlowCapacity(50.0); // 50 kg/s
flare.setDesignMolarFlowCapacity(2000.0); // 2000 mol/s
// Check if within capacity
flare.run();
CapacityCheckResult result = flare.getCapacityCheckResult();
if (result.isOverCapacity()) {
System.out.println("WARNING: Flare over capacity!");
System.out.println("Heat duty: " + result.getHeatDutyPercent() + "% of design");
System.out.println("Mass flow: " + result.getMassFlowPercent() + "% of design");
}
// Track cumulative values during transient
double timeStep = 1.0; // seconds
double totalTime = 900.0; // 15 minutes
for (double t = 0; t < totalTime; t += timeStep) {
// Update inlet conditions (e.g., from blowdown)
updateFlareInlet(t);
flare.run();
flare.updateCumulative(timeStep);
}
// Get cumulative values
double totalHeatGJ = flare.getCumulativeHeatReleased(); // GJ
double totalGasBurned = flare.getCumulativeGasBurned(); // kg
double totalCO2 = flare.getCumulativeCO2Emission(); // kg
System.out.println("Total heat released: " + totalHeatGJ + " GJ");
System.out.println("Total gas burned: " + totalGasBurned + " kg");
System.out.println("Total CO2 emitted: " + totalCO2 + " kg");
// Reset for new transient event
flare.resetCumulative();
The FlareStack class includes additional features for dispersion modeling:
import neqsim.process.equipment.flare.FlareStack;
FlareStack flareStack = new FlareStack("Main Flare Stack", inletStream);
flareStack.setStackHeight(100.0); // m
flareStack.setTipDiameter(0.6); // m
flareStack.run();
// Get dispersion parameters
FlareDispersionSurrogateDTO dispersion = flareStack.getDispersionSurrogate();
double effectiveHeight = dispersion.getEffectiveStackHeight();
double plumeMomentum = dispersion.getPlumeMomentum();
ProcessSystem process = new ProcessSystem();
// Vessel being depressured
Tank vessel = new Tank("HP Vessel", vesselFluid);
vessel.setVolume(100.0, "m3");
vessel.setPressure(100.0, "bara");
process.add(vessel);
// Blowdown valve
BlowdownValve bdv = new BlowdownValve("BDV-100", vessel);
bdv.setOrificeSize(100.0, "mm");
bdv.setDownstreamPressure(1.5, "barg");
process.add(bdv);
// Flare receiving blowdown
Flare flare = new Flare("HP Flare", bdv.getOutletStream());
flare.setDesignHeatDutyCapacity(200e6); // 200 MW design
process.add(flare);
// Run transient blowdown
double dt = 1.0;
for (double t = 0; t < 900; t += dt) {
vessel.runTransient(dt);
bdv.run();
flare.run();
flare.updateCumulative(dt);
System.out.println("t=" + t + "s, P=" + vessel.getPressure("barg") +
" barg, Q=" + flare.getHeatDuty()/1e6 + " MW");
}
// Multiple sources to common flare header
Mixer flareHeader = new Mixer("Flare Header");
flareHeader.addStream(lpFlareSource1);
flareHeader.addStream(lpFlareSource2);
flareHeader.addStream(lpFlareSource3);
process.add(flareHeader);
// Flare KO drum
Separator flareKODrum = new Separator("Flare KO Drum", flareHeader.getOutletStream());
process.add(flareKODrum);
// Flare tip
Flare flareTip = new Flare("LP Flare", flareKODrum.getGasOutStream());
flareTip.setDesignHeatDutyCapacity(50e6);
process.add(flareTip);
process.run();
// Check overall flare load
double totalLoad = flareTip.getHeatDuty();
double percentCapacity = totalLoad / flareTip.getDesignHeatDutyCapacity() * 100;
System.out.println("Flare load: " + percentCapacity + "% of design");
// Calculate flare load for blocked outlet scenario
double reliefRate = calculateBlockedOutletRelief(); // kg/hr
// Create relief stream
SystemInterface reliefFluid = vesselFluid.clone();
Stream reliefStream = new Stream("Relief Flow", reliefFluid);
reliefStream.setFlowRate(reliefRate, "kg/hr");
reliefStream.run();
// Calculate flare load
Flare flare = new Flare("Scenario Flare", reliefStream);
flare.run();
System.out.println("Relief scenario:");
System.out.println(" Flow rate: " + reliefRate + " kg/hr");
System.out.println(" Heat release: " + flare.getHeatDuty()/1e6 + " MW");
System.out.println(" CO2 emission: " + flare.getCO2Emission()*3600 + " kg/hr");
FlarePerformanceDTO performance = flare.getPerformance();
System.out.println("=== Flare Performance ===");
System.out.println("Heat release: " + performance.getHeatDutyMW() + " MW");
System.out.println("Mass flow: " + performance.getMassFlowKgS() + " kg/s");
System.out.println("Tip velocity: " + performance.getTipVelocity() + " m/s");
System.out.println("CO2 emission: " + performance.getCO2EmissionKgS() + " kg/s");
System.out.println("Radiant heat: " + performance.getRadiantHeatMW() + " MW");
// Calculate annual CO2 emissions from continuous flaring
double avgFlowRate = 1000.0; // Sm3/hr average
double hoursPerYear = 8760.0;
flare.getInletStream().setFlowRate(avgFlowRate, "Sm3/hr");
flare.run();
double annualCO2 = flare.getCO2Emission() * 3600 * hoursPerYear / 1000; // tonnes/year
System.out.println("Annual CO2 emissions: " + annualCO2 + " tonnes/year");
Documentation for adsorption equipment in NeqSim.
Location: neqsim.process.equipment.adsorber
The adsorber package provides equipment for modeling gas treatment processes using solid adsorbents. Adsorption is commonly used for:
The SimpleAdsorber class models a simplified adsorption column for gas treatment applications.
ProcessEquipmentBaseClass
└── SimpleAdsorber
import neqsim.process.equipment.adsorber.SimpleAdsorber;
import neqsim.process.equipment.stream.Stream;
// Basic constructor
SimpleAdsorber adsorber = new SimpleAdsorber("CO2 Adsorber");
// Constructor with inlet stream
SimpleAdsorber adsorber = new SimpleAdsorber("CO2 Adsorber", feedStream);
| Property | Description | Default |
|---|---|---|
numberOfStages |
Number of theoretical stages | 5 |
numberOfTheoreticalStages |
Theoretical stages (continuous) | 3.0 |
absorptionEfficiency |
Removal efficiency (0-1) | 0.5 |
HTU |
Height of Transfer Unit (m) | 0.85 |
NTU |
Number of Transfer Units | 2.0 |
stageEfficiency |
Per-stage efficiency | 0.25 |
The SimpleAdsorber is configured for CO2 removal using MDEA (methyldiethanolamine) solvent:
// Create gas feed with CO2
SystemInterface gasFluid = new SystemSrkEos(298.15, 50.0);
gasFluid.addComponent("methane", 0.85);
gasFluid.addComponent("CO2", 0.10);
gasFluid.addComponent("nitrogen", 0.05);
gasFluid.setMixingRule("classic");
Stream feedGas = new Stream("Feed Gas", gasFluid);
feedGas.setFlowRate(10000.0, "Sm3/hr");
// Create adsorber
SimpleAdsorber adsorber = new SimpleAdsorber("CO2 Removal", feedGas);
// Run
adsorber.run();
// Get treated gas
StreamInterface treatedGas = adsorber.getOutStream(0);
System.out.println("CO2 in treated gas: " +
treatedGas.getFluid().getComponent("CO2").getx() * 100 + " mol%");
The adsorber provides two output streams:
getOutStream(0) - Treated gas (clean gas)getOutStream(1) - Rich solvent (solvent loaded with absorbed component)import neqsim.process.equipment.adsorber.SimpleAdsorber;
import neqsim.process.equipment.stream.Stream;
import neqsim.thermo.system.SystemSrkEos;
// Create sour gas
SystemInterface sourGas = new SystemSrkEos(298.15, 50.0);
sourGas.addComponent("methane", 0.80);
sourGas.addComponent("ethane", 0.05);
sourGas.addComponent("CO2", 0.10);
sourGas.addComponent("H2S", 0.02);
sourGas.addComponent("nitrogen", 0.03);
sourGas.setMixingRule("classic");
Stream feed = new Stream("Sour Gas Feed", sourGas);
feed.setFlowRate(5.0, "MSm3/day");
// Create and run adsorber
SimpleAdsorber acidGasRemoval = new SimpleAdsorber("AGRU", feed);
acidGasRemoval.setAbsorptionEfficiency(0.95);
acidGasRemoval.run();
// Results
StreamInterface sweetGas = acidGasRemoval.getOutStream(0);
System.out.println("Sweet gas CO2: " +
sweetGas.getFluid().getComponent("CO2").getx() * 1e6 + " ppm");
import neqsim.process.processmodel.ProcessSystem;
ProcessSystem process = new ProcessSystem();
// Add equipment
process.add(feedStream);
process.add(adsorber);
process.add(treatedGasExport);
// Run complete process
process.run();
// Set 95% removal efficiency
adsorber.setAbsorptionEfficiency(0.95);
adsorber.setNumberOfStages(10);
adsorber.setNumberOfTheoreticalStages(7.5);
adsorber.setStageEfficiency(0.75);
adsorber.setHTU(0.5); // Height of Transfer Unit in meters
adsorber.setNTU(4.0); // Number of Transfer Units
The SimpleAdsorber supports mechanical design calculations through AdsorberMechanicalDesign:
import neqsim.process.mechanicaldesign.adsorber.AdsorberMechanicalDesign;
AdsorberMechanicalDesign design = adsorber.getMechanicalDesign();
design.calcDesign();
System.out.println("Vessel diameter: " + design.getInnerDiameter() + " m");
System.out.println("Vessel height: " + design.getTotalHeight() + " m");
neqsim.process.equipment.absorber - Alternative absorption equipmentneqsim.thermo.characterization - Fluid characterization for sour gasDocumentation for power generation equipment in NeqSim, including gas turbines, fuel cells, wind turbines, and solar panels.
Location: neqsim.process.equipment.powergeneration
The power generation package provides equipment models for converting chemical and renewable energy sources into electrical power:
| Equipment | Energy Source | Output |
|---|---|---|
GasTurbine |
Fuel gas combustion | Electricity + heat |
FuelCell |
Hydrogen + oxygen | Electricity + water |
WindTurbine |
Wind | Electricity |
SolarPanel |
Solar radiation | Electricity |
BatteryStorage |
Stored electricity | Electricity |
The GasTurbine class models a simple cycle gas turbine with integrated air compression, combustion, and expansion.
TwoPortEquipment
└── GasTurbine
import neqsim.process.equipment.powergeneration.GasTurbine;
import neqsim.process.equipment.stream.Stream;
// Basic constructor
GasTurbine turbine = new GasTurbine("GT-101");
// Constructor with fuel stream
GasTurbine turbine = new GasTurbine("GT-101", fuelGasStream);
| Property | Description | Unit |
|---|---|---|
combustionPressure |
Combustor pressure | bara |
airGasRatio |
Air to fuel ratio | - |
power |
Net electrical power output | W |
heat |
Heat output | W |
compressorPower |
Air compressor power | W |
expanderPower |
Expander power | W |
import neqsim.process.equipment.powergeneration.GasTurbine;
import neqsim.process.equipment.stream.Stream;
import neqsim.thermo.system.SystemSrkEos;
// Create fuel gas
SystemInterface fuelGas = new SystemSrkEos(288.15, 25.0);
fuelGas.addComponent("methane", 0.90);
fuelGas.addComponent("ethane", 0.05);
fuelGas.addComponent("propane", 0.03);
fuelGas.addComponent("nitrogen", 0.02);
fuelGas.setMixingRule("classic");
Stream fuelStream = new Stream("Fuel Gas", fuelGas);
fuelStream.setFlowRate(1000.0, "kg/hr");
// Create gas turbine
GasTurbine turbine = new GasTurbine("Power Turbine", fuelStream);
turbine.setCombustionPressure(15.0); // bara
turbine.setAirGasRatio(3.0);
// Run simulation
turbine.run();
// Results
System.out.println("Net power: " + turbine.getPower() / 1e6 + " MW");
System.out.println("Heat output: " + turbine.getHeat() / 1e6 + " MW");
System.out.println("Thermal efficiency: " + turbine.getEfficiency() * 100 + "%");
The FuelCell class models a hydrogen fuel cell that converts hydrogen and oxygen to electricity and water.
TwoPortEquipment
└── FuelCell
import neqsim.process.equipment.powergeneration.FuelCell;
// Basic constructor
FuelCell cell = new FuelCell("FC-101");
// Constructor with fuel and oxidant streams
FuelCell cell = new FuelCell("FC-101", hydrogenStream, airStream);
| Property | Description | Unit |
|---|---|---|
efficiency |
Electrical efficiency | 0-1 |
power |
Electrical power output | W |
heatLoss |
Heat loss to environment | W |
import neqsim.process.equipment.powergeneration.FuelCell;
import neqsim.process.equipment.stream.Stream;
import neqsim.thermo.system.SystemSrkEos;
// Create hydrogen fuel stream
SystemInterface h2Fluid = new SystemSrkEos(298.15, 5.0);
h2Fluid.addComponent("hydrogen", 1.0);
h2Fluid.setMixingRule("classic");
Stream hydrogenFeed = new Stream("Hydrogen", h2Fluid);
hydrogenFeed.setFlowRate(10.0, "kg/hr");
// Create air stream
SystemInterface airFluid = new SystemSrkEos(298.15, 1.01325);
airFluid.addComponent("nitrogen", 0.79);
airFluid.addComponent("oxygen", 0.21);
airFluid.setMixingRule("classic");
Stream airFeed = new Stream("Air", airFluid);
airFeed.setFlowRate(100.0, "kg/hr");
// Create fuel cell
FuelCell fuelCell = new FuelCell("SOFC", hydrogenFeed, airFeed);
fuelCell.setEfficiency(0.55);
// Run simulation
fuelCell.run();
// Results
System.out.println("Electrical power: " + fuelCell.getPower() / 1000 + " kW");
System.out.println("Heat loss: " + fuelCell.getHeatLoss() / 1000 + " kW");
The WindTurbine class models wind power generation based on wind speed and turbine characteristics.
import neqsim.process.equipment.powergeneration.WindTurbine;
WindTurbine turbine = new WindTurbine("WT-01");
turbine.setWindSpeed(12.0); // m/s
turbine.setRotorDiameter(120.0); // m
turbine.setEfficiency(0.45);
| Property | Description | Unit |
|---|---|---|
windSpeed |
Wind velocity | m/s |
rotorDiameter |
Rotor diameter | m |
efficiency |
Power coefficient | 0-0.593 (Betz limit) |
power |
Electrical power output | W |
The SolarPanel class models photovoltaic power generation.
import neqsim.process.equipment.powergeneration.SolarPanel;
SolarPanel panel = new SolarPanel("PV-Array");
panel.setPanelArea(1000.0); // m²
panel.setSolarIrradiance(800.0); // W/m²
panel.setEfficiency(0.20);
| Property | Description | Unit |
|---|---|---|
panelArea |
Total panel area | m² |
solarIrradiance |
Solar radiation | W/m² |
efficiency |
Panel efficiency | 0-1 |
power |
Electrical power output | W |
Location: neqsim.process.equipment.battery
The BatteryStorage class models electrical energy storage systems.
import neqsim.process.equipment.battery.BatteryStorage;
BatteryStorage battery = new BatteryStorage("BESS-01");
battery.setCapacity(100.0); // MWh
battery.setMaxPower(25.0); // MW
battery.setRoundTripEfficiency(0.90);
| Property | Description | Unit |
|---|---|---|
capacity |
Total energy capacity | MWh |
maxPower |
Maximum charge/discharge rate | MW |
roundTripEfficiency |
Charge-discharge efficiency | 0-1 |
stateOfCharge |
Current energy level | 0-1 |
import neqsim.process.processmodel.ProcessSystem;
import neqsim.process.equipment.powergeneration.GasTurbine;
import neqsim.process.equipment.heatexchanger.Heater;
ProcessSystem chpSystem = new ProcessSystem("CHP Plant");
// Create fuel gas stream
Stream fuelGas = new Stream("Fuel", fuelFluid);
fuelGas.setFlowRate(500.0, "kg/hr");
chpSystem.add(fuelGas);
// Gas turbine
GasTurbine turbine = new GasTurbine("GT", fuelGas);
turbine.setCombustionPressure(12.0);
chpSystem.add(turbine);
// Heat recovery steam generator (simplified)
Heater hrsg = new Heater("HRSG", turbine.getExhaustStream());
hrsg.setOutTemperature(150.0, "C");
chpSystem.add(hrsg);
// Run
chpSystem.run();
// Calculate efficiency
double electricalPower = turbine.getPower();
double thermalPower = hrsg.getDuty();
double fuelInput = fuelGas.getFlowRate("kg/hr") * 50e6 / 3600; // LHV ~ 50 MJ/kg
double electricalEff = electricalPower / fuelInput;
double totalEff = (electricalPower + thermalPower) / fuelInput;
System.out.println("Electrical efficiency: " + electricalEff * 100 + "%");
System.out.println("Total CHP efficiency: " + totalEff * 100 + "%");
// Solar + Wind + Battery system
SolarPanel solar = new SolarPanel("PV");
solar.setPanelArea(5000.0);
solar.setSolarIrradiance(600.0);
solar.setEfficiency(0.18);
WindTurbine wind = new WindTurbine("Wind");
wind.setWindSpeed(8.0);
wind.setRotorDiameter(80.0);
BatteryStorage battery = new BatteryStorage("Battery");
battery.setCapacity(10.0); // MWh
battery.setMaxPower(5.0); // MW
// Calculate total renewable generation
solar.run();
wind.run();
double totalGeneration = solar.getPower() + wind.getPower();
System.out.println("Total renewable power: " + totalGeneration / 1e6 + " MW");
Documentation for differential pressure measurement and flow restriction equipment in NeqSim.
Location: neqsim.process.equipment.diffpressure
The differential pressure package provides equipment for:
| Class | Description |
|---|---|
Orifice |
ISO 5167 orifice plate flow restriction |
DifferentialPressureFlowCalculator |
ΔP-based flow calculation |
The Orifice class models an orifice plate flow restriction device compliant with ISO 5167.
TwoPortEquipment
└── Orifice
import neqsim.process.equipment.diffpressure.Orifice;
// Basic constructor
Orifice orifice = new Orifice("FO-101");
// Full constructor with ISO 5167 parameters
Orifice orifice = new Orifice(
"FO-101", // Name
0.1, // Pipe diameter (m)
0.05, // Orifice diameter (m)
50.0, // Upstream pressure (bara)
5.0, // Downstream pressure (bara)
0.61 // Discharge coefficient
);
| Property | Description | Unit |
|---|---|---|
diameter |
Upstream pipe internal diameter | m |
orificeDiameter |
Orifice bore diameter | m |
pressureUpstream |
Upstream pressure | bara |
pressureDownstream |
Downstream boundary pressure | bara |
dischargeCoefficient |
Cd (typically 0.60-0.62) | - |
dp |
Pressure differential | bar |
The beta ratio (β) is the ratio of orifice diameter to pipe diameter:
β = d/D
Where:
Typical range: 0.2 ≤ β ≤ 0.75
The orifice uses the Reader-Harris/Gallagher equation for the discharge coefficient:
C = f(β, ReD, L₁, L₂)
Where:
For compressible flow, the expansibility factor ε accounts for gas expansion:
ε = f(β, κ, τ)
Where:
Mass flow rate through the orifice:
ṁ = C · ε · (π/4) · d² · √(2 · ρ₁ · ΔP)
The DifferentialPressureFlowCalculator provides utilities for ΔP-based flow calculations.
import neqsim.process.equipment.diffpressure.DifferentialPressureFlowCalculator;
DifferentialPressureFlowCalculator calc = new DifferentialPressureFlowCalculator();
calc.setPipeDiameter(0.1);
calc.setOrificeDiameter(0.05);
calc.setDifferentialPressure(0.5); // bar
double massFlow = calc.calculateMassFlow(fluid);
System.out.println("Mass flow: " + massFlow + " kg/s");
import neqsim.process.equipment.diffpressure.Orifice;
import neqsim.process.equipment.stream.Stream;
import neqsim.thermo.system.SystemSrkEos;
// Create gas stream
SystemInterface gas = new SystemSrkEos(288.15, 50.0);
gas.addComponent("methane", 0.90);
gas.addComponent("ethane", 0.05);
gas.addComponent("propane", 0.03);
gas.addComponent("nitrogen", 0.02);
gas.setMixingRule("classic");
Stream gasStream = new Stream("Process Gas", gas);
gasStream.setFlowRate(10000.0, "Sm3/hr");
// Create orifice meter
Orifice meter = new Orifice("FO-101");
meter.setInletStream(gasStream);
meter.setDiameter(0.15); // 150mm pipe
meter.setOrificeDiameter(0.075); // 75mm orifice (β = 0.5)
// Run
meter.run();
// Get differential pressure
System.out.println("ΔP: " + meter.getDp() * 1000 + " mbar");
System.out.println("Flow rate: " + meter.getOutletStream().getFlowRate("Sm3/hr") + " Sm3/hr");
The orifice can be used for transient depressurization simulations:
import neqsim.process.equipment.diffpressure.Orifice;
import neqsim.process.equipment.tank.Tank;
// Create vessel
Tank vessel = new Tank("V-101", vesselFluid);
vessel.setVolume(100.0); // m³
// Create blowdown orifice
Orifice blowdownOrifice = new Orifice(
"RO-101",
0.1, // 100mm pipe
0.025, // 25mm orifice
vessel.getPressure(),
1.5, // Flare header pressure
0.61 // Discharge coefficient
);
blowdownOrifice.setInletStream(vessel.getOutletStream());
// Run transient simulation
for (double t = 0; t < 3600; t += 1.0) {
blowdownOrifice.setPressureUpstream(vessel.getPressure());
blowdownOrifice.run();
double massFlow = blowdownOrifice.getOutletStream().getFlowRate("kg/s");
vessel.removeMass(massFlow * 1.0); // Remove mass for 1 second
if (t % 60 == 0) {
System.out.println("t=" + t + "s, P=" + vessel.getPressure() + " bara");
}
}
import neqsim.process.processmodel.ProcessSystem;
ProcessSystem process = new ProcessSystem("Flow Metering");
// Feed
process.add(feedStream);
// Flow meter
Orifice flowMeter = new Orifice("FO-101");
flowMeter.setInletStream(feedStream);
flowMeter.setDiameter(0.1);
flowMeter.setOrificeDiameter(0.05);
process.add(flowMeter);
// Downstream equipment
Separator separator = new Separator("V-101", flowMeter.getOutletStream());
process.add(separator);
// Run
process.run();
For accurate flow measurement:
Permanent pressure loss is approximately:
ΔP_permanent ≈ (1 - β⁴) × ΔP_measured
Avoid cavitation by ensuring:
P₂ > 2 × P_vapor
Documentation for manifold equipment that combines stream mixing and splitting in NeqSim.
Location: neqsim.process.equipment.manifold
A manifold is a process equipment that combines the functionality of a mixer and a splitter. It can:
This is particularly useful for:
| Class | Description |
|---|---|
Manifold |
Combined mixer/splitter unit |
The Manifold class extends ProcessEquipmentBaseClass and contains both a Mixer and a Splitter internally.
ProcessEquipmentBaseClass
└── Manifold
├── contains: Mixer
└── contains: Splitter
import neqsim.process.equipment.manifold.Manifold;
// Basic constructor
Manifold manifold = new Manifold("PM-101");
| Method | Description |
|---|---|
addStream(Stream) |
Add an input stream to the manifold |
getSplitStream(int index) |
Get a specific output stream by index |
setSplitNumber(int n) |
Set number of output streams |
setSplitFactors(double[]) |
Set split ratios for outputs |
getMixer() |
Access the internal mixer |
getSplitter() |
Access the internal splitter |
run() |
Execute mixing then splitting |
import neqsim.process.equipment.manifold.Manifold;
import neqsim.process.equipment.stream.Stream;
import neqsim.thermo.system.SystemSrkEos;
// Create input streams (e.g., from multiple wells)
SystemInterface well1Fluid = new SystemSrkEos(350.0, 100.0);
well1Fluid.addComponent("methane", 0.85);
well1Fluid.addComponent("ethane", 0.10);
well1Fluid.addComponent("propane", 0.05);
well1Fluid.setMixingRule("classic");
Stream well1Stream = new Stream("Well-1", well1Fluid);
well1Stream.setFlowRate(50000, "Sm3/day");
SystemInterface well2Fluid = new SystemSrkEos(340.0, 95.0);
well2Fluid.addComponent("methane", 0.82);
well2Fluid.addComponent("ethane", 0.12);
well2Fluid.addComponent("propane", 0.06);
well2Fluid.setMixingRule("classic");
Stream well2Stream = new Stream("Well-2", well2Fluid);
well2Stream.setFlowRate(75000, "Sm3/day");
// Run inlet streams
well1Stream.run();
well2Stream.run();
// Create manifold
Manifold productionManifold = new Manifold("PM-101");
productionManifold.addStream(well1Stream);
productionManifold.addStream(well2Stream);
// Configure splitting (2 outlet lines)
productionManifold.setSplitNumber(2);
productionManifold.setSplitFactors(new double[] {0.6, 0.4});
// Run manifold
productionManifold.run();
// Access output streams
Stream toSeparator1 = (Stream) productionManifold.getSplitStream(0);
Stream toSeparator2 = (Stream) productionManifold.getSplitStream(1);
System.out.println("Total inlet: " + (50000 + 75000) + " Sm3/day");
System.out.println("Outlet 1: " + toSeparator1.getFlowRate("Sm3/day") + " Sm3/day");
System.out.println("Outlet 2: " + toSeparator2.getFlowRate("Sm3/day") + " Sm3/day");
import neqsim.process.processmodel.ProcessSystem;
ProcessSystem facility = new ProcessSystem("Offshore Platform");
// Wells
Stream well1 = createWellStream("Well-1", 50000);
Stream well2 = createWellStream("Well-2", 45000);
Stream well3 = createWellStream("Well-3", 60000);
facility.add(well1);
facility.add(well2);
facility.add(well3);
// Production manifold
Manifold manifold = new Manifold("Production Manifold");
manifold.addStream(well1);
manifold.addStream(well2);
manifold.addStream(well3);
manifold.setSplitNumber(2); // Two production trains
manifold.setSplitFactors(new double[] {0.5, 0.5});
facility.add(manifold);
// Train separators
Separator separator1 = new Separator("V-101");
separator1.setInletStream(manifold.getSplitStream(0));
facility.add(separator1);
Separator separator2 = new Separator("V-201");
separator2.setInletStream(manifold.getSplitStream(1));
facility.add(separator2);
// Run facility
facility.run();
Manifold manifold = new Manifold("PM-101");
manifold.addStream(stream1);
manifold.addStream(stream2);
// Access internal mixer for mixed stream properties
Mixer mixer = manifold.getMixer();
mixer.run();
Stream mixedStream = mixer.getOutletStream();
System.out.println("Mixed temperature: " + mixedStream.getTemperature("C") + " °C");
System.out.println("Mixed pressure: " + mixedStream.getPressure("bara") + " bara");
// Access internal splitter
Splitter splitter = manifold.getSplitter();
Well-1 ──┐
│
Well-2 ──┼──► [Manifold] ──┬──► Train A
│ │
Well-3 ──┘ └──► Train B
// Distribute load evenly across processing trains
int numTrains = 3;
double[] splitFactors = new double[numTrains];
for (int i = 0; i < numTrains; i++) {
splitFactors[i] = 1.0 / numTrains;
}
manifold.setSplitFactors(splitFactors);
// Redirect all flow to Train A (e.g., Train B offline)
manifold.setSplitFactors(new double[] {1.0, 0.0});
manifold.run();
Input streams should have similar pressures. If pressures differ significantly, use chokes or control valves upstream.
The manifold performs adiabatic mixing. The outlet temperature is calculated from energy balance.
The mixed composition is the flow-weighted average of all inlet compositions.
| Aspect | Manifold | Mixer + Splitter |
|---|---|---|
| Construction | Single equipment | Two separate units |
| Modeling | Combined unit | Sequential execution |
| Use Case | Production headers | General mixing/splitting |
| Intermediate Access | Via getMixer()/getSplitter() | Direct access |
The BatteryStorage unit stores electrical energy for later use. It maintains an
internal state-of-charge and exchanges power with the rest of a process through
its energy stream. Positive duty on the energy stream represents charging while
negative duty represents power delivery.
The battery can be combined with other power generation units such as
FuelCell or GasTurbine to buffer excess electricity or supply power during
demand peaks.
BatteryStorage battery = new BatteryStorage("battery", 5.0e5);
FuelCell cell = new FuelCell("cell", fuel, oxidant);
cell.run();
// store half of the produced power for one hour
battery.charge(-cell.getEnergyStream().getDuty() / 2.0, 1.0);
battery.run();
The SolarPanel unit converts solar irradiance into electrical power using a
simple relation between the incoming radiation, panel area and efficiency:
Power = irradiance [W/m^2] × panel area [m^2] × efficiency
The produced power is available from the unit's energy stream as a negative duty (indicating power generation).
SolarPanel panel = new SolarPanel("panel");
panel.setIrradiance(800.0); // W/m^2
panel.setPanelArea(2.0); // m^2
panel.setEfficiency(0.2); // 20%
panel.run();
System.out.println(panel.getPower());
Documentation for well and reservoir equipment in NeqSim process simulation.
Location: neqsim.process.equipment.well
Classes:
SimpleWell - Simple well modelWellFlow - Well flow calculationsChokeValve - Wellhead chokeRelated: neqsim.process.equipment.reservoir
import neqsim.process.equipment.well.SimpleWell;
// Create reservoir fluid
SystemInterface reservoirFluid = new SystemPrEos(373.15, 250.0);
reservoirFluid.addComponent("methane", 0.70);
reservoirFluid.addComponent("ethane", 0.10);
reservoirFluid.addComponent("propane", 0.05);
reservoirFluid.addComponent("n-heptane", 0.10);
reservoirFluid.addComponent("water", 0.05);
reservoirFluid.setMixingRule("classic");
// Create well
SimpleWell well = new SimpleWell("Producer 1", reservoirFluid);
well.setWellheadPressure(50.0, "bara");
well.setFlowRate(10000.0, "Sm3/day");
well.run();
Stream wellheadStream = well.getOutletStream();
$$q = q_{max} \left[1 - 0.2\frac{P_{wf}}{P_r} - 0.8\left(\frac{P_{wf}}{P_r}\right)^2\right]$$
well.setIPRModel("vogel");
well.setReservoirPressure(300.0, "bara");
well.setMaxFlowRate(50000.0, "Sm3/day");
well.setWellheadPressure(50.0, "bara");
well.run();
double flowRate = well.getFlowRate("Sm3/day");
$$q = \frac{k \cdot h \cdot (P_r^2 - P_{wf}^2)}{1422 \cdot T \cdot \mu \cdot Z \cdot \ln(r_e/r_w)}$$
well.setIPRModel("darcy");
well.setPermeability(100.0, "mD");
well.setPayThickness(50.0, "m");
well.setDrainageRadius(500.0, "m");
well.setWellboreRadius(0.1, "m");
$$q = C (P_r^2 - P_{wf}^2)^n$$
well.setIPRModel("backpressure");
well.setBackpressureCoefficient(0.001);
well.setBackpressureExponent(0.85);
well.setWellDepth(3000.0, "m");
well.setTubingDiameter(0.1, "m");
well.setWallRoughness(0.00005, "m");
// Correlation selection
well.setPressureDropCorrelation("beggs-brill");
// Options: "beggs-brill", "hagedorn-brown", "duns-ros", "gray"
well.run();
double bottomholePressure = well.getBottomholePressure("bara");
double pressureDrop = well.getTubingPressureDrop("bar");
well.setReservoirTemperature(120.0, "C");
well.setSurfaceTemperature(30.0, "C");
well.setGeothermalGradient(0.03, "C/m");
well.run();
double wellheadT = well.getWellheadTemperature("C");
import neqsim.process.equipment.valve.ChokeValve;
ChokeValve choke = new ChokeValve("Wellhead Choke", well.getOutletStream());
choke.setOutletPressure(30.0, "bara");
choke.run();
// Or specify bean size
choke.setBeanSize(24, "64ths"); // 24/64" choke
choke.run();
double chokeDP = choke.getPressureDrop("bar");
boolean isCritical = choke.isCriticalFlow();
double criticalRatio = choke.getCriticalPressureRatio();
Find operating point by intersecting IPR and VLP curves.
// IPR curve (reservoir deliverability)
double[] Pwf_ipr = new double[20];
double[] q_ipr = new double[20];
double Pr = 300.0; // Reservoir pressure, bar
double qmax = 50000.0; // Max rate, Sm3/day
for (int i = 0; i < 20; i++) {
Pwf_ipr[i] = Pr * i / 20.0;
// Vogel equation
q_ipr[i] = qmax * (1 - 0.2 * (Pwf_ipr[i]/Pr) - 0.8 * Math.pow(Pwf_ipr[i]/Pr, 2));
}
// VLP curve (tubing performance)
double[] Pwf_vlp = new double[20];
double[] q_vlp = new double[20];
for (int i = 0; i < 20; i++) {
q_vlp[i] = i * 3000.0; // Flow rate
well.setFlowRate(q_vlp[i], "Sm3/day");
well.run();
Pwf_vlp[i] = well.getBottomholePressure("bara");
}
// Find intersection (operating point)
import neqsim.process.equipment.well.GasLiftWell;
GasLiftWell glWell = new GasLiftWell("GL Producer", reservoirFluid);
glWell.setGasLiftRate(1.0, "MMSm3/day");
glWell.setGasLiftDepth(2500.0, "m");
glWell.run();
double liftedRate = glWell.getOilRate("Sm3/day");
import neqsim.process.equipment.well.ESPWell;
ESPWell espWell = new ESPWell("ESP Producer", reservoirFluid);
espWell.setPumpDepth(2800.0, "m");
espWell.setPumpDifferentialPressure(100.0, "bar");
espWell.setPumpEfficiency(0.6);
espWell.run();
double pumpPower = espWell.getPumpPower("kW");
double liftedRate = espWell.getOilRate("Sm3/day");
ProcessSystem process = new ProcessSystem();
// Reservoir fluid
SystemInterface oil = new SystemPrEos(380.0, 280.0);
oil.addComponent("methane", 0.40);
oil.addComponent("ethane", 0.08);
oil.addComponent("propane", 0.06);
oil.addComponent("n-butane", 0.04);
oil.addComponent("n-pentane", 0.03);
oil.addComponent("n-heptane", 0.15);
oil.addTBPfraction("C10+", 0.20, 200.0/1000.0, 0.85);
oil.addComponent("water", 0.04);
oil.setMixingRule("classic");
// Well 1
SimpleWell well1 = new SimpleWell("P-1", oil.clone());
well1.setReservoirPressure(280.0, "bara");
well1.setWellheadPressure(50.0, "bara");
well1.setFlowRate(3000.0, "Sm3/day");
process.add(well1);
// Choke 1
ChokeValve choke1 = new ChokeValve("XV-1", well1.getOutletStream());
choke1.setOutletPressure(30.0, "bara");
process.add(choke1);
// Well 2
SimpleWell well2 = new SimpleWell("P-2", oil.clone());
well2.setReservoirPressure(260.0, "bara");
well2.setWellheadPressure(45.0, "bara");
well2.setFlowRate(2500.0, "Sm3/day");
process.add(well2);
// Choke 2
ChokeValve choke2 = new ChokeValve("XV-2", well2.getOutletStream());
choke2.setOutletPressure(30.0, "bara");
process.add(choke2);
// Manifold
Mixer manifold = new Mixer("Production Manifold");
manifold.addStream(choke1.getOutletStream());
manifold.addStream(choke2.getOutletStream());
process.add(manifold);
// First stage separator
ThreePhaseSeparator hpSep = new ThreePhaseSeparator("HP Separator",
manifold.getOutletStream());
process.add(hpSep);
process.run();
// Results
System.out.println("Total oil rate: " + hpSep.getOilOutStream().getFlowRate("Sm3/day") + " Sm3/day");
System.out.println("Total gas rate: " + hpSep.getGasOutStream().getFlowRate("MSm3/day") + " MSm3/day");
System.out.println("Total water rate: " + hpSep.getWaterOutStream().getFlowRate("m3/day") + " m3/day");
// Gas-oil ratio
double GOR = hpSep.getGasOutStream().getFlowRate("Sm3/day") /
hpSep.getOilOutStream().getFlowRate("Sm3/day");
// Water cut
double waterCut = hpSep.getWaterOutStream().getFlowRate("m3/day") /
(hpSep.getOilOutStream().getFlowRate("m3/day") +
hpSep.getWaterOutStream().getFlowRate("m3/day"));
System.out.println("GOR: " + GOR + " Sm3/Sm3");
System.out.println("Water cut: " + (waterCut * 100) + " %");
This guide covers NeqSim's well simulation capabilities, providing functionality for production system modeling including IPR models, VLP correlations, operating point calculation, lift curve generation, and multi-layer commingled production.
NeqSim provides three main classes for well simulation:
| Class | Purpose | Key Features |
|---|---|---|
WellFlow |
Inflow Performance (IPR) | Vogel, Fetkovich, Backpressure, Table, Multi-layer |
TubingPerformance |
Vertical Lift (VLP) | Beggs-Brill, Hagedorn-Brown, Gray, Hasan-Kabir, Duns-Ros |
WellSystem |
Integrated Well Model | IPR+VLP coupling, Operating point solver, Lift curves |
The WellFlow class models reservoir-to-wellbore inflow using several IPR models.
For single-phase or undersaturated liquid flow:
q = PI × (P_res² - P_wf²)
WellFlow well = new WellFlow("producer");
well.setInletStream(reservoirStream);
well.setWellProductionIndex(1.5e-6); // Sm³/day/bar²
well.setOutletPressure(150.0, "bara");
well.solveFlowFromOutletPressure(true);
well.run();
System.out.println("Flow rate: " + well.getOutletStream().getFlowRate("MSm3/day"));
For solution-gas-drive reservoirs below bubble point:
q/q_max = 1 - 0.2(P_wf/P_res) - 0.8(P_wf/P_res)²
// From well test data: 500 Sm³/day at 120 bara, reservoir at 200 bara
well.setVogelIPR(500.0, 120.0, 200.0);
well.setOutletPressure(100.0, "bara");
well.solveFlowFromOutletPressure(true);
well.run();
Empirical model for gas wells:
q = C × (P_res² - P_wf²)^n
well.setFetkovichIPR(0.012, 0.85); // C and n coefficients
Gas wells with non-Darcy (turbulent) flow:
P_res² - P_wf² = A×q + B×q²
Where A is the Darcy term and B is the non-Darcy (rate-dependent) term.
well.setBackpressureIPR(0.5, 0.001); // A and B coefficients
For measured IPR curves from well tests:
double[] pressures = {50, 80, 100, 120, 150, 180}; // bara
double[] rates = {2.5, 2.0, 1.6, 1.2, 0.7, 0.2}; // MSm³/day
well.setTableIPR(pressures, rates);
Load IPR curves from external files (e.g., from well test analysis software):
// Load IPR curve from CSV file
WellFlow well = new WellFlow("producer");
well.setInletStream(reservoirStream);
well.loadIPRFromFile("path/to/ipr_curve.csv");
well.run();
CSV file format:
Pwf(bara),Rate(MSm3/day)
50,5.2
80,4.1
100,3.2
120,2.4
150,1.5
180,0.8
200,0.2
The TubingPerformance class calculates pressure drop in tubing using multiphase correlations.
| Correlation | Best For | Flow Patterns |
|---|---|---|
| Beggs-Brill | All inclinations | All patterns |
| Hagedorn-Brown | Vertical oil wells | Slug, bubble |
| Gray | Gas wells | Mist, annular |
| Hasan-Kabir | Mechanistic | All patterns |
| Duns-Ros | Gas-liquid | All patterns |
import neqsim.process.equipment.pipeline.TubingPerformance;
import neqsim.thermo.system.SystemSrkEos;
// Create tubing model
TubingPerformance tubing = new TubingPerformance("tubing");
tubing.setInletStream(feedStream);
tubing.setDiameter(0.1); // 100 mm ID
tubing.setLength(3000.0); // 3000 m TVD
tubing.setInclination(90.0); // Vertical
tubing.setRoughness(0.00005); // 50 microns
// Select correlation
tubing.setCorrelationType(TubingPerformance.CorrelationType.BEGGS_BRILL);
// Run calculation
tubing.run();
// Get results
double outletPressure = tubing.getOutletStream().getPressure("bara");
double pressureDrop = tubing.getPressureDrop();
// Beggs-Brill (default, all inclinations)
tubing.setCorrelationType(TubingPerformance.CorrelationType.BEGGS_BRILL);
// Hagedorn-Brown (vertical oil wells)
tubing.setCorrelationType(TubingPerformance.CorrelationType.HAGEDORN_BROWN);
// Gray (gas wells)
tubing.setCorrelationType(TubingPerformance.CorrelationType.GRAY);
// Hasan-Kabir (mechanistic)
tubing.setCorrelationType(TubingPerformance.CorrelationType.HASAN_KABIR);
// Duns-Ros (gas-liquid)
tubing.setCorrelationType(TubingPerformance.CorrelationType.DUNS_ROS);
Use pre-calculated or measured VLP curves instead of correlations:
// Set VLP table programmatically
double[] flowRates = {0.5, 1.0, 2.0, 3.0, 4.0, 5.0}; // MSm³/day
double[] bhpValues = {85, 92, 115, 145, 182, 225}; // bara
double whp = 50.0; // Wellhead pressure (bara)
TubingPerformance tubing = new TubingPerformance("tubing");
tubing.setTableVLP(flowRates, bhpValues, whp);
// Interpolate BHP for a given flow rate
double bhp = tubing.interpolateBHPFromTable(2.5); // MSm³/day
Load VLP curves from external files (e.g., from PROSPER, Pipesim, or other tools):
TubingPerformance tubing = new TubingPerformance("tubing");
tubing.loadVLPFromFile("path/to/vlp_curve.csv", 50.0); // WHP = 50 bara
// Use interpolation
double bhp = tubing.interpolateBHPFromTable(3.0); // Get BHP at 3 MSm³/day
CSV file format:
FlowRate(MSm3/day),BHP(bara)
0.5,85
1.0,92
2.0,115
3.0,145
4.0,182
5.0,225
The WellSystem class finds the intersection of IPR and VLP curves using an optimized
bisection algorithm.
import neqsim.process.equipment.reservoir.WellSystem;
import neqsim.process.equipment.stream.Stream;
// Create reservoir stream
Stream reservoirStream = new Stream("reservoir", reservoirFluid);
reservoirStream.setFlowRate(5000.0, "Sm3/day");
reservoirStream.setTemperature(100.0, "C");
reservoirStream.setPressure(280.0, "bara");
// Create well system with inlet stream
WellSystem well = new WellSystem("production_well", reservoirStream);
// Configure IPR model
well.setIPRModel(WellSystem.IPRModel.PRODUCTION_INDEX);
well.setProductionIndex(2.5e-6, "Sm3/day/bar2");
// Configure tubing (VLP)
well.setWellheadPressure(60.0, "bara");
well.setTubingDiameter(4.0, "in");
well.setTubingLength(3000.0, "m");
well.setInclination(85.0); // degrees from horizontal
// Configure temperature model
well.setBottomHoleTemperature(100.0, "C");
well.setWellheadTemperature(50.0, "C");
// Find operating point
well.run();
// Results
double flowRate = well.getOperatingFlowRate("Sm3/day");
double bhp = well.getBottomHolePressure("bara");
double drawdown = well.getDrawdown("bar");
System.out.println("Operating point: " + flowRate + " Sm³/day at " + bhp + " bara BHP");
System.out.println("Drawdown: " + drawdown + " bar");
| Model | Enum Value | Parameters |
|---|---|---|
| Production Index | PRODUCTION_INDEX |
setProductionIndex(pi, unit) |
| Vogel (1968) | VOGEL |
setVogelParameters(qMax, pwfTest, pRes) |
| Fetkovich (1973) | FETKOVICH |
setFetkovichParameters(C, n, pRes) |
| Backpressure | BACKPRESSURE |
setBackpressureParameters(A, B) |
| Method | Description |
|---|---|
getOperatingFlowRate(unit) |
Flow rate at IPR-VLP intersection |
getBottomHolePressure(unit) |
Bottom-hole pressure at operating point |
getWellheadPressure(unit) |
Wellhead pressure (target constraint) |
getDrawdown(unit) |
Reservoir pressure - BHP |
getOutletStream() |
Output stream for downstream connection |
Generate IPR and VLP curves for nodal analysis.
TubingPerformance tubing = new TubingPerformance("tubing");
tubing.setInletStream(gasStream);
tubing.setDiameter(0.1);
tubing.setLength(3000.0);
tubing.setCorrelationType(TubingPerformance.CorrelationType.BEGGS_BRILL);
// Generate VLP curve
double[] flowRates = {0.1, 0.5, 1.0, 2.0, 3.0, 4.0, 5.0}; // MSm³/day
double[][] vlpCurve = tubing.generateVLPCurve(flowRates);
// vlpCurve[0] = flow rates
// vlpCurve[1] = required bottom-hole pressures
for (int i = 0; i < vlpCurve[0].length; i++) {
System.out.printf("Flow: %.2f MSm³/day, BHP: %.1f bara%n",
vlpCurve[0][i], vlpCurve[1][i]);
}
WellSystem well = new WellSystem("producer");
well.setReservoirPressure(280.0, "bara");
well.setProductivityIndex(2.5e-6);
well.setIprModel(WellSystem.IPRModel.PRODUCTION_INDEX);
// Generate IPR curve (flowing BHP vs flow rate)
double minPwf = 50.0;
double maxPwf = 270.0;
int points = 20;
double[][] iprCurve = well.generateIPRCurve(minPwf, maxPwf, points);
// iprCurve[0] = flowing BHP values
// iprCurve[1] = corresponding flow rates
WellSystem well = new WellSystem("nodal_analysis");
// ... configure well ...
// Get both curves
double[][] iprCurve = well.generateIPRCurve(50, 270, 20);
double[][] vlpCurve = well.generateVLPCurve(new double[]{0.5, 1.0, 2.0, 3.0, 4.0, 5.0});
// Operating point
well.run();
double opFlow = well.getOperatingFlowRate("MSm3/day");
double opBHP = well.getOperatingBHP("bara");
// Export to CSV or plot
System.out.println("IPR Curve:");
for (int i = 0; i < iprCurve[0].length; i++) {
System.out.printf("%.1f, %.3f%n", iprCurve[0][i], iprCurve[1][i]);
}
System.out.println("\nVLP Curve:");
for (int i = 0; i < vlpCurve[0].length; i++) {
System.out.printf("%.3f, %.1f%n", vlpCurve[0][i], vlpCurve[1][i]);
}
System.out.printf("\nOperating Point: %.3f MSm³/day at %.1f bara%n", opFlow, opBHP);
Model wells producing from multiple reservoir layers.
// Create fluid streams for each layer
SystemInterface layer1Fluid = new SystemSrkEos(80, 200);
layer1Fluid.addComponent("methane", 0.90);
layer1Fluid.addComponent("ethane", 0.07);
layer1Fluid.addComponent("propane", 0.03);
layer1Fluid.setMixingRule("classic");
Stream layer1Stream = new Stream("layer1", layer1Fluid);
layer1Stream.run();
SystemInterface layer2Fluid = new SystemSrkEos(95, 220);
layer2Fluid.addComponent("methane", 0.85);
layer2Fluid.addComponent("ethane", 0.10);
layer2Fluid.addComponent("propane", 0.05);
layer2Fluid.setMixingRule("classic");
Stream layer2Stream = new Stream("layer2", layer2Fluid);
layer2Stream.run();
// Create multi-layer well
WellFlow well = new WellFlow("commingled_well");
well.addLayer("Upper Sand", layer1Stream, 200.0, 1.0e-6); // 200 bara, PI
well.addLayer("Lower Sand", layer2Stream, 220.0, 1.5e-6); // 220 bara, PI
well.setOutletPressure(150.0, "bara"); // Common BHP
well.run();
// Get individual layer contributions
double[] layerRates = well.getLayerFlowRates("MSm3/day");
System.out.println("Layer 1 flow: " + layerRates[0] + " MSm³/day");
System.out.println("Layer 2 flow: " + layerRates[1] + " MSm³/day");
System.out.println("Total flow: " + well.getOutletStream().getFlowRate("MSm3/day"));
WellSystem well = new WellSystem("multi_zone_producer");
well.setWellheadPressure(50.0, "bara");
well.setTubingDiameter(0.1);
well.setTubingLength(3500.0);
// Add layers with different properties
well.addLayer("Zone A", streamA, 250.0, 1.2e-6);
well.addLayer("Zone B", streamB, 280.0, 0.8e-6);
well.addLayer("Zone C", streamC, 265.0, 1.5e-6);
// Find operating point for commingled production
well.run();
double totalFlow = well.getOperatingFlowRate("MSm3/day");
double bhp = well.getOperatingBHP("bara");
double[] zoneFlows = well.getLayerFlowRates("MSm3/day");
Configure wellbore temperature profile for accurate property calculations.
| Model | Description | Use Case |
|---|---|---|
| ISOTHERMAL | Constant temperature | Quick estimates |
| LINEAR_GRADIENT | Linear geothermal | Simple wells |
| RAMEY | Ramey (1962) steady-state | Established production |
| HASAN_KABIR | Energy balance | Transient, accurate |
// Isothermal (default)
tubing.setTemperatureModel(TubingPerformance.TemperatureModel.ISOTHERMAL);
// Linear gradient (specify surface and BH temperatures)
tubing.setTemperatureModel(TubingPerformance.TemperatureModel.LINEAR_GRADIENT);
tubing.setSurfaceTemperature(25.0); // °C
tubing.setBottomholeTemperature(90.0);
// Ramey model (needs formation properties)
tubing.setTemperatureModel(TubingPerformance.TemperatureModel.RAMEY);
tubing.setFormationThermalConductivity(2.5); // W/m·K
tubing.setOverallHeatTransferCoefficient(25.0);
// Hasan-Kabir energy balance
tubing.setTemperatureModel(TubingPerformance.TemperatureModel.HASAN_KABIR);
The Ramey (1962) model accounts for:
tubing.setTemperatureModel(TubingPerformance.TemperatureModel.RAMEY);
tubing.setGeothermalGradient(0.03); // °C/m
tubing.setSurfaceTemperature(15.0); // °C
tubing.setFormationThermalConductivity(2.5);
tubing.setOverallHeatTransferCoefficient(20.0);
tubing.setProductionTime(365.0); // days
import neqsim.process.processmodel.ProcessSystem;
import neqsim.process.equipment.stream.Stream;
import neqsim.process.equipment.reservoir.*;
import neqsim.process.equipment.pipeline.TubingPerformance;
import neqsim.process.equipment.separator.Separator;
import neqsim.process.equipment.compressor.Compressor;
// 1. Reservoir and Well
SimpleReservoir reservoir = new SimpleReservoir("reservoir");
reservoir.setReservoirFluid(reservoirFluid);
reservoir.setReservoirPressure(280.0, "bara");
reservoir.setTemperature(85.0, "C");
WellFlow inflow = new WellFlow("IPR");
inflow.setInletStream(reservoir.getOutletStream());
inflow.setWellProductionIndex(2.0e-6);
// 2. Tubing
TubingPerformance tubing = new TubingPerformance("tubing");
tubing.setInletStream(inflow.getOutletStream());
tubing.setDiameter(0.1);
tubing.setLength(3000.0);
tubing.setCorrelationType(TubingPerformance.CorrelationType.BEGGS_BRILL);
// 3. Surface Facilities
Separator separator = new Separator("HP_sep");
separator.setInletStream(tubing.getOutletStream());
Compressor compressor = new Compressor("export_comp");
compressor.setInletStream(separator.getGasOutStream());
compressor.setOutletPressure(150.0, "bara");
// 4. Build Process System
ProcessSystem plant = new ProcessSystem();
plant.add(reservoir);
plant.add(inflow);
plant.add(tubing);
plant.add(separator);
plant.add(compressor);
// 5. Run
plant.run();
// 6. Results
System.out.println("Wellhead pressure: " + tubing.getOutletStream().getPressure("bara"));
System.out.println("Gas export rate: " + compressor.getOutletStream().getFlowRate("MSm3/day"));
// Multiple wells feeding a gathering network
WellFlowlineNetwork network = new WellFlowlineNetwork("field_network");
// Add wells
network.addWell(well1);
network.addWell(well2);
network.addWell(well3);
// Set manifold back-pressure
network.setManifoldPressure(40.0, "bara");
// Solve network
network.run();
// Get individual well rates
for (WellFlow well : network.getWells()) {
System.out.println(well.getName() + ": " +
well.getOutletStream().getFlowRate("MSm3/day") + " MSm³/day");
}
// Rich gas well with Vogel IPR and Beggs-Brill VLP
SystemInterface gas = new SystemSrkEos(85, 250);
gas.addComponent("nitrogen", 0.02);
gas.addComponent("CO2", 0.03);
gas.addComponent("methane", 0.80);
gas.addComponent("ethane", 0.08);
gas.addComponent("propane", 0.04);
gas.addComponent("n-butane", 0.02);
gas.addComponent("n-pentane", 0.01);
gas.setMixingRule("classic");
gas.init(0);
Stream reservoir = new Stream("reservoir", gas);
reservoir.setFlowRate(3.0, "MSm3/day");
reservoir.run();
// Well system
WellSystem well = new WellSystem("gas_producer");
well.setInletStream(reservoir);
well.setReservoirPressure(250.0, "bara");
well.setProductivityIndex(3.0e-6);
well.setWellheadPressure(50.0, "bara");
well.setTubingDiameter(0.088); // 3.5" tubing
well.setTubingLength(2800.0);
well.setVlpCorrelation(WellSystem.VLPCorrelation.GRAY);
well.run();
System.out.println("=== Gas Well Operating Point ===");
System.out.println("Flow rate: " + well.getOperatingFlowRate("MSm3/day") + " MSm³/day");
System.out.println("BHP: " + well.getOperatingBHP("bara") + " bara");
System.out.println("Drawdown: " + well.getDrawdown() + " bar");
// Compare production with different wellhead pressures (simulating lift)
WellSystem well = new WellSystem("oil_producer");
well.setReservoirPressure(180.0, "bara");
well.setProductivityIndex(5.0e-6);
well.setTubingDiameter(0.076); // 3" tubing
well.setTubingLength(2000.0);
well.setVlpCorrelation(WellSystem.VLPCorrelation.HAGEDORN_BROWN);
System.out.println("Wellhead Pressure | Flow Rate | BHP");
for (double whp = 10; whp <= 50; whp += 10) {
well.setWellheadPressure(whp, "bara");
well.run();
System.out.printf("%17.0f | %9.2f | %6.1f%n",
whp, well.getOperatingFlowRate("Sm3/day"), well.getOperatingBHP("bara"));
}
// Three-zone commingled gas well
WellSystem well = new WellSystem("multizone_gas");
well.setWellheadPressure(45.0, "bara");
well.setTubingDiameter(0.1);
well.setTubingLength(3200.0);
// Zone A: Shallow gas, high perm
well.addLayer("Zone_A", shallowStream, 180.0, 4.0e-6);
// Zone B: Middle, moderate perm
well.addLayer("Zone_B", middleStream, 220.0, 2.0e-6);
// Zone C: Deep, low perm but high pressure
well.addLayer("Zone_C", deepStream, 280.0, 0.8e-6);
well.run();
System.out.println("=== Multi-Zone Production ===");
System.out.println("Total rate: " + well.getOperatingFlowRate("MSm3/day") + " MSm³/day");
double[] zoneFlows = well.getLayerFlowRates("MSm3/day");
System.out.println("Zone A: " + zoneFlows[0] + " MSm³/day");
System.out.println("Zone B: " + zoneFlows[1] + " MSm³/day");
System.out.println("Zone C: " + zoneFlows[2] + " MSm³/day");
| Method | Description |
|---|---|
setWellProductionIndex(pi) |
Set productivity index |
setVogelIPR(qTest, pwfTest, pRes) |
Configure Vogel IPR |
setFetkovichIPR(c, n) |
Configure Fetkovich IPR |
setBackpressureIPR(a, b) |
Configure Backpressure IPR |
setTableIPR(pwf[], rate[]) |
Set table-driven IPR |
addLayer(name, stream, pRes, pi) |
Add reservoir layer |
getLayerFlowRates(unit) |
Get layer flow contributions |
| Method | Description |
|---|---|
setDiameter(d) |
Set tubing ID (meters) |
setLength(L) |
Set tubing length (meters) |
setInclination(angle) |
Set inclination (degrees from horizontal) |
setRoughness(eps) |
Set pipe roughness (meters) |
setCorrelationType(type) |
Select VLP correlation |
setTemperatureModel(model) |
Select temperature model |
generateVLPCurve(rates) |
Generate lift curve |
getPressureDrop() |
Get calculated pressure drop |
| Method | Description |
|---|---|
setReservoirPressure(p, unit) |
Set reservoir pressure |
setProductivityIndex(pi) |
Set well PI |
setWellheadPressure(p, unit) |
Set tubing outlet pressure |
setIprModel(model) |
Select IPR model |
setVlpCorrelation(corr) |
Select VLP correlation |
getOperatingFlowRate(unit) |
Get operating flow rate |
getOperatingBHP(unit) |
Get operating BHP |
generateIPRCurve(min, max, n) |
Generate IPR curve |
generateVLPCurve(rates) |
Generate VLP curve |
For a comprehensive example demonstrating the full integration of well simulation with downstream processing, see WellToOilStabilizationExample.java.
This example includes:
The WellSystem class integrates seamlessly with ProcessSystem for building complete
production flowsheets. It uses an optimized IPR+VLP solver with:
getOutletStream()// Create reservoir and reservoir stream
SimpleReservoir reservoir = new SimpleReservoir("Main Reservoir");
reservoir.setReservoirFluid(reservoirFluid, 1e6, 10.0, 10.0);
Stream reservoirStream = new Stream("Reservoir Stream", reservoir.getReservoirFluid());
reservoirStream.setFlowRate(5000.0, "Sm3/day");
reservoirStream.setTemperature(100.0, "C");
reservoirStream.setPressure(250.0, "bara");
// Create WellSystem with integrated IPR and VLP
WellSystem well = new WellSystem("Producer-1", reservoirStream);
// Configure IPR model (Vogel for solution gas drive)
well.setIPRModel(WellSystem.IPRModel.VOGEL);
well.setVogelParameters(8000.0, 180.0, 250.0); // qMax, testPwf, Pr
// Configure VLP (tubing)
well.setTubingLength(2500.0, "m");
well.setTubingDiameter(4.0, "in");
well.setWellheadPressure(80.0, "bara");
well.setBottomHoleTemperature(100.0, "C");
well.setWellheadTemperature(65.0, "C");
// Connect downstream equipment
PipeBeggsAndBrills flowline = new PipeBeggsAndBrills("Flowline");
flowline.setInletStream(well.getOutletStream());
flowline.setLength(5000.0);
flowline.setDiameter(0.2);
// Build complete ProcessSystem
ProcessSystem process = new ProcessSystem();
process.add(well); // WellSystem as first equipment
process.add(flowline);
process.add(inletChoke);
process.add(hpSeparator);
// ... add remaining equipment
// Run complete simulation
process.run();
// Access well operating point results
double opRate = well.getOperatingFlowRate("Sm3/day");
double bhp = well.getBottomHolePressure("bara");
double drawdown = well.getDrawdown("bar");
The WellSystem solver is optimized for speed:
| Feature | Description |
|---|---|
| Simplified VLP | Uses hydrostatic + friction correlation instead of full TubingPerformance iteration |
| Bisection solver | Robust convergence in ~15-20 iterations |
| Single flash per iteration | Minimizes thermodynamic calculations |
| Typical solve time | < 1 second for complex fluids |
For detailed VLP calculations with full correlation support, use TubingPerformance directly.
WellSystem supports multiple VLP solver modes for different accuracy/speed tradeoffs:
import neqsim.process.equipment.reservoir.WellSystem.VLPSolverMode;
// Default: Fast simplified solver (hydrostatic + friction)
well.setVLPSolverMode(VLPSolverMode.SIMPLIFIED);
// Traditional correlations (via TubingPerformance)
well.setVLPSolverMode(VLPSolverMode.BEGGS_BRILL);
well.setVLPSolverMode(VLPSolverMode.HAGEDORN_BROWN);
well.setVLPSolverMode(VLPSolverMode.GRAY);
well.setVLPSolverMode(VLPSolverMode.HASAN_KABIR);
well.setVLPSolverMode(VLPSolverMode.DUNS_ROS);
// Advanced multiphase models
well.setVLPSolverMode(VLPSolverMode.DRIFT_FLUX); // Drift-flux with slip
well.setVLPSolverMode(VLPSolverMode.TWO_FLUID); // Separate momentum equations
| VLP Solver Mode | Description | Speed | Accuracy |
|---|---|---|---|
| SIMPLIFIED | Hydrostatic + friction correlation | Fastest | Approximate |
| BEGGS_BRILL | Beggs & Brill empirical correlation | Medium | Good for general use |
| HAGEDORN_BROWN | Hagedorn-Brown for vertical wells | Medium | Good for oil wells |
| GRAY | Gray correlation for gas wells | Medium | Good for gas wells |
| HASAN_KABIR | Mechanistic model | Slow | High accuracy |
| DUNS_ROS | Duns & Ros correlation | Medium | Good for gas-liquid |
| DRIFT_FLUX | Accounts for phase slip velocity | Medium | Better for high GOR |
| TWO_FLUID | Separate gas/liquid momentum | Slowest | Highest accuracy |
NeqSim combines well inflow performance relationships with hydraulic flowline models and production chokes to represent surface networks. A WellFlowlineNetwork assembles wells, optional chokes, and pipelines into branches that are gathered in manifolds for steady-state or transient calculations.
WellFlow supports several inflow performance relationships that can either solve for outlet pressure from a specified flow or compute flow from a specified outlet pressure:
All models can switch between computing outlet pressure or flow via solveFlowFromOutletPressure(boolean), enabling backpressure solves from downstream network pressure when desired.
Production chokes are modeled as ThrottlingValve instances using IEC 60534 sizing. Chokes can be attached per branch and run in steady-state or transient mode. Valve travel and characterization are captured through the underlying valve model, and choking conditions can be toggled and tuned at the valve level.
WellFlowlineNetwork wires wells and optional chokes into PipeBeggsAndBrills flowlines, collects them in manifolds, and optionally sends the combined stream downstream. The network offers steady-state and transient execution modes, supports target endpoint pressure solving, and can propagate arrival pressures back to well outlets for iterative backpressure calculations.
Documentation for pipeline equipment in NeqSim.
Location: neqsim.process.equipment.pipeline
Classes:
| Class | Description |
|---|---|
PipeBeggsAndBrills |
Beggs-Brill correlation |
AdiabaticPipe |
Adiabatic pipe segment |
OnePhasePipe |
Single-phase pipe |
TwoPhasePipeLine |
Two-phase pipeline |
For detailed pipe flow modeling, see also Fluid Mechanics.
import neqsim.process.equipment.pipeline.PipeBeggsAndBrills;
PipeBeggsAndBrills pipe = new PipeBeggsAndBrills("Pipe", inletStream);
pipe.setLength(1000.0, "m");
pipe.setDiameter(0.3, "m");
pipe.setAngle(0.0); // Horizontal
pipe.run();
double pressureDrop = pipe.getPressureDrop("bara");
Stream outlet = pipe.getOutletStream();
// Length
pipe.setLength(5000.0, "m");
// Inner diameter
pipe.setDiameter(0.254, "m"); // 10 inch
pipe.setInnerDiameter(0.254, "m");
// Wall roughness
pipe.setRoughness(0.0001, "m");
// Elevation change
pipe.setElevationChange(100.0, "m"); // 100m rise
pipe.setAngle(5.7); // degrees from horizontal
For longer pipelines with multiple segments.
import neqsim.process.equipment.pipeline.TwoPhasePipeLine;
TwoPhasePipeLine pipeline = new TwoPhasePipeLine("Export Pipeline", inletStream);
pipeline.setLength(50000.0, "m");
pipeline.setDiameter(0.4, "m");
pipeline.setNumberOfNodes(100);
pipeline.run();
// Get profile
double[] pressure = pipeline.getPressureProfile();
double[] temperature = pipeline.getTemperatureProfile();
double[] holdup = pipeline.getLiquidHoldupProfile();
| Method | Application |
|---|---|
| Beggs-Brill | Two-phase flow |
| Moody | Single-phase turbulent |
| Colebrook | Single-phase implicit |
PipeBeggsAndBrills pipe = new PipeBeggsAndBrills("Pipe", stream);
pipe.setLength(1000.0, "m");
pipe.setDiameter(0.3, "m");
pipe.run();
// Flow regime
String regime = pipe.getFlowRegime(); // Segregated, Intermittent, Distributed
// Liquid holdup
double holdup = pipe.getLiquidHoldup();
$$\Delta P_{total} = \Delta P_{friction} + \Delta P_{elevation} + \Delta P_{acceleration}$$
import neqsim.process.equipment.pipeline.AdiabaticPipe;
AdiabaticPipe pipe = new AdiabaticPipe("Adiabatic Pipe", stream);
pipe.setLength(500.0, "m");
pipe.setDiameter(0.2, "m");
pipe.run();
// Temperature remains constant (no heat loss)
double Tin = pipe.getInletStream().getTemperature("C");
double Tout = pipe.getOutletStream().getTemperature("C");
PipeBeggsAndBrills pipe = new PipeBeggsAndBrills("Pipe", stream);
pipe.setLength(1000.0, "m");
pipe.setDiameter(0.3, "m");
// Heat transfer coefficient
pipe.setOverallHeatTransferCoefficient(25.0, "W/m2K");
// Ambient temperature
pipe.setAmbientTemperature(5.0, "C");
pipe.run();
double heatLoss = pipe.getHeatLoss("kW");
import neqsim.thermo.system.SystemSrkEos;
import neqsim.process.equipment.stream.Stream;
import neqsim.process.equipment.pipeline.PipeBeggsAndBrills;
// Natural gas
SystemSrkEos gas = new SystemSrkEos(303.15, 80.0);
gas.addComponent("methane", 0.90);
gas.addComponent("ethane", 0.07);
gas.addComponent("propane", 0.03);
gas.setMixingRule("classic");
Stream inlet = new Stream("Inlet", gas);
inlet.setFlowRate(1000000.0, "Sm3/day");
inlet.run();
// Pipe
PipeBeggsAndBrills pipe = new PipeBeggsAndBrills("Pipe", inlet);
pipe.setLength(5000.0, "m");
pipe.setDiameter(0.254, "m"); // 10 inch
pipe.setRoughness(0.00005, "m");
pipe.run();
System.out.println("Inlet P: " + inlet.getPressure("bara") + " bara");
System.out.println("Outlet P: " + pipe.getOutletStream().getPressure("bara") + " bara");
System.out.println("ΔP: " + pipe.getPressureDrop("bara") + " bara");
System.out.println("Velocity: " + pipe.getVelocity("m/s") + " m/s");
// Wellstream (gas + condensate)
SystemSrkEos wellstream = new SystemSrkEos(350.0, 100.0);
wellstream.addComponent("methane", 0.70);
wellstream.addComponent("ethane", 0.10);
wellstream.addComponent("propane", 0.08);
wellstream.addComponent("n-pentane", 0.05);
wellstream.addComponent("n-heptane", 0.05);
wellstream.addComponent("n-decane", 0.02);
wellstream.setMixingRule("classic");
Stream inlet = new Stream("Wellstream", wellstream);
inlet.setFlowRate(50000.0, "kg/hr");
inlet.run();
// Two-phase pipeline
TwoPhasePipeLine pipeline = new TwoPhasePipeLine("Flowline", inlet);
pipeline.setLength(10000.0, "m");
pipeline.setDiameter(0.2, "m");
pipeline.setNumberOfNodes(50);
pipeline.run();
System.out.println("Inlet: " + inlet.getPressure("bara") + " bara, " +
inlet.getTemperature("C") + " °C");
System.out.println("Outlet: " + pipeline.getOutletStream().getPressure("bara") + " bara, " +
pipeline.getOutletStream().getTemperature("C") + " °C");
// Subsea conditions
SystemSrkEos gas = new SystemSrkEos(350.0, 150.0);
gas.addComponent("methane", 0.92);
gas.addComponent("ethane", 0.05);
gas.addComponent("CO2", 0.02);
gas.addComponent("water", 0.01);
gas.setMixingRule("classic");
Stream inlet = new Stream("Wellhead", gas);
inlet.setFlowRate(5000000.0, "Sm3/day");
inlet.run();
// Subsea pipeline
PipeBeggsAndBrills pipeline = new PipeBeggsAndBrills("Subsea", inlet);
pipeline.setLength(50000.0, "m");
pipeline.setDiameter(0.4, "m");
pipeline.setOverallHeatTransferCoefficient(15.0, "W/m2K");
pipeline.setAmbientTemperature(4.0, "C"); // Seabed temperature
pipeline.run();
System.out.println("Outlet temperature: " + pipeline.getOutletStream().getTemperature("C") + " °C");
System.out.println("Heat loss: " + pipeline.getHeatLoss("MW") + " MW");
// Check hydrate temperature
double hdtTemp = pipeline.getOutletStream().getFluid().getHydrateTemperature();
System.out.println("Hydrate temperature: " + (hdtTemp - 273.15) + " °C");
// Gas lift production
Stream production = new Stream("Production", wellFluid);
production.setFlowRate(10000.0, "kg/hr");
production.run();
// Riser (500m water depth)
PipeBeggsAndBrills riser = new PipeBeggsAndBrills("Riser", production);
riser.setLength(550.0, "m"); // Account for catenary
riser.setDiameter(0.2, "m");
riser.setElevationChange(500.0, "m"); // Rise from seabed
riser.run();
System.out.println("Bottom P: " + production.getPressure("bara") + " bara");
System.out.println("Top P: " + riser.getOutletStream().getPressure("bara") + " bara");
System.out.println("Flow regime: " + riser.getFlowRegime());
The PipeBeggsAndBrills class implements the Beggs and Brill (1973) empirical correlations for pressure drop and liquid holdup prediction in multiphase pipeline flow. It supports single-phase (gas or liquid) and multiphase (gas-liquid, gas-oil-water) flow in horizontal, inclined, and vertical pipes.
Beggs, H.D. and Brill, J.P., "A Study of Two-Phase Flow in Inclined Pipes", Journal of Petroleum Technology, May 1973, pp. 607-617. SPE-4007-PA
The pipeline supports two primary calculation modes:
| Mode | Description | Use Case |
|---|---|---|
CALCULATE_OUTLET_PRESSURE |
Given inlet conditions and flow rate, calculate outlet pressure | Production pipelines with known flow |
CALCULATE_FLOW_RATE |
Given inlet and outlet pressures, calculate flow rate | Pipeline capacity analysis |
// Default: Calculate outlet pressure from known flow
pipe.setCalculationMode(CalculationMode.CALCULATE_OUTLET_PRESSURE);
// Alternative: Calculate flow from known pressures
pipe.setCalculationMode(CalculationMode.CALCULATE_FLOW_RATE);
pipe.setSpecifiedOutletPressure(40.0, "bara");
pipe.setMaxFlowIterations(50);
pipe.setFlowConvergenceTolerance(1e-4);
The Beggs and Brill correlation classifies two-phase flow into four regimes based on dimensionless parameters.
| Parameter | Symbol | Formula |
|---|---|---|
| Superficial liquid velocity | v_SL | Q_L / A |
| Superficial gas velocity | v_SG | Q_G / A |
| Mixture velocity | v_m | v_SL + v_SG |
| Input liquid fraction | λ_L | v_SL / v_m |
| Froude number | Fr | v_m² / (g × D) |
The flow regime is determined by comparing the Froude number with boundary correlations L1-L4:
L1 = 316 × λL^0.302
L2 = 0.0009252 × λL^(-2.4684)
L3 = 0.1 × λL^(-1.4516)
L4 = 0.5 × λL^(-6.738)
| Regime | Conditions | Description |
|---|---|---|
| SEGREGATED | λL < 0.01 and Fr < L1, or λL ≥ 0.01 and Fr < L2 | Stratified, wavy, or annular flow |
| INTERMITTENT | λL < 0.4 and L3 < Fr ≤ L1, or λL ≥ 0.4 and L3 < Fr ≤ L4 | Slug or plug flow |
| DISTRIBUTED | λL < 0.4 and Fr ≥ L4, or λL ≥ 0.4 and Fr > L4 | Bubble or mist flow |
| TRANSITION | L2 < Fr < L3 | Interpolation zone |
| SINGLE_PHASE | Only gas or only liquid | No two-phase effects |
Fr (Froude Number)
↑
1000 ─────┼─────────────────────────
│ DISTRIBUTED
│
100 ─────┼───────────────┬─────────
│ INTERMITTENT │
10 ─────┼───────────────┤
│ TRANSITION │
1 ─────┼───────────────┴─────────
│ SEGREGATED
0.1 ─────┼─────────────────────────
└────┬────┬────┬────┬────→ λL
0.01 0.1 0.4 1.0
Total pressure drop consists of three components:
ΔP_total = ΔP_friction + ΔP_hydrostatic + ΔP_acceleration
ΔP_hydrostatic = ρ_m × g × Δh
where:
ρ_m = ρ_L × E_L + ρ_G × (1 - E_L) [mixture density]
E_L = liquid holdup (volume fraction)
Δh = elevation change
| Flow Regime | Horizontal Holdup (E_L0) |
|---|---|
| Segregated | E_L0 = 0.98 × λL^0.4846 / Fr^0.0868 |
| Intermittent | E_L0 = 0.845 × λL^0.5351 / Fr^0.0173 |
| Distributed | E_L0 = 1.065 × λL^0.5824 / Fr^0.0609 |
| Transition | Weighted average of Segregated and Intermittent |
Inclination Correction:
E_L = E_L0 × B_θ
B_θ = 1 + β × [sin(1.8θ) - (1/3)sin³(1.8θ)]
where β depends on flow regime and inclination direction (uphill vs downhill).
ΔP_friction = f_tp × (L/D) × (ρ_ns × v_m²) / 2
where:
f_tp = f × exp(S) [two-phase friction factor]
ρ_ns = no-slip density
S = slip correction factor
Friction Factor Correlations:
| Flow Regime | Friction Factor |
|---|---|
| Laminar (Re < 2300) | f = 64/Re |
| Transition (2300-4000) | Linear interpolation |
| Turbulent (Re > 4000) | Haaland: f = [1/(-1.8 log((ε/D/3.7)^1.11 + 6.9/Re))]² |
Slip Correction Factor (S):
y = λL / E_L²
For 1 < y < 1.2: S = ln(2.2y - 1.2)
Otherwise: S = ln(y) / [-0.0523 + 3.18ln(y) - 0.872ln²(y) + 0.01853ln⁴(y)]
| Mode | Description | When to Use |
|---|---|---|
ADIABATIC |
No heat transfer (Q=0) | Well-insulated pipelines, short pipes |
ISOTHERMAL |
Constant temperature | Slow flow, thermal equilibrium |
SPECIFIED_U |
User-specified U-value | Known overall heat transfer coefficient |
ESTIMATED_INNER_H |
Calculate inner h from flow | Quick estimate, inner resistance dominant |
DETAILED_U |
Full thermal resistance calculation | Subsea pipelines, insulated pipes |
Heat transfer uses the analytical NTU (Number of Transfer Units) method:
NTU = U × A / (ṁ × Cp)
T_out = T_wall + (T_in - T_wall) × exp(-NTU)
This provides an exact solution for constant wall temperature boundary conditions.
| Flow Regime | Nusselt Number |
|---|---|
| Laminar (Re < 2300) | Nu = 3.66 |
| Transition (2300-3000) | Linear interpolation |
| Turbulent (Re > 3000) | Gnielinski: Nu = (f/8)(Re-1000)Pr / [1 + 12.7(f/8)^0.5(Pr^(2/3)-1)] |
| Two-phase | Shah/Martinelli enhancement applied |
h_inner = Nu × k / D
The overall heat transfer coefficient includes thermal resistances in series:
1/U = 1/h_inner + R_wall + R_insulation + 1/h_outer
where:
R_wall = r_i × ln(r_o/r_i) / k_wall [pipe wall]
R_insulation = r_i × ln(r_ins/r_o) / k_ins [insulation layer]
Example calculation for 6" pipe with 50mm insulation:
Given:
D_inner = 0.1524 m (6 inch)
Wall thickness = 10 mm
Insulation thickness = 50 mm
k_steel = 45 W/(m·K)
k_insulation = 0.04 W/(m·K)
h_inner = 500 W/(m²·K)
h_outer = 500 W/(m²·K) (seawater)
Calculate:
r_i = 0.0762 m
r_o = 0.0862 m
r_ins = 0.1362 m
1/h_inner = 0.002 m²K/W
R_wall = 0.0762 × ln(0.0862/0.0762) / 45 = 0.0002 m²K/W
R_ins = 0.0762 × ln(0.1362/0.0862) / 0.04 = 0.87 m²K/W
1/h_outer = 0.002 m²K/W
1/U = 0.002 + 0.0002 + 0.87 + 0.002 = 0.874 m²K/W
U = 1.14 W/(m²·K)
The energy balance can include three optional components:
Heat exchange with surroundings using the NTU-effectiveness method:
Q_wall = ṁ × Cp × (T_out - T_in)
Temperature change due to gas expansion:
ΔT_JT = -μ_JT × ΔP
where μ_JT is the Joule-Thomson coefficient
Typical Joule-Thomson Coefficients:
| Fluid | μ_JT [K/bar] | μ_JT [K/Pa] |
|---|---|---|
| Methane | ~0.4 | ~4×10⁻⁶ |
| Natural gas | 0.3-0.5 | 3-5×10⁻⁶ |
| CO₂ | ~1.0 | ~10⁻⁵ |
| Nitrogen | ~0.25 | ~2.5×10⁻⁶ |
Viscous dissipation adds energy to the fluid:
Q_friction = ΔP_friction × V̇
where V̇ is the volumetric flow rate
Note: Only the friction component of pressure drop is used (not hydrostatic), as hydrostatic pressure change is reversible work.
// Enable Joule-Thomson effect
pipe.setIncludeJouleThomsonEffect(true);
// Enable friction heating
pipe.setIncludeFrictionHeating(true);
The class supports time-dependent simulation using the runTransient() method.
The transient solver uses explicit finite difference for:
Mass conservation:
∂ρ/∂t + ∂(ρv)/∂x = 0
Momentum conservation:
∂(ρv)/∂t + ∂(ρv²)/∂x = -∂P/∂x - τ_wall - ρg sin(θ)
Energy conservation:
∂(ρe)/∂t + ∂(ρvh)/∂x = Q_wall + Q_friction
// Initialize transient simulation
pipe.initTransientSimulation();
// Run time steps
double dt = 1.0; // seconds
for (int step = 0; step < 1000; step++) {
pipe.runTransient(dt);
// Access profiles
double outletT = pipe.getTransientTemperatureProfile().get(
pipe.getTransientTemperatureProfile().size() - 1);
}
For numerical stability, the time step must satisfy:
Δt ≤ Δx / (v + c)
where:
Δx = segment length
v = flow velocity
c = speed of sound
import neqsim.process.equipment.pipeline.PipeBeggsAndBrills;
import neqsim.process.equipment.stream.Stream;
import neqsim.thermo.system.SystemSrkEos;
// Create fluid system
SystemInterface fluid = new SystemSrkEos(303.15, 50.0);
fluid.addComponent("methane", 0.85);
fluid.addComponent("ethane", 0.10);
fluid.addComponent("propane", 0.05);
fluid.setMixingRule("classic");
// Create inlet stream
Stream inlet = new Stream("inlet", fluid);
inlet.setFlowRate(50000, "kg/hr");
inlet.run();
// Create pipeline
PipeBeggsAndBrills pipe = new PipeBeggsAndBrills("pipeline", inlet);
pipe.setDiameter(0.2032); // 8 inch
pipe.setLength(10000.0); // 10 km
pipe.setElevation(0.0); // horizontal
pipe.setNumberOfIncrements(20);
pipe.setPipeWallRoughness(4.5e-5); // Commercial steel
pipe.run();
// Results
System.out.println("Inlet pressure: " + inlet.getPressure("bara") + " bara");
System.out.println("Outlet pressure: " + pipe.getOutletStream().getPressure("bara") + " bara");
System.out.println("Pressure drop: " + pipe.getPressureDrop() + " bar");
System.out.println("Flow regime: " + pipe.getFlowRegime());
System.out.println("Liquid holdup: " + pipe.getLiquidHoldup());
// Hot production fluid in cold seawater
PipeBeggsAndBrills subseaPipe = new PipeBeggsAndBrills("subsea", hotStream);
subseaPipe.setDiameter(0.1524); // 6 inch
subseaPipe.setLength(15000.0); // 15 km
subseaPipe.setElevation(0.0); // horizontal
// Heat transfer settings
subseaPipe.setConstantSurfaceTemperature(4.0, "C"); // Deep sea temperature
subseaPipe.setHeatTransferCoefficient(15.0); // W/(m²·K) with insulation
subseaPipe.run();
System.out.println("Inlet temperature: " + hotStream.getTemperature("C") + " °C");
System.out.println("Outlet temperature: " + subseaPipe.getOutletStream().getTemperature("C") + " °C");
PipeBeggsAndBrills insulatedPipe = new PipeBeggsAndBrills("insulated", feedStream);
insulatedPipe.setDiameter(0.2032); // 8 inch
insulatedPipe.setThickness(0.0127); // 0.5 inch wall
insulatedPipe.setLength(20000.0); // 20 km
// Detailed thermal model
insulatedPipe.setConstantSurfaceTemperature(5.0, "C");
insulatedPipe.setOuterHeatTransferCoefficient(500.0); // Seawater
insulatedPipe.setPipeWallThermalConductivity(45.0); // Carbon steel
insulatedPipe.setInsulation(0.075, 0.04); // 75mm PU foam
insulatedPipe.setHeatTransferMode(HeatTransferMode.DETAILED_U);
insulatedPipe.run();
// Production riser from seabed to platform
PipeBeggsAndBrills riser = new PipeBeggsAndBrills("riser", subseaStream);
riser.setDiameter(0.1524); // 6 inch
riser.setLength(500.0); // 500 m length
riser.setElevation(500.0); // 500 m rise
riser.setAngle(90.0); // Vertical
riser.setNumberOfIncrements(50);
riser.run();
// Hydrostatic pressure difference
double hydrostaticHead = riser.getSegmentPressure(0) -
riser.getSegmentPressure(riser.getNumberOfIncrements());
System.out.println("Hydrostatic head: " + hydrostaticHead + " bar");
// High-pressure gas letdown - significant JT cooling expected
PipeBeggsAndBrills gasLine = new PipeBeggsAndBrills("gas_line", hpGasStream);
gasLine.setDiameter(0.1016); // 4 inch
gasLine.setLength(5000.0); // 5 km
gasLine.setElevation(0.0);
// Adiabatic with JT effect
gasLine.setHeatTransferMode(HeatTransferMode.ADIABATIC);
gasLine.setIncludeJouleThomsonEffect(true);
gasLine.run();
// For 20 bar pressure drop in natural gas:
// Expected JT cooling: ~8-10 K
double tempDrop = hpGasStream.getTemperature("C") -
gasLine.getOutletStream().getTemperature("C");
System.out.println("Temperature drop from JT: " + tempDrop + " °C");
// Determine pipeline capacity for given inlet/outlet pressures
PipeBeggsAndBrills pipe = new PipeBeggsAndBrills("flowline", feedStream);
pipe.setDiameter(0.2032);
pipe.setLength(25000.0);
pipe.setElevation(-50.0); // Slight descent
// Calculate flow rate mode
pipe.setCalculationMode(CalculationMode.CALCULATE_FLOW_RATE);
pipe.setSpecifiedOutletPressure(35.0, "bara");
pipe.setMaxFlowIterations(100);
pipe.setFlowConvergenceTolerance(1e-5);
pipe.run();
System.out.println("Calculated flow rate: " +
pipe.getOutletStream().getFlowRate("kg/hr") + " kg/hr");
// Gas-oil-water flow
SystemInterface fluid = new SystemSrkEos(323.15, 50.0);
fluid.addComponent("methane", 0.70);
fluid.addComponent("n-heptane", 0.20);
fluid.addComponent("water", 0.10);
fluid.setMixingRule("classic");
fluid.setMultiPhaseCheck(true);
Stream threePhase = new Stream("three_phase", fluid);
threePhase.setFlowRate(100000, "kg/hr");
threePhase.run();
PipeBeggsAndBrills pipe = new PipeBeggsAndBrills("flowline", threePhase);
pipe.setDiameter(0.3048); // 12 inch
pipe.setLength(10000.0);
pipe.setElevation(0.0);
pipe.setNumberOfIncrements(50);
pipe.run();
// Check water accumulation
System.out.println("Average liquid holdup: " + pipe.getLiquidHoldup());
| Environment | h [W/(m²·K)] |
|---|---|
| Still air (natural convection) | 5-25 |
| Forced air (5 m/s) | 25-50 |
| Forced air (30 m/s) | 100-250 |
| Still water | 100-500 |
| Flowing seawater (1 m/s) | 500-1000 |
| Buried in wet soil | 1-5 |
| Buried in dry soil | 0.5-2 |
| Material | k [W/(m·K)] |
|---|---|
| Carbon steel | 45-50 |
| Stainless steel | 15-20 |
| Copper | 380-400 |
| Mineral wool insulation | 0.03-0.05 |
| Polyurethane foam | 0.02-0.03 |
| Polypropylene | 0.1-0.2 |
| Concrete coating | 1.0-1.5 |
| Material | ε [mm] |
|---|---|
| Commercial steel | 0.045 |
| Stainless steel | 0.015 |
| Cast iron | 0.25 |
| Galvanized steel | 0.15 |
| PVC/Plastic | 0.0015 |
| Concrete | 0.3-3.0 |
| Method | Description |
|---|---|
setDiameter(double d) |
Set inner diameter [m] |
setLength(double L) |
Set pipe length [m] |
setElevation(double h) |
Set elevation change [m] |
setAngle(double θ) |
Set pipe inclination [degrees] |
setNumberOfIncrements(int n) |
Set number of calculation segments |
setPipeWallRoughness(double ε) |
Set wall roughness [m] |
setHeatTransferMode(HeatTransferMode mode) |
Set heat transfer calculation mode |
setHeatTransferCoefficient(double U) |
Set overall U-value [W/(m²·K)] |
setConstantSurfaceTemperature(double T, String unit) |
Set ambient/wall temperature |
setIncludeJouleThomsonEffect(boolean) |
Enable/disable JT cooling |
setIncludeFrictionHeating(boolean) |
Enable/disable friction heating |
run() |
Execute steady-state simulation |
runTransient(double dt) |
Execute one transient time step |
| Method | Returns |
|---|---|
getPressureDrop() |
Total pressure drop [bar] |
getFlowRegime() |
Current flow regime |
getLiquidHoldup() |
Average liquid holdup [-] |
getOutletStream() |
Outlet stream with results |
getPressureProfile() |
List of pressures along pipe |
getTemperatureProfile() |
List of temperatures along pipe |
getLiquidHoldupProfile() |
List of holdups along pipe |
The Beggs and Brill correlation has been validated against:
Expected accuracy:
Documentation for pipeline network modeling in NeqSim.
Location: neqsim.process.equipment.network
The network package provides classes for modeling interconnected pipeline systems where multiple pipelines converge at manifolds. This is essential for:
| Class | Description |
|---|---|
PipeFlowNetwork |
Compositional network with TDMA solver |
WellFlowlineNetwork |
Network using Beggs-Brill correlations |
The PipeFlowNetwork class models pipeline networks where multiple pipelines converge to manifolds, using compositional OnePhasePipeLine with TDMA (Tri-Diagonal Matrix Algorithm) solvers.
ProcessEquipmentBaseClass
└── PipeFlowNetwork
├── contains: ManifoldNode[]
└── contains: PipelineSegment[]
The network is modeled as a directed graph:
[Feed 1] [Feed 2]
│ │
▼ ▼
┌─────────┐ ┌─────────┐
│ Pipe 1 │ │ Pipe 2 │
└────┬────┘ └────┬────┘
│ │
▼ ▼
┌─────────────────────────┐
│ Manifold A (Mixer) │
└───────────┬─────────────┘
│
▼
┌─────────────┐
│ Export Pipe │
└──────┬──────┘
│
▼
┌─────────────────────────┐
│ End Manifold (Mixer) │
└───────────┬─────────────┘
│
▼
[Outlet Stream]
Represents a pipeline in the network:
public static class PipelineSegment {
String name;
OnePhasePipeLine pipeline;
String fromManifold; // null for inlet pipes
String toManifold;
boolean isInletPipeline(); // true if fromManifold is null
}
Represents a junction/manifold in the network:
public static class ManifoldNode {
String name;
Mixer mixer;
List<PipelineSegment> inboundPipelines;
PipelineSegment outboundPipeline;
boolean isTerminal(); // true if no outbound pipeline
}
import neqsim.process.equipment.network.PipeFlowNetwork;
// Basic constructor
PipeFlowNetwork network = new PipeFlowNetwork("gathering network");
| Method | Description |
|---|---|
createManifold(String name) |
Create a new manifold node |
addInletPipeline(name, feed, toManifold, length, diameter, nodes) |
Add inlet pipeline |
connectManifolds(from, to, name, length, diameter, nodes) |
Connect two manifolds |
run() |
Execute network simulation |
getOutletStream() |
Get the network outlet stream |
getPipeline(String name) |
Get a specific pipeline by name |
getManifold(String name) |
Get a manifold node by name |
import neqsim.process.equipment.network.PipeFlowNetwork;
import neqsim.process.equipment.stream.Stream;
import neqsim.thermo.system.SystemSrkEos;
// Create feed streams from wells
SystemInterface gas1 = new SystemSrkEos(320.0, 150.0);
gas1.addComponent("methane", 0.92);
gas1.addComponent("ethane", 0.05);
gas1.addComponent("propane", 0.03);
gas1.setMixingRule("classic");
Stream feed1 = new Stream("well-1", gas1);
feed1.setFlowRate(5.0, "MSm3/day");
feed1.run();
SystemInterface gas2 = new SystemSrkEos(315.0, 145.0);
gas2.addComponent("methane", 0.90);
gas2.addComponent("ethane", 0.06);
gas2.addComponent("propane", 0.04);
gas2.setMixingRule("classic");
Stream feed2 = new Stream("well-2", gas2);
feed2.setFlowRate(3.0, "MSm3/day");
feed2.run();
// Create network
PipeFlowNetwork network = new PipeFlowNetwork("gathering system");
// Create manifolds
String manifoldA = network.createManifold("manifold A");
String endManifold = network.createManifold("end manifold");
// Add inlet pipelines to manifold A
// Parameters: name, feedStream, toManifold, length(m), diameter(m), nodes
network.addInletPipeline("pipe1", feed1, manifoldA, 5000.0, 0.3, 50);
network.addInletPipeline("pipe2", feed2, manifoldA, 4500.0, 0.25, 45);
// Connect manifold A to end manifold with export pipeline
network.connectManifolds(manifoldA, endManifold, "export", 15000.0, 0.5, 100);
// Run steady-state simulation
network.run();
// Access results
StreamInterface outlet = network.getOutletStream();
System.out.println("Outlet pressure: " + outlet.getPressure("bara") + " bara");
System.out.println("Outlet temperature: " + outlet.getTemperature("C") + " °C");
System.out.println("Total flow: " + outlet.getFlowRate("MSm3/day") + " MSm3/day");
// Create complex gathering network
PipeFlowNetwork network = new PipeFlowNetwork("field gathering");
// Create manifold hierarchy
String tier1ManifoldA = network.createManifold("tier1-A");
String tier1ManifoldB = network.createManifold("tier1-B");
String tier2Manifold = network.createManifold("tier2");
String centralManifold = network.createManifold("central");
// Tier 1: Wells to first-level manifolds
network.addInletPipeline("well1", well1Stream, tier1ManifoldA, 2000.0, 0.2, 30);
network.addInletPipeline("well2", well2Stream, tier1ManifoldA, 2500.0, 0.2, 30);
network.addInletPipeline("well3", well3Stream, tier1ManifoldB, 1800.0, 0.2, 30);
network.addInletPipeline("well4", well4Stream, tier1ManifoldB, 3000.0, 0.2, 30);
// Tier 2: First-level to second-level
network.connectManifolds(tier1ManifoldA, tier2Manifold, "line-A", 5000.0, 0.35, 50);
network.connectManifolds(tier1ManifoldB, tier2Manifold, "line-B", 4500.0, 0.35, 50);
// Tier 3: Second-level to central
network.connectManifolds(tier2Manifold, centralManifold, "export", 10000.0, 0.5, 100);
// Run
network.run();
// Run network
network.run();
// Access specific pipeline
PipeFlowNetwork.PipelineSegment segment = network.getPipelineSegment("pipe1");
OnePhasePipeLine pipeline = segment.getPipeline();
// Get pressure profile
double[] pressures = pipeline.getPressureProfile();
double[] positions = pipeline.getPositionProfile();
for (int i = 0; i < positions.length; i++) {
System.out.printf("Position: %.1f m, Pressure: %.2f bara%n",
positions[i], pressures[i]);
}
A simplified network model using Beggs-Brill correlations instead of the full TDMA solver.
| Aspect | PipeFlowNetwork | WellFlowlineNetwork |
|---|---|---|
| Flow Model | TDMA solver | Beggs-Brill correlation |
| Compositional | Full | Simplified |
| Speed | Slower | Faster |
| Accuracy | Higher | Lower |
| Use Case | Detailed analysis | Quick screening |
PipeFlowNetwork network = new PipeFlowNetwork("transient network");
// ... setup network ...
// Configure transient parameters
network.setTransientMode(true);
network.setTimeStep(1.0); // seconds
// Run transient for 1 hour
for (double t = 0; t < 3600; t += 1.0) {
// Update boundary conditions if needed
feed1.setFlowRate(5.0 + 0.5 * Math.sin(t / 600), "MSm3/day");
feed1.run();
network.run();
if (t % 60 == 0) {
System.out.printf("t=%.0f s, P_outlet=%.2f bara%n",
t, network.getOutletStream().getPressure("bara"));
}
}
// Configure pipeline heat transfer
OnePhasePipeLine pipeline = network.getPipelineSegment("export").getPipeline();
pipeline.setOuterTemperature(278.15); // Ambient temperature (K)
pipeline.setOverallHeatTransferCoefficient(5.0); // W/m²/K
Documentation for reservoir modeling equipment in NeqSim, enabling coupled reservoir-process simulations.
Location: neqsim.process.equipment.reservoir
The reservoir package provides classes for simplified reservoir modeling that can be integrated with process simulations. This enables:
| Class | Description |
|---|---|
SimpleReservoir |
Material balance reservoir model |
Well |
Well connection to reservoir |
WellFlow |
Well flow calculations |
WellSystem |
System of connected wells |
ReservoirCVDsim |
Constant volume depletion simulation |
The SimpleReservoir class provides a material balance approach to reservoir modeling with support for multiple producers and injectors.
ProcessEquipmentBaseClass
└── SimpleReservoir
├── contains: Well[] gasProducers
├── contains: Well[] oilProducers
├── contains: Well[] waterProducers
├── contains: Well[] gasInjectors
└── contains: Well[] waterInjectors
import neqsim.process.equipment.reservoir.SimpleReservoir;
import neqsim.thermo.system.SystemSrkEos;
// Create reservoir fluid
SystemInterface reservoirFluid = new SystemSrkEos(373.0, 250.0); // 100°C, 250 bar
reservoirFluid.addComponent("methane", 50.0);
reservoirFluid.addComponent("ethane", 5.0);
reservoirFluid.addComponent("propane", 3.0);
reservoirFluid.addComponent("n-heptane", 40.0);
reservoirFluid.addComponent("water", 2.0);
reservoirFluid.setMixingRule("classic");
// Create reservoir
SimpleReservoir reservoir = new SimpleReservoir("North Sea Field");
reservoir.setReservoirFluid(reservoirFluid);
reservoir.setGasVolume(1.0e9, "Sm3"); // Initial gas volume
reservoir.setOilVolume(100.0e6, "Sm3"); // Initial oil volume
reservoir.setWaterVolume(50.0e6, "m3"); // Initial water volume
| Method | Description |
|---|---|
setReservoirFluid(SystemInterface) |
Set reservoir fluid composition |
setGasVolume(double, String) |
Set initial gas volume |
setOilVolume(double, String) |
Set initial oil volume |
setWaterVolume(double, String) |
Set initial water volume |
addGasProducer(String) |
Add gas production well |
addOilProducer(String) |
Add oil production well |
addWaterProducer(String) |
Add water production well |
addGasInjector(String) |
Add gas injection well |
addWaterInjector(String) |
Add water injection well |
getGasInPlace(String) |
Get remaining gas in place |
getOilInPlace(String) |
Get remaining oil in place |
getPressure() |
Get current reservoir pressure |
run() |
Execute material balance update |
// Add production wells
StreamInterface gasStream = reservoir.addGasProducer("Well-G1");
StreamInterface oilStream = reservoir.addOilProducer("Well-O1");
StreamInterface waterStream = reservoir.addWaterProducer("Well-W1");
// Add injection wells
StreamInterface gasInjStream = reservoir.addGasInjector("Well-GI1");
StreamInterface waterInjStream = reservoir.addWaterInjector("Well-WI1");
// Set production rates
reservoir.getGasProducer(0).getStream().setFlowRate(1.0, "MSm3/day");
reservoir.getOilProducer(0).getStream().setFlowRate(1000, "m3/day");
// By index
Well gasWell = reservoir.getGasProducer(0);
Well oilWell = reservoir.getOilProducer(0);
// By name
Well namedWell = reservoir.getOilProducer("Well-O1");
// Access well stream
StreamInterface wellStream = gasWell.getStream();
Represents a well connection to the reservoir.
import neqsim.process.equipment.reservoir.Well;
Well well = new Well("Production Well 1");
well.setStream(productionStream);
Provides well inflow performance relationship (IPR) calculations.
import neqsim.process.equipment.reservoir.WellFlow;
WellFlow wellFlow = new WellFlow("Well Inflow");
wellFlow.setReservoirPressure(250.0, "bara");
wellFlow.setWellheadPressure(80.0, "bara");
wellFlow.setProductivityIndex(10.0); // m³/day/bar
import neqsim.process.equipment.reservoir.SimpleReservoir;
import neqsim.thermo.system.SystemSrkEos;
// Create reservoir
SystemInterface fluid = new SystemSrkEos(373.0, 250.0);
fluid.addComponent("methane", 70.0);
fluid.addComponent("n-heptane", 30.0);
fluid.setMixingRule("classic");
SimpleReservoir reservoir = new SimpleReservoir("Test Reservoir");
reservoir.setReservoirFluid(fluid);
reservoir.setGasVolume(5.0e9, "Sm3"); // 5 GSm³
reservoir.setOilVolume(50.0e6, "Sm3"); // 50 MSm³
// Add producer
StreamInterface gasOut = reservoir.addGasProducer("GP-1");
reservoir.getGasProducer(0).getStream().setFlowRate(5.0, "MSm3/day");
// Simulate 10 years of production
double dt = 1.0; // day
for (int day = 0; day < 3650; day++) {
reservoir.run();
// Log every month
if (day % 30 == 0) {
double year = day / 365.0;
double gasInPlace = reservoir.getGasInPlace("GSm3");
double pressure = reservoir.getPressure();
System.out.printf("Year %.1f: GIP=%.2f GSm³, P=%.1f bara%n",
year, gasInPlace, pressure);
}
}
// Create oil reservoir
SimpleReservoir oilField = new SimpleReservoir("Oil Field");
oilField.setReservoirFluid(oilFluid);
oilField.setOilVolume(200.0e6, "Sm3");
oilField.setGasVolume(20.0e9, "Sm3");
oilField.setWaterVolume(100.0e6, "m3");
oilField.setLowPressureLimit(100.0); // Minimum reservoir pressure
// Production wells
StreamInterface oil1 = oilField.addOilProducer("OP-1");
StreamInterface oil2 = oilField.addOilProducer("OP-2");
StreamInterface gas1 = oilField.addGasProducer("GP-1");
// Injection well for pressure maintenance
StreamInterface waterInj = oilField.addWaterInjector("WI-1");
// Set rates
oilField.getOilProducer(0).getStream().setFlowRate(5000, "bbl/day");
oilField.getOilProducer(1).getStream().setFlowRate(4000, "bbl/day");
oilField.getGasProducer(0).getStream().setFlowRate(1.0, "MSm3/day");
// Water injection for voidage replacement
oilField.getWaterInjector(0).getStream().setFlowRate(6000, "bbl/day");
// Run simulation
oilField.run();
import neqsim.process.processmodel.ProcessSystem;
// Create reservoir
SimpleReservoir reservoir = new SimpleReservoir("Field A");
reservoir.setReservoirFluid(reservoirFluid);
reservoir.setOilVolume(100.0e6, "Sm3");
// Add producer
StreamInterface wellStream = reservoir.addOilProducer("OP-1");
// Create process system
ProcessSystem facility = new ProcessSystem("FPSO");
// Reservoir production
reservoir.getOilProducer(0).getStream().setFlowRate(10000, "bbl/day");
facility.add(reservoir);
// Production separator
ThreePhaseSeparator separator = new ThreePhaseSeparator("HP Separator");
separator.setInletStream(wellStream);
facility.add(separator);
// Gas processing
Stream gasStream = separator.getGasOutStream();
Compressor compressor = new Compressor("Export Compressor", gasStream);
compressor.setOutletPressure(200.0, "bara");
facility.add(compressor);
// Run integrated simulation
facility.run();
// Check OOIP recovery
double initialOOIP = 100.0e6;
double remainingOil = reservoir.getOilInPlace("MSm3") * 1e6;
double recovery = (initialOOIP - remainingOil) / initialOOIP * 100;
System.out.println("Oil recovery: " + recovery + " %");
The reservoir uses a simplified material balance approach:
G_p = G_i × (1 - P/P_i × Z_i/Z)
Where:
Includes gas cap expansion, oil zone compressibility, and water influx terms.
// Get original volumes
double OOIP = reservoir.getOOIP(); // Original oil in place
double OGIP = reservoir.getOGIP(); // Original gas in place
// Get current in-place volumes
double currentGIP = reservoir.getGasInPlace("GSm3");
double currentOIP = reservoir.getOilInPlace("MSm3");
// Get cumulative production
double cumGas = reservoir.getGasProductionTotal();
double cumOil = reservoir.getOilProductionTotal();
The reservoir can be integrated with surface facilities:
┌─────────────────┐
│ RESERVOIR │
│ │
│ ┌───────────┐ │
│ │ Gas Cap │ │
│ └─────┬─────┘ │
│ │ │
│ ┌─────┴─────┐ │ ┌─────────────┐ ┌──────────┐
│ │ Oil Zone │──┼──────│ Separator │──────│ Pipeline │
│ └─────┬─────┘ │ └─────────────┘ └──────────┘
│ │ │
│ ┌─────┴─────┐ │
│ │ Water │◄─┼───── Water Injection
│ └───────────┘ │
│ │
└─────────────────┘
Documentation for subsea production equipment in NeqSim.
Location: neqsim.process.equipment.subsea
The subsea package provides equipment for modeling subsea production systems, including:
| Class | Description |
|---|---|
SubseaWell |
Combined well and tubing model |
SimpleFlowLine |
Subsea flowline/riser model |
The SubseaWell class models a subsea production well including the wellbore/tubing flow using an adiabatic two-phase pipe model.
TwoPortEquipment
└── SubseaWell
└── contains: AdiabaticTwoPhasePipe (tubing)
import neqsim.process.equipment.subsea.SubseaWell;
import neqsim.process.equipment.stream.StreamInterface;
// Create subsea well with inlet stream from reservoir
SubseaWell well = new SubseaWell("Well-1", reservoirStream);
| Property | Description | Default |
|---|---|---|
height |
Vertical depth of well | 1000.0 m |
length |
Measured depth of well | 1200.0 m |
| Method | Description |
|---|---|
getPipeline() |
Access internal tubing model |
getOutletStream() |
Get wellhead stream |
run() |
Execute well flow calculation |
SubseaWell well = new SubseaWell("OP-1", reservoirStream);
// Configure tubing
AdiabaticTwoPhasePipe tubing = well.getPipeline();
tubing.setDiameter(0.15); // 6" tubing ID
tubing.setLength(3000.0); // 3000m MD
tubing.setInletElevation(-2500.0); // Reservoir depth TVD
tubing.setOutletElevation(-200.0); // Mudline depth
The SimpleFlowLine class models a subsea flowline or riser from the wellhead to the platform.
TwoPortEquipment
└── SimpleFlowLine
└── contains: AdiabaticTwoPhasePipe (flowline)
import neqsim.process.equipment.subsea.SimpleFlowLine;
// Create flowline from choke outlet
SimpleFlowLine flowline = new SimpleFlowLine("FL-1", chokeOutletStream);
SimpleFlowLine flowline = new SimpleFlowLine("Flowline", wellheadStream);
// Configure flowline
flowline.getPipeline().setDiameter(0.4); // 16" flowline
flowline.getPipeline().setLength(5000.0); // 5 km tieback
flowline.getPipeline().setInletElevation(-200.0); // Mudline
flowline.getPipeline().setOutletElevation(0.0); // Platform
┌─────────────┐
│ RESERVOIR │
└──────┬──────┘
│
▼
┌─────────────┐
│ SUBSEA WELL │ ← Tubing flow model
│ (tubing) │
└──────┬──────┘
│
▼
┌─────────────┐
│SUBSEA CHOKE │ ← Pressure control
└──────┬──────┘
│
▼
┌─────────────┐
│ FLOWLINE │ ← Multiphase transport
│ (riser) │
└──────┬──────┘
│
▼
┌─────────────┐
│TOPSIDE CHOKE│ ← Final pressure control
└──────┬──────┘
│
▼
┌─────────────┐
│ SEPARATOR │ ← First-stage separation
└─────────────┘
import neqsim.process.equipment.reservoir.SimpleReservoir;
import neqsim.process.equipment.subsea.SubseaWell;
import neqsim.process.equipment.subsea.SimpleFlowLine;
import neqsim.process.equipment.valve.ThrottlingValve;
import neqsim.process.processmodel.ProcessSystem;
import neqsim.thermo.system.SystemSrkEos;
// Create reservoir fluid
SystemInterface reservoirFluid = new SystemSrkEos(373.15, 250.0); // 100°C, 250 bar
reservoirFluid.addComponent("nitrogen", 0.1);
reservoirFluid.addComponent("methane", 70.0);
reservoirFluid.addComponent("ethane", 5.0);
reservoirFluid.addComponent("propane", 3.0);
reservoirFluid.addComponent("n-butane", 2.0);
reservoirFluid.addComponent("n-heptane", 10.0);
reservoirFluid.addComponent("nC10", 5.0);
reservoirFluid.addComponent("water", 10.0);
reservoirFluid.setMixingRule(2);
reservoirFluid.setMultiPhaseCheck(true);
// Create reservoir
SimpleReservoir reservoir = new SimpleReservoir("Deepwater Field");
reservoir.setReservoirFluid(reservoirFluid, 5.0e7, 550.0e6, 10.0e6);
// Add oil producer
StreamInterface producedStream = reservoir.addOilProducer("OP-1");
producedStream.setFlowRate(10000.0 * 24.0, "kg/day");
// Run reservoir
reservoir.run();
// Create subsea well
SubseaWell well = new SubseaWell("OP-1 Well", producedStream);
well.getPipeline().setDiameter(0.15); // 6" tubing
well.getPipeline().setLength(3500.0); // 3500m MD
well.getPipeline().setInletElevation(-2000.0); // Reservoir depth
well.getPipeline().setOutletElevation(-500.0); // Mudline
// Subsea choke
ThrottlingValve subseaChoke = new ThrottlingValve("Subsea Choke", well.getOutletStream());
subseaChoke.setOutletPressure(120.0, "bara");
subseaChoke.setAcceptNegativeDP(false);
// Flowline to platform
SimpleFlowLine flowline = new SimpleFlowLine("Flowline", subseaChoke.getOutletStream());
flowline.getPipeline().setDiameter(0.25); // 10" flowline
flowline.getPipeline().setLength(8000.0); // 8 km tieback
flowline.getPipeline().setInletElevation(-500.0);
flowline.getPipeline().setOutletElevation(0.0);
// Topside choke
ThrottlingValve topsideChoke = new ThrottlingValve("Topside Choke", flowline.getOutletStream());
topsideChoke.setOutletPressure(50.0, "bara");
// Create process system
ProcessSystem subsea = new ProcessSystem("Subsea System");
subsea.add(well);
subsea.add(subseaChoke);
subsea.add(flowline);
subsea.add(topsideChoke);
// Run
subsea.run();
// Results
System.out.println("Wellhead pressure: " + well.getOutletStream().getPressure("bara") + " bara");
System.out.println("Subsea choke DP: " + subseaChoke.getDeltaPressure("bar") + " bar");
System.out.println("Arrival pressure: " + flowline.getOutletStream().getPressure("bara") + " bara");
System.out.println("Topside pressure: " + topsideChoke.getOutletStream().getPressure("bara") + " bara");
import java.util.ArrayList;
// Setup as above...
// Production rate control with adjuster
Adjuster rateControl = new Adjuster("Rate Adjuster");
rateControl.setActivateWhenLess(true);
rateControl.setTargetVariable(flowline.getOutletStream(), "pressure", 80.0, "bara");
rateControl.setAdjustedVariable(producedStream, "flow rate");
// Add adjuster to process
subsea.add(rateControl);
// Run transient simulation
ArrayList<double[]> productionHistory = new ArrayList<double[]>();
for (int day = 0; day < 365; day++) {
// Run reservoir for one day
reservoir.runTransient(60 * 60 * 24); // seconds in day
// Run subsea system
subsea.run();
// Record data
productionHistory.add(new double[] {
day,
producedStream.getFlowRate("kg/hr"),
reservoir.getOilProductionTotal("MSm3"),
reservoir.getPressure()
});
// Monthly output
if (day % 30 == 0) {
System.out.printf("Day %d: Rate=%.0f kg/hr, Cum=%.2f MSm3, P_res=%.1f bara%n",
day,
producedStream.getFlowRate("kg/hr"),
reservoir.getOilProductionTotal("MSm3"),
reservoir.getPressure());
}
}
// Create three subsea wells
SubseaWell well1 = new SubseaWell("OP-1", reservoir.getOilProducer("OP-1").getStream());
SubseaWell well2 = new SubseaWell("OP-2", reservoir.getOilProducer("OP-2").getStream());
SubseaWell well3 = new SubseaWell("OP-3", reservoir.getOilProducer("OP-3").getStream());
// Configure wells
for (SubseaWell well : new SubseaWell[] {well1, well2, well3}) {
well.getPipeline().setDiameter(0.15);
well.getPipeline().setLength(3000.0);
well.getPipeline().setInletElevation(-2000.0);
well.getPipeline().setOutletElevation(-400.0);
}
// Create subsea manifold
Manifold subseaManifold = new Manifold("Subsea Manifold");
subseaManifold.addStream(well1.getOutletStream());
subseaManifold.addStream(well2.getOutletStream());
subseaManifold.addStream(well3.getOutletStream());
// Export flowline from manifold
SimpleFlowLine exportLine = new SimpleFlowLine("Export", subseaManifold.getOutletStream());
exportLine.getPipeline().setDiameter(0.4);
exportLine.getPipeline().setLength(15000.0);
This folder contains documentation for utility and control equipment in NeqSim.
| Equipment | File | Description |
|---|---|---|
| Adjusters | adjusters.md | Parameter adjustment to meet specifications |
| Recycles | recycles.md | Recycle stream handling |
| Setters | setters.md | Variable setters |
| Calculators | calculators.md | Custom calculations |
| Equipment | File | Description |
|---|---|---|
| Set Points | setpoints.md | Process set points |
| Flow Rate Adjusters | flow_adjusters.md | Flow rate control |
Adjuster adjuster = new Adjuster("Controller");
adjuster.setAdjustedVariable(equipment, "parameter");
adjuster.setTargetVariable(stream, "property", targetValue, unit);
process.add(adjuster);
Recycle recycle = new Recycle("RecycleName");
recycle.addStream(recycleStream);
recycle.setOutletStream(targetMixer);
recycle.setTolerance(1e-6);
process.add(recycle);
Documentation for adjuster equipment in NeqSim process simulation.
Location: neqsim.process.equipment.util
Class: Adjuster
Adjusters are iterative solvers that modify one process variable to achieve a target specification. They are essential for solving design problems where:
NeqSim supports two configuration modes:
setAdjustedVariable, setTargetVariable)import neqsim.process.equipment.util.Adjuster;
// Create adjuster
Adjuster adjuster = new Adjuster("Temperature Controller");
// Set the variable to adjust
adjuster.setAdjustedVariable(heater, "outTemperature");
// Set the target specification
adjuster.setTargetVariable(stream, "temperature", 80.0, "C");
// Add to process
process.add(adjuster);
process.run();
// Get the adjusted value
double adjustedTemp = heater.getOutTemperature("C");
The variable that the adjuster will modify:
| Equipment | Variable | Description |
|---|---|---|
Heater/Cooler |
"duty" |
Heat duty (W) |
Heater/Cooler |
"outTemperature" |
Outlet temperature |
Compressor |
"outletPressure" |
Discharge pressure |
Valve |
"outletPressure" |
Outlet pressure |
Valve |
"percentValveOpening" |
Valve position |
Splitter |
"splitFactor" |
Split ratio |
Stream |
"flowRate" |
Flow rate |
// Examples
adjuster.setAdjustedVariable(heater, "duty");
adjuster.setAdjustedVariable(compressor, "outletPressure");
adjuster.setAdjustedVariable(valve, "percentValveOpening");
adjuster.setAdjustedVariable(splitter, "splitFactor", 0); // First split
The specification to be achieved:
| Equipment | Variable | Description |
|---|---|---|
Stream |
"temperature" |
Stream temperature |
Stream |
"pressure" |
Stream pressure |
Stream |
"flowRate" |
Stream flow rate |
Stream |
"moleFraction" |
Component mole fraction |
Separator |
"liquidLevel" |
Liquid level fraction |
| Any | Custom | User-defined property |
// Examples
adjuster.setTargetVariable(stream, "temperature", 80.0, "C");
adjuster.setTargetVariable(separator, "liquidLevel", 0.5);
adjuster.setTargetVariable(stream, "moleFraction", 0.02, "CO2");
// Maximum iterations
adjuster.setMaximumIterations(100);
// Convergence tolerance
adjuster.setTolerance(1e-6);
// Bounds on adjusted variable
adjuster.setMinimumValue(-1e7); // Lower bound
adjuster.setMaximumValue(1e7); // Upper bound
// Step size for numerical derivatives
adjuster.setStepSize(0.001);
For complex scenarios where standard variable names are insufficient, the Adjuster supports functional interfaces (lambda expressions) for complete flexibility. This allows you to:
| Method | Signature | Description |
|---|---|---|
setAdjustedValueSetter |
Consumer<Double> |
Lambda to set the adjusted value |
setAdjustedValueGetter |
Supplier<Double> |
Lambda to get the current adjusted value |
setTargetValueCalculator |
Supplier<Double> |
Lambda to calculate the target (measured) value |
setTargetValue |
double |
The setpoint that the target should reach |
Adjuster adjuster = new Adjuster("adjuster");
// Set the setpoint (what targetValueCalculator should return)
adjuster.setTargetValue(desiredValue);
// Set bounds for the adjusted variable
adjuster.setMinAdjustedValue(minValue);
adjuster.setMaxAdjustedValue(maxValue);
// Lambda: How to SET the adjusted variable
adjuster.setAdjustedValueSetter((val) -> {
// Your logic to set the value
equipment.setSomeProperty(val);
});
// Lambda: How to GET the current adjusted variable value
adjuster.setAdjustedValueGetter(() -> {
// Your logic to get the current value
return equipment.getSomeProperty();
});
// Lambda: How to CALCULATE the measured variable (compared to setpoint)
adjuster.setTargetValueCalculator(() -> {
// Your logic to calculate current target value
return someCalculation();
});
Adjust a splitter's second outlet flow rate to achieve a target flow in the first outlet:
// Create process equipment
Stream feed = new Stream("feed", fluid);
feed.setFlowRate(1000.0, "kg/hr");
Splitter splitter = new Splitter("splitter", feed);
splitter.setSplitNumber(2);
splitter.setSplitFactors(new double[] {0.5, 0.5});
Stream stream1 = new Stream("stream1", splitter.getSplitStream(0));
Stream stream2 = new Stream("stream2", splitter.getSplitStream(1));
// Create adjuster with functional interfaces
Adjuster adjuster = new Adjuster("Flow Adjuster");
// Setpoint: we want stream1 to have 800 kg/hr
adjuster.setTargetValue(800.0);
// Bounds: stream2 flow must be between 0 and 1000 kg/hr
adjuster.setMinAdjustedValue(0.0);
adjuster.setMaxAdjustedValue(1000.0);
// Setter: Adjust the flow rate of stream2 via splitter
// Note: Splitter.setFlowRates uses -1 for "calculate this one"
adjuster.setAdjustedValueSetter((val) -> {
splitter.setFlowRates(new double[] {-1, val}, "kg/hr");
});
// Getter: Get current flow rate of stream2
adjuster.setAdjustedValueGetter(() -> {
return splitter.getSplitStream(1).getFlowRate("kg/hr");
});
// Target Calculator: Get flow rate of stream1 (what we're controlling)
adjuster.setTargetValueCalculator(() -> {
return stream1.getFlowRate("kg/hr");
});
// Add to process
ProcessSystem process = new ProcessSystem();
process.add(feed);
process.add(splitter);
process.add(stream1);
process.add(stream2);
process.add(adjuster);
process.run();
// Result: stream1 = 800 kg/hr, stream2 = 200 kg/hr
Adjust temperature to achieve a target product of flow × temperature:
Stream inletStream = new Stream("inlet", fluid);
inletStream.setFlowRate(100.0, "kg/hr");
inletStream.setTemperature(200.0, "K");
Adjuster adjuster = new Adjuster("Custom Adjuster");
// Setpoint: flow × temperature = 30000
adjuster.setTargetValue(30000.0);
adjuster.setMinAdjustedValue(100.0); // Min temp 100 K
adjuster.setMaxAdjustedValue(500.0); // Max temp 500 K
// Setter: Adjust temperature
adjuster.setAdjustedValueSetter((val) -> {
inletStream.setTemperature(val, "K");
});
// Getter: Get current temperature
adjuster.setAdjustedValueGetter(() -> {
return inletStream.getTemperature("K");
});
// Target Calculator: flow × temperature
adjuster.setTargetValueCalculator(() -> {
return inletStream.getFlowRate("kg/hr") * inletStream.getTemperature("K");
});
ProcessSystem process = new ProcessSystem();
process.add(inletStream);
process.add(adjuster);
process.run();
// Result: temperature adjusts to 300 K (100 × 300 = 30000)
For cleaner code when working with specific equipment, use the equipment-aware signatures:
// Set the equipment references
adjuster.setAdjustedVariable(inletStream);
adjuster.setTargetVariable(inletStream);
// Setter with equipment reference
adjuster.setAdjustedValueSetter((equipment, val) -> {
Stream s = (Stream) equipment;
s.setTemperature(val, "K");
});
// Getter with equipment reference
adjuster.setAdjustedValueGetter((equipment) -> {
Stream s = (Stream) equipment;
return s.getTemperature("K");
});
// Target calculator with equipment reference
adjuster.setTargetValueCalculator((equipment) -> {
Stream s = (Stream) equipment;
return s.getFlowRate("kg/hr") * s.getTemperature("K");
});
| Scenario | Recommended Approach |
|---|---|
| Standard properties (temperature, pressure, flow) | Standard mode with setAdjustedVariable |
| Properties not in predefined list | Functional interface mode |
| Complex calculations for target | Use setTargetValueCalculator |
| Adjusting one equipment to affect another | Functional interface mode |
| Multiple variables combined in target | Functional interface mode |
| Conditional logic in getting/setting | Functional interface mode |
// Adjust heater duty to achieve target outlet temperature
Adjuster tempControl = new Adjuster("TC-100");
tempControl.setAdjustedVariable(heater, "duty");
tempControl.setTargetVariable(heater.getOutletStream(), "temperature", 100.0, "C");
process.add(tempControl);
// Adjust cooler to achieve hydrocarbon dew point
Adjuster dewPointControl = new Adjuster("HCDP Controller");
dewPointControl.setAdjustedVariable(cooler, "outTemperature");
dewPointControl.setTargetPhaseCondition(stream, "cricondenbar", 50.0, "bara");
process.add(dewPointControl);
// Adjust column reflux to achieve product purity
Adjuster purityControl = new Adjuster("Purity Controller");
purityControl.setAdjustedVariable(column, "refluxRatio");
purityControl.setTargetVariable(overhead, "moleFraction", 0.99, "methane");
process.add(purityControl);
// Adjust outlet valve to maintain liquid level
Adjuster levelControl = new Adjuster("LC-100");
levelControl.setAdjustedVariable(outletValve, "percentValveOpening");
levelControl.setTargetVariable(separator, "liquidLevel", 0.5);
process.add(levelControl);
// Adjust split ratio to achieve target flow in branch
Adjuster flowControl = new Adjuster("FC-100");
flowControl.setAdjustedVariable(splitter, "splitFactor", 0);
flowControl.setTargetVariable(branchStream, "flowRate", 5000.0, "kg/hr");
process.add(flowControl);
When using multiple adjusters, add them in order of priority:
// First adjuster (higher priority)
Adjuster adj1 = new Adjuster("Primary");
adj1.setAdjustedVariable(heater, "duty");
adj1.setTargetVariable(stream1, "temperature", 80.0, "C");
process.add(adj1);
// Second adjuster (solved after first converges)
Adjuster adj2 = new Adjuster("Secondary");
adj2.setAdjustedVariable(cooler, "duty");
adj2.setTargetVariable(stream2, "temperature", 30.0, "C");
process.add(adj2);
// Increase iterations
adjuster.setMaximumIterations(200);
// Widen bounds
adjuster.setMinimumValue(-1e8);
adjuster.setMaximumValue(1e8);
// Check if converged
if (!adjuster.isConverged()) {
System.out.println("Adjuster did not converge");
System.out.println("Current error: " + adjuster.getError());
}
Some specifications may be physically impossible:
Check that specifications are achievable before troubleshooting solver settings.
Documentation for recycle handling in NeqSim process simulation.
Location: neqsim.process.equipment.util
Classes:
| Class | Description |
|---|---|
Recycle |
Main recycle handler |
RecycleController |
Advanced recycle control |
AccelerationMethod |
Convergence acceleration |
BroydenAccelerator |
Broyden's method acceleration |
Recycles handle iterative loops in process flowsheets where a downstream stream feeds back into an upstream unit. Common examples:
import neqsim.process.equipment.util.Recycle;
// Create recycle
Recycle recycle = new Recycle("Solvent Recycle");
// Add the stream coming from downstream
recycle.addStream(returnStream);
// Set where the recycle feeds into
recycle.setOutletStream(feedMixer);
// Add to process
process.add(recycle);
process.run();
// Set convergence tolerance
recycle.setTolerance(1e-6);
// Separate tolerances for flow and composition
recycle.setFlowTolerance(1e-4);
recycle.setCompositionTolerance(1e-6);
recycle.setTemperatureTolerance(0.1); // K
recycle.setPressureTolerance(0.01); // bar
// Limit iterations
recycle.setMaximumIterations(50);
Damping helps prevent oscillation:
// Set damping factor (0-1, lower = more damping)
recycle.setDampingFactor(0.5); // 50% of new value, 50% of old
For faster convergence, acceleration methods can be used:
recycle.setAccelerationMethod("wegstein");
import neqsim.process.equipment.util.BroydenAccelerator;
BroydenAccelerator accelerator = new BroydenAccelerator();
recycle.setAccelerationMethod(accelerator);
Simple successive substitution (default):
recycle.setAccelerationMethod("direct");
ProcessSystem process = new ProcessSystem();
// Feed stream
Stream feed = new Stream("Feed", feedFluid);
process.add(feed);
// Mixer for feed and recycle
Mixer mixer = new Mixer("Feed Mixer");
mixer.addStream(feed);
process.add(mixer);
// Process unit (e.g., absorber)
Absorber absorber = new Absorber("TEG Contactor", mixer.getOutletStream());
process.add(absorber);
// Regeneration
Heater regenerator = new Heater("TEG Regenerator", absorber.getLiquidOutStream());
regenerator.setOutTemperature(200.0, "C");
process.add(regenerator);
// Cooler
Cooler cooler = new Cooler("TEG Cooler", regenerator.getOutletStream());
cooler.setOutTemperature(40.0, "C");
process.add(cooler);
// Recycle lean solvent back to mixer
Recycle solventRecycle = new Recycle("TEG Recycle");
solventRecycle.addStream(cooler.getOutletStream());
solventRecycle.setOutletStream(mixer);
solventRecycle.setTolerance(1e-5);
process.add(solventRecycle);
// Connect mixer to absorber with recycle
mixer.addStream(solventRecycle.getOutletStream());
// Run
process.run();
// Check convergence
if (solventRecycle.isConverged()) {
System.out.println("Recycle converged in " +
solventRecycle.getIterations() + " iterations");
}
// Fresh feed
Stream freshFeed = new Stream("Fresh Feed", freshFeedFluid);
process.add(freshFeed);
// Mix fresh feed with recycle
Mixer reactorFeed = new Mixer("Reactor Feed");
reactorFeed.addStream(freshFeed);
process.add(reactorFeed);
// Reactor
GibbsReactor reactor = new GibbsReactor("Synthesis Reactor");
reactor.setInletStream(reactorFeed.getOutletStream());
process.add(reactor);
// Separator
Separator productSep = new Separator("Product Separator", reactor.getOutletStream());
process.add(productSep);
// Recycle unreacted gas
Recycle gasRecycle = new Recycle("Unreacted Gas Recycle");
gasRecycle.addStream(productSep.getGasOutStream());
gasRecycle.setOutletStream(reactorFeed);
gasRecycle.setTolerance(1e-5);
gasRecycle.setDampingFactor(0.7);
process.add(gasRecycle);
reactorFeed.addStream(gasRecycle.getOutletStream());
process.run();
For processes with multiple recycle loops:
// Outer recycle (converges first)
Recycle outerRecycle = new Recycle("Outer Recycle");
outerRecycle.addStream(outerStream);
outerRecycle.setOutletStream(outerMixer);
outerRecycle.setPriority(1); // Lower priority converges first
process.add(outerRecycle);
// Inner recycle (converges second)
Recycle innerRecycle = new Recycle("Inner Recycle");
innerRecycle.addStream(innerStream);
innerRecycle.setOutletStream(innerMixer);
innerRecycle.setPriority(2); // Higher priority
process.add(innerRecycle);
// Check if converged
boolean converged = recycle.isConverged();
// Get number of iterations
int iterations = recycle.getIterations();
// Get current error
double error = recycle.getError();
System.out.println("Recycle status:");
System.out.println(" Converged: " + converged);
System.out.println(" Iterations: " + iterations);
System.out.println(" Error: " + error);
// Get convergence history for debugging
double[] errorHistory = recycle.getErrorHistory();
for (int i = 0; i < errorHistory.length; i++) {
System.out.println("Iteration " + i + ": error = " + errorHistory[i]);
}
// Try Wegstein acceleration
recycle.setAccelerationMethod("wegstein");
recycle.setDampingFactor(0.8);
// Heavy damping for oscillating systems
recycle.setDampingFactor(0.3);
recycle.setMaximumIterations(100);
// Debug mode
recycle.setVerbose(true);
process.run();
Documentation for calculator and setter equipment in NeqSim process simulation.
Location: neqsim.process.equipment.util
Classes:
| Class | Description |
|---|---|
Calculator |
Custom calculation unit |
CalculatorLibrary |
Pre-built calculation functions |
Setter |
Variable setter |
FlowSetter |
Flow rate setter |
MoleFractionControllerUtil |
Composition control |
Performs custom calculations based on process variables. The Calculator supports two configuration modes:
import neqsim.process.equipment.util.Calculator;
// Create calculator
Calculator calc = new Calculator("Duty Calculator");
// Add input variables
calc.addInputVariable(stream1);
calc.addInputVariable(stream2);
// Set output variable
calc.setOutputVariable(heater);
// Add to process
process.add(calc);
// Add streams individually
calc.addInputVariable(stream1);
calc.addInputVariable(stream2);
// Or add multiple at once using varargs
calc.addInputVariable(stream1, stream2, stream3);
The Calculator class supports lambda expressions for defining custom calculation logic. This provides full flexibility to implement any calculation without expression parsing limitations.
| Method | Signature | Description |
|---|---|---|
setCalculationMethod |
BiConsumer<ArrayList<ProcessEquipmentInterface>, ProcessEquipmentInterface> |
Full access to registered inputs and output |
setCalculationMethod |
Runnable |
Simple lambda that captures variables from enclosing scope |
Use this pattern when you want to work with formally registered input/output variables:
Calculator calculator = new Calculator("Energy Calculator");
calculator.addInputVariable(inletStream);
calculator.setOutputVariable(outletStream);
calculator.setCalculationMethod((inputs, output) -> {
Stream in = (Stream) inputs.get(0);
Stream out = (Stream) output;
double energy = in.LCV() * in.getFlowRate("Sm3/hr");
out.setTemperature(350.0, "K");
});
calculator.run();
Calculator calculator = new Calculator("Total Flow Calculator");
// Add multiple inputs using varargs
calculator.addInputVariable(inletStream1, inletStream2);
calculator.setOutputVariable(outletStream);
calculator.setCalculationMethod((inputs, output) -> {
double totalFlow = 0.0;
for (ProcessEquipmentInterface input : inputs) {
totalFlow += ((Stream) input).getFlowRate("kg/hr");
}
((Stream) output).setFlowRate(totalFlow, "kg/hr");
});
calculator.run();
Use this pattern when you want to capture equipment directly in the lambda closure:
Calculator calculator = new Calculator("Energy Calculator");
// No need to register inputs/outputs - capture them directly
calculator.setCalculationMethod(() -> {
double energy = inletStream.LCV() * inletStream.getFlowRate("Sm3/hr");
outletStream.setTemperature(350.0, "K");
});
calculator.run();
| Pattern | Use When |
|---|---|
BiConsumer |
Building reusable calculations, working with variable number of inputs |
Runnable |
Quick calculations, capturing specific equipment from scope |
Pre-built calculation presets for common thermodynamic operations. These presets provide declarative building blocks that encourage consistent logic across simulations.
| Preset | Description |
|---|---|
ENERGY_BALANCE |
Flashes output stream to match summed input enthalpy |
DEW_POINT_TARGETING |
Sets output temperature to hydrocarbon dew point |
import neqsim.process.equipment.util.CalculatorLibrary;
// Using the preset directly
Calculator calculator = new Calculator("Energy Balance");
calculator.addInputVariable(inlet);
calculator.setOutputVariable(outlet);
calculator.setCalculationMethod(CalculatorLibrary.energyBalance());
calculator.run();
Performs an enthalpy-based energy balance. The output stream is flashed at its current pressure to match the summed input enthalpies:
SystemSrkEos fluid = new SystemSrkEos(280.0, 50.0);
fluid.addComponent("methane", 0.9);
fluid.addComponent("ethane", 0.1);
fluid.createDatabase(true);
fluid.setMixingRule(2);
Stream inlet = new Stream("inlet", fluid);
inlet.setTemperature(280.0, "K");
inlet.setPressure(50.0, "bara");
inlet.run();
Stream outlet = new Stream("outlet", fluid.clone());
outlet.setTemperature(320.0, "K");
outlet.setPressure(50.0, "bara");
outlet.run();
Calculator calculator = new Calculator("Energy Balance");
calculator.addInputVariable(inlet);
calculator.setOutputVariable(outlet);
calculator.setCalculationMethod(CalculatorLibrary.energyBalance());
calculator.run();
// Outlet enthalpy now matches inlet enthalpy
Sets the output stream temperature to the hydrocarbon dew point of the first input stream at the output stream's pressure:
Stream source = new Stream("source", fluid);
source.setPressure(15.0, "bara");
source.run();
Stream target = new Stream("target", fluid.clone());
target.setPressure(12.0, "bara");
target.run();
Calculator calculator = new Calculator("Dew Point Targeter");
calculator.addInputVariable(source);
calculator.setOutputVariable(target);
calculator.setCalculationMethod(CalculatorLibrary.byName("dewPointTargeting"));
calculator.run();
// Target temperature now equals dew point at 12 bara
Add a safety margin above the dew point:
// Add 5 K margin above dew point
calculator.setCalculationMethod(CalculatorLibrary.dewPointTargeting(5.0));
Useful for declarative configuration or AI-generated instructions:
// Case-insensitive, supports various formats
CalculatorLibrary.byName("energyBalance");
CalculatorLibrary.byName("ENERGY_BALANCE");
CalculatorLibrary.byName("energy-balance");
CalculatorLibrary.byName("dewPointTargeting");
Sets process variables to specific values.
import neqsim.process.equipment.util.Setter;
// Create setter
Setter setter = new Setter("Temperature Setter");
// Set target equipment and property
setter.setEquipment(heater);
setter.setProperty("outTemperature");
setter.setValue(80.0);
setter.setUnit("C");
// Add to process
process.add(setter);
// Set valve position
Setter valveSetter = new Setter("Valve Opener");
valveSetter.setEquipment(valve);
valveSetter.setProperty("percentValveOpening");
valveSetter.setValue(75.0);
// Set compressor speed
Setter speedSetter = new Setter("Speed Setter");
speedSetter.setEquipment(compressor);
speedSetter.setProperty("speed");
speedSetter.setValue(5000.0);
Specifically for setting flow rates.
import neqsim.process.equipment.util.FlowSetter;
// Create flow setter
FlowSetter flowSetter = new FlowSetter("Production Rate", stream);
flowSetter.setFlowRate(10000.0, "kg/hr");
// Add to process
process.add(flowSetter);
// Ramp flow rate over time
flowSetter.setRampRate(1000.0, "kg/hr/min");
flowSetter.setTargetFlowRate(20000.0, "kg/hr");
Control stream composition.
import neqsim.process.equipment.util.MoleFractionControllerUtil;
// Control CO2 content
MoleFractionControllerUtil co2Control =
new MoleFractionControllerUtil("CO2 Spec", stream);
co2Control.setTargetMoleFraction("CO2", 0.02); // 2 mol%
// Add to process
process.add(co2Control);
// Calculator to maximize production
Calculator optimizer = new Calculator("Production Optimizer");
optimizer.addInputVariable(separator, "pressure");
optimizer.addInputVariable(feedStream, "flowRate");
optimizer.setOutputVariable(exportValve, "percentValveOpening");
optimizer.setExpression("calculateOptimalOpening(pressure, flowRate)");
process.add(optimizer);
// Primary controller output sets secondary setpoint
Calculator cascade = new Calculator("Cascade");
cascade.addInputVariable(temperatureController, "output");
cascade.setOutputVariable(flowController, "setpoint");
cascade.setExpression("output * flowGain + flowBias");
process.add(cascade);
// Maintain fuel/air ratio
Calculator ratioCalc = new Calculator("F/A Ratio");
ratioCalc.addInputVariable(fuelStream, "flowRate");
ratioCalc.setOutputVariable(airDamper, "position");
ratioCalc.setExpression("fuelFlow * stoichRatio * excessAir");
process.add(ratioCalc);
// Calculate required cooling duty
Calculator dutyCalc = new Calculator("Cooling Duty");
dutyCalc.addInputVariable(inletStream, "temperature");
dutyCalc.addInputVariable(inletStream, "flowRate");
dutyCalc.addInputVariable(inletStream, "heatCapacity");
dutyCalc.setOutputVariable(cooler, "duty");
dutyCalc.setExpression("-flowRate * heatCapacity * (targetTemp - inletTemp)");
process.add(dutyCalc);
Sets the value of a variable in a target equipment based on a source equipment. Used for feed-forward control or copying values between equipment.
import neqsim.process.equipment.util.SetPoint;
// Create set point to copy pressure
SetPoint setPoint = new SetPoint("Pressure Copy");
setPoint.setSourceVariable(sourceStream, "pressure");
setPoint.setTargetVariable(targetStream, "pressure");
// Add to process
process.add(setPoint);
| Equipment Type | Supported Variables |
|---|---|
Stream |
pressure, temperature |
ThrottlingValve |
pressure (outlet) |
Compressor |
pressure (outlet) |
Pump |
pressure (outlet) |
Heater/Cooler |
pressure, temperature |
Use setSourceValueCalculator to define a custom function that calculates the value to set on the target equipment:
SetPoint setPoint = new SetPoint("Custom SetPoint");
setPoint.setSourceVariable(sourceStream);
setPoint.setTargetVariable(targetStream, "pressure");
// Set target pressure based on source temperature: P = T / 10.0
setPoint.setSourceValueCalculator((equipment) -> {
Stream s = (Stream) equipment;
return s.getTemperature("K") / 10.0;
});
setPoint.run();
// Target pressure is now 30.0 bara (if source temp = 300 K)
| Method | Type | Description |
|---|---|---|
setSourceValueCalculator |
Function<ProcessEquipmentInterface, Double> |
Custom function to compute the value to set |
| Use Case | Example |
|---|---|
| Non-linear relationships | Pressure = f(temperature, flow) |
| Unit conversions | Convert from source units to target units |
| Computed ratios | Set valve to percentage of max flow |
| Conditional logic | Different values based on operating mode |
Smooth transition between operating states.
import neqsim.process.equipment.util.StreamTransition;
// Create transition
StreamTransition transition = new StreamTransition("Startup Ramp");
transition.setStream(feedStream);
transition.setInitialFlowRate(0.0, "kg/hr");
transition.setFinalFlowRate(10000.0, "kg/hr");
transition.setTransitionTime(3600.0); // 1 hour ramp
// Run transition
for (double t = 0; t < 3600; t += 60) {
transition.setTime(t);
process.run();
}
Documentation for controllers, adjusters, recycles, and process logic in NeqSim.
Location: neqsim.process.equipment.util, neqsim.process.controllerdevice, neqsim.process.logic
Classes:
Adjuster - Adjust variable to meet specificationRecycle - Handle recycle streamsSetter - Set variable valuesCalculator - Custom calculationsPIDController - PID controlProcessLogicController - Conditional logicAdjusters modify one variable to achieve a target specification.
import neqsim.process.equipment.util.Adjuster;
// Adjust heater duty to achieve target outlet temperature
Adjuster tempControl = new Adjuster("TC-100");
tempControl.setAdjustedVariable(heater, "outTemperature");
tempControl.setTargetVariable(stream, "temperature", 80.0, "C");
process.add(tempControl);
| Equipment | Variable | Description |
|---|---|---|
| Heater/Cooler | "duty" |
Heat duty |
| Heater/Cooler | "outTemperature" |
Outlet temperature |
| Compressor | "outletPressure" |
Discharge pressure |
| Valve | "outletPressure" |
Outlet pressure |
| Splitter | "splitFactor" |
Split ratio |
| Stream | "flowRate" |
Flow rate |
| Equipment | Variable | Description |
|---|---|---|
| Stream | "temperature" |
Temperature |
| Stream | "pressure" |
Pressure |
| Stream | "flowRate" |
Flow rate |
| Stream | "moleFraction" |
Component mole fraction |
| Separator | "liquidLevel" |
Liquid level |
// Adjust cooler to achieve hydrocarbon dew point
Adjuster dewPointControl = new Adjuster("Dew Point Controller");
dewPointControl.setAdjustedVariable(cooler, "outTemperature");
dewPointControl.setTargetPhaseCondition(stream, "dewpoint", 50.0, "bara");
process.add(dewPointControl);
adjuster.setMaximumIterations(50);
adjuster.setTolerance(1e-6);
adjuster.setMinimumValue(-1e6); // Duty lower bound
adjuster.setMaximumValue(1e6); // Duty upper bound
Handle recycle streams in process flowsheets.
import neqsim.process.equipment.util.Recycle;
// Define recycle
Recycle recycle = new Recycle("Solvent Recycle");
recycle.addStream(recycleStream);
recycle.setOutletStream(inletMixer);
recycle.setTolerance(1e-6);
process.add(recycle);
ProcessSystem process = new ProcessSystem();
// Feed
process.add(feed);
// Mixer (combines feed and recycle)
Mixer mixer = new Mixer("M-100");
mixer.addStream(feed);
process.add(mixer);
// Process equipment
process.add(reactor);
process.add(separator);
// Splitter for recycle
Splitter splitter = new Splitter("Splitter", separator.getLiquidOutStream());
splitter.setSplitFactors(new double[]{0.9, 0.1}); // 10% recycle
process.add(splitter);
// Recycle stream
Recycle recycle = new Recycle("Recycle");
recycle.addStream(splitter.getSplitStream(1));
recycle.setOutletStream(mixer);
process.add(recycle);
// Connect mixer to recycle
mixer.addStream(recycle.getOutletStream());
process.run();
recycle.setTolerance(1e-6);
recycle.setMaximumIterations(100);
// Acceleration methods
recycle.setAccelerationMethod("wegstein");
// Options: "direct", "wegstein", "broyden"
Set variable values directly.
import neqsim.process.equipment.util.Setter;
// Set flow rate
Setter flowSetter = new Setter("Flow Setter", stream);
flowSetter.setVariable("flowRate", 1000.0, "kg/hr");
process.add(flowSetter);
import neqsim.process.equipment.util.MoleFractionSetter;
// Set component mole fraction
MoleFractionSetter compSetter = new MoleFractionSetter("CO2 Setter", stream);
compSetter.setMoleFraction("CO2", 0.02);
process.add(compSetter);
Perform custom calculations.
import neqsim.process.equipment.util.Calculator;
Calculator calc = new Calculator("Energy Balance");
calc.addInputVariable(stream1);
calc.addInputVariable(stream2);
calc.setOutputVariable(heater, "duty");
// Custom calculation (override in subclass or use expression)
calc.setExpression("stream1.enthalpy - stream2.enthalpy");
process.add(calc);
For dynamic simulation with feedback control.
import neqsim.process.controllerdevice.PIDController;
PIDController levelControl = new PIDController("LC-100");
levelControl.setMeasuredVariable(separator, "liquidLevel");
levelControl.setControlledVariable(valve, "opening");
levelControl.setSetPoint(0.5); // 50% level
// Tuning parameters
levelControl.setKp(2.0); // Proportional gain
levelControl.setKi(0.1); // Integral gain (1/s)
levelControl.setKd(0.0); // Derivative gain (s)
process.add(levelControl);
// Action
levelControl.setReverseAction(true); // Increase output decreases PV
// Output limits
levelControl.setOutputMin(0.0);
levelControl.setOutputMax(100.0);
// Anti-windup
levelControl.setAntiWindup(true);
// Run transient with controllers
for (double t = 0; t < 3600; t += 1.0) {
process.runTransient();
double pv = levelControl.getProcessVariable();
double sp = levelControl.getSetPoint();
double out = levelControl.getOutput();
System.out.printf("%.1f, %.3f, %.3f, %.1f%n", t, pv, sp, out);
}
Conditional logic for process decisions.
import neqsim.process.logic.ProcessLogicController;
ProcessLogicController logic = new ProcessLogicController("Emergency Logic");
// Define condition
logic.setCondition(pressure, ">", 100.0, "bara");
// Define action
logic.setAction(shutoffValve, "close");
process.add(logic);
// AND condition
logic.addCondition(pressure, ">", 100.0, "bara", "AND");
logic.addCondition(temperature, ">", 150.0, "C", "AND");
// OR condition
logic.addCondition(level, "<", 0.1, "ratio", "OR");
logic.addCondition(level, ">", 0.9, "ratio", "OR");
import neqsim.process.alarm.ProcessAlarmManager;
ProcessAlarmManager alarms = process.getAlarmManager();
// High pressure alarm
alarms.addAlarm(separator, "pressure", 95.0, "high", "bara");
alarms.addAlarm(separator, "pressure", 100.0, "highHigh", "bara");
// Low level alarm
alarms.addAlarm(separator, "liquidLevel", 0.2, "low", "ratio");
ProcessSystem process = new ProcessSystem();
// Feed stream
Stream feed = new Stream("Feed", feedFluid);
feed.setFlowRate(1000.0, "kg/hr");
process.add(feed);
// Heater with temperature control
Heater heater = new Heater("E-100", feed);
process.add(heater);
Adjuster tempControl = new Adjuster("TC-100");
tempControl.setAdjustedVariable(heater, "duty");
tempControl.setTargetVariable(heater.getOutletStream(), "temperature", 80.0, "C");
process.add(tempControl);
// Separator with level control
Separator separator = new Separator("V-100", heater.getOutletStream());
process.add(separator);
ThrottlingValve liquidValve = new ThrottlingValve("LV-100", separator.getLiquidOutStream());
liquidValve.setOutletPressure(5.0, "bara");
process.add(liquidValve);
// Level controller (for dynamic)
PIDController levelControl = new PIDController("LC-100");
levelControl.setMeasuredVariable(separator, "liquidLevel");
levelControl.setControlledVariable(liquidValve, "opening");
levelControl.setSetPoint(0.5);
levelControl.setKp(5.0);
levelControl.setKi(0.5);
process.add(levelControl);
// Pressure control
ThrottlingValve gasValve = new ThrottlingValve("PV-100", separator.getGasOutStream());
process.add(gasValve);
Adjuster pressControl = new Adjuster("PC-100");
pressControl.setAdjustedVariable(gasValve, "outletPressure");
pressControl.setTargetVariable(separator, "pressure", 20.0, "bara");
process.add(pressControl);
// Run steady state
process.run();
// Run dynamic
for (double t = 0; t < 3600; t += 1.0) {
// Disturbance at t=600
if (Math.abs(t - 600) < 0.5) {
feed.setFlowRate(1200.0, "kg/hr");
}
process.runTransient();
}
NeqSim contains a flexible process control framework for dynamic simulations. The framework provides:
ControllerDeviceBaseClass implementing proportional,
integral and derivative actions with anti-windup, derivative filtering and
configurable output limits.ControlStructureInterface for multi‑loop coordination.See the unit tests in src/test/java/neqsim/process/controllerdevice for examples
of how the controllers and control structures are used in simulations.
The ModelPredictiveController
class adds multivariable model predictive control (MPC) to the framework. The
controller uses a first-order process model with configurable gain, time
constant, bias and prediction horizon to calculate an optimal control move that
balances tracking accuracy, absolute energy usage and aggressive movement. MPC
integrates with the rest of the process-control package through the common
ControllerDeviceInterface, allowing it to replace or work alongside
traditional PID loops.
MeasurementDeviceInterface (for example a temperature or pressure
transmitter) via setTransmitter. The MPC will read samples from the device
whenever runTransient is invoked.setControllerSetPoint, describe the internal process model with
setProcessModel and setProcessBias, then choose a prediction horizon and
tuning weights with setPredictionHorizon and setWeights.setOutputLimits to cap the actuator
and setPreferredControlValue to encode an economic target such as minimum
heater duty.runTransient(previousControl, dt)
every control interval. The return value from getResponse() is the new
manipulated-variable value to apply to the process.ModelPredictiveController controller = new ModelPredictiveController("heaterMpc");
controller.setTransmitter(temperatureSensor);
controller.setControllerSetPoint(328.15, "K");
controller.setProcessModel(0.18, 45.0); // gain, time constant [s]
controller.setProcessBias(298.15);
controller.setPredictionHorizon(20);
controller.setWeights(1.0, 0.03, 0.2); // tracking, energy, move penalties
controller.setPreferredControlValue(20.0);
controller.setOutputLimits(0.0, 100.0);
double manipulated = controller.getResponse();
for (double t = 0.0; t < 1800.0; t += 5.0) {
controller.runTransient(manipulated, 5.0);
manipulated = controller.getResponse();
heater.setDuty(manipulated, "kW");
}
The single-input mode automatically handles reverse-acting processes via
setReverseActing(true) and can be paused/resumed with setActive(false).
For flowsheets with several manipulated variables the MPC is configured with an ordered control vector:
controller.configureControls("dewPointCooler", "stabiliserHeater", "compressorSpeed");
controller.setInitialControlValues(6.0, 65.0, 0.78);
controller.setControlLimits("dewPointCooler", -10.0, 25.0);
controller.setControlLimits("stabiliserHeater", 40.0, 90.0);
controller.setControlLimits("compressorSpeed", 0.5, 1.05);
controller.setControlWeights(0.6, 0.2, 0.05); // energy usage penalty
controller.setMoveWeights(0.2, 0.05, 0.02); // movement smoothing
controller.setPreferredControlVector(0.0, 55.0, 0.8);
getControlVector() returns the most recent actuation proposal for all
manipulated variables, while setPrimaryControlIndex determines which entry is
exposed via getResponse() for backwards compatibility with controller
structures expecting a single output.
MPC quality constraints describe how key product indicators respond to each manipulated variable and to feed composition/rate changes. Limits are handled as soft constraints, letting the optimiser trade off specification margin and energy usage.
ModelPredictiveController.QualityConstraint wobbeConstraint =
ModelPredictiveController.QualityConstraint.builder("WobbeIndex")
.measurement(wobbeTransmitter)
.unit("MJ/Sm3")
.limit(51.7)
.margin(0.2)
.controlSensitivity(0.04, -0.01, 0.03)
.compositionSensitivity("nitrogen", -2.8)
.rateSensitivity(0.005)
.build();
controller.addQualityConstraint(wobbeConstraint);
The controller stores predicted specification values for diagnostics via
getPredictedQuality. Call clearQualityConstraints() when the control
structure changes or before reconfiguring sensitivities.
updateFeedConditions injects the expected upstream composition and rate into
the next optimisation. Supplying these predictions enables proactive responses
to known feed changes and improves constraint tracking on multivariate systems.
controller.updateFeedConditions(Map.of(
"methane", 0.82,
"ethane", 0.08,
"propane", 0.03),
12.4); // kmol/hr
When the underlying process characteristics drift, enable the embedded moving horizon estimator so the internal model follows the plant:
controller.enableMovingHorizonEstimation(60); // keep the last 60 samples
After the estimator has gathered enough samples getLastMovingHorizonEstimate()
returns identified gain, time constant, bias and prediction error. Call
clearMovingHorizonHistory() to restart the identification window, or
disableMovingHorizonEstimation() to lock the controller to its current model.
Digital twins are most valuable when they continuously reconcile with plant data. The MPC supports blending measured values from a facility with simulated predictions:
ingestPlantSample(measurement, appliedControl, dt) each time a fresh
transmitter value arrives from the plant. The controller uses the injected
sample as the baseline for optimisation and feeds it into the moving-horizon
estimator, allowing the internal model to track real-world drift even when no
NeqSim MeasurementDeviceInterface is configured.updateQualityMeasurement("wobbe", value)
(or the map-based overload) to store the real measurement against the relevant
constraint. The MPC then combines that measured baseline with the process
sensitivities and feedforward model to predict how upcoming moves will affect
the specification.This approach allows existing plant instrumentation to update the MPC while the NeqSim model still contributes predictive behaviour for future disturbances.
getLastSampledValue(), getLastAppliedControl() and
getPredictionHorizon() to verify tuning.setControlLimits and
setOutputLimits to protect equipment.ControllerDeviceInterface.MovingHorizonEstimationExampleTest and
OffshoreProcessMpcIntegrationTest in the test suite for end-to-end
demonstrations covering adaptive tuning and constrained optimisation.Comprehensive guide to transient and dynamic simulation in NeqSim.
NeqSim supports both steady-state and transient (dynamic) simulation modes. Dynamic simulation enables modeling of:
| Scenario | Mode |
|---|---|
| Design calculations | Steady-state |
| Equipment sizing | Steady-state |
| Controller tuning | Transient |
| Startup/shutdown analysis | Transient |
| Safety studies (blowdown) | Transient |
| Slug catcher sizing | Transient |
| Training simulators | Transient |
In steady-state mode, each run() call calculates the equilibrium solution assuming constant boundary conditions:
ProcessSystem process = new ProcessSystem();
process.add(stream);
process.add(separator);
process.add(compressor);
// Steady-state - each run() finds equilibrium
process.run();
In transient mode, equipment remembers state between time steps. Enable by setting setCalculateSteadyState(false):
// Enable transient mode on equipment
separator.setCalculateSteadyState(false);
pipeline.setCalculateSteadyState(false);
// Time step through simulation
UUID id = UUID.randomUUID();
for (int step = 0; step < 100; step++) {
process.runTransient(1.0, id); // 1 second time step
}
| Aspect | Steady-State | Transient |
|---|---|---|
| State memory | None | Equipment retains state |
| Method | run() |
runTransient(dt, id) |
| Holdup | Ignored | Tracked (level, pressure) |
| Controllers | Converged | Time-stepping response |
| Initialization | Automatic | Requires steady-state first |
import neqsim.process.processmodel.ProcessSystem;
import neqsim.thermo.system.SystemSrkEos;
import java.util.UUID;
// 1. Create thermodynamic system
SystemInterface fluid = new SystemSrkEos(298.15, 50.0);
fluid.addComponent("methane", 0.9);
fluid.addComponent("ethane", 0.1);
fluid.setMixingRule("classic");
// 2. Build process flowsheet
Stream feed = new Stream("Feed", fluid);
feed.setFlowRate(1000.0, "kg/hr");
Separator separator = new Separator("V-100", feed);
separator.setInternalDiameter(2.0);
separator.setSeparatorLength(5.0);
ThrottlingValve gasValve = new ThrottlingValve("Gas Valve", separator.getGasOutStream());
gasValve.setOutletPressure(20.0, "bara");
ThrottlingValve liquidValve = new ThrottlingValve("Liq Valve", separator.getLiquidOutStream());
liquidValve.setOutletPressure(20.0, "bara");
// 3. Create process system
ProcessSystem process = new ProcessSystem();
process.add(feed);
process.add(separator);
process.add(gasValve);
process.add(liquidValve);
// 4. Initialize with steady-state
process.run();
System.out.println("Initial level: " + separator.getLiquidLevel());
// 5. Switch to transient mode
separator.setCalculateSteadyState(false);
// 6. Run transient simulation
UUID calcId = UUID.randomUUID();
double dt = 1.0; // 1 second time step
for (int step = 0; step < 60; step++) {
process.runTransient(dt, calcId);
if (step % 10 == 0) {
System.out.printf("t=%d s, Level=%.3f m, P=%.2f bara%n",
step, separator.getLiquidLevel(), separator.getPressure());
}
}
The time step should be small enough to capture dynamics but large enough for efficiency:
| Application | Typical Time Step |
|---|---|
| Fast control loops | 0.1 - 1.0 s |
| Separator level control | 1.0 - 10.0 s |
| Pipeline transients | 0.5 - 5.0 s |
| Reservoir depletion | 3600 s (1 hour) to 86400 s (1 day) |
| Blowdown studies | 0.1 - 1.0 s |
For pipeline transients, the time step should satisfy the CFL (Courant-Friedrichs-Lewy) condition:
$$\Delta t \leq \frac{\Delta x}{v + c}$$
Where:
double t = 0;
double tEnd = 3600; // 1 hour
double dt = 1.0;
while (t < tEnd) {
process.runTransient(dt, calcId);
// Adjust time step based on rate of change
double rateOfChange = Math.abs(separator.getLiquidLevel() - previousLevel) / dt;
if (rateOfChange > 0.1) {
dt = Math.max(0.1, dt / 2); // Decrease time step
} else if (rateOfChange < 0.01) {
dt = Math.min(10.0, dt * 1.5); // Increase time step
}
t += dt;
previousLevel = separator.getLiquidLevel();
}
Separator separator = new Separator("V-100", feed);
separator.setInternalDiameter(2.0);
separator.setSeparatorLength(5.0);
separator.setLiquidLevel(0.5); // Initial level (m)
separator.setCalculateSteadyState(false);
// During transient, level changes based on in/out flow imbalance
separator.runTransient(dt, id);
double newLevel = separator.getLiquidLevel();
PipeBeggsAndBrills pipe = new PipeBeggsAndBrills("Pipeline", feed);
pipe.setLength(10000); // 10 km
pipe.setDiameter(0.3); // 0.3 m
pipe.setNumberOfIncrements(50);
pipe.setCalculateSteadyState(false);
// Transient tracks pressure waves and liquid holdup
pipe.runTransient(dt, id);
Tank tank = new Tank("T-100", feed);
tank.setVolume(100.0); // m³
tank.setCalculateSteadyState(false);
// Tank pressure/composition evolves based on in/out flows
tank.runTransient(dt, id);
Compressor comp = new Compressor("K-100", feed);
comp.setOutletPressure(100.0, "bara");
// Enable dynamic features (state machine, events)
comp.setDynamicSimulationEnabled(true);
comp.setRotorInertia(100.0); // kg·m²
// Compressor responds to speed changes, surge, etc.
comp.runTransient(dt, id);
SimpleReservoir reservoir = new SimpleReservoir("Field");
reservoir.setReservoirFluid(fluid);
reservoir.setGasVolume(1e9, "Sm3");
StreamInterface producer = reservoir.addGasProducer("GP-1");
producer.setFlowRate(5.0, "MSm3/day");
// Run for one day
reservoir.runTransient(86400, id);
double remainingGas = reservoir.getGasInPlace("Sm3");
Controllers require transmitters to measure process variables:
// Level transmitter
LevelTransmitter levelTransmitter = new LevelTransmitter("LT-100", separator);
levelTransmitter.setMinimumValue(0.0);
levelTransmitter.setMaximumValue(2.0);
// Level controller
ControllerDeviceBaseClass levelController = new ControllerDeviceBaseClass("LC-100");
levelController.setTransmitter(levelTransmitter);
levelController.setControllerSetPoint(0.5); // 0.5 m target level
levelController.setControllerParameters(0.5, 100.0, 0.0); // Kp, Ti, Td
levelController.setReverseActing(true); // Open valve to lower level
// Connect controller to valve
liquidValve.setController(levelController);
// Pressure transmitter
PressureTransmitter pressureTransmitter = new PressureTransmitter("PT-100", separator);
pressureTransmitter.setMinimumValue(0.0);
pressureTransmitter.setMaximumValue(100.0);
// Pressure controller
ControllerDeviceBaseClass pressureController = new ControllerDeviceBaseClass("PC-100");
pressureController.setTransmitter(pressureTransmitter);
pressureController.setControllerSetPoint(50.0); // 50 bara target
pressureController.setControllerParameters(1.0, 50.0, 0.0);
gasValve.setController(pressureController);
// Volume flow transmitter
VolumeFlowTransmitter flowTransmitter = new VolumeFlowTransmitter("FT-100", feed);
flowTransmitter.setMeasuredPhase("gas");
flowTransmitter.setMinimumValue(0.0);
flowTransmitter.setMaximumValue(10000.0);
ControllerDeviceBaseClass flowController = new ControllerDeviceBaseClass("FC-100");
flowController.setTransmitter(flowTransmitter);
flowController.setControllerSetPoint(5000.0); // Target flow
flowController.setControllerParameters(0.1, 200.0, 0.0);
inletValve.setController(flowController);
// PID parameters
double Kp = 1.0; // Proportional gain
double Ti = 100.0; // Integral time (seconds)
double Td = 0.0; // Derivative time (seconds)
controller.setControllerParameters(Kp, Ti, Td);
// Action direction
controller.setReverseActing(true); // Increase output to decrease PV
controller.setReverseActing(false); // Increase output to increase PV
The TransientPipe class provides drift-flux based transient simulation:
import neqsim.fluidmechanics.flowsystem.twophaseflowsystem.twophasepipeflowsystem.TwoPhasePipeFlowSystem;
// Create transient pipeline
TransientPipe pipeline = new TransientPipe("FL-100", feed);
pipeline.setLength(5000.0); // 5 km
pipeline.setDiameter(0.25);
pipeline.setNumberOfNodes(50);
pipeline.setElevationProfile(elevations); // Terrain effects
// Configure flow regime detection
pipeline.setFlowRegimeDetectionMethod("mechanistic");
// Run transient
pipeline.setCalculateSteadyState(false);
for (int step = 0; step < 600; step++) {
pipeline.runTransient(1.0, id);
// Track slugs
SlugTracker slugs = pipeline.getSlugTracker();
System.out.println("Active slugs: " + slugs.getSlugCount());
}
// Get slug statistics
SlugTracker tracker = pipeline.getSlugTracker();
int slugCount = tracker.getSlugCount();
double avgSlugLength = tracker.getAverageSlugLength();
double maxSlugVolume = tracker.getMaxSlugVolume();
double slugFrequency = tracker.getSlugFrequency();
System.out.printf("Slugs: %d, Avg length: %.1f m, Frequency: %.2f /min%n",
slugCount, avgSlugLength, slugFrequency * 60);
// Create vessel with initial conditions
SystemInterface vesselFluid = new SystemSrkEos(350.0, 100.0);
vesselFluid.addComponent("methane", 0.8);
vesselFluid.addComponent("ethane", 0.15);
vesselFluid.addComponent("propane", 0.05);
vesselFluid.setMixingRule("classic");
Tank vessel = new Tank("V-100", vesselFluid);
vessel.setVolume(50.0); // 50 m³
// Blowdown valve to atmosphere
ThrottlingValve blowdownValve = new ThrottlingValve("BDV", vessel.getOutletStream());
blowdownValve.setOutletPressure(1.5, "bara");
blowdownValve.setCv(50.0);
ProcessSystem blowdown = new ProcessSystem();
blowdown.add(vessel);
blowdown.add(blowdownValve);
// Initialize
blowdown.run();
vessel.setCalculateSteadyState(false);
// Run blowdown for 15 minutes
UUID id = UUID.randomUUID();
double dt = 0.5; // 0.5 second steps
ArrayList<double[]> results = new ArrayList<>();
for (double t = 0; t <= 900; t += dt) {
blowdown.runTransient(dt, id);
results.add(new double[] {
t,
vessel.getPressure(),
vessel.getTemperature() - 273.15, // °C
blowdownValve.getFlowRate("kg/hr")
});
}
// Report minimum temperature (for MDMT assessment)
double minTemp = results.stream()
.mapToDouble(r -> r[2])
.min()
.orElse(Double.NaN);
System.out.println("Minimum temperature: " + minTemp + " °C");
// Add heat input for fire case
vessel.setHeatInput(1000000.0); // 1 MW fire load
for (double t = 0; t <= 900; t += dt) {
blowdown.runTransient(dt, id);
// Check for two-phase relief (wetted surface)
double liquidFraction = vessel.getLiquidVolumeFraction();
if (liquidFraction > 0) {
System.out.println("Two-phase relief at t=" + t);
}
}
The calculation identifier (UUID) ensures all equipment in a process system is synchronized during transient runs:
UUID calcId = UUID.randomUUID();
// All equipment should have same ID after runTransient
process.runTransient(dt, calcId);
// Verify synchronization
for (ProcessEquipmentInterface eq : process.getUnitOperations()) {
if (!eq.getCalculationIdentifier().equals(calcId)) {
System.err.println("Equipment out of sync: " + eq.getName());
}
}
// After transient step, check all equipment updated
UUID expectedId = process.getCalculationIdentifier();
boolean allSynced = true;
for (ProcessEquipmentInterface eq : process.getUnitOperations()) {
if (!eq.getCalculationIdentifier().equals(expectedId)) {
allSynced = false;
System.err.println("Stale: " + eq.getName());
}
}
if (!allSynced) {
// Reinitialize process
process.run();
}
// CORRECT: Initialize first
process.run(); // Steady-state initialization
separator.setCalculateSteadyState(false);
process.runTransient(dt, id);
// WRONG: Skip initialization
separator.setCalculateSteadyState(false);
process.runTransient(dt, id); // May fail or give wrong results
// Equipment that needs transient mode:
separator.setCalculateSteadyState(false); // Has holdup
tank.setCalculateSteadyState(false); // Has volume
pipeline.setCalculateSteadyState(false); // Has segments
// Equipment that typically stays steady-state:
// - Streams (boundary conditions)
// - Heat exchangers (fast thermal equilibrium)
// - Compressors (unless modeling inertia)
double dt = 1.0; // 1 second time step
// Controller integral time should be >> dt
double Ti = 100.0; // 100 seconds >> 1 second
// Avoid Ti close to dt (causes oscillation)
// Ti = 1.0 would be problematic with dt = 1.0
try (PrintWriter log = new PrintWriter("transient.csv")) {
log.println("time,pressure,temperature,level,flow");
for (double t = 0; t < tEnd; t += dt) {
process.runTransient(dt, id);
log.printf("%.1f,%.2f,%.2f,%.3f,%.1f%n",
t,
separator.getPressure(),
separator.getTemperature("C"),
separator.getLiquidLevel(),
gasValve.getFlowRate("kg/hr"));
}
}
try {
process.run();
separator.setCalculateSteadyState(false);
for (int step = 0; step < 100; step++) {
process.runTransient(dt, id);
}
} finally {
// Reset to steady-state for future runs
separator.setCalculateSteadyState(true);
}
// Initialize at 1000 kg/hr
feed.setFlowRate(1000.0, "kg/hr");
process.run();
separator.setCalculateSteadyState(false);
// Step change to 1500 kg/hr
feed.setFlowRate(1500.0, "kg/hr");
// Observe response
for (int step = 0; step < 300; step++) {
process.runTransient(1.0, id);
System.out.printf("t=%d s, Level=%.3f m%n", step, separator.getLiquidLevel());
}
See transient_slug_separator_control_example.md
Compressor comp = new Compressor("K-100", feed);
comp.setDynamicSimulationEnabled(true);
comp.setRotorInertia(150.0);
// Start from standby
comp.setState(CompressorState.STANDBY);
process.run();
// Initiate startup
comp.startStartupSequence();
for (int step = 0; step < 600; step++) {
process.runTransient(0.5, id);
System.out.printf("t=%.1f s, Speed=%.0f RPM, State=%s%n",
step * 0.5,
comp.getSpeed(),
comp.getState());
if (comp.getState() == CompressorState.RUNNING) {
System.out.println("Startup complete at t=" + step * 0.5 + " s");
break;
}
}
These Colab notebooks provide hands-on dynamic simulation examples:
| Notebook | Description |
|---|---|
| Dynamic Simulation Basics | Introduction to transient simulation |
| Dynamic Compressor | Compressor dynamics and control |
| Single Component Dynamics | Single component transient behavior |
| Dynamic Separator | Separator level dynamics and control |
Dynamic process behavior in NeqSim is validated through ProcessSystemRunTransientTest, which assembles streams, valves, separators, transmitters, and controllers before stepping a transient solver. Reusing the tested scaffolding ensures that custom flowsheets converge and maintain synchronized calculation identifiers across unit operations.
The first scenario creates a single feed stream, lets it down through a valve into a separator, and attaches a flow controller to the inlet valve based on a volume-flow transmitter.【F:src/test/java/neqsim/process/processmodel/ProcessSystemRunTransientTest.java†L58-L120】 Key steps reproduced below:
Stream, set mass flow and pressure, and connect it to a ThrottlingValve with a target outlet pressure.Separator, configure geometry (diameter, length) and initial liquid level.VolumeFlowTransmitter to the inlet stream, wire it to a ControllerDeviceBaseClass, and assign the controller to the inlet valve.p.run()), choose a transient timestep, and iterate runTransient() to observe controller action converging toward the setpoint (73.5 kg/hr in the test).The assertions in the test check that every unit operation shares the same calculation identifier during the loop and that the transmitter stabilizes near the requested flow, confirming correct coupling of controller logic and transport equations.【F:src/test/java/neqsim/process/processmodel/ProcessSystemRunTransientTest.java†L106-L120】 Use this template when debugging control loops or valve responses in your own cases.
A second scenario adds a purge stream to the separator, introduces level and pressure transmitters, and binds each to a dedicated controller that manipulates the liquid and gas outlet valves respectively.【F:src/test/java/neqsim/process/processmodel/ProcessSystemRunTransientTest.java†L122-L232】 After a steady-state start, the process is marched forward with a 10-second timestep and the level transmitter reading is checked against the 0.45 m setpoint, demonstrating how controller gains drive the separator toward balanced holdup.
When replicating this pattern:
setMaximumValue, setMinimumValue) to guard against unrealistic signals.setCalculateSteadyState(false) on dynamic equipment to ensure the transient integrator, not a steady solver, advances the state.runTransient() to confirm that all equipment is synchronized before trusting control trajectories.Throughout the transient loops the test asserts that each unit operation's getCalculationIdentifier() matches the process system's identifier.【F:src/test/java/neqsim/process/processmodel/ProcessSystemRunTransientTest.java†L114-L118】【F:src/test/java/neqsim/process/processmodel/ProcessSystemRunTransientTest.java†L221-L229】 This guards against stale states or partial updates when complex equipment is added. If you see divergence, reinitialize the process system or inspect units for disabled steady-state flags.
NeqSim provides a comprehensive mechanical design framework for sizing and specifying process equipment according to industry standards. This document describes the architecture, usage patterns, and JSON export capabilities.
The mechanical design system calculates:
MechanicalDesign (base class)
├── SeparatorMechanicalDesign → ASME VIII / API 12J
├── GasScrubberMechanicalDesign → ASME VIII / API 12J
├── CompressorMechanicalDesign → API 617
├── PumpMechanicalDesign → API 610
├── ValveMechanicalDesign → IEC 60534 / ANSI/ISA-75
├── ExpanderMechanicalDesign → API 617
├── TankMechanicalDesign → API 650/620
├── HeatExchangerMechanicalDesign → TEMA
├── PipelineMechanicalDesign → ASME B31.3
├── AdsorberMechanicalDesign → ASME VIII
├── AbsorberMechanicalDesign → ASME VIII
├── EjectorMechanicalDesign → HEI
└── SafetyValveMechanicalDesign → API 520/521
MechanicalDesignResponse (base class)
├── CompressorMechanicalDesignResponse
├── PumpMechanicalDesignResponse
├── ValveMechanicalDesignResponse
├── SeparatorMechanicalDesignResponse
└── HeatExchangerMechanicalDesignResponse
SystemMechanicalDesign
└── Aggregates all equipment in a ProcessSystem
├── Total weights and volumes
├── Weight breakdown by equipment type
├── Weight breakdown by discipline
├── Utility requirements summary
└── Equipment list with design parameters
// Create and run process equipment
SystemInterface fluid = new SystemSrkEos(298.0, 50.0);
fluid.addComponent("methane", 0.9);
fluid.addComponent("ethane", 0.1);
fluid.setMixingRule("classic");
Stream inlet = new Stream("feed", fluid);
inlet.setFlowRate(10000.0, "kg/hr");
inlet.run();
Separator separator = new Separator("V-100", inlet);
separator.run();
// Access mechanical design
MechanicalDesign mecDesign = separator.getMechanicalDesign();
// Set design standards (optional - uses defaults if not specified)
mecDesign.setCompanySpecificDesignStandards("Equinor");
// Calculate design
mecDesign.calcDesign();
// Access results
double weight = mecDesign.getWeightTotal(); // kg
double wallThickness = mecDesign.getWallThickness(); // mm
double innerDiameter = mecDesign.getInnerDiameter(); // m
double length = mecDesign.getTantanLength(); // m
double designPressure = mecDesign.getMaxDesignPressure(); // bara
// Display results in GUI
mecDesign.displayResults();
// Build a process system
ProcessSystem process = new ProcessSystem();
process.add(inlet);
process.add(separator);
process.add(gasStream);
process.add(compressor);
process.add(cooler);
process.add(outlet);
process.run();
// Create system mechanical design
SystemMechanicalDesign sysMecDesign = new SystemMechanicalDesign(process);
// Set company standards for all equipment
sysMecDesign.setCompanySpecificDesignStandards("Equinor");
// Run design calculations for all equipment
sysMecDesign.runDesignCalculation();
// Access aggregated results
double totalWeight = sysMecDesign.getTotalWeight(); // kg
double totalVolume = sysMecDesign.getTotalVolume(); // m³
double plotSpace = sysMecDesign.getTotalPlotSpace(); // m²
double powerRequired = sysMecDesign.getTotalPowerRequired(); // kW
double heatingDuty = sysMecDesign.getTotalHeatingDuty(); // kW
double coolingDuty = sysMecDesign.getTotalCoolingDuty(); // kW
// Get breakdowns
Map<String, Double> weightByType = sysMecDesign.getWeightByEquipmentType();
Map<String, Double> weightByDiscipline = sysMecDesign.getWeightByDiscipline();
Map<String, Integer> countByType = sysMecDesign.getEquipmentCountByType();
// Print summary report
System.out.println(sysMecDesign.generateSummaryReport());
// Calculate design
separator.getMechanicalDesign().calcDesign();
// Export to JSON
String json = separator.getMechanicalDesign().toJson();
// Example output:
/*
{
"name": "V-100",
"equipmentType": "Separator",
"equipmentClass": "Separator",
"designStandard": "ASME VIII / API 12J",
"isSystemLevel": false,
"totalWeight": 15420.5,
"vesselWeight": 8500.0,
"internalsWeight": 1200.0,
"pipingWeight": 2100.0,
"eiWeight": 1500.0,
"structuralWeight": 2120.5,
"maxDesignPressure": 55.0,
"maxDesignTemperature": 80.0,
"innerDiameter": 2.4,
"tangentLength": 7.2,
"wallThickness": 28.5,
"moduleLength": 10.0,
"moduleWidth": 5.0,
"moduleHeight": 4.5,
...
}
*/
SystemMechanicalDesign sysMecDesign = new SystemMechanicalDesign(process);
sysMecDesign.runDesignCalculation();
String json = sysMecDesign.toJson();
// Example output:
/*
{
"isSystemLevel": true,
"processName": "Gas Processing Unit",
"equipmentCount": 12,
"totalWeight": 185000.0,
"totalVolume": 450.5,
"totalPlotSpace": 1200.0,
"totalPowerRequired": 2500.0,
"totalPowerRecovered": 150.0,
"netPower": 2350.0,
"totalHeatingDuty": 500.0,
"totalCoolingDuty": 1800.0,
"footprintLength": 40.0,
"footprintWidth": 30.0,
"maxHeight": 15.0,
"weightByType": {
"Separator": 45000.0,
"Compressor": 85000.0,
"HeatExchanger": 25000.0,
"Valve": 5000.0,
"Pump": 12000.0,
"Other": 13000.0
},
"weightByDiscipline": {
"Mechanical": 120000.0,
"Piping": 35000.0,
"E&I": 18000.0,
"Structural": 12000.0
},
"equipmentList": [
{
"name": "V-100",
"type": "Separator",
"weight": 15420.5,
"designPressure": 55.0,
"designTemperature": 80.0,
"power": 0.0,
"duty": 0.0,
"dimensions": "ID 2.4m x TT 7.2m"
},
...
]
}
*/
For equipment-specific data, use the typed response:
// Compressor-specific response
CompressorMechanicalDesignResponse response =
(CompressorMechanicalDesignResponse) compressor.getMechanicalDesign().getResponse();
int stages = response.getNumberOfStages();
double impellerDiameter = response.getImpellerDiameter(); // mm
double tipSpeed = response.getTipSpeed(); // m/s
double driverPower = response.getDriverPower(); // kW
double tripSpeed = response.getTripSpeed(); // rpm
// Valve-specific response
ValveMechanicalDesignResponse valveResponse =
(ValveMechanicalDesignResponse) valve.getMechanicalDesign().getResponse();
int ansiClass = valveResponse.getAnsiPressureClass();
double cvMax = valveResponse.getCvMax();
double faceToFace = valveResponse.getFaceToFace(); // mm
String valveType = valveResponse.getValveType();
// Export to JSON
String json = sysMecDesign.toJson();
// Parse back to object
MechanicalDesignResponse parsed = MechanicalDesignResponse.fromJson(json);
// Access parsed data
double weight = parsed.getTotalWeight();
boolean isSystem = parsed.isSystemLevel();
// Get mechanical design response
MechanicalDesignResponse mecResponse = sysMecDesign.getResponse();
// Get process simulation JSON
String processJson = process.toJson();
// Merge into combined document
String combined = mecResponse.mergeWithEquipmentJson(processJson);
// Result has both "processData" and "mechanicalDesign" sections
SeparatorMechanicalDesign sepDesign =
(SeparatorMechanicalDesign) separator.getMechanicalDesign();
// Key parameters
double gasLoadFactor = sepDesign.getGasLoadFactor(); // K-factor
double retentionTime = sepDesign.getRetentionTime(); // seconds
double liquidLevelFraction = sepDesign.getFg(); // Fg factor
Design calculations include:
CompressorMechanicalDesign compDesign =
(CompressorMechanicalDesign) compressor.getMechanicalDesign();
// Key parameters
int stages = compDesign.getNumberOfStages();
double headPerStage = compDesign.getHeadPerStage(); // kJ/kg
double impellerDia = compDesign.getImpellerDiameter(); // mm
double tipSpeed = compDesign.getTipSpeed(); // m/s
double driverPower = compDesign.getDriverPower(); // kW
Design calculations include:
PumpMechanicalDesign pumpDesign =
(PumpMechanicalDesign) pump.getMechanicalDesign();
// Key parameters
double specificSpeed = pumpDesign.getSpecificSpeed();
double npshRequired = pumpDesign.getNpshRequired(); // m
double impellerDia = pumpDesign.getImpellerDiameter(); // mm
double driverPower = pumpDesign.getDriverPower(); // kW
Design calculations include:
ValveMechanicalDesign valveDesign =
(ValveMechanicalDesign) valve.getMechanicalDesign();
// Key parameters
double cvMax = valveDesign.getValveCvMax();
int ansiClass = valveDesign.getAnsiPressureClass();
double faceToFace = valveDesign.getFaceToFace(); // mm
double actuatorThrust = valveDesign.getRequiredActuatorThrust(); // N
Design calculations include:
Design calculations include:
Design calculations include:
The framework applies industry-standard margins:
| Parameter | Margin | Standard |
|---|---|---|
| Design Pressure | +10% above max operating | ASME VIII |
| Design Temperature | +30°C above max operating | ASME VIII |
| Driver Power (small) | +25% for < 22 kW | API 610/617 |
| Driver Power (medium) | +15% for 22-75 kW | API 610/617 |
| Driver Power (large) | +10% for > 75 kW | API 610/617 |
| Wall Thickness | +CA (corrosion allowance) | ASME VIII |
Each mechanical design class has an associated cost estimation class:
// Access cost estimate
UnitCostEstimateBaseClass costEstimate = mecDesign.getCostEstimate();
double equipmentCost = costEstimate.getEquipmentCost(); // USD
double installedCost = costEstimate.getInstalledCost(); // USD
Always run equipment before calculating design - The mechanical design uses process conditions from the simulation.
Set design standards early - Call setCompanySpecificDesignStandards() before calcDesign().
Use system-level design for complete estimates - SystemMechanicalDesign handles all equipment consistently.
Export JSON for documentation - The toJson() method provides comprehensive, structured output.
Verify critical parameters - Check that design pressure/temperature exceed operating conditions.
// 1. Create fluid system
SystemInterface fluid = new SystemSrkEos(298.0, 50.0);
fluid.addComponent("methane", 0.85);
fluid.addComponent("ethane", 0.10);
fluid.addComponent("propane", 0.05);
fluid.setMixingRule("classic");
// 2. Build process
Stream feed = new Stream("feed", fluid);
feed.setFlowRate(50000.0, "kg/hr");
Separator separator = new Separator("V-100", feed);
Stream gas = new Stream("gas", separator.getGasOutStream());
Compressor compressor = new Compressor("K-100", gas);
compressor.setOutletPressure(80.0, "bara");
Cooler cooler = new Cooler("E-100", compressor.getOutletStream());
cooler.setOutTemperature(40.0, "C");
ProcessSystem process = new ProcessSystem();
process.add(feed);
process.add(separator);
process.add(gas);
process.add(compressor);
process.add(cooler);
process.run();
// 3. Calculate mechanical design
SystemMechanicalDesign sysMecDesign = new SystemMechanicalDesign(process);
sysMecDesign.setCompanySpecificDesignStandards("Equinor");
sysMecDesign.runDesignCalculation();
// 4. Generate reports
System.out.println(sysMecDesign.generateSummaryReport());
// 5. Export JSON for documentation/integration
String json = sysMecDesign.toJson();
Files.write(Paths.get("mechanical_design.json"), json.getBytes());
// 6. Access specific equipment details
CompressorMechanicalDesignResponse compResponse =
(CompressorMechanicalDesignResponse) compressor.getMechanicalDesign().getResponse();
System.out.println("Compressor stages: " + compResponse.getNumberOfStages());
System.out.println("Driver power: " + compResponse.getDriverPower() + " kW");
NeqSim provides comprehensive support for international design standards used in process equipment mechanical design. The framework enables engineers to apply company-specific and international standards consistently across all equipment in a process simulation.
The StandardType enum catalogs 30+ international design standards organized by category:
| Category | Standards |
|---|---|
| Pressure Vessel Codes | ASME Section VIII Div.1/2, EN 13445, PD 5500, DNV-OS-F101 |
| Piping Codes | ASME B31.3, ASME B31.4, ASME B31.8, EN 13480, NORSOK L-002 |
| Process Design | NORSOK P-001, NORSOK P-002, API RP 14E, API RP 521 |
| Material Standards | ASTM A516, ASTM A106, EN 10028, NORSOK M-001 |
| Safety Standards | API RP 520, API RP 521, ISO 23251 |
import neqsim.process.mechanicaldesign.designstandards.StandardType;
// Get standard by code
StandardType standard = StandardType.fromCode("ASME-VIII-1");
// Get standard properties
String code = standard.getCode(); // "ASME-VIII-1"
String name = standard.getName(); // "ASME Section VIII Division 1"
String version = standard.getDefaultVersion(); // "2023"
String category = standard.getDesignStandardCategory(); // "pressure vessel design code"
// Check equipment applicability
boolean applies = standard.appliesTo("separator"); // true
boolean applies2 = standard.appliesTo("pump"); // false
// Get all standards for an equipment type
List<StandardType> applicable = StandardType.getApplicableStandards("compressor");
NeqSim uses category-based standard assignment to ensure appropriate standards are applied to each equipment type:
| Category Key | Description | Example Standards |
|---|---|---|
pressure vessel design code |
Pressure containment design | ASME VIII, EN 13445 |
separator process design |
Separator sizing rules | NORSOK P-002, API 12J |
compressor design |
Compressor design requirements | API 617, API 618 |
pipeline design codes |
Pipeline design | DNV-OS-F101, ASME B31.4 |
valve design |
Valve sizing and selection | API 6D, EN ISO 10497 |
material plate design |
Plate material selection | ASTM A516, EN 10028 |
material pipe design |
Pipe material selection | ASTM A106, API 5L |
The StandardRegistry class provides factory methods for creating DesignStandard instances:
import neqsim.process.mechanicaldesign.designstandards.StandardRegistry;
import neqsim.process.mechanicaldesign.designstandards.DesignStandard;
// Create a design standard from StandardType
DesignStandard standard = StandardRegistry.createStandard(StandardType.ASME_VIII_DIV1);
// Create with specific version
DesignStandard standard2 = StandardRegistry.createStandard(StandardType.NORSOK_P002, "Rev 3");
// Get recommended standards for equipment
List<StandardType> recommended = StandardRegistry.getRecommendedStandards("separator", "Equinor");
import neqsim.process.equipment.separator.Separator;
import neqsim.process.mechanicaldesign.MechanicalDesign;
import neqsim.process.mechanicaldesign.designstandards.StandardType;
// Create equipment
Separator separator = new Separator("HP Separator", feedStream);
// Get mechanical design
MechanicalDesign mechDesign = separator.getMechanicalDesign();
// Apply single standard
mechDesign.setDesignStandard(StandardType.ASME_VIII_DIV1);
// Apply standard with version
mechDesign.setDesignStandard(StandardType.NORSOK_P002, "Rev 3");
// Apply multiple standards
List<StandardType> standards = Arrays.asList(
StandardType.ASME_VIII_DIV1,
StandardType.NORSOK_P002,
StandardType.ASTM_A516
);
mechDesign.setDesignStandards(standards);
import neqsim.process.mechanicaldesign.SystemMechanicalDesign;
import neqsim.process.processmodel.ProcessSystem;
// Create process system
ProcessSystem process = new ProcessSystem();
process.add(separator);
process.add(compressor);
process.add(heatExchanger);
// Apply company standards to all equipment
SystemMechanicalDesign sysMechDesign = new SystemMechanicalDesign(process);
sysMechDesign.setCompanySpecificDesignStandards("Equinor");
// Run design calculations
sysMechDesign.runDesignCalculation();
Standards are applied hierarchically based on specificity:
1. Equipment-specific standard (highest priority)
↓
2. TORG project standards
↓
3. Company default standards
↓
4. NeqSim default standards (lowest priority)
NeqSim includes specialized design standard implementations:
| Class | Purpose |
|---|---|
PressureVesselDesignStandard |
ASME/EN pressure vessel calculations |
SeparatorDesignStandard |
Separator sizing per NORSOK/API |
CompressorDesignStandard |
Compressor design per API 617/618 |
PipelineDesignStandard |
Pipeline wall thickness per DNV/ASME |
MaterialPlateDesignStandard |
Plate material properties |
MaterialPipeDesignStandard |
Pipe material properties |
JointEfficiencyPlateStandard |
Weld joint efficiency factors |
GasScrubberDesignStandard |
Gas scrubber sizing rules |
AdsorptionDehydrationDesignStandard |
Dehydration unit design |
import neqsim.process.equipment.separator.Separator;
import neqsim.process.equipment.compressor.Compressor;
import neqsim.process.mechanicaldesign.SystemMechanicalDesign;
import neqsim.process.mechanicaldesign.designstandards.StandardType;
import neqsim.process.processmodel.ProcessSystem;
import neqsim.thermo.system.SystemSrkEos;
// Create fluid
SystemInterface fluid = new SystemSrkEos(298.15, 50.0);
fluid.addComponent("methane", 0.85);
fluid.addComponent("ethane", 0.10);
fluid.addComponent("propane", 0.05);
fluid.setMixingRule("classic");
// Create feed stream
Stream feed = new Stream("Feed", fluid);
feed.setFlowRate(10000, "kg/hr");
feed.setTemperature(25, "C");
feed.setPressure(50, "bara");
// Create equipment with standards
Separator separator = new Separator("HP Separator", feed);
separator.getMechanicalDesign().setDesignStandard(StandardType.ASME_VIII_DIV1);
separator.getMechanicalDesign().setDesignStandard(StandardType.NORSOK_P002);
Compressor compressor = new Compressor("Export Compressor", separator.getGasOutStream());
compressor.setOutletPressure(150, "bara");
compressor.getMechanicalDesign().setDesignStandard(StandardType.API_617);
// Build process
ProcessSystem process = new ProcessSystem();
process.add(feed);
process.add(separator);
process.add(compressor);
process.run();
// Run mechanical design
SystemMechanicalDesign sysMechDesign = new SystemMechanicalDesign(process);
sysMechDesign.runDesignCalculation();
// Get results
System.out.println("Total Weight: " + sysMechDesign.getTotalWeight() + " kg");
System.out.println("Total Volume: " + sysMechDesign.getTotalVolume() + " m³");
NeqSim supports loading mechanical design parameters from various data sources including databases and CSV files. This allows organizations to maintain centralized repositories of design data, material properties, and company-specific standards.
┌─────────────────────────────────────────────────────────────────┐
│ MechanicalDesignDataSource │
│ (Interface) │
├─────────────────────────────────────────────────────────────────┤
│ │ │
│ ┌────────────────────┼────────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌───────────────┐ ┌─────────────────┐ ┌──────────────────┐ │
│ │ Database │ │ CSV Data │ │ Standard-Based │ │
│ │ DataSource │ │ Source │ │ CSV DataSource │ │
│ └───────────────┘ └─────────────────┘ └──────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
The core interface for all design data sources:
public interface MechanicalDesignDataSource {
/**
* Get a design parameter value.
* @param category Parameter category (e.g., "material", "safety_factor")
* @param parameterName Parameter name (e.g., "tensile_strength")
* @param equipmentType Equipment type filter
* @return Parameter value as double
*/
double getParameter(String category, String parameterName, String equipmentType);
/**
* Get a string property.
*/
String getProperty(String category, String propertyName, String equipmentType);
/**
* Check if data source is available.
*/
boolean isAvailable();
/**
* Get the standard type this data source provides data for.
*/
StandardType getStandardType();
}
The DatabaseMechanicalDesignDataSource connects to the NeqSim database:
import neqsim.process.mechanicaldesign.data.DatabaseMechanicalDesignDataSource;
// Create database source (uses default neqsim database)
DatabaseMechanicalDesignDataSource dbSource = new DatabaseMechanicalDesignDataSource();
// Or specify connection
DatabaseMechanicalDesignDataSource dbSource = new DatabaseMechanicalDesignDataSource(
"jdbc:derby:neqsimthermodatabase",
"TechnicalRequirements_Process"
);
The primary table TechnicalRequirements_Process stores design parameters:
CREATE TABLE TechnicalRequirements_Process (
ID INTEGER PRIMARY KEY,
COMPANY VARCHAR(50),
CATEGORY VARCHAR(100),
PARAMETER_NAME VARCHAR(100),
EQUIPMENT_TYPE VARCHAR(50),
VALUE_NUMERIC DOUBLE,
VALUE_TEXT VARCHAR(255),
UNIT VARCHAR(20),
STANDARD_CODE VARCHAR(20),
VERSION VARCHAR(10),
NOTES VARCHAR(500)
);
-- Example data
INSERT INTO TechnicalRequirements_Process VALUES
(1, 'Equinor', 'safety_factor', 'pressure_margin', 'separator', 1.10, NULL, '-', 'NORSOK-P002', 'Rev3', NULL),
(2, 'Equinor', 'material', 'min_wall_thickness', 'pressure_vessel', 6.0, NULL, 'mm', 'ASME-VIII-1', '2023', NULL),
(3, 'Equinor', 'sizing', 'liquid_retention_time', 'separator', 180, NULL, 's', 'NORSOK-P002', 'Rev3', 'Minimum 3 minutes');
// Get numeric parameter
double pressureMargin = dbSource.getParameter("safety_factor", "pressure_margin", "separator");
// Get text property
String material = dbSource.getProperty("material", "default_plate_grade", "pressure_vessel");
// Check availability
if (dbSource.isAvailable()) {
// Use database source
}
Create CSV files for design parameters:
File: designdata/company_standards.csv
category,parameter_name,equipment_type,value_numeric,value_text,unit,standard_code
safety_factor,pressure_margin,separator,1.10,,−,NORSOK-P002
safety_factor,temperature_margin,all,25.0,,C,NORSOK-P001
material,default_plate_grade,pressure_vessel,,SA-516-70,,ASTM-A516
sizing,liquid_retention_time,separator,180.0,,s,NORSOK-P002
sizing,gas_velocity_factor,scrubber,0.07,,-,API-12J
import neqsim.process.mechanicaldesign.data.StandardBasedCsvDataSource;
import neqsim.process.mechanicaldesign.designstandards.StandardType;
// Load from file path
StandardBasedCsvDataSource csvSource = new StandardBasedCsvDataSource(
"path/to/company_standards.csv",
StandardType.NORSOK_P002
);
// Load from classpath resource
StandardBasedCsvDataSource csvSource = new StandardBasedCsvDataSource(
"designdata/equinor_standards.csv",
StandardType.NORSOK_P002
);
// Get parameters
double retentionTime = csvSource.getParameter("sizing", "liquid_retention_time", "separator");
For standards-specific data, use the enhanced format:
File: designdata/asme_viii_parameters.csv
standard_code,category,parameter_name,equipment_type,value,unit,version,notes
ASME-VIII-1,joint_efficiency,full_radiograph,all,1.0,-,2023,Category A and B joints
ASME-VIII-1,joint_efficiency,spot_radiograph,all,0.85,-,2023,Category A and B joints
ASME-VIII-1,joint_efficiency,no_radiograph,all,0.70,-,2023,Category A and B joints
ASME-VIII-1,material,min_tensile_strength,SA-516-70,485,MPa,2023,Grade 70
ASME-VIII-1,material,allowable_stress,SA-516-70,138,MPa,2023,At 100°C
import neqsim.process.mechanicaldesign.MechanicalDesign;
MechanicalDesign mechDesign = separator.getMechanicalDesign();
// Add database source
mechDesign.addDataSource(new DatabaseMechanicalDesignDataSource());
// Add CSV source
mechDesign.addDataSource(new StandardBasedCsvDataSource(
"designdata/norsok_p002.csv",
StandardType.NORSOK_P002
));
// Data sources are queried in order added (first match wins)
import neqsim.process.mechanicaldesign.SystemMechanicalDesign;
SystemMechanicalDesign sysMech = new SystemMechanicalDesign(process);
// Configure data sources for entire system
sysMech.addDataSource(new DatabaseMechanicalDesignDataSource());
sysMech.addDataSource(new StandardBasedCsvDataSource("company_stds.csv", StandardType.NORSOK_P001));
NeqSim looks for design data in these locations:
src/main/resources/designdata/./designdata/~/.neqsim/designdata/| File | Description |
|---|---|
asme_viii_materials.csv |
ASME Section VIII material allowables |
norsok_p002_sizing.csv |
NORSOK P-002 sizing parameters |
api_617_compressors.csv |
API 617 compressor requirements |
dnv_os_f101_pipeline.csv |
DNV pipeline design factors |
Implement the MechanicalDesignDataSource interface:
public class MyCompanyDataSource implements MechanicalDesignDataSource {
private Map<String, Double> parameters = new HashMap<>();
@Override
public double getParameter(String category, String parameterName, String equipmentType) {
String key = category + ":" + parameterName + ":" + equipmentType;
return parameters.getOrDefault(key, Double.NaN);
}
@Override
public String getProperty(String category, String propertyName, String equipmentType) {
// Implementation
return null;
}
@Override
public boolean isAvailable() {
return true;
}
@Override
public StandardType getStandardType() {
return StandardType.NORSOK_P001;
}
}
NeqSim validates data source values:
import neqsim.process.mechanicaldesign.data.DataSourceValidator;
// Validate a data source
DataSourceValidator validator = new DataSourceValidator();
List<String> errors = validator.validate(csvSource);
if (!errors.isEmpty()) {
for (String error : errors) {
System.err.println("Validation error: " + error);
}
}
Keep CSV files in version control alongside your simulations:
project/
├── simulations/
│ └── hp_separator_sizing.java
├── designdata/
│ ├── project_standards.csv
│ └── material_data.csv
└── README.md
Always reference standards by their StandardType code:
# Good
standard_code,category,parameter
NORSOK-P002,sizing,liquid_retention
# Avoid
standard_code,category,parameter
NORSOK P-002,sizing,liquid_retention
Norsok-P002,sizing,liquid_retention
Always include units in your data:
parameter_name,value,unit
min_wall_thickness,6.0,mm
design_pressure,50.0,barg
temperature_margin,25.0,C
Use multiple sources with appropriate priority:
// Priority order: company-specific → project-specific → defaults
mechDesign.addDataSource(new CsvDataSource("company_standards.csv")); // 1st priority
mechDesign.addDataSource(new CsvDataSource("project_overrides.csv")); // 2nd priority
mechDesign.addDataSource(new DatabaseMechanicalDesignDataSource()); // 3rd priority (fallback)
A TORG (Technical Requirements Document, also known as TR or Technical Requirements Governing Document) defines the standards, methods, and requirements to be used in process design for a specific project. NeqSim provides comprehensive support for loading, managing, and applying TORG requirements across process simulations.
┌──────────────────────────────────────────────────────────────────────────────┐
│ TORG Framework │
├──────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ TechnicalRequirementsDocument │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌─────────────┐ │ │
│ │ │ Project Info │ │ Standards │ │ Environmental│ │ Safety │ │ │
│ │ │ - projectId │ │ - pressure │ │ Conditions │ │ Factors │ │ │
│ │ │ - company │ │ - separator │ │ - minTemp │ │ - pressure │ │ │
│ │ │ - revision │ │ - pipeline │ │ - maxTemp │ │ - corrosion │ │ │
│ │ └──────────────┘ └──────────────┘ │ - seismic │ └─────────────┘ │ │
│ │ └──────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ TorgManager │ │
│ │ - load(projectId) - apply(torg, processSystem) │ │
│ │ - loadAndApply(id, system) - applyToEquipment(torg, equipment) │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌──────────────────────────┼──────────────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
│ │ CsvTorgData │ │ DatabaseTorg │ │ Custom │ │
│ │ Source │ │ DataSource │ │ DataSource │ │
│ └───────────────┘ └───────────────┘ └───────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
The TechnicalRequirementsDocument class represents a complete TORG with all project-specific requirements.
| Property | Description |
|---|---|
projectId |
Unique project identifier (e.g., "JOHAN-SVERDRUP-01") |
projectName |
Human-readable project name |
companyIdentifier |
Company code (e.g., "EQUINOR") |
revision |
Document revision (e.g., "Rev 3") |
issueDate |
Document issue date |
designLifeYears |
Design life in years |
TechnicalRequirementsDocument.EnvironmentalConditions env = torg.getEnvironmentalConditions();
double minAmbient = env.getMinAmbientTemperature(); // °C
double maxAmbient = env.getMaxAmbientTemperature(); // °C
double minSeawater = env.getMinSeawaterTemperature(); // °C
double maxSeawater = env.getMaxSeawaterTemperature(); // °C
String seismicZone = env.getSeismicZone();
String location = env.getLocation();
TechnicalRequirementsDocument.SafetyFactors safety = torg.getSafetyFactors();
double pressureSF = safety.getPressureSafetyFactor(); // e.g., 1.10
double tempMargin = safety.getTemperatureSafetyMargin(); // °C
double corrosion = safety.getCorrosionAllowance(); // mm
double wallTol = safety.getWallThicknessTolerance(); // fraction
double loadFactor = safety.getLoadFactor(); // multiplier
TechnicalRequirementsDocument.MaterialSpecifications mats = torg.getMaterialSpecifications();
String plateMaterial = mats.getDefaultPlateMaterial(); // e.g., "SA-516-70"
String pipeMaterial = mats.getDefaultPipeMaterial(); // e.g., "API-5L-X65"
double minDesignTemp = mats.getMinDesignTemperature(); // °C
double maxDesignTemp = mats.getMaxDesignTemperature(); // °C
boolean impactTest = mats.isRequireImpactTesting();
String materialStd = mats.getMaterialStandard(); // e.g., "ASTM"
Use the Builder pattern for flexible TORG creation:
import neqsim.process.mechanicaldesign.torg.TechnicalRequirementsDocument;
import neqsim.process.mechanicaldesign.designstandards.StandardType;
TechnicalRequirementsDocument torg = new TechnicalRequirementsDocument.Builder()
// Project identification
.projectId("TROLL-WEST-2025")
.projectName("Troll West Field Development")
.companyIdentifier("EQUINOR")
.revision("Rev 2")
.issueDate("2025-01-15")
.designLifeYears(25)
// Add design standards
.addStandard("pressure_vessel", StandardType.ASME_VIII_DIV1)
.addStandard("separator_process", StandardType.NORSOK_P002)
.addStandard("pipeline", StandardType.DNV_OS_F101)
.addStandard("compressor", StandardType.API_617)
// Environmental conditions
.environmentalConditions(new TechnicalRequirementsDocument.EnvironmentalConditions(
-30.0, // minAmbientTemp °C
35.0, // maxAmbientTemp °C
2.0, // minSeawaterTemp °C
20.0, // maxSeawaterTemp °C
"Zone 1", // seismicZone
"Norwegian Sea" // location
))
// Safety factors
.safetyFactors(new TechnicalRequirementsDocument.SafetyFactors(
1.10, // pressureSafetyFactor
25.0, // temperatureSafetyMargin °C
3.0, // corrosionAllowance mm
0.125, // wallThicknessTolerance (12.5%)
1.0 // loadFactor
))
// Material specifications
.materialSpecifications(new TechnicalRequirementsDocument.MaterialSpecifications(
"SA-516-70", // defaultPlateMaterial
"API-5L-X65", // defaultPipeMaterial
-46.0, // minDesignTemp °C (for impact testing)
150.0, // maxDesignTemp °C
true, // requireImpactTesting
"ASTM" // materialStandard
))
.build();
Create a CSV file for TORG data:
File: torg_projects.csv
project_id,project_name,company,revision,issue_date,design_life_years
TROLL-WEST-2025,Troll West Development,EQUINOR,Rev 2,2025-01-15,25
SNORRE-EXPANSION,Snorre Expansion Project,EQUINOR,Rev 1,2024-06-01,30
File: torg_standards.csv
project_id,category,standard_code,version,notes
TROLL-WEST-2025,pressure_vessel,ASME-VIII-1,2023,Primary code
TROLL-WEST-2025,separator_process,NORSOK-P002,Rev 3,Process sizing
TROLL-WEST-2025,pipeline,DNV-OS-F101,2021,Subsea pipelines
Loading from CSV:
import neqsim.process.mechanicaldesign.torg.CsvTorgDataSource;
import neqsim.process.mechanicaldesign.torg.TorgManager;
// Create CSV data source
CsvTorgDataSource csvSource = new CsvTorgDataSource("path/to/torg_projects.csv");
// Create manager and add source
TorgManager manager = new TorgManager();
manager.addDataSource(csvSource);
// Load TORG
Optional<TechnicalRequirementsDocument> optTorg = manager.load("TROLL-WEST-2025");
if (optTorg.isPresent()) {
TechnicalRequirementsDocument torg = optTorg.get();
System.out.println("Loaded: " + torg.getProjectName());
}
Load TORG from the NeqSim database:
import neqsim.process.mechanicaldesign.torg.DatabaseTorgDataSource;
// Create database source
DatabaseTorgDataSource dbSource = new DatabaseTorgDataSource();
// Or with custom connection
DatabaseTorgDataSource dbSource = new DatabaseTorgDataSource(
"jdbc:derby:neqsimthermodatabase"
);
// Add to manager
TorgManager manager = new TorgManager();
manager.addDataSource(dbSource);
// Load by company and project
Optional<TechnicalRequirementsDocument> torg =
manager.load("EQUINOR", "TROLL-WEST-2025");
-- Main TORG projects table
CREATE TABLE TORG_Projects (
PROJECT_ID VARCHAR(50) PRIMARY KEY,
PROJECT_NAME VARCHAR(200),
COMPANY VARCHAR(50),
REVISION VARCHAR(20),
ISSUE_DATE DATE,
DESIGN_LIFE INTEGER,
STATUS VARCHAR(20)
);
-- Standards mapping table
CREATE TABLE TORG_Standards (
ID INTEGER PRIMARY KEY,
PROJECT_ID VARCHAR(50) REFERENCES TORG_Projects(PROJECT_ID),
CATEGORY VARCHAR(50),
STANDARD_CODE VARCHAR(20),
VERSION VARCHAR(20),
NOTES VARCHAR(500)
);
-- Environmental conditions
CREATE TABLE TORG_Environment (
PROJECT_ID VARCHAR(50) PRIMARY KEY REFERENCES TORG_Projects(PROJECT_ID),
MIN_AMBIENT_TEMP DOUBLE,
MAX_AMBIENT_TEMP DOUBLE,
MIN_SEAWATER_TEMP DOUBLE,
MAX_SEAWATER_TEMP DOUBLE,
SEISMIC_ZONE VARCHAR(20),
LOCATION VARCHAR(100)
);
The TorgManager orchestrates TORG loading and application:
import neqsim.process.mechanicaldesign.torg.TorgManager;
TorgManager manager = new TorgManager();
// Add multiple data sources (checked in order)
manager.addDataSource(new CsvTorgDataSource("project_torg.csv"));
manager.addDataSource(new DatabaseTorgDataSource());
// Load TORG
Optional<TechnicalRequirementsDocument> optTorg = manager.load("PROJECT-001");
// Get active TORG (most recently loaded)
TechnicalRequirementsDocument activeTorg = manager.getActiveTorg();
// Load and apply in one step
boolean success = manager.loadAndApply("PROJECT-001", processSystem);
// Apply to specific equipment
manager.applyToEquipment(torg, separator);
import neqsim.process.processmodel.ProcessSystem;
import neqsim.process.mechanicaldesign.torg.TorgManager;
// Build process system
ProcessSystem process = new ProcessSystem();
process.add(feed);
process.add(separator);
process.add(compressor);
process.run();
// Load and apply TORG
TorgManager manager = new TorgManager();
manager.addDataSource(new CsvTorgDataSource("project_torg.csv"));
// This applies standards and parameters to all equipment
boolean applied = manager.loadAndApply("TROLL-WEST-2025", process);
if (applied) {
System.out.println("TORG applied successfully");
}
// Load TORG first
Optional<TechnicalRequirementsDocument> optTorg = manager.load("TROLL-WEST-2025");
if (optTorg.isPresent()) {
TechnicalRequirementsDocument torg = optTorg.get();
// Apply to entire system
manager.apply(torg, process);
// Or apply to specific equipment
manager.applyToEquipment(torg, separator);
manager.applyToEquipment(torg, compressor);
}
When a TORG is applied to equipment, the following are configured:
| Setting | Source | Applied To |
|---|---|---|
| Design standards | torg.getStandard(category) |
mechDesign.setDesignStandard() |
| Pressure safety factor | safetyFactors.getPressureSafetyFactor() |
mechDesign.setPressureMarginFactor() |
| Temperature margin | safetyFactors.getTemperatureSafetyMargin() |
Design temperature calculation |
| Corrosion allowance | safetyFactors.getCorrosionAllowance() |
mechDesign.setCorrosionAllowance() |
| Material grade | materialSpecs.getDefaultPlateMaterial() |
mechDesign.setMaterialDesignStandard() |
| Design life | torg.getDesignLifeYears() |
Fatigue and corrosion calculations |
// Generate summary of applied TORG
String summary = manager.generateSummary(torg, process);
System.out.println(summary);
Output:
TORG Summary: TROLL-WEST-2025
=============================
Project: Troll West Development
Company: EQUINOR
Revision: Rev 2
Design Life: 25 years
Applied Standards:
- pressure_vessel: ASME-VIII-1 (2023)
- separator_process: NORSOK-P002 (Rev 3)
- pipeline: DNV-OS-F101 (2021)
Equipment Coverage:
- HP Separator: Standards applied
- Export Compressor: Standards applied
- Subsea Pipeline: Standards applied
Environmental Conditions:
- Ambient Temperature: -30°C to 35°C
- Seawater Temperature: 2°C to 20°C
- Location: Norwegian Sea
Safety Factors:
- Pressure: 1.10
- Temperature Margin: 25°C
- Corrosion Allowance: 3.0 mm
Validate TORG completeness before applying:
import neqsim.process.mechanicaldesign.torg.TorgValidator;
TorgValidator validator = new TorgValidator();
List<String> issues = validator.validate(torg);
if (!issues.isEmpty()) {
System.out.println("TORG validation issues:");
for (String issue : issues) {
System.out.println(" - " + issue);
}
}
Each project should have exactly one TORG that governs all design:
// Good - single source of truth
TorgManager manager = new TorgManager();
manager.loadAndApply("PROJECT-001", process);
// Avoid - multiple conflicting TORGs
// manager.loadAndApply("PROJECT-001-TOPSIDES", process);
// manager.loadAndApply("PROJECT-001-SUBSEA", process);
Track TORG changes with revision numbers:
TechnicalRequirementsDocument torg = new TechnicalRequirementsDocument.Builder()
.projectId("PROJECT-001")
.revision("Rev 3") // Increment for changes
.issueDate("2025-01-06")
.build();
Always validate TORG against equipment:
// Run validation before final design
boolean isComplete = manager.validateTorgCoverage(torg, process);
if (!isComplete) {
throw new IllegalStateException("TORG does not cover all equipment types");
}
Log any deviations from TORG requirements:
// If equipment requires non-standard settings
mechDesign.setDesignStandard(StandardType.ASME_VIII_DIV2);
logger.warn("Deviation from TORG: Using ASME VIII Div 2 instead of Div 1 for {}",
equipment.getName());
The FieldDevelopmentDesignOrchestrator provides a unified workflow for coordinating process simulation, mechanical design, and design validation throughout a field development project lifecycle. It integrates TORG requirements, design standards, and design cases into a structured workflow.
┌──────────────────────────────────────────────────────────────────────────────┐
│ FieldDevelopmentDesignOrchestrator │
├──────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │
│ │ Design Phase │───▶│ Design Cases │───▶│ TORG │───▶│ Workflow │ │
│ │ (Lifecycle) │ │ (Scenarios) │ │ (Standards) │ │ Execute │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └────────────┘ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────────────────────────────────────────┐│
│ │ Workflow Steps ││
│ │ 1. Initialize 2. Run Process 3. Apply 4. Mechanical 5. Validate ││
│ │ Environment Simulation TORG Design Results ││
│ └──────────────────────────────────────────────────────────────────────────┘│
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────┐ ┌────────────────┐│
│ │ Design Case │ │ Validation ││
│ │ Results │ │ Results ││
│ └──────────────┘ └────────────────┘│
│ │
└──────────────────────────────────────────────────────────────────────────────┘
The DesignPhase enum represents project lifecycle stages with associated accuracy requirements:
| Phase | Description | Accuracy Range | Requires Full Design |
|---|---|---|---|
SCREENING |
Early opportunity screening | ±40-50% | No |
CONCEPT_SELECT |
Concept selection study | ±30% | No |
PRE_FEED |
Pre-FEED study | ±25% | No |
FEED |
Front-End Engineering Design | ±15-20% | Yes |
DETAIL_DESIGN |
Detailed engineering | ±10% | Yes |
AS_BUILT |
As-built verification | ±5% | Yes |
import neqsim.process.mechanicaldesign.designstandards.DesignPhase;
// Get phase properties
DesignPhase phase = DesignPhase.FEED;
String accuracy = phase.getAccuracyRange(); // "±15-20%"
boolean compliance = phase.requiresDetailedCompliance(); // true
boolean fullDesign = phase.requiresFullMechanicalDesign(); // true
// Phase comparisons
boolean isLate = phase.isLaterThan(DesignPhase.CONCEPT_SELECT); // true
boolean isEarly = phase.isEarlierThan(DesignPhase.DETAIL_DESIGN); // true
The DesignCase enum defines operating scenarios for equipment sizing:
| Case | Load Factor | Sizing Critical | Relief Required |
|---|---|---|---|
NORMAL |
1.0 | Yes | No |
MAXIMUM |
1.1 | Yes | Yes |
MINIMUM |
0.3 | No (turndown) | No |
STARTUP |
0.1 | No | No |
SHUTDOWN |
0.1 | No | No |
UPSET |
1.2 | Yes | Yes |
EMERGENCY |
1.0 | No | Yes |
WINTER |
1.0 | Yes | No |
SUMMER |
1.0 | Yes | No |
EARLY_LIFE |
1.0 | Yes | No |
LATE_LIFE |
0.8 | Yes | No |
import neqsim.process.mechanicaldesign.designstandards.DesignCase;
// Get case properties
DesignCase designCase = DesignCase.MAXIMUM;
double loadFactor = designCase.getTypicalLoadFactor(); // 1.1
boolean sizing = designCase.isSizingCritical(); // true
boolean turndown = designCase.isTurndownCase(); // false
boolean relief = designCase.requiresReliefSizing(); // true
// Get relevant cases for different purposes
List<DesignCase> sizingCases = DesignCase.getSizingCriticalCases();
List<DesignCase> reliefCases = DesignCase.getReliefSizingCases();
List<DesignCase> turndownCases = DesignCase.getTurndownCases();
import neqsim.process.mechanicaldesign.designstandards.FieldDevelopmentDesignOrchestrator;
import neqsim.process.mechanicaldesign.designstandards.DesignPhase;
import neqsim.process.mechanicaldesign.designstandards.DesignCase;
import neqsim.process.processmodel.ProcessSystem;
// Build process system
ProcessSystem process = new ProcessSystem();
Stream feed = new Stream("Feed", fluid);
feed.setFlowRate(100.0, "kg/hr");
process.add(feed);
Separator separator = new Separator("HP Separator", feed);
process.add(separator);
Compressor compressor = new Compressor("Export Compressor", separator.getGasOutStream());
compressor.setOutletPressure(80.0, "bara");
process.add(compressor);
// Create orchestrator
FieldDevelopmentDesignOrchestrator orchestrator =
new FieldDevelopmentDesignOrchestrator(process);
// Set design phase
orchestrator.setDesignPhase(DesignPhase.FEED);
// Add design cases to evaluate
orchestrator.addDesignCase(DesignCase.NORMAL);
orchestrator.addDesignCase(DesignCase.MAXIMUM);
orchestrator.addDesignCase(DesignCase.MINIMUM);
orchestrator.addDesignCase(DesignCase.UPSET);
import neqsim.process.mechanicaldesign.torg.TorgManager;
import neqsim.process.mechanicaldesign.torg.CsvTorgDataSource;
// Configure TORG source
TorgManager torgManager = new TorgManager();
torgManager.addDataSource(new CsvTorgDataSource("project_torg.csv"));
// Load TORG for project
boolean loaded = orchestrator.loadTorg(torgManager, "TROLL-WEST-2025");
if (!loaded) {
throw new IllegalStateException("Failed to load TORG");
}
// Run complete design workflow
orchestrator.runCompleteDesignWorkflow();
This executes the following steps:
// Get validation results
DesignValidationResult results = orchestrator.validateDesign();
if (results.isValid()) {
System.out.println("Design validation passed!");
} else {
System.out.println("Design validation failed:");
for (DesignValidationResult.ValidationMessage msg : results.getMessages()) {
System.out.println(" " + msg.getSeverity() + ": " + msg.getMessage());
}
}
// Get results for each design case
Map<DesignCase, DesignCaseResult> caseResults = orchestrator.getDesignCaseResults();
for (Map.Entry<DesignCase, DesignCaseResult> entry : caseResults.entrySet()) {
DesignCase dc = entry.getKey();
DesignCaseResult result = entry.getValue();
System.out.println(dc.name() + ": " + (result.isConverged() ? "Converged" : "Failed"));
}
The DesignValidationResult class provides structured validation feedback:
| Level | Description | Blocks Design |
|---|---|---|
INFO |
Informational messages | No |
WARNING |
Potential issues, review recommended | No |
ERROR |
Design problems, must be addressed | Yes |
CRITICAL |
Severe issues, safety implications | Yes |
import neqsim.process.mechanicaldesign.designstandards.DesignValidationResult;
DesignValidationResult result = orchestrator.validateDesign();
// Check overall status
boolean isValid = result.isValid(); // true if no ERROR/CRITICAL
boolean hasWarnings = result.hasWarnings(); // true if any WARNING
boolean hasCritical = result.hasCriticalIssues(); // true if any CRITICAL
// Get counts by severity
int errorCount = result.getErrorCount();
int warningCount = result.getWarningCount();
// Get all messages
List<ValidationMessage> allMessages = result.getMessages();
// Filter by severity
List<ValidationMessage> errors = result.getMessagesBySeverity(Severity.ERROR);
List<ValidationMessage> warnings = result.getMessagesBySeverity(Severity.WARNING);
// Print formatted summary
System.out.println(result.getSummary());
Example output:
Design Validation Summary
=========================
Status: PASSED WITH WARNINGS
Messages:
[INFO] HP Separator design completed successfully
[INFO] Export Compressor design completed successfully
[WARNING] HP Separator corrosion allowance (2.0 mm) is below TORG requirement (3.0 mm)
[WARNING] Minimum case shows separator efficiency at 85% (target 90%)
Statistics:
- Info: 2
- Warnings: 2
- Errors: 0
- Critical: 0
Generate comprehensive design reports:
// Generate design report
String report = orchestrator.generateDesignReport();
System.out.println(report);
// Save to file
Files.write(Paths.get("design_report.txt"), report.getBytes());
Example report:
Field Development Design Report
================================
Project: TROLL-WEST-2025
Phase: FEED (±15-20% accuracy)
Generated: 2025-01-06 14:30:00
TORG Information
----------------
Revision: Rev 2
Company: EQUINOR
Design Life: 25 years
Design Cases Evaluated
----------------------
1. NORMAL (Load Factor: 1.0)
Status: Converged
Iterations: 5
2. MAXIMUM (Load Factor: 1.1)
Status: Converged
Iterations: 7
3. MINIMUM (Load Factor: 0.3)
Status: Converged
Iterations: 4
4. UPSET (Load Factor: 1.2)
Status: Converged
Iterations: 9
Equipment Summary
-----------------
HP Separator:
- Design Pressure: 55.0 barg
- Design Temperature: 150°C
- Material: SA-516-70
- Wall Thickness: 25.4 mm
- Weight: 12,500 kg
- Standards: ASME VIII Div 1, NORSOK P-002
Export Compressor:
- Stages: 2
- Power: 2.5 MW
- Discharge Pressure: 80 bara
- Material: API 617 compliant
- Standards: API 617
Validation Summary
------------------
Overall Status: PASSED WITH WARNINGS
- 0 Critical issues
- 0 Errors
- 2 Warnings
- 4 Info messages
See detailed validation report for warning details.
// Add custom pre-processing step
orchestrator.addPreProcessStep("Custom Pre-Check", () -> {
// Custom validation logic
if (!checkCustomRequirements()) {
throw new IllegalStateException("Custom requirements not met");
}
});
// Add custom post-processing step
orchestrator.addPostProcessStep("Export Results", () -> {
// Export to external system
exportToExternalDatabase(orchestrator.getResults());
});
// Run only sizing-critical cases
orchestrator.clearDesignCases();
for (DesignCase dc : DesignCase.getSizingCriticalCases()) {
orchestrator.addDesignCase(dc);
}
orchestrator.runCompleteDesignWorkflow();
DesignPhase phase = orchestrator.getDesignPhase();
if (phase.requiresFullMechanicalDesign()) {
// Full mechanical design with detailed calculations
orchestrator.setDetailedCalculations(true);
} else {
// Simplified calculations for early phases
orchestrator.setDetailedCalculations(false);
}
// For each design case, update process conditions
for (DesignCase designCase : orchestrator.getDesignCases()) {
// Adjust feed rate based on case
double loadFactor = designCase.getTypicalLoadFactor();
feed.setFlowRate(baseFlowRate * loadFactor, "kg/hr");
// Adjust temperature for seasonal cases
if (designCase == DesignCase.WINTER) {
feed.setTemperature(-20.0, "C");
} else if (designCase == DesignCase.SUMMER) {
feed.setTemperature(35.0, "C");
}
// Run simulation
process.run();
// Store results
orchestrator.storeDesignCaseResult(designCase, process);
}
// Get sizing envelope across all cases
SizingEnvelope envelope = orchestrator.getSizingEnvelope();
double maxPressure = envelope.getMaxDesignPressure();
double maxTemperature = envelope.getMaxDesignTemperature();
double maxFlow = envelope.getMaxFlowRate();
System.out.println("Sizing Envelope:");
System.out.println(" Max Pressure: " + maxPressure + " barg");
System.out.println(" Max Temperature: " + maxTemperature + " °C");
System.out.println(" Max Flow: " + maxFlow + " kg/hr");
try {
orchestrator.runCompleteDesignWorkflow();
} catch (TorgNotFoundException e) {
System.err.println("TORG not found: " + e.getMessage());
// Fall back to default standards
orchestrator.applyDefaultStandards();
orchestrator.runCompleteDesignWorkflow();
} catch (DesignConvergenceException e) {
System.err.println("Design did not converge: " + e.getMessage());
// Get partial results
DesignValidationResult partial = e.getPartialResults();
System.out.println(partial.getSummary());
} catch (StandardNotSupportedException e) {
System.err.println("Standard not supported: " + e.getMessage());
String remediation = e.getRemediation();
System.out.println("Suggested action: " + remediation);
}
Start with coarse phases and refine:
// Screening phase - quick estimates
orchestrator.setDesignPhase(DesignPhase.SCREENING);
orchestrator.addDesignCase(DesignCase.NORMAL);
orchestrator.addDesignCase(DesignCase.MAXIMUM);
orchestrator.runCompleteDesignWorkflow();
// If viable, move to FEED
if (orchestrator.validateDesign().isValid()) {
orchestrator.setDesignPhase(DesignPhase.FEED);
// Add more cases for detailed analysis
orchestrator.addDesignCase(DesignCase.MINIMUM);
orchestrator.addDesignCase(DesignCase.UPSET);
orchestrator.addDesignCase(DesignCase.WINTER);
orchestrator.addDesignCase(DesignCase.SUMMER);
orchestrator.runCompleteDesignWorkflow();
}
// Add assumptions to report
orchestrator.addAssumption("Feed composition based on 2024 well test data");
orchestrator.addAssumption("Ambient temperature range from met-ocean study");
orchestrator.addAssumption("Design life 25 years per TORG Rev 2");
// Tag design run with version info
orchestrator.setRunMetadata("git_commit", getGitCommitHash());
orchestrator.setRunMetadata("torg_revision", torg.getRevision());
orchestrator.setRunMetadata("analyst", System.getProperty("user.name"));
// Save complete configuration for reproducibility
orchestrator.saveConfiguration("design_config_2025-01-06.json");
// Later, reload and re-run
FieldDevelopmentDesignOrchestrator restored =
FieldDevelopmentDesignOrchestrator.loadConfiguration("design_config_2025-01-06.json");
restored.runCompleteDesignWorkflow();
import neqsim.process.processmodel.ProcessSystem;
import neqsim.process.equipment.separator.Separator;
import neqsim.process.equipment.compressor.Compressor;
import neqsim.process.equipment.stream.Stream;
import neqsim.process.mechanicaldesign.designstandards.*;
import neqsim.process.mechanicaldesign.torg.*;
import neqsim.thermo.system.SystemSrkEos;
public class FieldDevelopmentDesignExample {
public static void main(String[] args) {
// 1. Create fluid and process
SystemSrkEos fluid = new SystemSrkEos(280.0, 50.0);
fluid.addComponent("methane", 0.85);
fluid.addComponent("ethane", 0.08);
fluid.addComponent("propane", 0.04);
fluid.addComponent("n-butane", 0.03);
fluid.setMixingRule("classic");
ProcessSystem process = new ProcessSystem();
Stream feed = new Stream("Well Feed", fluid);
feed.setFlowRate(50000.0, "kg/hr");
process.add(feed);
Separator hpSep = new Separator("HP Separator", feed);
process.add(hpSep);
Compressor exportComp = new Compressor("Export Compressor", hpSep.getGasOutStream());
exportComp.setOutletPressure(150.0, "bara");
process.add(exportComp);
// 2. Create orchestrator
FieldDevelopmentDesignOrchestrator orchestrator =
new FieldDevelopmentDesignOrchestrator(process);
// 3. Configure for FEED phase
orchestrator.setDesignPhase(DesignPhase.FEED);
// 4. Add design cases
orchestrator.addDesignCase(DesignCase.NORMAL);
orchestrator.addDesignCase(DesignCase.MAXIMUM);
orchestrator.addDesignCase(DesignCase.MINIMUM);
orchestrator.addDesignCase(DesignCase.UPSET);
orchestrator.addDesignCase(DesignCase.EARLY_LIFE);
orchestrator.addDesignCase(DesignCase.LATE_LIFE);
// 5. Load TORG
TorgManager torgManager = new TorgManager();
torgManager.addDataSource(new CsvTorgDataSource("project_torg.csv"));
orchestrator.loadTorg(torgManager, "TROLL-WEST-2025");
// 6. Run complete workflow
orchestrator.runCompleteDesignWorkflow();
// 7. Validate and report
DesignValidationResult validation = orchestrator.validateDesign();
System.out.println(validation.getSummary());
if (validation.isValid()) {
String report = orchestrator.generateDesignReport();
System.out.println(report);
} else {
System.err.println("Design validation failed!");
for (ValidationMessage msg : validation.getMessagesBySeverity(Severity.ERROR)) {
System.err.println(" ERROR: " + msg.getMessage());
}
}
}
}
This document describes how to save and load process simulations in NeqSim using JSON/XML serialization. NeqSim uses the XStream library for object serialization, supporting both uncompressed XML and compressed .neqsim file formats.
NeqSim provides multiple ways to persist and restore process simulations:
| Method | Format | Compression | Use Case |
|---|---|---|---|
ProcessSystem.saveToNeqsim() |
XML in ZIP | ✅ Yes | Recommended - compact storage |
ProcessSystem.saveAuto() |
Auto-detect | ✅ Auto | Convenience - format by extension |
NeqSimXtream.saveNeqsim() |
XML in ZIP | ✅ Yes | Low-level API |
ProcessSystemState |
JSON | ✅ Optional | Version control - Git-friendly |
save_xml() / open_xml() |
Plain XML | ❌ No | Debugging - human-readable |
The underlying serialization technology is XStream, a powerful Java library that converts objects to XML and back without requiring any modifications to the objects.
The .neqsim file format stores the XML serialization inside a ZIP archive, significantly reducing file size for large process models. This is the recommended format for production use.
Advantages:
Uncompressed XML files are useful for debugging and manual inspection but can become very large for complex simulations.
JSON-based state export provides a human-readable, Git-friendly format for version control and lifecycle management.
The simplest way to save and load process models is using the convenience methods directly on ProcessSystem:
import neqsim.process.processmodel.ProcessSystem;
import neqsim.process.equipment.stream.Stream;
import neqsim.thermo.system.SystemSrkEos;
// Create and configure a process
SystemInterface fluid = new SystemSrkEos(298.15, 50.0);
fluid.addComponent("methane", 10.0);
fluid.addComponent("ethane", 5.0);
fluid.setMixingRule("classic");
ProcessSystem process = new ProcessSystem("My Process");
Stream feed = new Stream("Feed", fluid);
feed.setFlowRate(1000.0, "kg/hr");
process.add(feed);
process.run();
// Save to compressed .neqsim file (recommended)
process.saveToNeqsim("my_process.neqsim");
// Load from .neqsim file (auto-runs after loading)
ProcessSystem loaded = ProcessSystem.loadFromNeqsim("my_process.neqsim");
System.out.println("Loaded: " + loaded.getName());
Use saveAuto() and loadAuto() to automatically detect format based on file extension:
// Format detected by extension
process.saveAuto("my_process.neqsim"); // → Compressed XStream XML
process.saveAuto("my_process.json"); // → JSON state export
process.saveAuto("my_process.ser"); // → Java binary serialization
// Load with auto-detection
ProcessSystem loaded = ProcessSystem.loadAuto("my_process.neqsim");
import neqsim.util.serialization.NeqSimXtream;
import neqsim.process.processmodel.ProcessSystem;
import neqsim.process.equipment.stream.Stream;
import neqsim.thermo.system.SystemSrkEos;
// Create and configure a process
SystemInterface fluid = new SystemSrkEos(298.15, 50.0);
fluid.addComponent("methane", 10.0);
fluid.addComponent("ethane", 5.0);
fluid.setMixingRule(2);
ProcessSystem process = new ProcessSystem("My Process");
Stream feed = new Stream("Feed", fluid);
feed.setFlowRate(1000.0, "kg/hr");
process.add(feed);
process.run();
// Save to compressed .neqsim file
boolean success = NeqSimXtream.saveNeqsim(process, "my_process.neqsim");
if (success) {
System.out.println("Process saved successfully!");
}
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.security.AnyTypePermission;
// Create XStream instance
XStream xstream = new XStream();
// Serialize to XML string
String xml = xstream.toXML(process);
// Save to file
try (FileWriter writer = new FileWriter("my_process.xml")) {
writer.write(xml);
}
import neqsim.util.serialization.NeqSimXtream;
import neqsim.process.processmodel.ProcessSystem;
try {
// Load from .neqsim file
Object loaded = NeqSimXtream.openNeqsim("my_process.neqsim");
if (loaded instanceof ProcessSystem) {
ProcessSystem process = (ProcessSystem) loaded;
// Run the restored process
process.run();
System.out.println("Process loaded: " + process.getName());
System.out.println("Number of units: " + process.getUnitOperations().size());
}
} catch (IOException e) {
System.err.println("Failed to load process: " + e.getMessage());
}
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.security.AnyTypePermission;
// Create XStream instance with security permissions
XStream xstream = new XStream();
xstream.addPermission(AnyTypePermission.ANY);
// Read XML from file
String xmlContent = new String(Files.readAllBytes(Paths.get("my_process.xml")));
// Deserialize
ProcessSystem process = (ProcessSystem) xstream.fromXML(xmlContent);
process.run();
NeqSim provides a ProcessSystemState class for JSON-based state management, ideal for version control and lifecycle tracking:
import neqsim.process.processmodel.lifecycle.ProcessSystemState;
import neqsim.process.processmodel.ProcessSystem;
import java.io.File;
// Create a state snapshot from existing process
ProcessSystemState state = ProcessSystemState.fromProcessSystem(process);
// Add metadata
state.setVersion("1.2.3");
state.setDescription("Post-commissioning tuned model");
// Save to JSON file (uncompressed) - using String path or File
state.saveToFile("asset_model_v1.2.3.json");
state.saveToFile(new File("asset_model_v1.2.3.json"));
// Later: Load the state - using String path or File
ProcessSystemState loadedState = ProcessSystemState.loadFromFile("asset_model_v1.2.3.json");
ProcessSystemState loadedState = ProcessSystemState.loadFromFile(new File("asset_model_v1.2.3.json"));
// Convert to ProcessSystem (limited reconstruction)
ProcessSystem restoredProcess = loadedState.toProcessSystem();
// Or apply state to existing process with matching structure
loadedState.applyTo(existingProcess);
For large process models, use GZIP compression to reduce file sizes (typically 5-20x reduction):
// Save to compressed file (.neqsim) - using String path or File
state.saveToCompressedFile("asset_model_v1.2.3.neqsim");
state.saveToCompressedFile(new File("asset_model_v1.2.3.neqsim"));
// Load from compressed file - using String path or File
ProcessSystemState loadedState = ProcessSystemState.loadFromCompressedFile("asset_model_v1.2.3.neqsim");
ProcessSystemState loadedState = ProcessSystemState.loadFromCompressedFile(new File("asset_model_v1.2.3.neqsim"));
// Auto-detect compression based on file extension - using String path or File
state.saveToFileAuto("asset_model.neqsim"); // Compressed
state.saveToFileAuto("asset_model.json"); // Uncompressed
state.saveToFileAuto(new File("asset_model.neqsim")); // Also works with File
ProcessSystemState loaded = ProcessSystemState.loadFromFileAuto("asset_model.neqsim");
ProcessSystemState loaded = ProcessSystemState.loadFromFileAuto(new File("asset_model.neqsim"));
When to use compressed state files (.neqsim):
When to use plain JSON (.json):
// Export to JSON string
String json = state.toJson();
// Import from JSON string
ProcessSystemState restored = ProcessSystemState.fromJson(json);
// Export current state to JSON file
process.exportStateToFile("process_state.json");
// Load and apply state from JSON file
process.loadStateFromFile("process_state.json");
The ProcessSystemState class includes validation capabilities to verify loaded states:
// Load a state file
ProcessSystemState state = ProcessSystemState.loadFromFile("old_model.json");
// Validate the state
ProcessSystemState.ValidationResult result = state.validate();
if (result.isValid()) {
System.out.println("State is valid!");
} else {
System.out.println("Validation errors: " + result.getErrors());
}
// Check for warnings even if valid
if (!result.getWarnings().isEmpty()) {
System.out.println("Warnings: " + result.getWarnings());
}
// Check schema version
System.out.println("Schema version: " + state.getSchemaVersion());
ProcessSystemState includes automatic schema version tracking for backward compatibility:
// Current schema version is embedded automatically
ProcessSystemState state = ProcessSystemState.fromProcessSystem(process);
System.out.println("Schema: " + state.getSchemaVersion()); // e.g., "1.1"
// Older files without schema version are automatically migrated on load
ProcessSystemState old = ProcessSystemState.loadFromFile("legacy_model.json");
// Migration is automatic - connectionStates initialized if missing
ProcessSystemState captures stream connections between equipment for topology analysis:
ProcessSystemState state = ProcessSystemState.fromProcessSystem(process);
// View captured connections
for (ProcessSystemState.ConnectionState conn : state.getConnectionStates()) {
System.out.println(conn.getSourceEquipmentName() + "." + conn.getSourcePortName()
+ " -> " + conn.getTargetEquipmentName() + "." + conn.getTargetPortName());
}
// Example output: separator.gasOutStream -> compressor.inlet
When your simulation requires multiple interconnected ProcessSystem instances (e.g., upstream production + downstream processing), use the ProcessModel class for coordinated execution and serialization.
ProcessModel manages a collection of ProcessSystem instances with:
| Class | Purpose | Scope |
|---|---|---|
ProcessSystem |
Single process flowsheet | Individual equipment + streams |
ProcessModel |
Multi-process container | Multiple ProcessSystems |
ProcessSystemState |
JSON state for single process | Equipment states + connections |
ProcessModelState |
JSON state for multi-process | All ProcessSystemStates + inter-process links |
import neqsim.process.processmodel.ProcessModel;
import neqsim.process.processmodel.ProcessSystem;
// Create a multi-process model
ProcessModel model = new ProcessModel();
model.add("upstream", createUpstreamProcess());
model.add("downstream", createDownstreamProcess());
model.setMaxIterations(50);
model.setFlowTolerance(1e-5);
model.run();
// Save to compressed .neqsim file (recommended)
model.saveToNeqsim("field_model.neqsim");
// Load later (auto-runs after loading)
ProcessModel loaded = ProcessModel.loadFromNeqsim("field_model.neqsim");
ProcessSystem upstream = loaded.get("upstream");
System.out.println("Upstream solved: " + upstream.solved());
// Save with auto-format detection by extension
model.saveAuto("field_model.neqsim"); // → Compressed XStream XML
model.saveAuto("field_model.json"); // → JSON state export
model.saveAuto("field_model.ser"); // → Java binary serialization
// Load with auto-detection
ProcessModel loaded = ProcessModel.loadAuto("field_model.neqsim");
import neqsim.util.serialization.NeqSimXtream;
// Save ProcessModel directly
NeqSimXtream.saveNeqsim(model, "field_model.neqsim");
// Load (returns Object, requires cast)
ProcessModel loaded = (ProcessModel) NeqSimXtream.openNeqsim("field_model.neqsim");
loaded.run();
For Git-friendly version control, use ProcessModelState to export multi-process models to JSON:
import neqsim.process.processmodel.lifecycle.ProcessModelState;
// Export state from model
ProcessModelState state = model.exportState();
state.setVersion("1.0.0");
state.setDescription("Full field development model");
state.setCreatedBy("engineering-team");
// Save to JSON (human-readable)
state.saveToFile("field_model_v1.0.0.json");
// Save to compressed JSON
state.saveToFile("field_model_v1.0.0.json.gz");
// Load later
ProcessModelState loadedState = ProcessModelState.loadFromFile("field_model_v1.0.0.json");
ProcessModel restoredModel = loadedState.toProcessModel();
ProcessModelState preserves execution settings:
ProcessModelState state = model.exportState();
// Access execution config
ProcessModelState.ExecutionConfig config = state.getExecutionConfig();
System.out.println("Max iterations: " + config.getMaxIterations());
System.out.println("Flow tolerance: " + config.getFlowTolerance());
System.out.println("Optimized execution: " + config.isUseOptimizedExecution());
ProcessModelState automatically captures streams shared between different ProcessSystems:
ProcessModelState state = model.exportState();
// View inter-process connections
for (ProcessModelState.InterProcessConnection conn : state.getInterProcessConnections()) {
System.out.println(conn.getSourceProcess() + "/" + conn.getStreamName()
+ " -> " + conn.getTargetProcess() + ":" + conn.getTargetPort());
}
// Example output: upstream/gas_export -> downstream:inlet
Validate multi-process model state before loading:
ProcessModelState state = ProcessModelState.loadFromFile("field_model.json");
ProcessModelState.ValidationResult result = state.validate();
if (!result.isValid()) {
for (String error : result.getErrors()) {
System.err.println("ERROR: " + error);
}
for (String warning : result.getWarnings()) {
System.out.println("WARNING: " + warning);
}
} else {
ProcessModel model = state.toProcessModel();
model.run();
}
From Python, use the direct Java API access pattern:
import jpype
from neqsim import jneqsim
# Access ProcessModel class
ProcessModel = jneqsim.process.processmodel.ProcessModel
ProcessSystem = jneqsim.process.processmodel.ProcessSystem
NeqSimXtream = jneqsim.util.serialization.NeqSimXtream
# Load a ProcessModel
loaded = NeqSimXtream.openNeqsim("field_model.neqsim")
model = jpype.JObject(loaded, ProcessModel)
model.run()
# Access individual ProcessSystems
upstream = model.get("upstream")
print(f"Upstream solved: {upstream.solved()}")
# Save ProcessModel
model.saveToNeqsim("field_model_updated.neqsim")
The neqsim-python package provides Python wrappers for saving and loading NeqSim objects.
import neqsim
from neqsim.thermo import fluid
from neqsim.process import stream, separator, runProcess, clearProcess, getProcess
# Build and run a process
clearProcess()
feed_fluid = fluid('srk')
feed_fluid.addComponent('methane', 0.9)
feed_fluid.addComponent('ethane', 0.1)
feed_fluid.setTemperature(30.0, 'C')
feed_fluid.setPressure(50.0, 'bara')
feed_fluid.setTotalFlowRate(10.0, 'MSm3/day')
inlet = stream('inlet', feed_fluid)
sep = separator('separator', inlet)
runProcess()
process = getProcess()
# Save to compressed .neqsim file
neqsim.save_neqsim(process, "my_process.neqsim")
# Load from .neqsim file
loaded = neqsim.open_neqsim("my_process.neqsim")
loaded.run()
print(f"Loaded: {loaded.getName()}")
The .neqsim format stores compressed XML, making it efficient for large process models:
import neqsim
from neqsim.thermo import fluid
from neqsim.process import stream, separator, runProcess, clearProcess, getProcess
# Build a process
clearProcess()
feed_fluid = fluid('srk')
feed_fluid.addComponent('methane', 0.9)
feed_fluid.addComponent('ethane', 0.1)
feed_fluid.setTemperature(30.0, 'C')
feed_fluid.setPressure(50.0, 'bara')
feed_fluid.setTotalFlowRate(10.0, 'MSm3/day')
inlet = stream('inlet', feed_fluid)
sep = separator('separator', inlet)
runProcess()
# Get the process object
process = getProcess()
# Save to compressed .neqsim file
neqsim.save_neqsim(process, "my_process.neqsim")
print("Process saved!")
# Load the process from .neqsim file
loaded_process = neqsim.open_neqsim("my_process.neqsim")
# Run the loaded process
loaded_process.run()
print(f"Loaded process: {loaded_process.getName()}")
print(f"Number of units: {loaded_process.getUnitOperations().size()}")
For debugging or when human-readable output is needed:
import neqsim
from neqsim.thermo import createfluid
# Create a fluid
fluid1 = createfluid("dry gas")
# Save to uncompressed XML
neqsim.save_xml(fluid1, "my_fluid.xml")
# Load from XML
fluid2 = neqsim.open_xml("my_fluid.xml")
# Verify the data was preserved
assert fluid1.getTemperature() == fluid2.getTemperature()
For full control, you can use the Java API directly from Python via JPype:
import jpype
import jpype.imports
from jpype.types import *
# Start the JVM (if not already started by neqsim)
if not jpype.isJVMStarted():
jpype.startJVM(classpath=['path/to/neqsim.jar'])
# Import Java classes directly
from neqsim.process.processmodel import ProcessSystem
from neqsim.process.processmodel.lifecycle import ProcessSystemState
from neqsim.thermo.system import SystemSrkEos
from neqsim.process.equipment.stream import Stream
# Create a process using Java API
fluid = SystemSrkEos(298.15, 50.0)
fluid.addComponent("methane", 10.0)
fluid.addComponent("ethane", 5.0)
fluid.setMixingRule("classic")
process = ProcessSystem("MyProcess")
feed = Stream("feed", fluid)
feed.setFlowRate(1000.0, "kg/hr")
process.add(feed)
process.run()
# Use the convenience methods
process.saveToNeqsim("model.neqsim")
# Load back
loaded = ProcessSystem.loadFromNeqsim("model.neqsim")
print(f"Loaded: {loaded.getName()}")
# Use ProcessSystemState for JSON export
state = ProcessSystemState.fromProcessSystem(process)
state.setVersion("1.0.0")
state.setDescription("Initial model")
state.saveToFile("model_state.json")
# Validate loaded state
loaded_state = ProcessSystemState.loadFromFile("model_state.json")
result = loaded_state.validate()
if result.isValid():
print("State is valid")
else:
print(f"Errors: {list(result.getErrors())}")
from neqsim.process.processmodel.lifecycle import ProcessSystemState
# Create state from process
state = ProcessSystemState.fromProcessSystem(process)
# Add metadata
state.setVersion("2.1.0")
state.setDescription("Updated heat exchanger configuration")
state.setCreatedBy("engineer@company.com")
# Save with auto-format detection
state.saveToFileAuto("model.neqsim") # Compressed
state.saveToFileAuto("model.json") # JSON (Git-friendly)
# Load and validate
loaded = ProcessSystemState.loadFromFileAuto("model.neqsim")
print(f"Schema version: {loaded.getSchemaVersion()}")
print(f"Equipment count: {loaded.getEquipmentStates().size()}")
# Check connections
for conn in loaded.getConnectionStates():
print(f" {conn.getSourceEquipmentName()} -> {conn.getTargetEquipmentName()}")
neqsim-python also supports JSON/YAML-based process configuration:
from neqsim.thermo import fluid
from neqsim.process import ProcessBuilder
# Create fluid
feed = fluid('srk')
feed.addComponent('methane', 0.9)
feed.addComponent('ethane', 0.1)
feed.setTemperature(30.0, 'C')
feed.setPressure(50.0, 'bara')
# Load process from JSON configuration
process = ProcessBuilder.from_json('process_config.json',
fluids={'feed': feed}).run()
# Get results as JSON
results = process.results_json()
# Save results to file
process.save_results('results.json', format='json')
{
"name": "Compression Train",
"equipment": [
{
"type": "stream",
"name": "inlet",
"fluid": "feed",
"flow_rate": 10.0,
"flow_unit": "MSm3/day"
},
{
"type": "separator",
"name": "inlet_separator",
"inlet": "inlet"
},
{
"type": "compressor",
"name": "stage1_compressor",
"inlet": "inlet_separator",
"outlet_pressure": 80.0
}
]
}
A .neqsim file is a standard ZIP archive containing a single XML file:
my_process.neqsim (ZIP archive)
└── process.xml (XStream-serialized XML)
| Process Complexity | XML Size | .neqsim Size | Compression Ratio |
|---|---|---|---|
| Simple (5 units) | ~500 KB | ~30 KB | ~17:1 |
| Medium (20 units) | ~2 MB | ~120 KB | ~17:1 |
| Complex (50+ units) | ~10 MB | ~600 KB | ~17:1 |
You can manually inspect .neqsim files using any ZIP tool:
# Linux/Mac
unzip -l my_process.neqsim
unzip -p my_process.neqsim process.xml | head -100
# Windows PowerShell
Expand-Archive -Path my_process.neqsim -DestinationPath extracted
Get-Content extracted\process.xml | Select-Object -First 100
Always use .neqsim format for production deployments to minimize storage and transfer costs:
// Good - compressed
NeqSimXtream.saveNeqsim(process, "production_model.neqsim");
// Avoid for large models - uncompressed
// xstream.toXML(process, new FileWriter("production_model.xml"));
Use ProcessSystemState with version metadata for proper model lifecycle management:
ProcessSystemState state = ProcessSystemState.fromProcessSystem(process);
state.setVersion("2.1.0");
state.setDescription("Updated valve Cv values based on commissioning data");
state.setCreatedBy("process_engineer@company.com");
state.saveToFile("model_v2.1.0.json");
Always run the process after loading to ensure the internal state is consistent:
ProcessSystem loaded = (ProcessSystem) NeqSimXtream.openNeqsim("model.neqsim");
loaded.run(); // Important: reinitialize calculations
When using XStream directly, explicitly set permissions:
XStream xstream = new XStream();
xstream.addPermission(AnyTypePermission.ANY); // Required for deserialization
xstream.allowTypesByWildcard(new String[]{"neqsim.**"});
JSON state files are ideal for Git-based version control:
# Track model changes in Git
git add asset_model_v1.2.3.json
git commit -m "Updated model with new heat exchanger configuration"
The .neqsim file is corrupted or not a valid ZIP archive.
Solution: Verify the file is a valid ZIP and contains process.xml:
unzip -l my_process.neqsim
The serialized object references a class that doesn't exist in the current NeqSim version.
Solution: Ensure you're using the same (or compatible) NeqSim version that created the file.
Complex processes with many components can create large files.
Solution:
.neqsim formatXStream cannot serialize ThreadLocal fields.
Solution: The NeqSimXtream class automatically handles this by skipping ThreadLocal fields. Use NeqSimXtream instead of raw XStream.
Enable detailed logging to diagnose problems:
// Check what's being serialized
String xml = xstream.toXML(process);
System.out.println("XML length: " + xml.length());
System.out.println("First 1000 chars: " + xml.substring(0, Math.min(1000, xml.length())));
| Class | Description |
|---|---|
neqsim.process.processmodel.ProcessSystem |
Main process container with saveToNeqsim(), loadFromNeqsim(), saveAuto(), loadAuto() methods |
neqsim.process.processmodel.ProcessModel |
Multi-process container with save/load methods for multiple ProcessSystems |
neqsim.util.serialization.NeqSimXtream |
Low-level compressed XML serialization to .neqsim files |
neqsim.process.processmodel.lifecycle.ProcessSystemState |
JSON-based state snapshots for single ProcessSystem |
neqsim.process.processmodel.lifecycle.ProcessModelState |
JSON-based state snapshots for multi-process models |
neqsim.process.processmodel.lifecycle.ProcessSystemState.ValidationResult |
Validation result with errors and warnings |
neqsim.process.processmodel.lifecycle.ProcessSystemState.ConnectionState |
Stream connection between equipment |
neqsim.process.processmodel.lifecycle.ProcessSystemState.EquipmentState |
Captured state of a single equipment unit |
neqsim.process.processmodel.lifecycle.ProcessModelState.InterProcessConnection |
Connection between ProcessSystems |
neqsim.process.processmodel.lifecycle.ProcessModelState.ExecutionConfig |
Execution configuration for ProcessModel |
| Method | Description |
|---|---|
saveToNeqsim(String filename) |
Save to compressed .neqsim file |
loadFromNeqsim(String filename) |
Load from .neqsim file (static, auto-runs) |
saveAuto(String filename) |
Save with auto-format detection by extension |
loadAuto(String filename) |
Load with auto-format detection (static) |
exportStateToFile(String filename) |
Export JSON state to file |
loadStateFromFile(String filename) |
Load and apply JSON state |
exportState() |
Get ProcessSystemState snapshot |
| Method | Description |
|---|---|
saveToNeqsim(String filename) |
Save entire model to compressed .neqsim file |
loadFromNeqsim(String filename) |
Load from .neqsim file (static, auto-runs) |
saveAuto(String filename) |
Save with auto-format detection by extension |
loadAuto(String filename) |
Load with auto-format detection (static) |
saveStateToFile(String filename) |
Export JSON state to file |
loadStateFromFile(String filename) |
Load JSON state (static) |
exportState() |
Get ProcessModelState snapshot |
add(String name, ProcessSystem) |
Add a ProcessSystem to the model |
get(String name) |
Get a ProcessSystem by name |
getAllProcesses() |
Get all ProcessSystems |
validateSetup() |
Validate all processes |
| Method | Description |
|---|---|
fromProcessSystem(ProcessSystem) |
Create state snapshot (static) |
saveToFile(String) / saveToFile(File) |
Save as JSON |
loadFromFile(String) / loadFromFile(File) |
Load JSON (static) |
saveToCompressedFile(String) |
Save as GZIP-compressed JSON |
loadFromCompressedFile(String) |
Load compressed JSON (static) |
saveToFileAuto(String) |
Auto-detect format by extension |
loadFromFileAuto(String) |
Auto-detect format on load (static) |
validate() |
Validate state, returns ValidationResult |
applyTo(ProcessSystem) |
Apply state to existing process |
toJson() / fromJson(String) |
JSON string conversion |
getSchemaVersion() |
Get schema version string |
getConnectionStates() |
Get list of stream connections |
getEquipmentStates() |
Get list of equipment states |
| Method | Description |
|---|---|
fromProcessModel(ProcessModel) |
Create state snapshot (static) |
saveToFile(String) |
Save as JSON (or .gz for compressed) |
loadFromFile(String) |
Load JSON (static) |
toProcessModel() |
Reconstruct ProcessModel from state |
validate() |
Validate state, returns ValidationResult |
getProcessStates() |
Get map of ProcessSystemStates |
getInterProcessConnections() |
Get inter-process connections |
getExecutionConfig() |
Get execution configuration |
getProcessCount() |
Get number of ProcessSystems |
setVersion(String) |
Set version metadata |
setDescription(String) |
Set description metadata |
setCreatedBy(String) |
Set creator metadata |
setCustomProperty(String, Object) |
Set custom property |
| Function | Description |
|---|---|
neqsim.save_neqsim(obj, filename) |
Save object to compressed .neqsim file |
neqsim.open_neqsim(filename) |
Load object from compressed .neqsim file |
neqsim.save_xml(obj, filename) |
Save object to uncompressed XML file |
neqsim.open_xml(filename) |
Load object from uncompressed XML file |
| Class (via JPype) | Description |
|---|---|
ProcessSystem.saveToNeqsim(filename) |
Save process to .neqsim |
ProcessSystem.loadFromNeqsim(filename) |
Load process from .neqsim |
ProcessModel.saveToNeqsim(filename) |
Save multi-process model to .neqsim |
ProcessModel.loadFromNeqsim(filename) |
Load multi-process model from .neqsim |
ProcessSystemState.fromProcessSystem(process) |
Create state snapshot |
ProcessModelState.fromProcessModel(model) |
Create multi-process state snapshot |
ProcessSystemState.loadFromFileAuto(filename) |
Load state with format detection |
state.validate() |
Validate loaded state |
The fluidmechanics package provides models for pipeline flow, pressure drop calculations, and transient flow simulation with rigorous non-equilibrium thermodynamic calculations for mass and heat transfer.
Location: neqsim.fluidmechanics
Purpose:
var, String.repeat(), etc.)The fluid mechanics module in NeqSim is based on the work presented in:
Solbraa, E. (2002). Equilibrium and Non-Equilibrium Thermodynamics of Natural Gas Processing. Dr.ing. thesis, Norwegian University of Science and Technology (NTNU). ISBN: 978-82-471-5541-7. Available at NVA
The key contributions from this work include:
The two-phase flow is modeled using separate conservation equations for each phase:
Mass Conservation (per component i): $$\frac{\partial (\alpha_k \rho_k x_{i,k})}{\partial t} + \frac{\partial (\alpha_k \rho_k x_{i,k} v_k)}{\partial z} = \dot{m}_{i,k}$$
Momentum Conservation: $$\frac{\partial (\alpha_k \rho_k v_k)}{\partial t} + \frac{\partial (\alpha_k \rho_k v_k^2)}{\partial z} = -\alpha_k \frac{\partial P}{\partial z} - F_{w,k} - F_{i,k} + \alpha_k \rho_k g \sin\theta$$
Energy Conservation: $$\frac{\partial (\alpha_k \rho_k h_k)}{\partial t} + \frac{\partial (\alpha_k \rho_k h_k v_k)}{\partial z} = \dot{Q}_{w,k} + \dot{Q}_{i,k}$$
Where:
fluidmechanics/
├── FluidMech.java # Package marker
│
├── flowsystem/ # Flow system definitions
│ ├── FlowSystem.java # Base flow system
│ ├── FlowSystemInterface.java # Interface
│ │
│ ├── onephaseflowsystem/ # Single-phase systems
│ │ ├── OnePhaseFlowSystem.java
│ │ └── pipeflowsystem/
│ │ └── OnePhasePipeFlowSystem.java
│ │
│ └── twophaseflowsystem/ # Two-phase systems
│ ├── TwoPhaseFlowSystem.java
│ └── pipeflowsystem/
│ ├── TwoPhasePipeFlowSystem.java
│ └── stratifiedflowsystem/
│ └── StratifiedFlowSystem.java
│
├── flownode/ # Flow nodes
│ ├── FlowNode.java # Base node
│ ├── FlowNodeInterface.java # Interface
│ ├── FlowNodeSelector.java # Node selection
│ │
│ ├── onephasenode/ # Single-phase nodes
│ │ ├── OnePhaseFlowNode.java
│ │ └── onephasepipeflownode/
│ │ └── OnePhasePipeFlowNode.java
│ │
│ ├── twophasenode/ # Two-phase nodes
│ │ ├── TwoPhaseFlowNode.java
│ │ └── twophasepipeflownode/
│ │ ├── TwoPhasePipeFlowNode.java
│ │ ├── AnnularFlow.java
│ │ ├── StratifiedFlow.java
│ │ └── DropletFlow.java
│ │
│ ├── multiphasenode/ # Multi-phase nodes
│ │ └── MultiPhaseFlowNode.java
│ │
│ └── fluidboundary/ # Boundary conditions
│ ├── FluidBoundary.java
│ └── InterphaseTransport.java
│
├── flowleg/ # Pipe segments
│ ├── FlowLeg.java
│ └── FlowLegInterface.java
│
├── flowsolver/ # Numerical solvers
│ ├── FlowSolver.java
│ ├── FlowSolverInterface.java
│ ├── OnePhaseFlowSolver.java
│ └── TwoPhaseFlowSolver.java
│
├── geometrydefinitions/ # Pipe geometry
│ ├── GeometryDefinition.java
│ ├── GeometryDefinitionInterface.java
│ ├── pipe/
│ │ └── PipeGeometry.java
│ └── internalgeometry/
│ └── InternalGeometry.java
│
└── util/ # Utilities
├── timeseries/
│ └── TimeSeries.java
└── fluidmechanicsvisualization/
└── flowsystemvisualization/
└── FlowSystemVisualization.java
import neqsim.fluidmechanics.flowsystem.FlowSystemInterface;
import neqsim.fluidmechanics.flowsystem.onephaseflowsystem.pipeflowsystem.OnePhasePipeFlowSystem;
import neqsim.fluidmechanics.geometrydefinitions.pipe.PipeGeometry;
// Create fluid
SystemInterface gas = new SystemSrkEos(300.0, 50.0);
gas.addComponent("methane", 0.95);
gas.addComponent("ethane", 0.05);
gas.setMixingRule("classic");
// Create pipe geometry
PipeGeometry pipe = new PipeGeometry("Pipeline");
pipe.setDiameter(0.5, "m"); // 0.5 m inner diameter
pipe.setLength(10000.0, "m"); // 10 km length
pipe.setRoughness(0.00005, "m"); // Pipe roughness
// Create flow system
OnePhasePipeFlowSystem flowSystem = new OnePhasePipeFlowSystem();
flowSystem.setInletFluid(gas);
flowSystem.setGeometry(pipe);
flowSystem.setInletPressure(50.0, "bara");
flowSystem.setOutletPressure(40.0, "bara");
flowSystem.setNumberOfNodes(100);
// Initialize and solve
flowSystem.init();
flowSystem.solveTransient(1);
// Get results
double pressureDrop = flowSystem.getPressureDrop();
double velocity = flowSystem.getFlowVelocity();
import neqsim.fluidmechanics.flowsystem.twophaseflowsystem.twophasepipeflowsystem.*;
import neqsim.thermo.system.SystemSrkEos;
// Create two-phase fluid
SystemInterface fluid = new SystemSrkEos(300.0, 30.0);
fluid.addComponent("methane", 0.80, 0); // Gas phase
fluid.addComponent("n-decane", 0.20, 1); // Liquid phase
fluid.createDatabase(true);
fluid.setMixingRule(2);
// Create horizontal pipe using factory method
TwoPhasePipeFlowSystem pipe = TwoPhasePipeFlowSystem.horizontalPipe(fluid, 0.15, 1000, 50);
// Solve with mass transfer and get structured results
PipeFlowResult result = pipe.solveWithMassTransfer();
// Access results
System.out.println("Pressure drop: " + result.getTotalPressureDrop() + " bar");
System.out.println("Outlet temperature: " + result.getOutletTemperature() + " K");
System.out.println(result); // Formatted summary
// Export for plotting (e.g., in Jupyter with neqsim-python)
Map<String, double[]> data = result.toMap();
| Method | Description |
|---|---|
horizontalPipe(fluid, diam, len, nodes) |
Horizontal pipe |
verticalPipe(fluid, diam, len, nodes, upward) |
Vertical pipe |
inclinedPipe(fluid, diam, len, nodes, angleDeg) |
Inclined pipe |
subseaPipe(fluid, diam, len, nodes, seawaterTempC) |
Subsea pipeline |
buriedPipe(fluid, diam, len, nodes, groundTempC) |
Buried pipeline |
// For advanced configurations, use the builder
TwoPhasePipeFlowSystem pipe = TwoPhasePipeFlowSystem.builder()
.withFluid(fluid)
.withDiameter(0.15, "m")
.withLength(1000, "m")
.withNodes(50)
.withFlowPattern(FlowPattern.STRATIFIED)
.withConvectiveBoundary(278.15, "K", 10.0)
.enableNonEquilibriumMassTransfer()
.build();
PipeFlowResult result = pipe.solve();
Flow nodes discretize the pipe and calculate local conditions.
| Property | Description |
|---|---|
| Pressure | Local pressure |
| Temperature | Local temperature |
| Velocity | Phase velocities |
| Holdup | Liquid holdup |
| Reynolds number | Flow regime indicator |
| Friction factor | Wall friction |
| Regime | Class | Description |
|---|---|---|
| Stratified | StratifiedFlow |
Separated gas-liquid layers |
| Annular | AnnularFlow |
Liquid film on wall, gas core |
| Droplet/Mist | DropletFlow |
Liquid droplets in gas |
| Slug | SlugFlow |
Intermittent gas-liquid slugs |
| Bubble | BubbleFlow |
Gas bubbles in liquid |
NeqSim distinguishes between equilibrium and non-equilibrium calculations at the gas-liquid interface. In equilibrium calculations, the phases are assumed to be at thermodynamic equilibrium at the interface. In non-equilibrium calculations, finite mass and heat transfer rates are considered.
FluidBoundary (abstract)
├── EquilibriumFluidBoundary # Interface at equilibrium
└── NonEquilibriumFluidBoundary # Finite transfer rates
└── KrishnaStandartFilmModel # Film theory implementation
└── ReactiveKrishnaStandartFilmModel # With chemical reactions
| Aspect | Equilibrium | Non-Equilibrium |
|---|---|---|
| Interface conditions | Thermodynamic equilibrium | Finite driving forces |
| Mass transfer | Instantaneous | Rate-limited |
| Heat transfer | Instantaneous | Rate-limited |
| Computation | Simpler | More rigorous |
| Applications | Long residence times | Short contact times, absorption |
// Get the flow node
FlowNodeInterface node = flowSystem.getNode(i);
// Enable mass and heat transfer calculations
node.getFluidBoundary().setMassTransferCalc(true);
node.getFluidBoundary().setHeatTransferCalc(true);
// Enable thermodynamic corrections (activity coefficients)
node.getFluidBoundary().setThermodynamicCorrections(0, true); // Gas phase
node.getFluidBoundary().setThermodynamicCorrections(1, true); // Liquid phase
// Enable finite flux corrections (Stefan flow)
node.getFluidBoundary().setFiniteFluxCorrection(0, true);
node.getFluidBoundary().setFiniteFluxCorrection(1, true);
The mass transfer in NeqSim is based on the film theory with multicomponent extensions. The key classes are:
| Class | Description |
|---|---|
FluidBoundary |
Abstract base for interphase calculations |
NonEquilibriumFluidBoundary |
Non-equilibrium mass/heat transfer |
KrishnaStandartFilmModel |
Krishna-Standart multicomponent model |
ReactiveKrishnaStandartFilmModel |
With chemical reaction enhancement |
For mass transfer from a flowing fluid to a wall (e.g., pipe wall, packing surface):
$$Sh = \frac{k_c \cdot d}{D_{AB}} = f(Re, Sc)$$
Where:
Correlations implemented:
| Flow Regime | Correlation | Range |
|---|---|---|
| Laminar | $Sh = 3.66$ | $Re < 2300$ |
| Turbulent | $Sh = 0.023 \cdot Re^{0.83} \cdot Sc^{0.33}$ | $Re > 10000$ |
| Transition | Interpolation | $2300 < Re < 10000$ |
Heat transfer to/from pipe walls follows analogous correlations to mass transfer:
$$Nu = \frac{h \cdot d}{k} = f(Re, Pr)$$
Where:
Correlations implemented:
| Flow Regime | Correlation |
|---|---|
| Laminar | $Nu = 3.66$ (constant wall temp) |
| Turbulent | Dittus-Boelter: $Nu = 0.023 \cdot Re^{0.8} \cdot Pr^{n}$ |
| Transition | Gnielinski correlation |
Where $n = 0.4$ for heating and $n = 0.3$ for cooling.
When mass transfer occurs, the heat transfer is coupled:
$$\dot{Q}_i = h_{eff} \cdot A \cdot (T_{bulk} - T_i) + \sum_j \dot{n}_j \cdot \Delta H_{vap,j}$$
The effective heat transfer coefficient accounts for the latent heat of evaporation/condensation.
For multicomponent systems, the mass transfer is described by the Maxwell-Stefan equations rather than Fick's law. The molar flux of component $i$ relative to the molar average velocity is:
$$-c_t \nabla x_i = \sum_{j=1, j \neq i}^{n} \frac{x_i N_j - x_j N_i}{c_t D_{ij}}$$
In NeqSim, this is solved using the Krishna-Standart film model:
The binary mass transfer coefficients are calculated from:
$$k_{ij} = \frac{Sh \cdot D_{ij}}{d}$$
Where $D_{ij}$ is the binary diffusion coefficient calculated from:
For multicomponent systems, the mass transfer coefficients form a matrix $[k]$:
// In KrishnaStandartFilmModel
public double calcMassTransferCoefficients(int phaseNum) {
int n = getNumberOfComponents() - 1;
for (int i = 0; i < n; i++) {
double tempVar = 0;
for (int j = 0; j < getNumberOfComponents(); j++) {
if (i != j) {
tempVar += x[j] / k_binary[i][j];
}
if (j < n) {
K[i][j] = -x[i] * (1.0/k_binary[i][j] - 1.0/k_binary[i][n]);
}
}
K[i][i] = tempVar + x[i] / k_binary[i][n];
}
return K.inverse(); // [k] matrix
}
The total molar flux vector is:
$$\mathbf{N} = c_t [\mathbf{k}] (\mathbf{x}_{bulk} - \mathbf{x}_{interface})$$
With corrections for:
The Schmidt number characterizes the ratio of momentum to mass diffusivity:
$$Sc_{ij} = \frac{\nu}{D_{ij}}$$
// Calculation in KrishnaStandartFilmModel
for (int i = 0; i < nComponents; i++) {
for (int j = 0; j < nComponents; j++) {
binarySchmidtNumber[phase][i][j] =
kinematicViscosity / diffusionCoefficient[i][j];
}
}
The interphase transport coefficients depend on the flow regime:
| Flow Regime | Gas-side $k_G$ | Liquid-side $k_L$ |
|---|---|---|
| Stratified | Smooth interface correlation | Penetration theory |
| Annular | Film correlation | Film flow correlation |
| Droplet | Droplet correlations | Internal circulation |
| Bubble | External mass transfer | Higbie penetration |
Heat transfer between gas and liquid phases occurs at the interface:
$$\dot{Q}_{GL} = h_{GL} \cdot A_i \cdot (T_G - T_L)$$
Where $A_i$ is the interfacial area per unit volume.
The interphase heat transfer coefficient is related to mass transfer through the Chilton-Colburn analogy:
$$\frac{h}{k_c \cdot \rho \cdot c_p} = \left(\frac{Sc}{Pr}\right)^{2/3}$$
Implemented correlations by flow regime:
| Flow Regime | Correlation Type |
|---|---|
| Stratified | Flat interface model |
| Annular | Film evaporation/condensation |
| Dispersed | Droplet/bubble heat transfer |
In non-equilibrium calculations, heat and mass transfer are coupled through:
The interphase heat flux includes both contributions:
$$\dot{Q}_i = h \cdot (T_{bulk} - T_i) + \sum_j N_j \cdot \bar{H}_j$$
Where $\bar{H}_j$ is the partial molar enthalpy of component $j$.
For heat transfer to the pipe wall in two-phase flow:
// Set overall heat transfer coefficient
pipe.setOverallHeatTransferCoefficient(10.0); // W/(m²·K)
// Or calculate from resistances
// 1/U = 1/h_inner + ln(r_o/r_i)/(2πkL) + 1/h_outer
For absorption with chemical reaction (e.g., CO₂ into amine solutions), the mass transfer is enhanced:
$$N_{CO2} = E \cdot k_L \cdot (C_{CO2,i} - C_{CO2,bulk})$$
Where $E$ is the enhancement factor.
| Model | Description | Application |
|---|---|---|
| Film theory | $E = \sqrt{1 + Ha^2}$ | Fast reactions |
| Penetration theory | Numerical solution | Moderate reactions |
| Danckwerts | Pseudo-first order | Industrial absorbers |
The Hatta number characterizes the reaction regime:
$$Ha = \frac{\sqrt{k_{rxn} \cdot D_A}}{k_L}$$
// ReactiveKrishnaStandartFilmModel extends KrishnaStandartFilmModel
// Enhancement factor calculation
EnhancementFactor enhancement = new EnhancementFactor();
double E = enhancement.calculate(hattaNumber, reactionOrder);
// Apply to mass transfer
double N_CO2 = E * k_L * (C_interface - C_bulk);
NeqSim includes specific models for CO₂ absorption into:
Reaction kinetics: $$r_{CO2} = k_2 \cdot [CO2] \cdot [Amine]$$
With temperature-dependent rate constants from experimental data.
// Darcy-Weisbach equation
// ΔP = f * (L/D) * (ρ * v²/2)
// Friction factor correlations:
// - Moody (explicit)
// - Colebrook-White (implicit)
// - Chen (explicit approximation)
| Correlation | Application |
|---|---|
| Beggs-Brill | General two-phase |
| Lockhart-Martinelli | Separated flow |
| Duns-Ros | Vertical wells |
| Hagedorn-Brown | Vertical wells |
| Gray | Gas-condensate wells |
// Set up transient simulation
flowSystem.init();
double simulationTime = 3600.0; // 1 hour
double timeStep = 1.0; // 1 second
for (double t = 0; t < simulationTime; t += timeStep) {
flowSystem.solveTransient(1);
// Get time series data
TimeSeries data = flowSystem.getTimeSeries();
// Log results
for (int i = 0; i < flowSystem.getNumberOfNodes(); i++) {
double x = flowSystem.getNode(i).getPosition();
double P = flowSystem.getNode(i).getPressure();
double T = flowSystem.getNode(i).getTemperature();
}
}
// Set ambient conditions
flowSystem.setSurroundingTemperature(288.15); // K
// Set overall heat transfer coefficient
pipe.setOverallHeatTransferCoefficient(10.0); // W/(m²·K)
// Or specify insulation
pipe.setInsulationThickness(0.05, "m");
pipe.setInsulationConductivity(0.04); // W/(m·K)
// Solve with heat transfer
flowSystem.setCalculateHeatTransfer(true);
flowSystem.solveTransient(1);
// Get temperature profile
for (int i = 0; i < flowSystem.getNumberOfNodes(); i++) {
double T = flowSystem.getNode(i).getTemperature();
}
PipeGeometry pipe = new PipeGeometry("Export Pipeline");
// Dimensions
pipe.setDiameter(0.4, "m");
pipe.setLength(50000.0, "m"); // 50 km
pipe.setRoughness(0.000045, "m");
// Inclination profile (optional)
double[] distances = {0, 10000, 20000, 30000, 40000, 50000};
double[] elevations = {0, 50, 100, 80, 120, 150};
pipe.setElevationProfile(distances, elevations);
For complex internal structures (coatings, deposits).
InternalGeometry internal = new InternalGeometry();
internal.setCoatingThickness(0.002, "m");
internal.setWaxThickness(0.001, "m");
pipe.setInternalGeometry(internal);
FlowSolverInterface solver = flowSystem.getSolver();
// Solver settings
solver.setMaxIterations(100);
solver.setConvergenceCriteria(1e-6);
solver.setRelaxationFactor(0.8);
import neqsim.process.equipment.pipeline.Pipeline;
// Use Pipeline equipment in ProcessSystem
Pipeline pipeline = new Pipeline("Export Line", inletStream);
pipeline.setLength(50.0, "km");
pipeline.setDiameter(0.5, "m");
pipeline.setOutletPressure(30.0, "bara");
pipeline.run();
Stream outlet = pipeline.getOutletStream();
double Tout = outlet.getTemperature("C");
// Get display interface
FlowSystemVisualizationInterface display = flowSystem.getDisplay();
// Plot pressure profile
display.plotPressureProfile();
// Plot temperature profile
display.plotTemperatureProfile();
// Plot holdup (two-phase)
display.plotHoldupProfile();
// Natural gas pipeline simulation
SystemInterface gas = new SystemSrkEos(288.15, 80.0);
gas.addComponent("nitrogen", 0.02);
gas.addComponent("CO2", 0.01);
gas.addComponent("methane", 0.90);
gas.addComponent("ethane", 0.05);
gas.addComponent("propane", 0.02);
gas.setMixingRule("classic");
// Set flow rate
gas.setTotalFlowRate(50.0, "MSm3/day");
// Pipeline geometry
PipeGeometry pipe = new PipeGeometry("Gas Export");
pipe.setDiameter(0.9, "m");
pipe.setLength(200000.0, "m"); // 200 km
pipe.setRoughness(0.00004, "m");
// Flow system
OnePhasePipeFlowSystem gasFlow = new OnePhasePipeFlowSystem();
gasFlow.setInletFluid(gas);
gasFlow.setGeometry(pipe);
gasFlow.setInletPressure(80.0, "bara");
gasFlow.setNumberOfNodes(200);
gasFlow.init();
gasFlow.solveTransient(1);
// Results
System.out.println("Inlet pressure: " + gasFlow.getInletPressure() + " bar");
System.out.println("Outlet pressure: " + gasFlow.getOutletPressure() + " bar");
System.out.println("Pressure drop: " + gasFlow.getPressureDrop() + " bar");
System.out.println("Flow velocity: " + gasFlow.getFlowVelocity() + " m/s");
The fluid mechanics package includes comprehensive unit tests:
| Test File | Coverage |
|---|---|
TwoPhasePipeFlowSystemTest.java |
System setup, steady-state solving, mass/heat transfer, model comparisons |
NonEquilibriumPipeFlowTest.java |
Non-equilibrium mass transfer, evaporation, dissolution, bidirectional transfer |
FlowPatternDetectorTest.java |
Flow pattern detection models (Taitel-Dukler, Baker, Barnea, Beggs-Brill) |
InterfacialAreaCalculatorTest.java |
Interfacial area calculations for all flow patterns |
MassTransferCoefficientCalculatorTest.java |
Mass transfer coefficient correlations |
TwoPhasePipeFlowSystemBuilderTest.java |
Builder API tests |
Some advanced test scenarios are disabled pending solver optimization:
See TwoPhasePipeFlowSystem_Development_Plan.md for details.
Solbraa, E. (2002). Equilibrium and Non-Equilibrium Thermodynamics of Natural Gas Processing. Dr.ing. thesis, NTNU. ISBN: 978-82-471-5541-7. Available at NVA
Krishna, R., Standart, G.L. (1976). Mass and energy transfer in multicomponent systems. Chemical Engineering Communications, 3(4-5), 201-275.
Taylor, R., Krishna, R. (1993). Multicomponent Mass Transfer. Wiley.
Bird, R.B., Stewart, W.E., Lightfoot, E.N. (2002). Transport Phenomena. 2nd ed. Wiley.
Danckwerts, P.V. (1970). Gas-Liquid Reactions. McGraw-Hill.
This documentation covers pipeline pressure drop, flow, and heat transfer calculations in NeqSim.
| Document | Description |
|---|---|
| Pipeline Pressure Drop | Overview of all pipeline models, quick start examples |
| Model Recommendations | Which model to use for your application |
| Document | Description |
|---|---|
| Beggs & Brill Correlation | Multiphase flow correlation theory and usage |
| Friction Factor Models | Haaland, Colebrook-White, laminar/turbulent |
| Heat Transfer | Non-adiabatic operation, cooling, Gnielinski |
| Transient Simulation | Dynamic simulation, slow wave propagation |
| Water Hammer | Fast transients, pressure surges, MOC solver |
┌─────────────────────────────────────────────────────────────────┐
│ PIPELINE MODELS │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Single-Phase Gas → AdiabaticPipe │
│ Single-Phase Liquid → PipeBeggsAndBrills │
│ Two-Phase (Gas-Liquid) → PipeBeggsAndBrills │
│ Three-Phase (G-O-W) → PipeBeggsAndBrills │
│ With Elevation → PipeBeggsAndBrills │
│ With Heat Transfer → PipeBeggsAndBrills │
│ Slow Transient/Dynamic → PipeBeggsAndBrills │
│ Water Hammer/Fast Trans. → WaterHammerPipe │
│ Quick Estimate → AdiabaticTwoPhasePipe │
│ │
└─────────────────────────────────────────────────────────────────┘
| Class | Package | Description |
|---|---|---|
PipeBeggsAndBrills |
neqsim.process.equipment.pipeline |
Multiphase, elevation, heat transfer, slow transient |
WaterHammerPipe |
neqsim.process.equipment.pipeline |
Water hammer, fast pressure transients (MOC) |
AdiabaticPipe |
neqsim.process.equipment.pipeline |
Single-phase compressible gas |
AdiabaticTwoPhasePipe |
neqsim.process.equipment.pipeline |
Two-phase, horizontal |
TwoFluidPipe |
neqsim.process.equipment.pipeline |
Two-fluid model with drift-flux |
TransientPipe |
neqsim.process.equipment.pipeline |
Transient drift-flux with AUSM+ scheme |
TwoPhasePipeFlowSystem |
neqsim.fluidmechanics.flowsystem |
Low-level non-equilibrium mass/heat transfer |
For detailed non-equilibrium mass and heat transfer calculations, the TwoPhasePipeFlowSystem in the fluidmechanics package provides:
See Fluid Mechanics README and Two-Phase Pipe Flow Model for details.
setLength(double meters) - Pipe lengthsetDiameter(double meters) - Inside diametersetElevation(double meters) - Elevation change (+ = uphill)setPipeWallRoughness(double meters) - Surface roughnesssetNumberOfIncrements(int n) - Number of calculation segmentssetOutletPressure(double bara) - Specify outlet pressure, calculate flow ratesetRunAdiabatic(boolean) - Enable/disable heat exchangesetConstantSurfaceTemperature(double K) - Ambient temperaturesetHeatTransferCoefficient(double W_m2K) - Overall U-valuesetCalculateSteadyState(boolean) - Switch steady/transient moderunTransient(double dt, UUID id) - Run one time step| Pipe Material | Roughness (mm) | Roughness (m) |
|---|---|---|
| New steel | 0.046 | 4.6×10⁻⁵ |
| Corroded steel | 0.15-0.3 | 1.5-3×10⁻⁴ |
| Stainless | 0.015 | 1.5×10⁻⁵ |
| Plastic/GRP | 0.005 | 5×10⁻⁶ |
| Test Case | Model | Deviation |
|---|---|---|
| Gas (Darcy-Weisbach) | All models | <1% |
| Liquid turbulent | Beggs-Brill | -1.4% |
| Liquid laminar | Beggs-Brill | 0% |
| Uphill two-phase | Beggs-Brill | Validated |
| Transient convergence | Beggs-Brill | <15% |
For questions or issues:
This document provides a comprehensive reference for single-phase pipeline flow simulation in NeqSim, including the governing equations, discretization schemes, and numerical solution methods.
The one-dimensional continuity equation for compressible flow in a pipe:
$$ \frac{\partial \rho}{\partial t} + \frac{\partial (\rho v)}{\partial x} = 0 $$
Where:
The momentum equation including friction and gravity:
$$ \frac{\partial (\rho v)}{\partial t} + \frac{\partial (\rho v^2)}{\partial x} = -\frac{\partial P}{\partial x} - \frac{f \rho v |v|}{2D} - \rho g \sin\theta $$
Where:
The energy equation with heat transfer:
$$ \frac{\partial (\rho e)}{\partial t} + \frac{\partial (\rho v h)}{\partial x} = \frac{q_{wall}}{\pi D^2 / 4} $$
Where:
For each component $i$:
$$ \frac{\partial (\rho w_i)}{\partial t} + \frac{\partial (\rho v w_i)}{\partial x} = 0 $$
Where:
For steady-state flow, the momentum equation simplifies to:
$$ \frac{dP}{dx} = -\frac{f \rho v^2}{2D} - \rho g \sin\theta $$
The Darcy friction factor $f$ is calculated using the Colebrook-White equation:
$$ \frac{1}{\sqrt{f}} = -2 \log_{10}\left(\frac{\varepsilon/D}{3.7} + \frac{2.51}{Re \sqrt{f}}\right) $$
Where:
The pipe is divided into $N$ nodes. For node $i$:
$$ P_{i+1} = P_i - \Delta x \left(\frac{f_i \rho_i v_i^2}{2D} + \rho_i g \sin\theta_i\right) $$
NeqSim uses a staggered grid finite volume method. The pipe is divided into control volumes with:
Implicit (backward) Euler scheme for stability:
$$ \frac{\phi^{n+1} - \phi^n}{\Delta t} + \frac{\partial F}{\partial x}\bigg|^{n+1} = S^{n+1} $$
Where:
For numerical stability, the Courant-Friedrichs-Lewy (CFL) number should satisfy:
$$ CFL = \frac{(v + c) \Delta t}{\Delta x} \leq 1 $$
Where $c$ is the speed of sound in the fluid.
The discretized equations form a tri-diagonal matrix system:
$$ a_i \phi_{i-1} + b_i \phi_i + c_i \phi_{i+1} = r_i $$
Solved using the Thomas algorithm (TDMA).
The mass fraction transport equation in conservative form:
$$ \frac{\partial (\rho A w)}{\partial t} + \frac{\partial (\dot{m} w)}{\partial x} = 0 $$
Where:
For control volume $i$:
$$ \frac{(\rho A w)_i^{n+1} - (\rho A w)_i^{n}}{\Delta t} + \frac{F_e - F_w}{\Delta x} = 0 $$
Where $F_e$ and $F_w$ are the convective fluxes at east and west faces.
The convective flux at a face is:
$$ F_e = \max(\dot{m}_e, 0) w_i + \min(\dot{m}_e, 0) w_{i+1} $$
This leads to the coefficient matrix:
First-order upwind introduces artificial (numerical) diffusion:
$$ D_{num} = \frac{v \Delta x}{2} (1 - CFL) $$
This causes composition fronts to spread over distance:
$$ \sigma = \sqrt{2 D_{num} t} = \sqrt{\Delta x \cdot L \cdot (1 - CFL)} $$
Where $L$ is the transport distance.
| Scheme | Order | Numerical Dispersion | Stability |
|---|---|---|---|
| First-Order Upwind | 1 | High | Unconditional |
| Second-Order Upwind | 2 | Low | CFL ≤ 0.5 |
| QUICK | 3 | Very Low | CFL ≤ 0.5 |
| TVD Van Leer | 2 | Low | CFL ≤ 1.0 |
| TVD Minmod | 2 | Medium | CFL ≤ 1.0 |
| TVD Superbee | 2 | Very Low | CFL ≤ 1.0 |
| TVD Van Albada | 2 | Low | CFL ≤ 1.0 |
| MUSCL Van Leer | 2 | Low | CFL ≤ 1.0 |
TVD schemes use flux limiters to achieve high accuracy in smooth regions while preventing oscillations near discontinuities.
The flux limiter $\psi(r)$ depends on the gradient ratio:
$$ r = \frac{\phi_i - \phi_{i-1}}{\phi_{i+1} - \phi_i} $$
Minmod (most diffusive): $$ \psi(r) = \max(0, \min(r, 1)) $$
Van Leer (recommended): $$ \psi(r) = \frac{r + |r|}{1 + |r|} $$
Superbee (least diffusive): $$ \psi(r) = \max(0, \min(2r, 1), \min(r, 2)) $$
Van Albada (smooth): $$ \psi(r) = \frac{r^2 + r}{r^2 + 1} $$
The higher-order flux correction is:
$$ F_{HO} = F_{LO} + \frac{1}{2} \psi(r) |F| (1 - |CFL|) (\phi_{downstream} - \phi_{upstream}) $$
Where $F_{LO}$ is the first-order upwind flux.
The effective numerical diffusion with TVD schemes:
$$ D_{eff} = D_{num} \times \text{ReductionFactor} $$
Typical reduction factors:
Fixed conditions from upstream:
Typically one of:
// Inlet: Dirichlet condition
a[0] = 0;
b[0] = 1;
c[0] = 0;
r[0] = w_inlet;
// Interior nodes: discretized conservation equation
// ... (TDMA coefficients)
// Outlet: extrapolation or fixed value
for each time step:
1. Apply inlet boundary conditions
2. Calculate time step (CFL condition)
3. Assemble coefficient matrices
4. Solve TDMA for each conservation equation:
- Momentum (pressure/velocity)
- Energy (temperature)
- Species (composition)
5. Update fluid properties (EOS flash)
6. Update outlet stream
7. Store results
// Create pipeline
OnePhasePipeLine pipe = new OnePhasePipeLine("GasPipe", inletStream);
pipe.setNumberOfLegs(1);
pipe.setNumberOfNodesInLeg(100);
pipe.setPipeDiameters(new double[] {0.3, 0.3});
pipe.setLegPositions(new double[] {0.0, 5000.0});
// Enable compositional tracking with TVD scheme
pipe.setCompositionalTracking(true);
pipe.setAdvectionScheme(AdvectionScheme.TVD_VAN_LEER);
// Run steady-state initialization
pipe.run();
// Run transient simulation
UUID id = UUID.randomUUID();
for (int step = 0; step < 100; step++) {
pipe.runTransient(1.0, id); // 1 second time step
}
NeqSim provides single-phase gas pipeline simulation capabilities through the PipeFlowSystem class, implementing a staggered grid finite volume method with TDMA (Tri-Diagonal Matrix Algorithm) solver.
FlowSystem (abstract)
└── OnePhaseFlowSystem (abstract)
└── PipeFlowSystem (concrete)
| Component | Description |
|---|---|
PipeFlowSystem |
Main flow system for single-phase pipe flow |
OnePhaseFixedStaggeredGrid |
Staggered grid solver with TDMA |
onePhasePipeFlowNode |
Flow node for single-phase pipe segments |
TimeSeries |
Time-varying inlet conditions for transient simulation |
The solver implements the following conservation equations:
$$\frac{\partial \rho}{\partial t} + \frac{\partial (\rho v)}{\partial x} = 0$$
$$\frac{\partial (\rho v)}{\partial t} + \frac{\partial (\rho v^2)}{\partial x} = -\frac{\partial P}{\partial x} - \rho g \sin(\theta) - \frac{f \rho v |v|}{2D}$$
where:
$$\frac{\partial (\rho h)}{\partial t} + \frac{\partial (\rho v h)}{\partial x} = Q_{wall} + \rho v g \sin(\theta)$$
where:
For each component $i$:
$$\frac{\partial (\rho \omega_i)}{\partial t} + \frac{\partial (\rho v \omega_i)}{\partial x} = 0$$
where $\omega_i$ is the mass fraction of component $i$.
The solver uses a staggered grid approach:
The Tri-Diagonal Matrix Algorithm efficiently solves the linearized system:
a[i] * φ[i-1] + b[i] * φ[i] + c[i] * φ[i+1] = r[i]
Convective terms use upwind differencing for stability:
a[i] = Math.max(Fw, 0); // West face flux
c[i] = Math.max(-Fe, 0); // East face flux
The solver supports different levels of physics:
| Type | Description |
|---|---|
| 0 | Momentum only (isothermal, incompressible) |
| 1 | Momentum + mass (compressible) |
| 10 | Momentum + mass + energy |
| 20 | Momentum + mass + energy + composition |
import neqsim.fluidmechanics.flowsystem.onephaseflowsystem.pipeflowsystem.PipeFlowSystem;
import neqsim.fluidmechanics.geometrydefinitions.pipe.PipeData;
import neqsim.thermo.system.SystemSrkEos;
// Create gas system
SystemInterface gas = new SystemSrkEos(288.15, 100.0); // 15°C, 100 bar
gas.addComponent("methane", 0.90);
gas.addComponent("ethane", 0.10);
gas.createDatabase(true);
gas.init(0);
gas.init(3);
gas.initPhysicalProperties();
gas.setTotalFlowRate(10.0, "MSm3/day");
// Configure pipeline
FlowSystemInterface pipe = new PipeFlowSystem();
pipe.setInletThermoSystem(gas);
pipe.setNumberOfLegs(10);
pipe.setNumberOfNodesInLeg(20);
// Set geometry (10 segments)
double[] heights = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
double[] positions = {0, 10000, 20000, 30000, 40000, 50000,
60000, 70000, 80000, 90000, 100000}; // meters
GeometryDefinitionInterface[] geometry = new PipeData[11];
for (int i = 0; i <= 10; i++) {
geometry[i] = new PipeData();
geometry[i].setDiameter(1.0); // 1 meter diameter
geometry[i].setInnerSurfaceRoughness(1e-5);
}
pipe.setEquipmentGeometry(geometry);
pipe.setLegHeights(heights);
pipe.setLegPositions(positions);
pipe.setLegOuterTemperatures(new double[]{278, 278, 278, 278, 278, 278,
278, 278, 278, 278, 278});
pipe.setLegWallHeatTransferCoefficients(new double[]{15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15});
pipe.setLegOuterHeatTransferCoefficients(new double[]{5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5});
// Solve
pipe.createSystem();
pipe.init();
pipe.solveSteadyState(10); // Type 10: with energy equation
// Get results
double pressureDrop = pipe.getTotalPressureDrop();
double outletTemp = pipe.getNode(pipe.getTotalNumberOfNodes() - 1)
.getBulkSystem().getTemperature();
The transient solver supports time-varying inlet conditions including changes in:
// First solve steady state to initialize
pipe.createSystem();
pipe.init();
pipe.solveSteadyState(10);
// Setup time series with varying inlet conditions
// Note: times array has N points, systems array has N-1 entries (one per interval)
double[] times = {0, 3000, 6000}; // 3 time points = 2 intervals
pipe.getTimeSeries().setTimes(times);
// Initial cold gas
SystemInterface coldGas = new SystemSrkEos(280.0, 100.0);
coldGas.addComponent("methane", 0.90);
coldGas.addComponent("ethane", 0.10);
coldGas.createDatabase(true);
coldGas.init(0);
coldGas.init(3);
coldGas.initPhysicalProperties();
coldGas.setTotalFlowRate(10.0, "MSm3/day");
// Hot gas with different composition
SystemInterface hotGas = new SystemSrkEos(320.0, 100.0);
hotGas.addComponent("methane", 0.80);
hotGas.addComponent("ethane", 0.20);
hotGas.createDatabase(true);
hotGas.init(0);
hotGas.init(3);
hotGas.initPhysicalProperties();
hotGas.setTotalFlowRate(10.0, "MSm3/day");
// 2 intervals: [0-3000] cold, [3000-6000] hot
SystemInterface[] systems = {coldGas, hotGas};
pipe.getTimeSeries().setInletThermoSystems(systems);
pipe.getTimeSeries().setNumberOfTimeStepsInInterval(5);
// Run transient simulation with full physics (type 20 = momentum + mass + energy + composition)
pipe.solveTransient(20);
In steady-state single-phase flow, composition is uniform throughout the pipeline:
Dynamic compositional tracking enables simulating slug flow, batch processing, and compositional transitions:
oldComposition[component][node] stores previous time step valuessetComponentConservationMatrix() builds the discretized equationsinitComposition() updates node compositions after each time stepExample - Compositional Change at Inlet:
// Initial gas with ethane
SystemInterface initialGas = new SystemSrkEos(298.0, 30.0);
initialGas.addComponent("methane", 0.9);
initialGas.addComponent("ethane", 0.1);
initialGas.initPhysicalProperties();
initialGas.setTotalFlowRate(10.0, "MSm3/day");
// New gas (pure methane)
SystemInterface newGas = initialGas.clone();
newGas.addComponent("methane", 0.1); // Shift to 100% methane
newGas.initPhysicalProperties();
// TimeSeries with 2 intervals
SystemInterface[] systems = {initialGas, newGas};
pipe.getTimeSeries().setInletThermoSystems(systems);
pipe.getTimeSeries().setNumberOfTimeStepsInInterval(10);
// Run with compositional tracking (type 20)
pipe.solveTransient(20);
The steady-state solver has been validated for:
| Test | Result |
|---|---|
| Pressure monotonically decreases | ✓ Pass |
| Temperature approaches surroundings | ✓ Pass |
| Mass conservation (inlet ≈ outlet) | ✓ Pass (within 15%) |
| Reynolds number physically correct | ✓ Pass |
| Friction factor in reasonable range | ✓ Pass |
| Composition preserved | ✓ Pass |
| Numerical stability (high flow) | ✓ Pass |
| Inclined pipeline handling | ✓ Pass |
Consider implementing:
When setting up transient simulations:
// CORRECT: 3 time points → 2 systems (one per interval)
double[] times = {0, 3000, 6000};
pipe.getTimeSeries().setOutletMolarFlowRates(times, "kg/sec");
SystemInterface[] systems = {gasForInterval1, gasForInterval2};
pipe.getTimeSeries().setInletThermoSystems(systems);
NeqSim provides three main pipeline models for calculating pressure drop:
| Model | Class | Best For |
|---|---|---|
AdiabaticPipe |
Single-phase compressible gas | High-pressure gas transmission |
AdiabaticTwoPhasePipe |
General two-phase flow | Moderate accuracy, fast computation |
PipeBeggsAndBrills |
Multiphase flow with correlations | Wells, flowlines, complex terrain |
// Create gas system
SystemInterface gas = new SystemSrkEos(298.15, 100.0); // 25°C, 100 bara
gas.addComponent("methane", 0.9);
gas.addComponent("ethane", 0.1);
gas.setMixingRule("classic");
Stream feed = new Stream("feed", gas);
feed.setFlowRate(50000, "kg/hr");
feed.run();
// Simple pipe model
AdiabaticPipe pipe = new AdiabaticPipe("pipeline", feed);
pipe.setLength(10000); // 10 km
pipe.setDiameter(0.3); // 300 mm
pipe.run();
double pressureDrop = feed.getPressure() - pipe.getOutletPressure();
// Create two-phase system
SystemInterface fluid = new SystemSrkEos(333.15, 50.0); // 60°C, 50 bara
fluid.addComponent("methane", 5000, "kg/hr");
fluid.addComponent("nC10", 50000, "kg/hr");
fluid.setMixingRule(2);
Stream feed = new Stream("feed", fluid);
feed.run();
// Beggs & Brill correlation
PipeBeggsAndBrills pipe = new PipeBeggsAndBrills("flowline", feed);
pipe.setLength(1000); // 1 km
pipe.setDiameter(0.1); // 100 mm
pipe.setElevation(50); // 50 m uphill
pipe.setPipeWallRoughness(4.6e-5); // Steel roughness
pipe.setNumberOfIncrements(20);
pipe.run();
// Get results
double dp = pipe.getInletPressure() - pipe.getOutletPressure();
String flowRegime = pipe.getFlowRegime().toString();
double liquidHoldup = pipe.getSegmentLiquidHoldup(20);
All pipeline models support a reverse calculation mode where you specify the desired outlet pressure and the model calculates the required flow rate:
// Create gas system with initial flow estimate
SystemInterface gas = new SystemSrkEos(298.15, 100.0); // 25°C, 100 bara
gas.addComponent("methane", 1.0);
gas.setMixingRule("classic");
gas.setTotalFlowRate(10000, "kg/hr"); // Initial estimate (will be recalculated)
Stream feed = new Stream("feed", gas);
feed.run();
// PipeBeggsAndBrills - most accurate for flow calculation
PipeBeggsAndBrills pipe = new PipeBeggsAndBrills("pipeline", feed);
pipe.setLength(10000); // 10 km
pipe.setDiameter(0.3); // 300 mm
pipe.setPipeWallRoughness(4.6e-5);
pipe.setNumberOfIncrements(10);
pipe.setOutletPressure(90.0); // Specify target outlet pressure (bara)
pipe.run();
// Get calculated flow rate
double flowRate = pipe.getInletStream().getFlowRate("kg/hr");
double achievedOutletP = pipe.getOutletPressure();
// flowRate ≈ 144,000 kg/hr for 10 bar pressure drop
| Model | Method | Accuracy | Notes |
|---|---|---|---|
PipeBeggsAndBrills |
setOutletPressure(double) |
Best | Uses bisection iteration |
AdiabaticPipe |
setOutPressure(double) |
Good | Single-phase gas only |
AdiabaticTwoPhasePipe |
setOutPressure(double) |
Moderate | Two-phase capable |
AdiabaticPipe when:AdiabaticTwoPhasePipe when:PipeBeggsAndBrills when:Based on validation against Darcy-Weisbach reference:
| Condition | AdiabaticPipe | TwoPhasePipe | BeggsAndBrills |
|---|---|---|---|
| Single-phase gas | +0.9% | -0.1% | +0.5% |
| Single-phase liquid (turbulent) | -4.1% | -1.4% | -1.4% |
| Single-phase liquid (laminar) | N/A | ~0% | ~0% |
| Two-phase horizontal | N/A | N/A | Validated |
| Inclined pipe | N/A | N/A | Validated |
Specify flow rate → Calculate outlet pressure
feed.setFlowRate(50000, "kg/hr"); // Known flow rate
pipe.run();
double pOut = pipe.getOutletPressure(); // Calculated
Specify outlet pressure → Calculate flow rate
pipe.setOutletPressure(90.0); // Target outlet pressure
pipe.run();
double flow = feed.getFlowRate("kg/hr"); // Calculated
The reverse calculation uses a bisection algorithm that iteratively adjusts the flow rate until the calculated outlet pressure matches the specified target.
The Beggs & Brill correlation (1973) is a widely-used empirical method for predicting pressure drop and liquid holdup in multiphase pipe flow. It handles:
The total pressure gradient consists of three components:
$$\frac{dP}{dL} = \frac{dP}{dL}_{friction} + \frac{dP}{dL}_{hydrostatic} + \frac{dP}{dL}_{acceleration}$$
In NeqSim, the acceleration term is typically neglected (small for steady flow), so:
$$\Delta P = \Delta P_{friction} + \Delta P_{hydrostatic}$$
The correlation identifies four flow regimes based on dimensionless parameters:
| Flow Regime | Description | Typical Conditions |
|---|---|---|
| Segregated | Stratified or wavy flow | Low velocities, horizontal |
| Intermittent | Slug or plug flow | Moderate velocities |
| Distributed | Bubble or mist flow | High velocities |
| Transition | Between segregated and intermittent | Transitional |
Flow regime is determined by:
Where:
Liquid holdup ($H_L$ or $E_L$) is the fraction of pipe cross-section occupied by liquid:
$$H_L = H_L(0) \cdot \psi$$
Where:
Horizontal holdup correlations:
| Regime | Correlation |
|---|---|
| Segregated | $H_L(0) = \frac{0.98 \lambda_L^{0.4846}}{Fr^{0.0868}}$ |
| Intermittent | $H_L(0) = \frac{0.845 \lambda_L^{0.5351}}{Fr^{0.0173}}$ |
| Distributed | $H_L(0) = \frac{1.065 \lambda_L^{0.5824}}{Fr^{0.0609}}$ |
$$\Delta P_{friction} = \frac{f_{tp} \cdot \rho_{ns} \cdot v_m^2 \cdot L}{2D}$$
Where:
$$\Delta P_{hydrostatic} = \rho_m \cdot g \cdot \Delta h$$
Where:
PipeBeggsAndBrills pipe = new PipeBeggsAndBrills("flowline", inletStream);
// Geometry
pipe.setLength(1000); // meters
pipe.setDiameter(0.1); // meters
pipe.setElevation(100); // meters (positive = uphill)
pipe.setAngle(5.7); // degrees (alternative to elevation)
pipe.setPipeWallRoughness(4.6e-5); // meters (steel ≈ 0.046 mm)
// Numerical settings
pipe.setNumberOfIncrements(20); // segments for integration
pipe.run();
// Overall results
double pressureDrop = pipe.getInletPressure() - pipe.getOutletPressure();
double outletTemp = pipe.getOutletTemperature();
// Flow regime
PipeBeggsAndBrills.FlowRegime regime = pipe.getFlowRegime();
// Returns: SEGREGATED, INTERMITTENT, DISTRIBUTED, TRANSITION, or SINGLE_PHASE
// Profile data (for segment i)
double holdup = pipe.getSegmentLiquidHoldup(i);
double mixtureDensity = pipe.getSegmentMixtureDensity(i);
double velocity = pipe.getSegmentMixtureSuperficialVelocity(i);
// Full profiles
List<Double> pressureProfile = pipe.getPressureProfile();
List<Double> temperatureProfile = pipe.getTemperatureProfile();
// Adiabatic (default)
pipe.setRunAdiabatic(true);
// With heat transfer
pipe.setRunAdiabatic(false);
pipe.setConstantSurfaceTemperature(283.15); // 10°C ambient
pipe.setHeatTransferCoefficient(10.0); // W/m²K
// Or let it estimate:
pipe.setHeatTransferCoefficientMethod("Estimated");
For three-phase systems, the liquid phase properties are calculated as volume-weighted averages:
SystemInterface fluid = new SystemSrkEos(333.15, 30.0);
fluid.addComponent("methane", 3000, "kg/hr");
fluid.addComponent("nC10", 40000, "kg/hr");
fluid.addComponent("water", 20000, "kg/hr");
fluid.setMixingRule("classic");
fluid.setMultiPhaseCheck(true); // Enable water phase
Stream feed = new Stream("feed", fluid);
feed.run();
PipeBeggsAndBrills pipe = new PipeBeggsAndBrills("pipe", feed);
// ... configure and run
NeqSim's implementation has been validated against:
| Test Case | Reference | Deviation |
|---|---|---|
| Single-phase gas (turbulent) | Darcy-Weisbach | +0.5% |
| Single-phase liquid (turbulent) | Darcy-Weisbach | -1.4% |
| Single-phase liquid (laminar) | Darcy-Weisbach | 0.0% |
| Two-phase horizontal | Dukler, Homogeneous | Reasonable |
| Inclined pipes | Steady-state physics | Validated |
Beggs, H.D. and Brill, J.P. (1973). "A Study of Two-Phase Flow in Inclined Pipes". Journal of Petroleum Technology, 25(5), 607-617.
Brill, J.P. and Mukherjee, H. (1999). Multiphase Flow in Wells. SPE Monograph Series.
Shoham, O. (2006). Mechanistic Modeling of Gas-Liquid Two-Phase Flow in Pipes. SPE Books.
Friction factor is a critical parameter in pressure drop calculations. NeqSim implements industry-standard correlations for both laminar and turbulent flow.
For laminar flow, the Darcy friction factor is:
$$f = \frac{64}{Re}$$
Where Reynolds number: $$Re = \frac{\rho v D}{\mu}$$
Linear interpolation between laminar and turbulent:
$$f = f_{laminar} + \frac{Re - 2300}{1700}(f_{turbulent,4000} - f_{laminar,2300})$$
NeqSim uses the Haaland equation, an explicit approximation of Colebrook-White:
$$f = \left[ -1.8 \log_{10}\left( \left(\frac{\varepsilon/D}{3.7}\right)^{1.11} + \frac{6.9}{Re} \right) \right]^{-2}$$
Where:
Advantages:
The implicit Colebrook-White equation (used for validation):
$$\frac{1}{\sqrt{f}} = -2 \log_{10}\left( \frac{\varepsilon/D}{3.7} + \frac{2.51}{Re\sqrt{f}} \right)$$
Solved iteratively using Newton-Raphson method.
For multiphase flow, the single-phase friction factor is modified:
$$f_{tp} = f_{ns} \cdot e^S$$
Where:
The slip factor $S$ depends on the liquid holdup ratio: $$y = \frac{\lambda_L}{H_L^2}$$
For $1 < y < 1.2$: $$S = \ln(2.2y - 1.2)$$
Otherwise: $$S = \frac{\ln(y)}{-0.0523 + 3.18\ln(y) - 0.872[\ln(y)]^2 + 0.01853[\ln(y)]^4}$$
| Material | Roughness ε (mm) | Roughness ε (m) |
|---|---|---|
| Commercial steel (new) | 0.046 | 4.6×10⁻⁵ |
| Commercial steel (rusted) | 0.15-0.3 | 1.5-3×10⁻⁴ |
| Stainless steel | 0.015 | 1.5×10⁻⁵ |
| Drawn tubing (copper, brass) | 0.0015 | 1.5×10⁻⁶ |
| Cast iron | 0.26 | 2.6×10⁻⁴ |
| Concrete | 0.3-3.0 | 3×10⁻⁴ to 3×10⁻³ |
| PVC/Plastic | 0.0015-0.007 | 1.5-7×10⁻⁶ |
| GRP/FRP | 0.01 | 1×10⁻⁵ |
// For PipeBeggsAndBrills
pipe.setPipeWallRoughness(4.6e-5); // meters
// For AdiabaticPipe
pipe.setWallRoughness(4.6e-5); // meters
For two-phase flow, the no-slip Reynolds number is used:
$$Re_{ns} = \frac{\rho_{ns} \cdot v_m \cdot D}{\mu_{ns}}$$
Where:
// Get friction-related results
PipeBeggsAndBrills pipe = new PipeBeggsAndBrills("pipe", stream);
pipe.setLength(1000);
pipe.setDiameter(0.1);
pipe.setPipeWallRoughness(4.6e-5);
pipe.run();
// Access Reynolds number and friction factor for each segment
for (int i = 1; i <= pipe.getNumberOfIncrements(); i++) {
double Re = pipe.getSegmentMixtureReynoldsNumber(i);
// Friction factor is internal but affects pressure drop
}
Comparison of NeqSim friction factor implementation against Colebrook-White:
| Reynolds | ε/D | Haaland f | Colebrook f | Deviation |
|---|---|---|---|---|
| 10,000 | 0.001 | 0.0380 | 0.0382 | -0.5% |
| 100,000 | 0.001 | 0.0227 | 0.0228 | -0.4% |
| 1,000,000 | 0.001 | 0.0197 | 0.0197 | 0.0% |
| 10,000,000 | 0.001 | 0.0191 | 0.0191 | 0.0% |
Haaland, S.E. (1983). "Simple and Explicit Formulas for the Friction Factor in Turbulent Pipe Flow". Journal of Fluids Engineering, 105(1), 89-90.
Colebrook, C.F. (1939). "Turbulent Flow in Pipes with Particular Reference to the Transition Region Between Smooth and Rough Pipe Laws". Journal of the Institution of Civil Engineers, 11, 133-156.
Moody, L.F. (1944). "Friction Factors for Pipe Flow". Transactions of the ASME, 66, 671-684.
NeqSim's PipeBeggsAndBrills class supports non-adiabatic operation with heat exchange to/from the surroundings. This is important for:
No heat exchange with surroundings:
pipe.setRunAdiabatic(true); // Default
Heat transfer with fixed ambient temperature:
pipe.setRunAdiabatic(false);
pipe.setRunConstantSurfaceTemperature(true);
pipe.setConstantSurfaceTemperature(277.15); // 4°C (seawater)
pipe.setHeatTransferCoefficient(50.0); // W/m²K
Uses internal correlations:
pipe.setHeatTransferCoefficientMethod("Estimated");
The temperature change across a segment is calculated from:
$$\dot{Q} = U \cdot A \cdot \Delta T_{lm}$$
Where:
$$\Delta T_{lm} = \frac{(T_s - T_{out}) - (T_s - T_{in})}{\ln\left(\frac{T_s - T_{out}}{T_s - T_{in}}\right)}$$
Where:
For internal convection in turbulent flow (3000 < Re < 5×10⁶):
$$Nu = \frac{(f/8)(Re - 1000)Pr}{1 + 12.7\sqrt{f/8}(Pr^{2/3} - 1)}$$
Where:
The internal heat transfer coefficient: $$h_{internal} = \frac{Nu \cdot k}{D}$$
| Configuration | U (W/m²K) | Application |
|---|---|---|
| Bare steel in air | 10-25 | Onshore exposed |
| Bare steel in water | 300-500 | Uninsulated subsea |
| 25mm insulation | 3-5 | Standard insulated |
| 50mm insulation | 1.5-3 | Well insulated |
| 75mm+ insulation | <1.5 | Heavily insulated |
| Pipe-in-pipe | 0.5-2 | High spec subsea |
| Fluid | h_internal (W/m²K) |
|---|---|
| Gas (low P) | 20-50 |
| Gas (high P) | 100-300 |
| Light oil | 100-300 |
| Heavy oil | 50-150 |
| Water | 1000-5000 |
| Two-phase | 200-1000 |
SystemInterface gas = new SystemSrkEos(353.15, 100.0); // 80°C wellhead
gas.addComponent("methane", 0.85);
gas.addComponent("ethane", 0.10);
gas.addComponent("propane", 0.05);
gas.setMixingRule(2);
Stream wellhead = new Stream("wellhead", gas);
wellhead.setFlowRate(100000, "kg/hr");
wellhead.run();
PipeBeggsAndBrills subsea = new PipeBeggsAndBrills("subsea", wellhead);
subsea.setLength(20000); // 20 km
subsea.setDiameter(0.254); // 10 inch
subsea.setElevation(-200); // 200m water depth
subsea.setPipeWallRoughness(4.6e-5);
subsea.setNumberOfIncrements(40);
// Heat transfer to seawater
subsea.setRunAdiabatic(false);
subsea.setRunConstantSurfaceTemperature(true);
subsea.setConstantSurfaceTemperature(277.15); // 4°C seabed
subsea.setHeatTransferCoefficient(5.0); // Insulated
subsea.run();
double outletTemp = subsea.getOutletTemperature() - 273.15;
System.out.println("Arrival temperature: " + outletTemp + " °C");
// Get temperature along the pipeline
List<Double> tempProfile = subsea.getTemperatureProfile();
double segmentLength = 20000.0 / 40;
for (int i = 0; i < tempProfile.size(); i++) {
double distance = i * segmentLength / 1000.0; // km
double tempC = tempProfile.get(i) - 273.15;
System.out.println(distance + " km: " + tempC + " °C");
}
Monitor temperature relative to hydrate equilibrium:
double hydroEqTemp = ...; // From hydrate flash
double margin = outletTemp - hydroEqTemp;
if (margin < 5.0) {
System.out.println("WARNING: Close to hydrate region");
}
Check against wax appearance temperature (WAT):
double WAT = ...; // From wax analysis
if (outletTemp < WAT) {
System.out.println("WARNING: Below WAT - wax may deposit");
}
Use more segments for accurate temperature profiles:
pipe.setNumberOfIncrements(50); // For long, cooling pipelines
For long pipes with large temperature change, check: $$T_{out} \approx T_s + (T_{in} - T_s) \cdot e^{-UAL/(\dot{m}c_p)}$$
Heat transfer coefficients are higher for two-phase flow due to turbulence.
This document provides detailed documentation of the heat transfer models implemented in the NeqSim fluid mechanics package.
NeqSim implements comprehensive heat transfer models for:
The models are based on established correlations and are coupled with the rigorous thermodynamic calculations in NeqSim.
The energy conservation equation for pipe flow:
$$\frac{\partial (\rho h)}{\partial t} + \frac{\partial (\rho v h)}{\partial z} = \dot{Q}_{wall} + \dot{Q}_{interphase}$$
Where:
| Mechanism | Equation | Application |
|---|---|---|
| Conduction | $q = -k \nabla T$ | Through solid walls, stagnant fluids |
| Convection | $q = h (T_w - T_f)$ | Flowing fluids to walls |
| Radiation | $q = \epsilon \sigma (T_1^4 - T_2^4)$ | High temperature systems |
| Latent heat | $\dot{Q} = \dot{m} \Delta H_{vap}$ | Phase change |
| Number | Definition | Physical Meaning |
|---|---|---|
| Nusselt (Nu) | $h \cdot d / k$ | Ratio of convective to conductive heat transfer |
| Prandtl (Pr) | $\mu c_p / k$ | Ratio of momentum to thermal diffusivity |
| Reynolds (Re) | $\rho v d / \mu$ | Ratio of inertial to viscous forces |
| Péclet (Pe) | $Re \cdot Pr$ | Ratio of advective to diffusive heat transport |
The Prandtl number characterizes the relative thickness of thermal and velocity boundary layers:
// Calculated in FlowNode
double Pr = viscosity * heatCapacity / thermalConductivity;
Typical values:
| Fluid | Pr |
|---|---|
| Gases | 0.7 - 1.0 |
| Water | 1.7 - 13 |
| Light oils | 10 - 1000 |
| Heavy oils | 100 - 100,000 |
Constant wall temperature: $$Nu = 3.66$$
Constant heat flux: $$Nu = 4.36$$
Developing flow (Sieder-Tate): $$Nu = 1.86 \left(\frac{Re \cdot Pr \cdot d}{L}\right)^{1/3} \left(\frac{\mu}{\mu_w}\right)^{0.14}$$
Dittus-Boelter equation: $$Nu = 0.023 \cdot Re^{0.8} \cdot Pr^{n}$$
Where:
Gnielinski correlation (more accurate): $$Nu = \frac{(f/8)(Re - 1000)Pr}{1 + 12.7(f/8)^{0.5}(Pr^{2/3} - 1)}$$
Valid for: $3000 < Re < 5 \times 10^6$, $0.5 < Pr < 2000$
Gnielinski correlation or linear interpolation between laminar and turbulent.
// In InterphaseTransportCoefficientBaseClass
public double calcWallHeatTransferCoefficient(int phase, double prandtlNumber,
FlowNodeInterface node) {
double Re = node.getReynoldsNumber(phase);
double Nu;
if (Re < 2300) {
Nu = 3.66; // Laminar
} else if (Re < 10000) {
// Transition - Gnielinski
double f = calcWallFrictionFactor(phase, node);
Nu = (f/8) * (Re - 1000) * prandtlNumber
/ (1 + 12.7 * Math.sqrt(f/8) * (Math.pow(prandtlNumber, 2.0/3.0) - 1));
} else {
// Turbulent - Dittus-Boelter
Nu = 0.023 * Math.pow(Re, 0.8) * Math.pow(prandtlNumber, 0.4);
}
return Nu * thermalConductivity / hydraulicDiameter;
}
Heat transfer in two-phase flow depends strongly on the flow pattern:
| Flow Pattern | Dominant Mechanism | Heat Transfer Characteristics |
|---|---|---|
| Stratified | Convection in each phase | Independent gas/liquid correlations |
| Annular | Film evaporation/condensation | High liquid-side coefficients |
| Slug | Alternating mechanisms | Time-averaged values |
| Bubble | Enhanced liquid mixing | Increased liquid-side coefficient |
| Mist | Droplet evaporation | Reduced wall wetting |
Some correlations use a two-phase multiplier:
$$h_{TP} = F \cdot h_{LO}$$
Where:
Gas and liquid are treated separately:
$$h_{gas} = \text{Single-phase correlation with } d_h = 4A_G/P_G$$ $$h_{liquid} = \text{Single-phase correlation with } d_h = 4A_L/P_L$$
Liquid film: $$h_L = 0.023 \cdot Re_f^{0.8} \cdot Pr_L^{0.4} \cdot \frac{k_L}{\delta}$$
Where $\delta$ is the film thickness and $Re_f = 4\Gamma/\mu_L$ is the film Reynolds number.
Gas core: $$h_G = 0.023 \cdot Re_G^{0.8} \cdot Pr_G^{0.4} \cdot \frac{k_G}{d - 2\delta}$$
For heat transfer from fluid to surroundings through the pipe wall:
$$\frac{1}{U} = \frac{1}{h_i} + \frac{r_i \ln(r_o/r_i)}{k_{wall}} + \frac{r_i}{r_o \cdot h_o}$$
Where:
For insulated pipes, add insulation resistance:
$$\frac{1}{U} = \frac{1}{h_i} + \frac{r_i \ln(r_o/r_i)}{k_{wall}} + \frac{r_i \ln(r_{ins}/r_o)}{k_{ins}} + \frac{r_i}{r_{ins} \cdot h_o}$$
For buried pipelines, the outer resistance includes soil conduction:
$$R_{soil} = \frac{\ln(2z/r_o)}{2\pi k_{soil}}$$
Where $z$ is the burial depth.
// Set overall heat transfer coefficient directly
pipe.setOverallHeatTransferCoefficient(10.0); // W/(m²·K)
// Or specify components
pipe.setInnerHeatTransferCoefficient(1000.0); // W/(m²·K)
pipe.setWallThickness(0.01); // m
pipe.setWallConductivity(50.0); // W/(m·K)
pipe.setInsulationThickness(0.05); // m
pipe.setInsulationConductivity(0.04); // W/(m·K)
pipe.setOuterHeatTransferCoefficient(10.0); // W/(m²·K)
// Set ambient conditions
flowSystem.setSurroundingTemperature(288.15); // K
At the gas-liquid interface:
$$\dot{Q}_{GL} = h_{GL} \cdot a_i \cdot (T_G - T_L)$$
Where:
The interfacial area depends on the flow pattern:
| Flow Pattern | Interfacial Area $a_i$ |
|---|---|
| Stratified | $W/A_{pipe}$ (width / cross-section) |
| Annular | $\pi(d - 2\delta)/A_{pipe}$ |
| Bubble | $6\epsilon_G/d_b$ |
| Droplet | $6\epsilon_L/d_d$ |
The heat and mass transfer coefficients are related:
$$\frac{h}{k_c \cdot \rho \cdot c_p} = \left(\frac{Sc}{Pr}\right)^{2/3}$$
Or in terms of j-factors:
$$j_H = j_D$$
Where: $$j_H = \frac{Nu}{Re \cdot Pr^{1/3}} = St \cdot Pr^{2/3}$$ $$j_D = \frac{Sh}{Re \cdot Sc^{1/3}}$$
When mass transfer occurs, the associated enthalpy must be considered:
$$\dot{Q}_{total} = \dot{Q}_{sensible} + \dot{Q}_{latent}$$
$$\dot{Q}_{latent} = \sum_j N_j \cdot \Delta H_{vap,j}$$
At the gas-liquid interface, the energy balance:
$$h_G (T_G - T_i) + \sum_j N_j H_j^G = h_L (T_i - T_L) + \sum_j N_j H_j^L$$
Rearranging:
$$h_G (T_G - T_i) - h_L (T_i - T_L) = \sum_j N_j (H_j^L - H_j^G) = -\sum_j N_j \Delta H_{vap,j}$$
For high mass transfer rates, the sensible heat transfer is modified:
$$\dot{Q}_{sensible} = h \cdot \Phi \cdot (T_{bulk} - T_i)$$
Where the Ackermann correction factor:
$$\Phi = \frac{\phi}{e^\phi - 1}$$
And: $$\phi = \frac{\sum_j N_j c_{p,j}}{h}$$
// In FluidBoundary
public double[] calcInterphaseHeatFlux() {
// Sensible heat
double Q_sensible = heatTransferCoefficient[0] * heatTransferCorrection[0]
* (T_bulk - T_interface);
// Latent heat
double Q_latent = 0;
for (int j = 0; j < nComponents; j++) {
Q_latent += nFlux.get(j, 0) * deltaHvap[j];
}
interphaseHeatFlux[0] = Q_sensible + Q_latent;
return interphaseHeatFlux;
}
FluidBoundary
├── heatTransferCoefficient[2] // Gas, Liquid
├── heatTransferCorrection[2] // Ackermann factors
├── prandtlNumber[2]
└── interphaseHeatFlux[2]
InterphaseTransportCoefficientBaseClass
├── calcWallHeatTransferCoefficient()
├── calcInterphaseHeatTransferCoefficient()
└── calcWallFrictionFactor()
// InterphaseTransportCoefficientInterface
double calcWallHeatTransferCoefficient(int phase, double prandtlNumber, FlowNodeInterface node);
double calcInterphaseHeatTransferCoefficient(int phase, double prandtlNumber, FlowNodeInterface node);
// FluidBoundary
void setHeatTransferCalc(boolean calc);
double[] getInterphaseHeatFlux();
double getHeatTransferCoefficient(int phase);
import neqsim.fluidmechanics.flowsystem.onephaseflowsystem.pipeflowsystem.OnePhasePipeFlowSystem;
import neqsim.fluidmechanics.geometrydefinitions.pipe.PipeGeometry;
import neqsim.thermo.system.SystemSrkEos;
// Create hot gas
SystemSrkEos gas = new SystemSrkEos(373.15, 50.0); // 100°C, 50 bar
gas.addComponent("methane", 0.95);
gas.addComponent("ethane", 0.05);
gas.setMixingRule("classic");
// Pipe geometry
PipeGeometry pipe = new PipeGeometry("Pipeline");
pipe.setDiameter(0.3, "m");
pipe.setLength(10000.0, "m");
// Heat transfer setup
pipe.setOverallHeatTransferCoefficient(5.0); // W/(m²·K)
// Flow system
OnePhasePipeFlowSystem flow = new OnePhasePipeFlowSystem();
flow.setInletFluid(gas);
flow.setGeometry(pipe);
flow.setSurroundingTemperature(283.15); // 10°C ambient
flow.setCalculateHeatTransfer(true);
flow.setNumberOfNodes(100);
flow.init();
flow.solveTransient(1);
// Temperature profile
for (int i = 0; i < flow.getNumberOfNodes(); i++) {
double x = flow.getNode(i).getPosition();
double T = flow.getNode(i).getTemperature() - 273.15; // °C
System.out.println("x = " + x + " m, T = " + T + " °C");
}
import neqsim.fluidmechanics.flownode.twophasenode.twophasepipeflownode.StratifiedFlowNode;
// Create two-phase system
SystemSrkEos fluid = new SystemSrkEos(280.0, 30.0);
fluid.addComponent("methane", 0.85);
fluid.addComponent("n-pentane", 0.10);
fluid.addComponent("n-decane", 0.05);
fluid.setMixingRule("classic");
// Initialize with phase split
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
// Create flow node
PipeData pipe = new PipeData(0.2); // 0.2 m diameter
StratifiedFlowNode node = new StratifiedFlowNode(fluid, pipe);
node.init();
// Enable heat transfer calculations
node.getFluidBoundary().setHeatTransferCalc(true);
node.getFluidBoundary().setMassTransferCalc(true);
// Solve
node.getFluidBoundary().solve();
// Get interphase heat flux
double[] Q = node.getFluidBoundary().getInterphaseHeatFlux();
System.out.println("Gas-side heat flux: " + Q[0] + " W/m²");
System.out.println("Liquid-side heat flux: " + Q[1] + " W/m²");
// Hot gas entering cold pipeline
SystemSrkEos gas = new SystemSrkEos(320.0, 80.0); // Hot, high pressure
gas.addComponent("methane", 0.80);
gas.addComponent("ethane", 0.10);
gas.addComponent("propane", 0.05);
gas.addComponent("n-butane", 0.03);
gas.addComponent("n-pentane", 0.02);
gas.setMixingRule("classic");
// Cold seabed pipeline
TwoPhasePipeFlowSystem flow = new TwoPhasePipeFlowSystem();
flow.setInletFluid(gas);
flow.setGeometry(seabedPipe);
flow.setSurroundingTemperature(277.15); // 4°C seabed
flow.setCalculateHeatTransfer(true);
flow.init();
flow.solveTransient(1);
// Check for liquid formation
for (int i = 0; i < flow.getNumberOfNodes(); i++) {
double liquidHoldup = flow.getNode(i).getPhaseFraction(1);
if (liquidHoldup > 0.01) {
System.out.println("Condensation at x = " + flow.getNode(i).getPosition() + " m");
break;
}
}
Incropera, F.P., DeWitt, D.P., et al. (2007). Fundamentals of Heat and Mass Transfer. 6th ed. Wiley.
Gnielinski, V. (1976). New equations for heat and mass transfer in turbulent pipe and channel flow. Int. Chem. Eng., 16(2), 359-368.
Dittus, F.W., Boelter, L.M.K. (1930). Heat transfer in automobile radiators of the tubular type. Univ. Calif. Publ. Eng., 2(13), 443-461.
Chilton, T.H., Colburn, A.P. (1934). Mass transfer (absorption) coefficients: Prediction from data on heat transfer and fluid friction. Ind. Eng. Chem., 26(11), 1183-1187.
Solbraa, E. (2002). Equilibrium and Non-Equilibrium Thermodynamics of Natural Gas Processing. Dr.ing. thesis, NTNU. NVA
Bird, R.B., Stewart, W.E., Lightfoot, E.N. (2002). Transport Phenomena. 2nd ed. Wiley.
This document describes the pipe wall construction and heat transfer modeling capabilities in NeqSim, including material properties, multi-layer walls, and surrounding environment modeling.
The pipe wall modeling system in NeqSim provides:
NeqSim includes pre-defined pipe materials with thermal properties:
| Material | Thermal Conductivity (W/m·K) | Density (kg/m³) | Specific Heat (J/kg·K) |
|---|---|---|---|
| Carbon Steel | 50.0 | 7850 | 490 |
| Stainless Steel 316 | 16.3 | 8000 | 500 |
| Duplex Steel | 15.0 | 7800 | 500 |
| Super Duplex | 14.0 | 7800 | 500 |
| Titanium | 21.9 | 4500 | 523 |
| Inconel 625 | 9.8 | 8440 | 410 |
| Monel 400 | 21.8 | 8800 | 427 |
| Copper | 401.0 | 8960 | 385 |
| HDPE | 0.5 | 960 | 1800 |
| PVC | 0.19 | 1400 | 1000 |
| GRP (Fiberglass) | 0.3 | 1850 | 900 |
// Using standard material
PipeMaterial steel = PipeMaterial.CARBON_STEEL;
// Creating custom material
PipeMaterial custom = new PipeMaterial(
"Custom Alloy",
25.0, // thermalConductivity (W/m·K)
7500, // density (kg/m³)
480 // specificHeatCapacity (J/kg·K)
);
A MaterialLayer combines a material with its thickness:
// Create insulation layer
MaterialLayer insulation = new MaterialLayer(
"Polyurethane Foam",
0.025, // thermalConductivity (W/m·K)
40, // density (kg/m³)
1500, // specificHeatCapacity (J/kg·K)
0.05 // thickness (m) = 50 mm
);
// Using pipe material
MaterialLayer pipeWall = new MaterialLayer(
PipeMaterial.CARBON_STEEL,
0.012 // thickness = 12 mm
);
| Property | Description | Units |
|---|---|---|
thickness |
Layer thickness | m |
thermalConductivity |
Heat conduction coefficient | W/(m·K) |
density |
Material density | kg/m³ |
specificHeatCapacity |
Thermal capacity | J/(kg·K) |
The PipeWall class represents a complete pipe wall with multiple layers:
// Method 1: Create layer by layer
PipeWall wall = new PipeWall(0.15); // inner radius = 150 mm
wall.addLayer(new MaterialLayer(PipeMaterial.CARBON_STEEL, 0.012));
wall.addLayer(new MaterialLayer("Insulation", 0.025, 40, 1500, 0.050));
wall.addLayer(new MaterialLayer("Coating", 0.3, 1200, 1400, 0.005));
// Method 2: Using PipeWallBuilder (fluent API)
PipeWall wall = new PipeWallBuilder()
.innerRadius(0.15)
.addPipeLayer(PipeMaterial.CARBON_STEEL, 0.012)
.addInsulationLayer(0.025, 0.050)
.addCoatingLayer(0.3, 0.005)
.build();
The total radial thermal resistance through a cylindrical wall:
$$ R_{total} = \sum_{i=1}^{n} R_i = \sum_{i=1}^{n} \frac{\ln(r_{i+1}/r_i)}{2\pi k_i L} $$
Where:
Per unit length:
$$ R'_{total} = \sum_{i=1}^{n} \frac{\ln(r_{i+1}/r_i)}{2\pi k_i} \quad \text{(m·K/W)} $$
double outerRadius = wall.getOuterRadius(); // m
double totalThickness = wall.getTotalWallThickness(); // m
double resistance = wall.getTotalResistancePerLength(); // m·K/W
double heatCapacity = wall.getThermalMass(); // J/(m·K)
int layerCount = wall.getLayerCount();
The PipeSurroundingEnvironment class models the external conditions:
| Environment | Description | Typical $h$ (W/m²·K) |
|---|---|---|
| Still Air | Natural convection | 5-25 |
| Moving Air | Forced convection | 10-200 |
| Seawater | Subsea pipelines | 150-1000 |
| Soil | Buried pipelines | 1-10 |
// Using factory methods
PipeSurroundingEnvironment air =
PipeSurroundingEnvironment.stillAir(25.0); // 25°C
PipeSurroundingEnvironment seawater =
PipeSurroundingEnvironment.seawater(4.0); // 4°C
PipeSurroundingEnvironment soil =
PipeSurroundingEnvironment.soil(15.0, 1.5); // 15°C, k=1.5 W/m·K
// Custom environment
PipeSurroundingEnvironment custom =
new PipeSurroundingEnvironment("Wind", 10.0, 50.0);
// 10°C ambient, h = 50 W/m²·K
Still Air (Natural Convection): $$ h \approx 5 + 5\sqrt{T_{surface} - T_{ambient}} \quad \text{W/(m²·K)} $$
Seawater: $$ h \approx 150 + 70 \cdot v_{current}^{0.8} \quad \text{W/(m²·K)} $$
Soil (Buried Pipe): $$ h_{equiv} = \frac{k_{soil}}{r_o \cdot \ln(2H/r_o)} \quad \text{W/(m²·K)} $$
Where $H$ is burial depth and $r_o$ is outer radius.
The overall U-value combining all resistances:
$$ \frac{1}{U A} = \frac{1}{h_i A_i} + \sum \frac{\ln(r_{o}/r_i)}{2\pi k L} + \frac{1}{h_o A_o} $$
Based on outer surface area:
$$ U_o = \frac{1}{r_o \left(\frac{1}{h_i r_i} + \sum \frac{\ln(r_{i+1}/r_i)}{k_i} + \frac{1}{h_o}\right)} $$
Heat flow per unit length:
$$ q' = U_o \cdot 2\pi r_o \cdot (T_{fluid} - T_{ambient}) \quad \text{W/m} $$
Total heat flow:
$$ Q = q' \cdot L = U_o \cdot A_o \cdot \Delta T \quad \text{W} $$
The fluid temperature along the pipe (steady-state):
$$ T(x) = T_{ambient} + (T_{inlet} - T_{ambient}) \exp\left(-\frac{U_o \cdot \pi D_o}{\dot{m} C_p} x\right) $$
Temperature at interface between layers $j$ and $j+1$:
$$ T_j = T_{fluid} - q' \cdot \left(\frac{1}{h_i \cdot 2\pi r_i} + \sum_{k=1}^{j} \frac{\ln(r_{k+1}/r_k)}{2\pi k_k}\right) $$
public enum PipeMaterial {
CARBON_STEEL(50.0, 7850, 490),
STAINLESS_316(16.3, 8000, 500),
// ... more materials
double getThermalConductivity();
double getDensity();
double getSpecificHeatCapacity();
double getThermalDiffusivity();
}
public class MaterialLayer {
// Constructors
MaterialLayer(String name, double k, double rho, double cp, double t);
MaterialLayer(PipeMaterial material, double thickness);
// Properties
double getThickness();
double getThermalConductivity();
double getDensity();
double getSpecificHeatCapacity();
// Calculations
double getRadialResistance(double innerRadius);
double getHeatCapacityPerLength(double innerRadius);
}
public class PipeWall {
// Construction
PipeWall(double innerRadius);
void addLayer(MaterialLayer layer);
// Properties
double getInnerRadius();
double getOuterRadius();
double getTotalWallThickness();
int getLayerCount();
// Thermal calculations
double getTotalResistancePerLength();
double getUValuePerLength(double hInner, double hOuter);
double getThermalMass();
}
public class PipeSurroundingEnvironment {
// Factory methods
static stillAir(double ambientTemp);
static movingAir(double ambientTemp, double windSpeed);
static seawater(double ambientTemp);
static soil(double ambientTemp, double thermalConductivity);
// Properties
double getAmbientTemperature();
double getConvectionCoefficient();
}
public class PipeWallBuilder {
PipeWallBuilder innerRadius(double r);
PipeWallBuilder innerDiameter(double d);
PipeWallBuilder addLayer(MaterialLayer layer);
PipeWallBuilder addPipeLayer(PipeMaterial material, double thickness);
PipeWallBuilder addInsulationLayer(double k, double thickness);
PipeWallBuilder addCoatingLayer(double k, double thickness);
PipeWall build();
}
// Create multi-layer subsea pipe wall
PipeWall subseaPipe = new PipeWallBuilder()
.innerDiameter(0.254) // 10" ID
.addPipeLayer(PipeMaterial.DUPLEX_STEEL, 0.0127)
.addInsulationLayer(0.15, 0.060) // Syntactic foam
.addCoatingLayer(0.22, 0.006) // Polypropylene
.build();
// Seawater environment at 4°C
PipeSurroundingEnvironment seawater =
PipeSurroundingEnvironment.seawater(4.0);
// Calculate overall U-value
double hInner = 500; // W/m²·K (turbulent gas flow)
double hOuter = seawater.getConvectionCoefficient();
double U = subseaPipe.getUValuePerLength(hInner, hOuter);
System.out.printf("U-value: %.2f W/(m²·K)%n", U);
// Create insulated buried pipeline
PipeWall buriedPipe = new PipeWallBuilder()
.innerDiameter(0.508) // 20" ID
.addPipeLayer(PipeMaterial.CARBON_STEEL, 0.0127)
.addCoatingLayer(0.22, 0.003) // FBE coating
.build();
// Soil at 12°C, buried 1.5m deep
PipeSurroundingEnvironment soil =
PipeSurroundingEnvironment.soil(12.0, 1.2);
// Print configuration
System.out.printf("Wall thickness: %.1f mm%n",
buriedPipe.getTotalWallThickness() * 1000);
System.out.printf("Total resistance: %.4f m·K/W%n",
buriedPipe.getTotalResistancePerLength());
// Pipeline parameters
double length = 50000; // 50 km
double mDot = 15.0; // kg/s
double Cp = 2500; // J/(kg·K) - gas
double Tinlet = 80; // °C
double Tambient = 5; // °C
double Uo = 2.5; // W/(m²·K) - overall U-value
double Do = 0.32; // m - outer diameter
// Calculate outlet temperature
double exponent = -Uo * Math.PI * Do * length / (mDot * Cp);
double Toutlet = Tambient + (Tinlet - Tambient) * Math.exp(exponent);
System.out.printf("Outlet temperature: %.1f °C%n", Toutlet);
System.out.printf("Heat loss: %.0f kW%n", mDot * Cp * (Tinlet - Toutlet) / 1000);
// Create fluid
SystemInterface gas = new SystemSrkEos(323.15, 50e5);
gas.addComponent("methane", 0.9);
gas.addComponent("ethane", 0.07);
gas.addComponent("propane", 0.03);
gas.setMixingRule("classic");
// Create inlet stream
Stream inlet = new Stream("Inlet", gas);
inlet.setFlowRate(500000, "kg/hr");
inlet.run();
// Create pipeline with heat transfer
OnePhasePipeLine pipe = new OnePhasePipeLine("Export", inlet);
pipe.setNumberOfLegs(1);
pipe.setNumberOfNodesInLeg(100);
pipe.setPipeDiameters(new double[] {0.508, 0.508});
pipe.setLegPositions(new double[] {0.0, 50000.0});
pipe.setOuterTemperature(278.15); // 5°C ambient
// Run steady-state
pipe.run();
// Get outlet conditions
System.out.printf("Outlet T: %.1f °C%n",
pipe.getOutStream().getTemperature("C"));
System.out.printf("Outlet P: %.1f bara%n",
pipe.getOutStream().getPressure("bara"));
This document provides a detailed description of the theoretical models and numerical methods used in NeqSim for calculating interphase mass and heat transfer in two-phase gas-liquid pipe flow. The approach is based on non-equilibrium thermodynamics where the gas and liquid phases are not assumed to be in thermodynamic equilibrium at the interface.
Key Concepts:
In the equilibrium approach, phases are assumed to be in complete thermodynamic equilibrium:
$$y_i = K_i(T, P) \cdot x_i \quad \text{for all components } i$$
$$T_G = T_L = T$$
This is computationally simple but fails when:
The non-equilibrium model accounts for finite-rate mass and heat transfer:
$$y_i^{bulk} \neq K_i \cdot x_i^{bulk}$$
$$T_G^{bulk} \neq T_L^{bulk}$$
Interface Equilibrium: $$y_i^{int} = K_i(T^{int}, P) \cdot x_i^{int}$$
The driving forces are:
For multicomponent diffusion, the Maxwell-Stefan equations describe the relationship between fluxes and driving forces:
$$-\frac{x_i}{RT}\nabla\mu_i = \sum_{j=1, j\neq i}^{n} \frac{x_i N_j - x_j N_i}{c_t D_{ij}}$$
| Symbol | Description | Units |
|---|---|---|
| $x_i$ | Mole fraction of component $i$ | [-] |
| $\mu_i$ | Chemical potential of component $i$ | [J/mol] |
| $N_i$ | Molar flux of component $i$ | [mol/(m²·s)] |
| $c_t$ | Total molar concentration | [mol/m³] |
| $D_{ij}$ | Maxwell-Stefan diffusivity | [m²/s] |
| $R$ | Gas constant | [J/(mol·K)] |
| $T$ | Temperature | [K] |
📘 Diffusivity Models: The Maxwell-Stefan diffusivities $D_{ij}$ are calculated using correlations documented in mass_transfer.md. NeqSim provides multiple models:
- Gas phase: Chapman-Enskog kinetic theory
- Liquid phase: Siddiqi-Lucas, Hayduk-Minhas (hydrocarbons), CO2-water (Tamimi)
- High pressure: Mathur-Thodos correction for P > 100 bar
See the Model Selection Guide for recommendations.
The Maxwell-Stefan equations can be written in matrix form:
$$(\mathbf{J}) = -c_t [\mathbf{B}]^{-1} [\mathbf{\Gamma}] \nabla(\mathbf{x})$$
Where:
Elements of [B]:
$$B_{ii} = \frac{x_i}{D_{i,n}} + \sum_{k=1, k\neq i}^{n} \frac{x_k}{D_{ik}}$$
$$B_{ij} = -x_i \left(\frac{1}{D_{ij}} - \frac{1}{D_{i,n}}\right), \quad i \neq j$$
Thermodynamic Factor Matrix:
$$\Gamma_{ij} = \delta_{ij} + x_i \frac{\partial \ln \gamma_i}{\partial x_j}$$
Where $\gamma_i$ is the activity coefficient and $\delta_{ij}$ is the Kronecker delta.
Film theory assumes that mass transfer resistance is confined to a thin stagnant film at the interface:
Bulk Gas | Gas Film | Interface | Liquid Film | Bulk Liquid
─────────────┼────────────┼───────────┼───────────────┼─────────────
y_i^bulk → y_i^int = K_i · x_i^int ← x_i^bulk
T_G^bulk → T^int = T^int = T^int ← T_L^bulk
Film thickness:
The Krishna-Standart model extends the Maxwell-Stefan equations to film theory for multicomponent systems:
$$(\mathbf{N}) = c_t \mathbf{k} + x_t^{avg} N_t$$
Where $[\mathbf{k}]$ is the matrix of mass transfer coefficients:
$$[\mathbf{k}] = [\mathbf{B}]^{-1} [\mathbf{\Xi}]$$
Bootstrap Matrix [Ξ]:
The bootstrap matrix $[\mathbf{\Xi}]$ accounts for the effect of finite mass transfer rates (high flux correction):
$$[\mathbf{\Xi}] = \mathbf{\Phi} [\exp(\mathbf{\Phi}) - \mathbf{I}]^{-1}$$
Where: $$\mathbf{\Phi} = [\mathbf{B}_0]^{-1} N_t / c_t$$
At low fluxes: $[\mathbf{\Xi}] \rightarrow \mathbf{I}$ (identity matrix)
Gas-phase mass transfer coefficient:
$$k_G = \frac{Sh \cdot D_G}{D_h}$$
Liquid-phase mass transfer coefficient:
$$k_L = \frac{Sh \cdot D_L}{D_h}$$
Sherwood Number Correlations:
| Flow Regime | Correlation |
|---|---|
| Turbulent (Re > 10,000) | $Sh = 0.023 \cdot Re^{0.83} \cdot Sc^{0.44}$ |
| Transitional | Interpolation |
| Laminar (Re < 2,300) | $Sh = 3.66$ (constant wall) |
The interface compositions $(x_i^{int}, y_i^{int})$ are found by solving simultaneously:
Flux continuity: $$N_i^G = N_i^L \quad \text{for each component}$$
Interface equilibrium: $$y_i^{int} = K_i(T^{int}, P) \cdot x_i^{int}$$
Summation constraints: $$\sum_{i=1}^n x_i^{int} = 1, \quad \sum_{i=1}^n y_i^{int} = 1$$
This requires iterative solution (Newton-Raphson method).
Heat flows from bulk gas → interface → bulk liquid (or reverse):
$$q = h_G (T_G^{bulk} - T^{int}) = h_L (T^{int} - T_L^{bulk})$$
Overall heat transfer coefficient:
$$\frac{1}{h_{overall}} = \frac{1}{h_G} + \frac{1}{h_L}$$
When mass transfer occurs, the energy balance includes:
Total interfacial heat flux:
$$Q^{int} = h_{GL}(T_G - T_L) + \sum_{i=1}^n N_i \cdot \Delta H_{vap,i}$$
Where:
The interface temperature $T^{int}$ is found from the energy balance:
$$h_G (T_G^{bulk} - T^{int}) + \sum_{i=1}^n N_i H_i^G = h_L (T^{int} - T_L^{bulk}) + \sum_{i=1}^n N_i H_i^L$$
Rearranging:
$$T^{int} = \frac{h_G T_G^{bulk} + h_L T_L^{bulk} + \sum_i N_i (H_i^G - H_i^L)}{h_G + h_L}$$
Dittus-Boelter Correlation (Turbulent):
$$Nu = 0.023 \cdot Re^{0.8} \cdot Pr^n$$
Where:
$$h = \frac{Nu \cdot k_{thermal}}{D_h}$$
Gnielinski Correlation (Transitional, 2300 < Re < 10,000):
$$Nu = \frac{(f/8)(Re - 1000)Pr}{1 + 12.7\sqrt{f/8}(Pr^{2/3} - 1)}$$
Laminar Flow (Re < 2,300):
$$Nu = 3.66 \quad \text{(constant wall temperature)}$$ $$Nu = 4.36 \quad \text{(constant heat flux)}$$
Heat and mass transfer are related through:
$$\frac{h}{c_p G} Pr^{2/3} = \frac{k_m}{u} Sc^{2/3} = \frac{f}{2}$$
This allows estimation of mass transfer coefficients from heat transfer data:
$$k_m = h \cdot \frac{1}{\rho c_p} \left(\frac{Sc}{Pr}\right)^{-2/3}$$
Or equivalently:
$$Sh = Nu \cdot \left(\frac{Sc}{Pr}\right)^{1/3}$$
The interfacial area per unit volume depends on the flow pattern:
$$a = \frac{\text{Interface Area}}{\text{Pipe Volume}} \quad [m^2/m^3]$$
For stratified flow with liquid height $h_L$:
$$a = \frac{S_i}{A} = \frac{2\sqrt{h_L(D - h_L)}}{\frac{\pi D^2}{4}}$$
Where $S_i$ is the interfacial chord length.
For annular flow with liquid film thickness $\delta$:
$$a = \frac{\pi (D - 2\delta)}{\frac{\pi D^2}{4}} = \frac{4(D - 2\delta)}{D^2}$$
For thin films: $a \approx \frac{4}{D}$
For spherical bubbles of diameter $d_b$:
$$a = \frac{6\alpha_G}{d_b}$$
Bubble size can be estimated from the Weber number:
$$d_b = \frac{We_{crit} \cdot \sigma}{\rho_L u_L^2}$$
Slug flow has complex geometry. The effective interfacial area includes:
$$a_{slug} = \alpha_{Taylor} \cdot a_{Taylor} + \alpha_{dispersed} \cdot a_{dispersed}$$
The coupled heat and mass transfer problem requires iterative solution:
1. Initialize: Guess T^int, x_i^int, y_i^int
2. Calculate K-values:
K_i = K_i(T^int, P)
3. Calculate diffusivities:
D_ij^G, D_ij^L at current T^int
4. Calculate mass transfer coefficients:
[k_G], [k_L] from correlations
5. Calculate component fluxes:
N_i^G = c_G [k_G](y_i^bulk - y_i^int)
N_i^L = c_L [k_L](x_i^int - x_i^bulk)
6. Check flux balance:
If |N_i^G - N_i^L| > tolerance, update x_i^int, y_i^int
7. Calculate heat transfer coefficients:
h_G, h_L from correlations
8. Calculate interface temperature:
T^int from energy balance
9. Check convergence:
If T^int, x_i^int, y_i^int converged, exit
Else goto step 2
For efficiency, the interface conditions can be solved using Newton-Raphson:
$$\mathbf{F}(\mathbf{X}) = \mathbf{0}$$
Where: $$\mathbf{X} = [T^{int}, x_1^{int}, x_2^{int}, ..., x_{n-1}^{int}]^T$$
$$\mathbf{F} = \begin{bmatrix} Q^G - Q^L \ N_1^G - N_1^L \ N_2^G - N_2^L \ \vdots \ N_{n-1}^G - N_{n-1}^L \end{bmatrix}$$
Update: $\mathbf{X}^{new} = \mathbf{X}^{old} - [\mathbf{J}]^{-1} \mathbf{F}$
Where $[\mathbf{J}]$ is the Jacobian matrix.
Under-relaxation: To improve convergence stability:
$$\mathbf{X}^{new} = \omega \cdot \mathbf{X}^{calc} + (1-\omega) \cdot \mathbf{X}^{old}$$
Typical $\omega = 0.3$ to $0.7$.
Damping: Limit changes per iteration:
$$|\Delta T^{int}| < \Delta T_{max}$$ $$|\Delta x_i^{int}| < \Delta x_{max}$$
The total interphase mass transfer rate:
$$\Gamma = \sum_{i=1}^n M_i \cdot N_i \cdot a \quad [kg/(m^3 \cdot s)]$$
Where:
Condensation occurs when:
Heat released: $$Q_{cond} = \sum_i N_i \cdot \Delta H_{vap,i}$$
Evaporation occurs when:
Heat absorbed: $$Q_{evap} = -\sum_i N_i \cdot \Delta H_{vap,i}$$
In multicomponent systems, components transfer at different rates based on:
Light components (high $K$) tend to evaporate preferentially. Heavy components (low $K$) tend to condense preferentially.
Gas Phase (Chapman-Enskog):
$$D_{ij}^G = \frac{0.00266 T^{3/2}}{P M_{ij}^{1/2} \sigma_{ij}^2 \Omega_D}$$
Where:
Liquid Phase (Wilke-Chang):
$$D_{ij}^L = \frac{7.4 \times 10^{-8} (\phi M_j)^{1/2} T}{\mu_L V_i^{0.6}}$$
Where:
Gas Phase (Eucken correlation):
$$k_G = \mu_G \left(c_{p,G} + \frac{5R}{4M}\right)$$
Liquid Phase: From NeqSim thermodynamic model or correlations.
From equation of state:
$$\Delta H_{vap,i} = H_i^{vapor} - H_i^{liquid}$$
Or from Watson correlation for pure components:
$$\Delta H_{vap} = \Delta H_{vap,0} \left(\frac{1 - T_r}{1 - T_{r,0}}\right)^{0.38}$$
TwoPhasePipeFlowSystem pipe = TwoPhasePipeFlowSystem.builder()
.withFluid(fluid)
.withDiameter(0.1, "m")
.withLength(100, "m")
.withNodes(50)
.enableNonEquilibriumMassTransfer() // Enable mass transfer calculation
.enableNonEquilibriumHeatTransfer() // Enable heat transfer calculation
.build();
// Get mass transfer rates
double[] massTransferRate = pipe.getInterphaseMassTransferRate();
// Get component fluxes at each node
double[][] componentFluxes = pipe.getComponentFluxProfile();
// Get interfacial area profile
double[] interfacialArea = pipe.getInterfacialAreaProfile();
// Get mass transfer coefficients
double[] k_G = pipe.getGasMassTransferCoefficientProfile();
double[] k_L = pipe.getLiquidMassTransferCoefficientProfile();
// Get heat transfer coefficients
double[] h_G = pipe.getGasHeatTransferCoefficientProfile();
double[] h_L = pipe.getLiquidHeatTransferCoefficientProfile();
double[] h_overall = pipe.getOverallInterphaseHeatTransferCoefficientProfile();
// Get interface temperature
double[] T_interface = pipe.getInterfaceTemperatureProfile();
// Get heat flux
double[] q = pipe.getInterphaseHeatFluxProfile();
// Get total heat transferred
double totalHeat = pipe.getTotalInterphaseHeatTransfer();
| Class | Description |
|---|---|
FluidBoundaryInterface |
Interface between phases, calculates mass/heat transfer |
HeatTransferCoefficientCalculator |
Heat transfer coefficient correlations |
InterphaseTwoPhase |
Interphase calculations for two-phase flow |
FluidBoundaryInterfaceHMT |
Heat and mass transfer at interface |
import neqsim.fluidmechanics.flowsystem.twophaseflowsystem.twophasepipeflowsystem.*;
import neqsim.thermo.system.*;
public class HeatMassTransferExample {
public static void main(String[] args) {
// Create multicomponent fluid
SystemInterface fluid = new SystemSrkEos(300.0, 50.0);
fluid.addComponent("methane", 0.70, 0);
fluid.addComponent("ethane", 0.15, 0);
fluid.addComponent("propane", 0.05, 0);
fluid.addComponent("water", 0.10, 1);
fluid.createDatabase(true);
fluid.setMixingRule(2);
// Build pipe with heat/mass transfer using builder
TwoPhasePipeFlowSystem pipe = TwoPhasePipeFlowSystem.builder()
.withFluid(fluid)
.withDiameter(0.1, "m")
.withLength(500, "m")
.withNodes(100)
.withFlowPattern(FlowPattern.ANNULAR)
.withConvectiveBoundary(278.15, "K", 15.0) // Cold ambient
.build();
// Solve with heat and mass transfer, get structured results
PipeFlowResult result = pipe.solveWithHeatAndMassTransfer();
// Access results via PipeFlowResult container
System.out.println("Temperature change: " + result.getTemperatureChange() + " K");
System.out.println("Pressure drop: " + result.getTotalPressureDrop() + " bar");
System.out.println("Total heat loss: " + result.getTotalHeatLoss() + " W");
System.out.println(result); // Formatted summary
// Export profiles for analysis
Map<String, double[]> profiles = result.toMap();
}
}
The NeqSim two-phase pipe flow model has been validated against OLGA simulations for:
| Test Case | Literature | NeqSim | Deviation |
|---|---|---|---|
| Dittus-Boelter (turbulent) | Experimental | +3.2% | Within uncertainty |
| Lockhart-Martinelli | Original data | +8.5% | Acceptable |
| Stratified flow transition | Taitel-Dukler | Good agreement | - |
Krishna, R. and Standart, G.L. (1976). "A multicomponent film model incorporating a general matrix method of solution to the Maxwell-Stefan equations." AIChE Journal, 22(2), 383-389.
Taylor, R. and Krishna, R. (1993). Multicomponent Mass Transfer. Wiley Series in Chemical Engineering.
Bird, R.B., Stewart, W.E., and Lightfoot, E.N. (2002). Transport Phenomena, 2nd Edition. John Wiley & Sons.
Chilton, T.H. and Colburn, A.P. (1934). "Mass Transfer (Absorption) Coefficients Prediction from Data on Heat Transfer and Fluid Friction." Industrial & Engineering Chemistry, 26(11), 1183-1187.
Incropera, F.P. and DeWitt, D.P. (2002). Fundamentals of Heat and Mass Transfer, 5th Edition. John Wiley & Sons.
Solbraa, E. (2002). "Measurement and Calculation of Two-Phase Flow in Pipes." PhD Thesis, Norwegian University of Science and Technology.
Dittus, F.W. and Boelter, L.M.K. (1930). "Heat transfer in automobile radiators of the tubular type." University of California Publications in Engineering, 2, 443-461.
Gnielinski, V. (1976). "New equations for heat and mass transfer in turbulent pipe and channel flow." International Chemical Engineering, 16(2), 359-368.
Document generated for NeqSim Interphase Heat and Mass Transfer Module
This document provides detailed documentation of the mass transfer models implemented in the NeqSim fluid mechanics package.
NeqSim implements rigorous multicomponent mass transfer models based on non-equilibrium thermodynamics. The models are suitable for:
The theoretical foundation is described in:
Solbraa, E. (2002). Equilibrium and Non-Equilibrium Thermodynamics of Natural Gas Processing. Dr.ing. thesis, NTNU. Available at NVA
For binary systems, Fick's law is adequate:
$$J_A = -D_{AB} \cdot c_t \cdot \nabla x_A$$
For multicomponent systems, NeqSim uses the Maxwell-Stefan equations:
$$-\frac{c_t}{RT} \nabla \mu_i = \sum_{j=1, j \neq i}^{n} \frac{x_i N_j - x_j N_i}{c_t D_{ij}}$$
Where:
The film theory assumes mass transfer occurs across a stagnant film of thickness $\delta$:
$$N_i = k_i \cdot c_t \cdot (x_{i,bulk} - x_{i,interface})$$
Where $k_i = D_i / \delta$ is the mass transfer coefficient.
For unsteady-state mass transfer (short contact times):
$$k_L = 2 \sqrt{\frac{D}{\pi t_c}}$$
Where $t_c$ is the contact time.
Mass transfer from a flowing fluid to a solid wall (pipe wall, packing surface):
| Number | Definition | Physical Meaning |
|---|---|---|
| Sherwood (Sh) | $k_c \cdot d / D$ | Ratio of convective to diffusive mass transfer |
| Schmidt (Sc) | $\nu / D$ | Ratio of momentum to mass diffusivity |
| Reynolds (Re) | $\rho v d / \mu$ | Ratio of inertial to viscous forces |
Laminar Flow (Re < 2300): $$Sh = 3.66$$
Turbulent Flow (Re > 10000): $$Sh = 0.023 \cdot Re^{0.83} \cdot Sc^{0.33}$$
Transition Region (2300 < Re < 4000): Linear interpolation between laminar and turbulent values.
Chapman-Enskog theory for binary diffusion:
$$D_{AB} = \frac{0.00266 \cdot T^{3/2}}{P \cdot M_{AB}^{1/2} \cdot \sigma_{AB}^2 \cdot \Omega_D}$$
Where:
NeqSim provides multiple liquid-phase diffusivity models, each optimized for different applications:
$$D_{AB} = 7.4 \times 10^{-8} \cdot \frac{(\phi M_B)^{0.5} \cdot T}{\mu_B \cdot V_A^{0.6}}$$
Where:
Uses group contribution based on molecular weight and solvent viscosity:
Best for: General aqueous and organic liquid systems at low to moderate pressures.
Optimized for hydrocarbon systems (Hayduk & Minhas, 1982):
Paraffin solvents: $D_{AB} = 13.3 \times 10^{-8} \cdot \frac{T^{1.47} \cdot \mu_B^{(\epsilon_B)}}{V_A^{0.71}}$
Aqueous solvents: $D_{AB} = 1.25 \times 10^{-8} \cdot (V_A^{-0.19} - 0.292) \cdot T^{1.52} \cdot \mu_B^{\epsilon}$
Best for: Hydrocarbon-hydrocarbon diffusion in oil/gas applications.
// Example: Using Hayduk-Minhas for oil system
PhysicalProperties physProps = system.getPhase(1).getPhysicalProperties();
Diffusivity diffModel = new HaydukMinhasDiffusivity(physProps);
diffModel.calcDiffusionCoefficients(0, 0); // binaryMethod, multicomponentMethod
double Dij = diffModel.getMaxwellStefanBinaryDiffusionCoefficient(0, 1);
Specialized for CO2 diffusion in water, validated against experimental data:
$$D_{CO_2} = 2.35 \times 10^{-6} \cdot \exp\left(\frac{-2119}{T}\right)$$
Best for: Carbon capture applications, CO2 absorption/desorption studies.
For reservoir and deep-water conditions (>100 bar), apply Mathur-Thodos correction:
$$D_P = D_0 \cdot f(\rho_r)$$
The correction factor accounts for increased molecular crowding at high pressures and can reduce diffusivity by 10× at 400 bar.
// Example: High-pressure diffusivity
PhysicalProperties physProps = system.getPhase(1).getPhysicalProperties();
HighPressureDiffusivity hpModel = new HighPressureDiffusivity(physProps);
hpModel.calcDiffusionCoefficients(0, 0); // applies HP correction automatically
double correctionFactor = hpModel.getPressureCorrectionFactor();
| Application | Recommended Model | Notes |
|---|---|---|
| General aqueous | Siddiqi-Lucas (aqueous) | Well-validated for dilute solutions |
| General organic | Siddiqi-Lucas (non-aqueous) | Good for organic solvents |
| Oil/gas hydrocarbons | Hayduk-Minhas (paraffin) | 2-3× higher than Siddiqi-Lucas, physically appropriate for oils |
| CO2 in water | CO2-water (Tamimi) | Best accuracy (±11% of literature) |
| Reservoir conditions | High-pressure + Hayduk-Minhas | Critical for P > 100 bar |
Based on validation testing at 300 K, 1 atm for CO2 in water (literature: 1.9×10⁻⁹ m²/s):
| Model | Predicted (m²/s) | Error |
|---|---|---|
| Hayduk-Minhas (aqueous) | 1.71×10⁻⁹ | -10% |
| CO2-water (Tamimi) | 2.12×10⁻⁹ | +11% |
| Siddiqi-Lucas | 1.39×10⁻⁹ | -27% |
For hydrocarbon systems, Hayduk-Minhas produces values 2-3.5× higher than Siddiqi-Lucas, which is consistent with the different physical basis of the correlations.
The DiffusivityModelSelector class can automatically choose the optimal model:
// Automatic model selection based on composition and conditions
PhaseInterface phase = system.getPhase(1);
PhysicalProperties physProps = phase.getPhysicalProperties();
DiffusivityModelSelector.DiffusivityModelType modelType =
DiffusivityModelSelector.selectOptimalModel(phase);
Diffusivity model = DiffusivityModelSelector.createModel(physProps, modelType);
// Or use auto-selection directly:
Diffusivity autoModel = DiffusivityModelSelector.createAutoSelectedModel(physProps);
Selection criteria:
Concentration-dependent diffusivity (Vignes mixing rule): $$D_{AB,mix} = D_{AB}^{x_B} \cdot D_{BA}^{x_A}$$ Currently implemented but may cause numerical issues with very different diffusivities.
Binary interaction parameters: Allow user-tuning for specific component pairs.
Additional correlations:
Temperature extrapolation warnings: Alert users when operating outside correlation validity ranges (typically 273-400 K).
At the gas-liquid interface, mass transfer occurs from both sides:
Gas Bulk | Interface | Liquid Bulk
| |
x_i,G,bulk --|-- x_i,I ----|-- x_i,L,bulk
| |
k_G | Equilibrium| k_L
| K_i |
The overall mass transfer coefficient combines gas and liquid resistances:
$$\frac{1}{K_{OG}} = \frac{1}{k_G} + \frac{m}{k_L}$$
$$\frac{1}{K_{OL}} = \frac{1}{k_L} + \frac{1}{m \cdot k_G}$$
Where $m = dy/dx$ is the slope of the equilibrium line.
The mass transfer coefficients depend strongly on the flow regime:
| Flow Regime | Interfacial Area | Gas-side $k_G$ | Liquid-side $k_L$ |
|---|---|---|---|
| Stratified | $A_i = W \cdot L$ (flat interface) | Smooth surface correlation | Penetration theory |
| Annular | $A_i = \pi d L$ (film on wall) | Core flow correlation | Film flow correlation |
| Droplet/Mist | $A_i = 6\epsilon/d_p$ (droplet surface) | External mass transfer | Internal circulation |
| Bubble | $A_i = 6\epsilon/d_b$ (bubble surface) | External mass transfer | Higbie penetration |
| Slug | Combined film + slug | Varies with position | Varies with position |
For stratified gas-liquid flow in pipes:
Gas-side (smooth interface): $$Sh_G = 0.023 \cdot Re_G^{0.83} \cdot Sc_G^{0.33}$$
Liquid-side (penetration theory): $$k_L = 2 \sqrt{\frac{D_L \cdot v_L}{\pi \cdot L}}$$
For annular flow with liquid film:
Gas core: $$Sh_G = 0.023 \cdot Re_G^{0.8} \cdot Sc_G^{0.33} \cdot \left(1 + 0.1 \cdot (d/\delta)^{0.5}\right)$$
Liquid film: $$k_L = \frac{D_L}{\delta} \cdot f(Re_{film})$$
NeqSim implements the Krishna-Standart multicomponent mass transfer model. For a system with $n$ components:
First, calculate binary coefficients from correlations:
for (int i = 0; i < nComponents; i++) {
for (int j = 0; j < nComponents; j++) {
// Schmidt number
Sc[i][j] = kinematicViscosity / D[i][j];
// Binary mass transfer coefficient
k_binary[i][j] = calcInterphaseMassTransferCoefficient(phase, Sc[i][j], node);
}
}
Build the $(n-1) \times (n-1)$ matrix $[\mathbf{k}]$:
$$k_{ii} = \sum_{j \neq i} \frac{x_j}{k_{ij}} + \frac{x_i}{k_{in}}$$
$$k_{ij} = -x_i \left(\frac{1}{k_{ij}} - \frac{1}{k_{in}}\right) \quad (i \neq j)$$
Where component $n$ is the reference (typically the most abundant).
The molar flux vector:
$$\mathbf{N} = c_t [\mathbf{k}]^{-1} (\mathbf{x}_{bulk} - \mathbf{x}_{interface})$$
For non-ideal solutions, the driving force includes activity coefficient gradients:
$$[\Gamma] = [\delta_{ij} + x_i \frac{\partial \ln \gamma_i}{\partial x_j}]$$
The corrected flux: $$\mathbf{N} = c_t [\mathbf{k}][\Gamma]^{-1} (\mathbf{x}_{bulk} - \mathbf{x}_{interface})$$
For high mass transfer rates, the film theory correction:
$$[\Xi] = \Phi^{-1}$$
Where $[\Phi]$ is the rate factor matrix.
Chemical reactions in the liquid phase enhance mass transfer:
$$N_A = E \cdot k_L \cdot (C_{A,i} - C_{A,bulk})$$
Where $E \geq 1$ is the enhancement factor.
The Hatta number characterizes the reaction regime:
$$Ha = \frac{\sqrt{k_{rxn} \cdot D_A}}{k_L}$$
| Ha Range | Regime | Location of Reaction |
|---|---|---|
| Ha < 0.3 | Slow | Bulk liquid |
| 0.3 < Ha < 3 | Intermediate | Film and bulk |
| Ha > 3 | Fast | Within film |
| Ha >> 3 | Instantaneous | At interface |
$$E = \frac{Ha}{\tanh(Ha)}$$
$$E_{\infty} = 1 + \frac{D_B \cdot C_{B,bulk}}{\nu_B \cdot D_A \cdot C_{A,i}}$$
Where $\nu_B$ is the stoichiometric coefficient.
$$E = \frac{\sqrt{1 + Ha^2 \cdot (E_{\infty} - 1)/E_{\infty}}}{1 + (E_{\infty} - 1)^{-1}}$$
NeqSim includes specific models for CO₂ absorption:
CO₂ + MDEA + H₂O ⇌ MDEAH⁺ + HCO₃⁻ (slow, base-catalyzed)
CO₂ + OH⁻ ⇌ HCO₃⁻ (parallel)
$$r_{CO2} = k_2 \cdot [CO_2] \cdot [MDEA]$$
With Arrhenius temperature dependence:
$$k_2 = A \cdot \exp\left(-\frac{E_a}{RT}\right)$$
| Amine | A (m³/mol·s) | Eₐ (kJ/mol) | Source |
|---|---|---|---|
| MDEA | 4.01×10⁸ | 42.0 | Rinker et al. (1995) |
| MEA | 4.4×10¹¹ | 50.5 | Hikita et al. (1977) |
| DEA | 1.3×10¹⁰ | 47.5 | Blauwhoff et al. (1984) |
From the thesis work, high-pressure effects on CO₂ absorption include:
FluidBoundary (abstract)
├── EquilibriumFluidBoundary
└── NonEquilibriumFluidBoundary (abstract)
└── KrishnaStandartFilmModel
└── ReactiveKrishnaStandartFilmModel
└── ReactiveFluidBoundary
| Class | Location | Purpose |
|---|---|---|
FluidBoundary |
flownode.fluidboundary.heatmasstransfercalc |
Base class for interphase calculations |
NonEquilibriumFluidBoundary |
...nonequilibriumfluidboundary |
Non-equilibrium base |
KrishnaStandartFilmModel |
...filmmodelboundary |
Multicomponent film model |
ReactiveKrishnaStandartFilmModel |
...reactivefilmmodel |
With chemical reactions |
EnhancementFactor |
...enhancementfactor |
Enhancement factor calculations |
// FluidBoundary
public abstract void solve();
public double[] getMolarFlux();
public double[] getHeatFlux();
// KrishnaStandartFilmModel
public double calcBinarySchmidtNumbers(int phase);
public double calcBinaryMassTransferCoefficients(int phase);
public double calcMassTransferCoefficients(int phase);
public void calcPhiMatrix(int phase); // Finite flux correction
import neqsim.fluidmechanics.flownode.twophasenode.twophasepipeflownode.AnnularFlow;
import neqsim.fluidmechanics.geometrydefinitions.pipe.PipeData;
import neqsim.thermo.system.SystemSrkEos;
// Create two-phase system
SystemSrkEos fluid = new SystemSrkEos(300.0, 50.0);
fluid.addComponent("methane", 0.90);
fluid.addComponent("CO2", 0.05);
fluid.addComponent("water", 0.05);
fluid.setMixingRule("classic");
// Create pipe geometry
PipeData pipe = new PipeData(0.1); // 0.1 m diameter
// Create flow node
AnnularFlow node = new AnnularFlow(fluid, pipe);
node.init();
node.initFlowCalc();
// Enable mass transfer
node.getFluidBoundary().setMassTransferCalc(true);
node.getFluidBoundary().solve();
// Get results
double[] molarFlux = node.getFluidBoundary().getMolarFlux();
System.out.println("CO2 flux: " + molarFlux[1] + " mol/m²·s");
// Enable activity coefficient corrections
node.getFluidBoundary().setThermodynamicCorrections(0, true); // Gas
node.getFluidBoundary().setThermodynamicCorrections(1, true); // Liquid
// Enable Stefan flow correction
node.getFluidBoundary().setFiniteFluxCorrection(0, true);
node.getFluidBoundary().setFiniteFluxCorrection(1, true);
node.getFluidBoundary().solve();
import neqsim.thermo.system.SystemSrkCPAstatoil;
// Create system with amine
SystemSrkCPAstatoil fluid = new SystemSrkCPAstatoil(313.15, 30.0);
fluid.addComponent("nitrogen", 0.85);
fluid.addComponent("CO2", 0.10);
fluid.addComponent("water", 0.04);
fluid.addComponent("MDEA", 0.01);
fluid.setMixingRule(10); // CPA mixing rule
// Use reactive film model
// The enhancement factor is calculated automatically
// based on reaction kinetics and Hatta number
Solbraa, E. (2002). Equilibrium and Non-Equilibrium Thermodynamics of Natural Gas Processing. Dr.ing. thesis, NTNU. NVA
Krishna, R., Standart, G.L. (1976). Mass and energy transfer in multicomponent systems. Chem. Eng. Commun., 3(4-5), 201-275.
Taylor, R., Krishna, R. (1993). Multicomponent Mass Transfer. Wiley.
Danckwerts, P.V. (1970). Gas-Liquid Reactions. McGraw-Hill.
Poling, B.E., Prausnitz, J.M., O'Connell, J.P. (2001). The Properties of Gases and Liquids. 5th ed. McGraw-Hill.
Rinker, E.B., Ashour, S.S., Sandall, O.C. (1995). Kinetics and modelling of carbon dioxide absorption into aqueous solutions of N-methyldiethanolamine. Chem. Eng. Sci., 50(5), 755-768.
The NeqSim two-phase pipe flow model implements a non-equilibrium thermodynamic approach for simulating gas-liquid flow in pipes. This document describes the theoretical foundation, numerical methods, and practical usage in NeqSim.
Key Features:
Compatibility: Java 8 and above (no Java 9+ features used)
For each phase $k$ (gas $G$ or liquid $L$):
$$\frac{\partial}{\partial t}(\alpha_k \rho_k) + \frac{\partial}{\partial z}(\alpha_k \rho_k u_k) = \Gamma_k$$
| Symbol | Description | Units |
|---|---|---|
| $\alpha_k$ | Volume fraction of phase $k$ | [-] |
| $\rho_k$ | Density of phase $k$ | [kg/m³] |
| $u_k$ | Velocity of phase $k$ | [m/s] |
| $\Gamma_k$ | Mass transfer rate to phase $k$ | [kg/(m³·s)] |
| $z$ | Axial position | [m] |
Constraint: $\alpha_G + \alpha_L = 1$
$$\frac{\partial}{\partial t}(\alpha_k \rho_k u_k) + \frac{\partial}{\partial z}(\alpha_k \rho_k u_k^2) = -\alpha_k \frac{\partial P}{\partial z} - \tau_{wk}\frac{S_k}{A} \mp \tau_i\frac{S_i}{A} - \alpha_k \rho_k g \sin\theta$$
| Symbol | Description | Units |
|---|---|---|
| $P$ | Pressure | [Pa] |
| $\tau_{wk}$ | Wall shear stress for phase $k$ | [Pa] |
| $\tau_i$ | Interfacial shear stress | [Pa] |
| $S_k$ | Wetted perimeter of phase $k$ | [m] |
| $S_i$ | Interfacial perimeter | [m] |
| $A$ | Pipe cross-sectional area | [m²] |
| $g$ | Gravitational acceleration | [m/s²] |
| $\theta$ | Pipe inclination angle | [rad] |
$$\frac{\partial}{\partial t}(\alpha_k \rho_k H_k) + \frac{\partial}{\partial z}(\alpha_k \rho_k u_k H_k) = q_{ik} + q_{wk} + \Gamma_k H_k^{int}$$
| Symbol | Description | Units |
|---|---|---|
| $H_k$ | Specific enthalpy of phase $k$ | [J/kg] |
| $q_{ik}$ | Interphase heat flux to phase $k$ | [W/m³] |
| $q_{wk}$ | Wall heat flux to phase $k$ | [W/m³] |
| $H_k^{int}$ | Interface enthalpy | [J/kg] |
Heat transfer between gas and liquid phases at the interface:
$$q_{GL} = h_{GL} \cdot a \cdot (T_G - T_L)$$
Where:
$$Nu = 0.023 \cdot Re^{0.8} \cdot Pr^n$$
Where $n = 0.4$ for heating, $n = 0.3$ for cooling.
$$h = \frac{Nu \cdot k}{D_h}$$
$$Nu = \frac{(f/8)(Re - 1000)Pr}{1 + 12.7\sqrt{f/8}(Pr^{2/3} - 1)}$$
| Boundary Condition | Nusselt Number |
|---|---|
| Constant wall temperature | $Nu = 3.66$ |
| Constant heat flux | $Nu = 4.36$ |
| Model | Equation | NeqSim Setting |
|---|---|---|
| Adiabatic | $q_w = 0$ | WallHeatTransferModel.ADIABATIC |
| Constant Wall Temp | $q_w = h_w(T_w - T_{fluid})$ | WallHeatTransferModel.CONSTANT_WALL_TEMPERATURE |
| Constant Heat Flux | $q_w = q_0$ | WallHeatTransferModel.CONSTANT_HEAT_FLUX |
| Convective Boundary | $q_w = U(T_{ambient} - T_{fluid})$ | WallHeatTransferModel.CONVECTIVE_BOUNDARY |
For a pipe with wall and insulation:
$$\frac{1}{U} = \frac{1}{h_{inner}} + \frac{r_i \ln(r_o/r_i)}{k_{wall}} + \frac{r_i \ln(r_{ins}/r_o)}{k_{ins}} + \frac{r_i}{r_{ins} \cdot h_{outer}}$$
The model uses the Krishna-Standart film theory for multicomponent mass transfer:
$$N_i = k_{i,L} \cdot a \cdot (x_i^{int} - x_i^{bulk}) = k_{i,G} \cdot a \cdot (y_i^{bulk} - y_i^{int})$$
| Symbol | Description | Units |
|---|---|---|
| $N_i$ | Molar flux of component $i$ | [mol/(m²·s)] |
| $k_{i,L}, k_{i,G}$ | Mass transfer coefficients | [m/s] |
| $a$ | Specific interfacial area | [m²/m³] |
| $x_i, y_i$ | Mole fractions in liquid/gas | [-] |
Mass transfer coefficients are related to heat transfer via:
$$Sh = Nu \cdot \left(\frac{Sc}{Pr}\right)^{1/3}$$
Where:
$$\Gamma = \sum_{i=1}^{n} M_i \cdot N_i \cdot a$$
Interface equilibrium: $y_i^{int} = K_i(T^{int}, P) \cdot x_i^{int}$
$$-\frac{dP}{dz} = \left(-\frac{dP}{dz}\right)_{friction} + \left(-\frac{dP}{dz}\right)_{gravity} + \left(-\frac{dP}{dz}\right)_{acceleration}$$
$$\left(-\frac{dP}{dz}\right)_{friction} = \phi_L^2 \cdot \left(-\frac{dP}{dz}\right)_{L,alone}$$
Two-phase multiplier: $$\phi_L^2 = 1 + \frac{C}{X} + \frac{1}{X^2}$$
Lockhart-Martinelli parameter: $$X = \sqrt{\frac{(dP/dz)_L}{(dP/dz)_G}}$$
| Gas Flow | Liquid Flow | C Value |
|---|---|---|
| Turbulent | Turbulent | 20 |
| Laminar | Turbulent | 12 |
| Turbulent | Laminar | 10 |
| Laminar | Laminar | 5 |
$$\left(-\frac{dP}{dz}\right)_{gravity} = \rho_m \cdot g \cdot \sin\theta$$
Mixture density: $\rho_m = \alpha_G \rho_G + (1-\alpha_G) \rho_L$
$$\left(-\frac{dP}{dz}\right)_{acc} = G^2 \frac{d}{dz}\left[\frac{x^2}{\rho_G \alpha_G} + \frac{(1-x)^2}{\rho_L \alpha_L}\right]$$
The solver uses a staggered grid where:
|----[i-1]----|-----[i]-----|----[i+1]----|
| ● | ● | ● | ← Scalars (P, T, ρ, α)
| ↑ ↑ | ← Velocities (u_G, u_L)
face i-½ face i+½
General conservation equation: $$\frac{\partial \phi}{\partial t} + \frac{\partial (u\phi)}{\partial z} = S_\phi$$
Discretized form: $$\frac{\phi_i^{n+1} - \phi_i^n}{\Delta t} + \frac{(u\phi)_{i+½} - (u\phi)_{i-½}}{\Delta z} = S_{\phi,i}$$
The tri-diagonal system: $$a_i \phi_{i-1} + b_i \phi_i + c_i \phi_{i+1} = d_i$$
Forward Sweep: $$c'_1 = \frac{c_1}{b_1}, \quad d'_1 = \frac{d_1}{b_1}$$ $$c'_i = \frac{c_i}{b_i - a_i c'_{i-1}}, \quad d'_i = \frac{d_i - a_i d'_{i-1}}{b_i - a_i c'_{i-1}}$$
Back Substitution: $$\phi_n = d'_n, \quad \phi_i = d'_i - c'_i \phi_{i+1}$$
1. Initialize: Set initial P, T, u_G, u_L, α profiles
2. Solve Momentum: Update velocities from pressure gradient
3. Solve Pressure: Apply pressure correction
4. Solve Mass: Update phase fractions from continuity
5. Solve Energy: Update temperatures from energy equation
6. Update Properties: Recalculate ρ, μ, k, h from EOS
7. Check Convergence: If ||residual|| < tolerance, stop; else goto 2
| Location | Variable | Condition |
|---|---|---|
| Inlet (z=0) | P, T, u, α | Dirichlet (specified) |
| Outlet (z=L) | P | Dirichlet or extrapolated |
| Outlet (z=L) | T, u, α | Zero-gradient (Neumann) |
| Pattern | Description | NeqSim Enum |
|---|---|---|
| Stratified | Liquid bottom, gas top | FlowPattern.STRATIFIED |
| Stratified-Wavy | With interfacial waves | FlowPattern.STRATIFIED_WAVY |
| Annular | Liquid film, gas core | FlowPattern.ANNULAR |
| Slug | Alternating slugs/bubbles | FlowPattern.SLUG |
| Bubble | Gas bubbles in liquid | FlowPattern.BUBBLE |
| Droplet/Mist | Liquid drops in gas | FlowPattern.DROPLET |
| Churn | Chaotic oscillating | FlowPattern.CHURN |
| Model | Description | NeqSim Enum |
|---|---|---|
| Manual | User-specified | FlowPatternModel.MANUAL |
| Taitel-Dukler | Horizontal/near-horizontal | FlowPatternModel.TAITEL_DUKLER |
| Baker Chart | General correlation | FlowPatternModel.BAKER_CHART |
| Barnea | Unified model | FlowPatternModel.BARNEA |
The simplified API provides static factory methods and a structured result container for the most common use cases:
// Create fluid system
SystemInterface fluid = new SystemSrkEos(293.15, 50.0);
fluid.addComponent("methane", 0.85, 0);
fluid.addComponent("water", 0.15, 1);
fluid.createDatabase(true);
fluid.setMixingRule(2);
// Create horizontal pipe using factory method
TwoPhasePipeFlowSystem pipe = TwoPhasePipeFlowSystem.horizontalPipe(fluid, 0.15, 500, 50);
// Solve and get structured results
PipeFlowResult result = pipe.solve();
// Access results via the container
System.out.println("Pressure drop: " + result.getTotalPressureDrop() + " bar");
System.out.println("Outlet temperature: " + result.getOutletTemperature() + " K");
System.out.println(result); // Prints formatted summary
// Export profiles for plotting
Map<String, double[]> data = result.toMap();
| Method | Description |
|---|---|
horizontalPipe(fluid, diameter, length, nodes) |
Horizontal pipe with stratified flow |
verticalPipe(fluid, diameter, length, nodes, upward) |
Vertical pipe (upward or downward flow) |
inclinedPipe(fluid, diameter, length, nodes, angleDeg) |
Inclined pipe at specified angle |
subseaPipe(fluid, diameter, length, nodes, seawaterTempC) |
Subsea pipeline with seawater cooling |
buriedPipe(fluid, diameter, length, nodes, groundTempC) |
Buried onshore pipeline |
| Method | Description |
|---|---|
solve() |
Solve and return PipeFlowResult |
solveWithMassTransfer() |
Enable mass transfer, solve, return result |
solveWithHeatAndMassTransfer() |
Enable heat & mass transfer, solve, return result |
For advanced configurations, use the fluent builder pattern:
// Build pipe system with full control
TwoPhasePipeFlowSystem pipe = TwoPhasePipeFlowSystem.builder()
.withFluid(fluid)
.withDiameter(0.15, "m")
.withLength(500, "m")
.withNodes(50)
.withFlowPattern(FlowPattern.STRATIFIED)
.withConvectiveBoundary(278.15, "K", 10.0) // Ambient temp, U-value
.build();
// Solve using simplified API
PipeFlowResult result = pipe.solve();
TwoPhasePipeFlowSystem pipe = TwoPhasePipeFlowSystem.inclinedPipe(fluid, 0.1, 1000, 100, 15.0);
// Or using builder for more control
TwoPhasePipeFlowSystem pipe = TwoPhasePipeFlowSystem.builder()
.withFluid(fluid)
.withDiameter(0.1, "m")
.withLength(1000, "m")
.withNodes(100)
.withInclination(15, "degrees") // 15° upward
.withFlowPattern(FlowPattern.SLUG)
.build();
// Check inclination
System.out.println("Inclination: " + pipe.getInclinationDegrees() + " degrees");
System.out.println("Is upward flow: " + pipe.isUpwardFlow());
PipeFlowResult result = pipe.solve();
// Get summary values
double pressureDrop = result.getTotalPressureDrop();
double outletTemp = result.getOutletTemperature();
double tempChange = result.getTemperatureChange();
// Get profiles
double[] pressure = result.getPressureProfile();
double[] temperature = result.getTemperatureProfile();
double[] holdup = result.getLiquidHoldupProfile();
double[] gasVelocity = result.getGasVelocityProfile();
double[] liquidVelocity = result.getLiquidVelocityProfile();
// Export to map (for Jupyter/Python integration)
Map<String, double[]> data = result.toMap();
Map<String, Object> summary = result.getSummary();
// Print formatted summary
System.out.println(result);
// Get profiles
double[] temperature = pipe.getTemperatureProfile();
double[] pressure = pipe.getPressureProfile();
double[] gasVelocity = pipe.getVelocityProfile(0);
double[] liquidVelocity = pipe.getVelocityProfile(1);
double[] liquidHoldup = pipe.getLiquidHoldupProfile();
// Get pressure drop breakdown
double totalDP = pipe.getTotalPressureDrop();
double frictionalDP = pipe.getFrictionalPressureDrop();
double gravitationalDP = pipe.getGravitationalPressureDrop();
// Get heat transfer info
double[] htc = pipe.getOverallInterphaseHeatTransferCoefficientProfile();
double totalHeatLoss = pipe.getTotalHeatLoss();
// Export to CSV
pipe.exportToCSV("results.csv");
// Get summary report
System.out.println(pipe.getSummaryReport());
Pressure drop (steady-state): The steady-state solver updates the pressure profile from an integrated momentum balance (friction + gravity) after solving the phase momentum equations. This ensures a physically consistent pressure drop along the pipe instead of keeping pressure nearly constant due to a density-only correction.
// Enable automatic flow pattern detection
pipe.setFlowPatternModel(FlowPatternModel.TAITEL_DUKLER);
pipe.detectFlowPatterns();
// Get flow pattern at each node
FlowPattern[] patterns = pipe.getFlowPatternProfile();
int transitions = pipe.getFlowPatternTransitionCount();
// Using simplified API
TwoPhasePipeFlowSystem pipe = TwoPhasePipeFlowSystem.horizontalPipe(fluid, 0.1, 100, 50);
PipeFlowResult result = pipe.solveWithHeatAndMassTransfer();
// Or using builder
TwoPhasePipeFlowSystem pipe = TwoPhasePipeFlowSystem.builder()
.withFluid(fluid)
.withDiameter(0.1, "m")
.withLength(100, "m")
.withNodes(50)
.enableNonEquilibriumMassTransfer() // Enable mass transfer
.enableNonEquilibriumHeatTransfer() // Enable heat transfer
.build();
PipeFlowResult result = pipe.solve();
| Method | Description |
|---|---|
horizontalPipe(fluid, diam, len, nodes) |
Create horizontal pipe |
verticalPipe(fluid, diam, len, nodes, up) |
Create vertical pipe |
inclinedPipe(fluid, diam, len, nodes, angle) |
Create inclined pipe |
subseaPipe(fluid, diam, len, nodes, seaTemp) |
Create subsea pipeline |
buriedPipe(fluid, diam, len, nodes, groundTemp) |
Create buried pipeline |
| Method | Description |
|---|---|
solve() |
Solve and return PipeFlowResult |
solveWithMassTransfer() |
Enable mass transfer, solve |
solveWithHeatAndMassTransfer() |
Enable heat & mass transfer, solve |
solveSteadyState(UUID) |
Solve steady-state equations |
solveTransient(int, UUID) |
Solve transient equations |
| Method | Description |
|---|---|
getTemperatureProfile() |
Temperature along pipe [K] |
getPressureProfile() |
Pressure along pipe [bar] |
getVelocityProfile(int phase) |
Phase velocity [m/s] |
getLiquidHoldupProfile() |
Liquid holdup [-] |
getTotalPressureDrop() |
Total ΔP [bar] |
exportToCSV(String path) |
Export results to CSV |
getSummaryReport() |
Formatted text summary |
| Method | Description |
|---|---|
getTotalPressureDrop() |
Total pressure drop [bar] |
getInletPressure() / getOutletPressure() |
Inlet/outlet pressure [bar] |
getInletTemperature() / getOutletTemperature() |
Inlet/outlet temperature [K] |
getTemperatureChange() |
Temperature change [K] |
getPressureGradient() |
Pressure gradient [bar/m] |
getPressureProfile() |
Pressure array [bar] |
getTemperatureProfile() |
Temperature array [K] |
getLiquidHoldupProfile() |
Liquid holdup array [-] |
getGasVelocityProfile() / getLiquidVelocityProfile() |
Velocity arrays [m/s] |
getFlowPatternProfile() |
Flow pattern array |
toMap() |
Export profiles to Map (Jupyter-friendly) |
getSummary() |
Export summary to Map |
| Method | Description |
|---|---|
withFluid(SystemInterface) |
Set thermodynamic system |
withDiameter(double, String) |
Set pipe diameter |
withLength(double, String) |
Set pipe length |
withNodes(int) |
Set number of nodes |
withInclination(double, String) |
Set pipe inclination |
withFlowPattern(FlowPattern) |
Set initial flow pattern |
withWallTemperature(double, String) |
Constant wall temp BC |
withConvectiveBoundary(double, String, double) |
Convective BC |
withAdiabaticWall() |
Adiabatic BC |
build() |
Create the pipe system |
Solbraa, E. (2002). "Measurement and Calculation of Two-Phase Flow in Pipes." PhD Thesis, Norwegian University of Science and Technology.
Taitel, Y., & Dukler, A.E. (1976). "A model for predicting flow regime transitions in horizontal and near horizontal gas-liquid flow." AIChE Journal, 22(1), 47-55.
Lockhart, R.W. and Martinelli, R.C. (1949). "Proposed Correlation of Data for Isothermal Two-Phase, Two-Component Flow in Pipes." Chemical Engineering Progress, 45(1), 39-48.
Krishna, R. and Standart, G.L. (1976). "A multicomponent film model incorporating a general matrix method of solution to the Maxwell-Stefan equations." AIChE Journal, 22(2), 383-389.
Gnielinski, V. (1976). "New equations for heat and mass transfer in turbulent pipe and channel flow." International Chemical Engineering, 16(2), 359-368.
Document generated for NeqSim Two-Phase Pipe Flow Module
This document describes the two-fluid model implementation in NeqSim for transient multiphase pipeline simulation.
The two-fluid model solves separate conservation equations for each phase (gas and liquid), providing more accurate predictions than drift-flux models for:
neqsim.process.equipment.pipeline.twophasepipe/
├── PipeSection.java # Base section state container
├── TwoFluidSection.java # Two-fluid section with conservative variables
├── TwoFluidConservationEquations.java # PDE RHS calculation
├── ThreeFluidSection.java # Extension for gas-oil-water systems
├── ThreeFluidConservationEquations.java # Three-phase equations
├── ThermodynamicCoupling.java # Flash calculation interface
├── FlashTable.java # Pre-computed property interpolation
├── EntrainmentDeposition.java # Droplet exchange model
├── FlowRegimeDetector.java # Flow pattern determination
├── LiquidAccumulationTracker.java # Low-point detection
├── SlugTracker.java # Slug tracking and statistics
├── closure/
│ ├── GeometryCalculator.java # Stratified geometry
│ ├── WallFriction.java # Wall shear correlations
│ └── InterfacialFriction.java # Interface shear correlations
└── numerics/
├── TimeIntegrator.java # Runge-Kutta integration
├── AUSMPlusFluxCalculator.java # Flux splitting scheme
└── MUSCLReconstructor.java # Higher-order reconstruction
The two-fluid model solves the following 1D PDEs:
Gas phase:
∂/∂t(αg·ρg·A) + ∂/∂x(αg·ρg·ug·A) = Γg
Liquid phase:
∂/∂t(αL·ρL·A) + ∂/∂x(αL·ρL·uL·A) = ΓL
Where:
αg, αL = Gas and liquid holdups (volume fractions)ρg, ρL = Phase densitiesug, uL = Phase velocities Γ = Mass transfer rate (evaporation/condensation)A = Pipe cross-sectional areaGas phase:
∂/∂t(αg·ρg·ug·A) + ∂/∂x(αg·ρg·ug²·A + αg·P·A) =
-τwg·Swg - τi·Si + αg·ρg·g·sin(θ)·A
Liquid phase:
∂/∂t(αL·ρL·uL·A) + ∂/∂x(αL·ρL·uL²·A + αL·P·A) =
-τwL·SwL + τi·Si + αL·ρL·g·sin(θ)·A
Where:
τwg, τwL = Wall shear stressesτi = Interfacial shear stressSwg, SwL, Si = Wetted perimetersθ = Pipe inclination angle∂/∂t(E·A) + ∂/∂x((E + P)·um·A) = Q - W
The model supports configurable heat transfer from the pipe wall using Newton's law of cooling:
Q_wall = U × π × D × (T_surface - T_fluid) [W/m]
Where:
U = Overall heat transfer coefficient [W/(m²·K)]D = Pipe diameter [m]T_surface = Ambient/seabed temperature [K]T_fluid = Fluid mixture temperature [K]API:
pipe.setSurfaceTemperature(5.0, "C"); // Seabed at 5°C
pipe.setHeatTransferCoefficient(25.0); // 25 W/(m²·K)
Typical U-values:
| Condition | U [W/(m²·K)] |
|---|---|
| Insulated subsea | 5-15 |
| Uninsulated subsea | 20-30 |
| Buried onshore | 2-5 |
| Exposed onshore | 50-100 |
Convenience method for setting heat transfer coefficient based on insulation type:
pipe.setInsulationType(TwoFluidPipe.InsulationType.PU_FOAM); // 10 W/(m²·K)
Available presets:
| InsulationType | U [W/(m²·K)] | Description |
|---|---|---|
NONE |
150 | Bare steel in seawater |
UNINSULATED_SUBSEA |
25 | Typical bare subsea pipe |
PU_FOAM |
10 | Standard PU foam insulation |
MULTI_LAYER |
5 | Multi-layer insulation |
PIPE_IN_PIPE |
2 | Pipe-in-pipe system |
VIT |
0.5 | Vacuum insulated tubing |
BURIED_ONSHORE |
3 | Buried onshore pipeline |
EXPOSED_ONSHORE |
75 | Wind-cooled exposed pipe |
Support for different U-values along the pipe (e.g., buried vs exposed sections):
double[] htcProfile = new double[numSections];
for (int i = 0; i < numSections; i++) {
htcProfile[i] = (i < 10) ? 5.0 : 50.0; // First 1 km insulated, rest exposed
}
pipe.setHeatTransferProfile(htcProfile);
For buried pipelines, add soil thermal resistance:
pipe.setSoilThermalResistance(0.5); // m²·K/W
// Effective U = 1 / (1/U + R_soil)
Temperature change from pressure drop (enabled by default):
pipe.setEnableJouleThomson(true); // Enable J-T cooling
// dT = μ_JT × dP (typical: 0.4 K/bar for natural gas)
For transient simulations, configure pipe wall properties:
pipe.setWallProperties(0.025, 7850.0, 500.0); // 25mm steel wall
// Parameters: thickness [m], density [kg/m³], heat capacity [J/(kg·K)]
Monitor for flow assurance issues:
pipe.setHydrateFormationTemperature(10.0, "C");
pipe.setWaxAppearanceTemperature(25.0, "C");
pipe.run();
if (pipe.hasHydrateRisk()) {
int section = pipe.getFirstHydrateRiskSection();
double distance = pipe.getDistanceToHydrateRisk();
System.out.println("Hydrate risk at " + distance + " m");
}
Get temperature profile in different units:
double[] tempK = pipe.getTemperatureProfile("K"); // Kelvin
double[] tempC = pipe.getTemperatureProfile("C"); // Celsius
double[] tempF = pipe.getTemperatureProfile("F"); // Fahrenheit
The FlowRegimeDetector uses Taitel-Dukler maps to identify:
Two detection methods are available:
FlowRegimeDetector detector = new FlowRegimeDetector();
// Default: Mechanistic approach (Taitel-Dukler, Barnea)
detector.setDetectionMethod(FlowRegimeDetector.DetectionMethod.MECHANISTIC);
// Alternative: Minimum slip criterion
detector.setDetectionMethod(FlowRegimeDetector.DetectionMethod.MINIMUM_SLIP);
// or
detector.setUseMinimumSlipCriterion(true);
The minimum slip criterion selects the flow regime that gives the minimum slip ratio (closest to 1.0), based on the principle that the system tends toward the flow pattern with minimum phase velocity difference.
WallFriction calculates wall shear using:
f = 16/ReThe InterfacialFriction class calculates the shear stress at the gas-liquid interface, which is a critical closure relation for the two-fluid model momentum equations. The interfacial friction affects the slip between phases and pressure drop distribution.
The interfacial shear stress follows the standard form:
τ_i = 0.5 × f_i × ρ_G × (v_G - v_L) × |v_G - v_L|
Where:
f_i = interfacial friction factor (dimensionless)ρ_G = gas density (kg/m³)v_G - v_L = slip velocity (m/s)The force per unit length appearing in the momentum equations is:
F_i = τ_i × S_i
Where S_i is the interfacial perimeter/width per unit length.
Positive interfacial shear acts to accelerate the liquid and decelerate the gas (when gas is faster than liquid). In the momentum equations:
τ_i × S_i (negative term)τ_i × S_i (positive term)| Flow Regime | Correlation | Reference | Key Features |
|---|---|---|---|
| Stratified Smooth | Taitel-Dukler | (1976) | Treats interface as smooth wall; Blasius for turbulent: f = 0.079/Re^0.25 |
| Stratified Wavy | Andritsos-Hanratty | (1987) | Wave roughness enhancement: f_i = f_smooth × (1 + 15√(h_L/D) × (v_G/v_G,t - 1)) |
| Annular | Wallis | (1969) | Film-core interaction: f_i = f_G × (1 + 300δ/D) where δ is film thickness |
| Slug | Oliemans | (1986) | Bubble swarm approach with Ishii-Zuber drag coefficient |
| Bubble/Dispersed | Schiller-Naumann | - | Drag on individual bubbles: C_D = (24/Re_b) × (1 + 0.15 × Re_b^0.687) for Re < 1000 |
| Churn | Enhanced Annular | - | Uses annular correlation with 1.5× enhancement factor |
For smooth stratified flow, the interface is treated as a smooth wall with gas-side friction:
// Gas-side Reynolds number
Re_G = ρ_G × |v_slip| × D_G / μ_G
// Friction factor
if (Re_G < 2300):
f_i = 16 / Re_G // Laminar
else:
f_i = 0.079 / Re_G^0.25 // Blasius (turbulent)
Accounts for wave-induced roughness at the interface:
// Transition gas velocity
v_G,t = 5.0 × √(ρ_L / ρ_G)
// Enhancement factor (for v_G > v_G,t)
enhancement = 1.0 + 15 × √(h_L/D) × (v_G/v_G,t - 1)
enhancement = min(enhancement, 20.0) // Cap
f_i = f_smooth × enhancement
For gas-core / liquid-film interaction:
// Film thickness
δ = D/2 × (1 - √(1 - α_L))
// Core diameter
D_core = D - 2δ
// Wallis enhancement
enhancement = 1.0 + 300 × δ/D
enhancement = min(enhancement, 50.0) // Cap
f_i = f_G × enhancement
For drag on individual bubbles in liquid continuum:
// Bubble diameter (Hinze correlation)
d_b = 2 × (0.725 × σ / ((ρ_L - ρ_G) × g))^0.5
d_b = min(d_b, D/5)
// Bubble Reynolds number
Re_b = ρ_L × |v_slip| × d_b / μ_L
// Drag coefficient
if (Re_b < 0.1):
C_D = 240 // Stokes limit
else if (Re_b < 1000):
C_D = 24/Re_b × (1 + 0.15 × Re_b^0.687)
else:
C_D = 0.44 // Newton regime
// Friction factor
f_i = C_D × d_b / (4 × D)
InterfacialFriction interfacialFriction = new InterfacialFriction();
InterfacialFrictionResult result = interfacialFriction.calculate(
FlowRegime.STRATIFIED_WAVY,
gasVelocity, // m/s
liquidVelocity, // m/s
gasDensity, // kg/m³
liquidDensity, // kg/m³
gasViscosity, // Pa·s
liquidViscosity, // Pa·s
liquidHoldup, // 0-1
diameter, // m
surfaceTension // N/m
);
double shearStress = result.interfacialShear; // Pa
double frictionFactor = result.frictionFactor; // dimensionless
double slipVelocity = result.slipVelocity; // m/s
double interfacialArea = result.interfacialAreaPerLength; // m²/m
For three-phase gas-oil-water systems, the ThreeFluidConservationEquations uses a simplified Froude-based correlation for oil-water interfaces:
// Froude number based on relative velocity
Fr = |v_rel| / √(g × D × |ρ_2 - ρ_1| / ρ_1)
// Simplified correlation
f_i = 0.01 × (1 + 10 × Fr²) // capped at 0.1
This simplified approach is justified because:
The three-layer stratified geometry has two interfaces:
┌─────────────────┐
│ Gas │ ← τ_wall,G + τ_i,GO (gas-oil)
├─────────────────┤
│ Oil │ ← τ_wall,O + τ_i,GO + τ_i,OW
├─────────────────┤
│ Water │ ← τ_wall,W + τ_i,OW (oil-water)
└─────────────────┘
Momentum exchange:
GeometryCalculator computes for stratified flow:
The AUSMPlusFluxCalculator implements AUSM+ flux splitting for:
TimeIntegrator supports:
MUSCLReconstructor provides:
ThermodynamicCoupling interfaces with NeqSim's flash routines:
ThermodynamicCoupling coupling = new ThermodynamicCoupling(referenceFluid);
ThermoProperties props = coupling.flashPT(pressure, temperature);
FlashTable provides fast property lookup via bilinear interpolation:
FlashTable table = new FlashTable();
table.build(fluid, pMin, pMax, nP, tMin, tMax, nT);
ThermoProperties props = table.interpolate(pressure, temperature);
For gas-oil-water systems, ThreeFluidSection and ThreeFluidConservationEquations extend the model to 7 equations:
┌─────────────────┐
│ Gas │
├─────────────────┤ ← Gas-Oil Interface
│ Oil │
├─────────────────┤ ← Oil-Water Interface
│ Water │
└─────────────────┘
The TwoFluidPipe supports two simulation modes: steady-state initialization via run() and incremental transient simulation via runTransient().
run()The run() method performs a complete steady-state initialization of the pipeline. This is typically called once at the start to establish initial conditions before transient simulation.
What happens during run():
┌──────────────────────────────────────────────────────────────┐
│ run(UUID id) │
├──────────────────────────────────────────────────────────────┤
│ 1. initializeSections() │
│ ├─ Create pipe sections with uniform spacing (dx) │
│ ├─ Flash inlet fluid to get phase properties │
│ ├─ Initialize all sections with inlet conditions │
│ ├─ Set elevation/inclination from terrain profile │
│ ├─ Set outlet pressure boundary condition │
│ └─ Identify liquid accumulation zones │
│ │
│ 2. runSteadyState() │
│ ├─ Iterative solver (max 100 iterations) │
│ │ ├─ Update flow regimes for all sections │
│ │ ├─ Calculate pressure gradient (momentum balance) │
│ │ ├─ Update local holdups using drift-flux model │
│ │ │ └─ Account for terrain effects (low points) │
│ │ ├─ Update phase velocities from mass conservation │
│ │ ├─ Update oil/water holdups for three-phase flow │
│ │ └─ Update temperature profile (if heat transfer on) │
│ └─ Converge when max change < tolerance (1e-4) │
│ │
│ 3. updateOutletStream() │
│ ├─ Flash outlet fluid at outlet P, T │
│ ├─ Calculate outlet mass flow from section state │
│ └─ Set outlet stream properties │
└──────────────────────────────────────────────────────────────┘
Key characteristics:
Example:
TwoFluidPipe pipe = new TwoFluidPipe("Pipeline", inletStream);
pipe.setLength(5000);
pipe.setDiameter(0.3);
pipe.setNumberOfSections(100);
pipe.run(); // Steady-state initialization
double[] pressures = pipe.getPressureProfile();
double[] holdups = pipe.getLiquidHoldupProfile();
runTransient(dt, id)The runTransient() method advances the simulation by a specified time step. It solves the full time-dependent conservation equations and is called repeatedly in a loop.
What happens during runTransient(dt, id):
┌──────────────────────────────────────────────────────────────┐
│ runTransient(double dt, UUID id) │
├──────────────────────────────────────────────────────────────┤
│ 1. Calculate stable time step │
│ ├─ dt_stable = CFL × dx / max(wave_speed) │
│ ├─ dt_actual = min(dt, dt_stable) │
│ └─ Determine number of sub-steps │
│ │
│ 2. For each sub-step: │
│ ├─ Update thermodynamics (every N steps) │
│ │ └─ PT flash at each section P, T │
│ │ │
│ ├─ Store previous state U_prev │
│ │ │
│ ├─ Time integration (RK4 by default) │
│ │ ├─ Calculate RHS of conservation equations │
│ │ │ ├─ Mass fluxes: ∂(αρuA)/∂x │
│ │ │ ├─ Momentum sources: wall friction, interfacial │
│ │ │ │ friction, gravity, pressure gradient │
│ │ │ └─ Energy: heat transfer, work terms │
│ │ └─ Advance state: U_new = U_prev + dt × RHS │
│ │ │
│ ├─ Validate and correct state │
│ │ ├─ Check for NaN/Inf → revert to previous │
│ │ ├─ Ensure mass ≥ 0 │
│ │ └─ Limit rate of change (50% per sub-step max) │
│ │ │
│ ├─ Apply state to sections │
│ │ │
│ ├─ Apply pressure gradient (semi-implicit) │
│ │ │
│ ├─ Apply boundary conditions │
│ │ ├─ Inlet: constant flow or constant pressure │
│ │ └─ Outlet: constant pressure │
│ │ │
│ ├─ Validate section states │
│ │ ├─ Fix invalid holdups (NaN, negative) │
│ │ ├─ Ensure holdup consistency (αL = αO + αW) │
│ │ └─ Ensure P, T are positive │
│ │ │
│ ├─ Update accumulation tracking (if enabled) │
│ │ │
│ ├─ Update temperature (if heat transfer enabled) │
│ │ │
│ └─ Advance simulation time │
│ │
│ 3. Update outlet stream │
│ │
│ 4. Update result arrays │
└──────────────────────────────────────────────────────────────┘
Key characteristics:
Example:
// After steady-state initialization
pipe.run();
// Transient simulation loop
UUID simId = UUID.randomUUID();
for (int step = 0; step < 1000; step++) {
// Change boundary conditions if needed
if (step == 100) {
inletStream.setFlowRate(15.0, "kg/sec"); // Flow increase
inletStream.run();
}
pipe.runTransient(0.1, simId); // Advance 0.1 seconds
// Monitor results
double outletFlow = pipe.getOutletMassFlow();
double liquidInventory = pipe.getLiquidInventory("m3");
}
Both methods integrate seamlessly with ProcessSystem for coupled simulations:
ProcessSystem process = new ProcessSystem();
process.add(inletStream);
process.add(pipe);
process.add(separator);
// Steady-state initialization
process.run();
// Transient loop
for (int t = 0; t < 300; t++) {
process.runTransient(1.0, UUID.randomUUID());
}
| Aspect | run() |
runTransient(dt, id) |
|---|---|---|
| Purpose | Initialize steady-state | Advance in time |
| Call frequency | Once at start | Repeatedly in loop |
| Time step | N/A (iterative) | User-specified + CFL limit |
| Solver | Iterative relaxation | Runge-Kutta (RK4) |
| Equations | Simplified momentum balance | Full conservation PDEs |
| Computation | Moderate | Higher (per call) |
| Use case | Initial conditions | Dynamic response |
// Create two-phase fluid
SystemInterface fluid = new SystemSrkEos(300, 50);
fluid.addComponent("methane", 0.85);
fluid.addComponent("n-pentane", 0.15);
fluid.setMixingRule("classic");
fluid.setMultiPhaseCheck(true);
// Create inlet stream
Stream inlet = new Stream("inlet", fluid);
inlet.setFlowRate(10, "kg/sec");
inlet.run();
// Create two-fluid pipe
TwoFluidPipe pipe = new TwoFluidPipe("Pipeline", inlet);
pipe.setLength(5000); // 5 km
pipe.setDiameter(0.3); // 300 mm
pipe.setNumberOfSections(100);
// Set terrain profile
double[] elevations = new double[100];
for (int i = 0; i < 100; i++) {
elevations[i] = 50.0 * Math.sin(i * Math.PI / 50);
}
pipe.setElevationProfile(elevations);
// Optional: Configure heat transfer to surroundings
pipe.setSurfaceTemperature(5.0, "C"); // Seabed at 5°C
pipe.setHeatTransferCoefficient(25.0); // 25 W/(m²·K)
// Run steady-state initialization
pipe.run();
// Transient simulation
UUID id = UUID.randomUUID();
for (int step = 0; step < 1000; step++) {
pipe.runTransient(0.1, id); // 0.1 second steps
}
// Get results
double[] pressures = pipe.getPressureProfile();
double[] holdups = pipe.getLiquidHoldupProfile();
double liquidInventory = pipe.getLiquidInventory("m3");
The TwoFluidPipe model includes a comprehensive terrain-induced slug tracking system that detects liquid accumulation at terrain low points and tracks the formation, propagation, and arrival of slugs at the outlet.
TwoFluidPipe pipe = new TwoFluidPipe("Pipeline", inlet);
pipe.setLength(20000); // 20 km
pipe.setDiameter(0.3); // 300 mm
pipe.setNumberOfSections(100);
pipe.setElevationProfile(terrain);
// Enable slug tracking
pipe.setEnableSlugTracking(true);
// Optional: Tune accumulation threshold (default 0.25)
pipe.getAccumulationTracker().setCriticalHoldup(0.35);
The slug tracking system consists of two components working together:
Terrain Profile with Slug Formation:
Inlet ─────┐ ┌───── Outlet
│ Valley │
└────────────────┘
▲
│
Liquid accumulates here
When zone overflows → slug released
The tracker automatically identifies terrain low points where liquid accumulates:
// Get accumulation zones after running
var zones = pipe.getAccumulationTracker().getAccumulationZones();
for (var zone : zones) {
System.out.println("Zone at position: " + zone.startPosition + " m");
System.out.println(" Volume: " + zone.liquidVolume + " m³");
System.out.println(" Max capacity: " + zone.maxVolume + " m³");
System.out.println(" Fill fraction: " + (zone.liquidVolume / zone.maxVolume));
System.out.println(" Is overflowing: " + zone.isOverflowing);
}
Access comprehensive slug statistics after simulation:
SlugTracker tracker = pipe.getSlugTracker();
// Summary statistics
System.out.println("Slugs generated: " + tracker.getTotalSlugsGenerated());
System.out.println("Slugs merged: " + tracker.getTotalSlugsMerged());
System.out.println("Active slugs: " + tracker.getSlugs().size());
System.out.println("Slugs at outlet: " + pipe.getOutletSlugCount());
System.out.println("Max slug length: " + tracker.getMaxSlugLength() + " m");
System.out.println("Avg slug length: " + tracker.getAverageSlugLength() + " m");
System.out.println("Slug frequency: " + tracker.getSlugFrequency() + " Hz");
// Detailed per-slug information
for (var slug : tracker.getSlugs()) {
System.out.println("Slug #" + slug.id);
System.out.println(" Position: " + slug.frontPosition + " m");
System.out.println(" Length: " + slug.slugBodyLength + " m");
System.out.println(" Volume: " + slug.liquidVolume + " m³");
System.out.println(" Velocity: " + slug.frontVelocity + " m/s");
System.out.println(" Age: " + slug.age + " s");
System.out.println(" Terrain-induced: " + slug.isTerrainInduced);
}
// Create gas-condensate fluid
SystemInterface fluid = new SystemSrkEos(288.15, 50);
fluid.addComponent("methane", 0.70);
fluid.addComponent("ethane", 0.10);
fluid.addComponent("propane", 0.05);
fluid.addComponent("n-pentane", 0.10);
fluid.addComponent("n-heptane", 0.05);
fluid.setMixingRule("classic");
fluid.setMultiPhaseCheck(true);
Stream inlet = new Stream("inlet", fluid);
inlet.setFlowRate(15, "kg/sec");
inlet.setTemperature(15, "C");
inlet.setPressure(50, "bara");
inlet.run();
// Create terrain with valleys
double[] terrain = new double[100];
for (int i = 0; i < 100; i++) {
double x = i * 200.0; // 20 km total
double xNorm = x / 20000.0;
terrain[i] = -20.0 * Math.exp(-Math.pow((xNorm - 0.4) / 0.1, 2));
}
TwoFluidPipe pipe = new TwoFluidPipe("SlugPipeline", inlet);
pipe.setLength(20000);
pipe.setDiameter(0.3);
pipe.setNumberOfSections(100);
pipe.setElevationProfile(terrain);
pipe.setEnableSlugTracking(true);
pipe.getAccumulationTracker().setCriticalHoldup(0.35);
// Steady-state initialization
pipe.run();
// Transient simulation (2 hours)
UUID id = UUID.randomUUID();
double simTime = 2 * 60 * 60; // 2 hours
double dt = 1.0;
int steps = (int)(simTime / dt);
for (int i = 0; i < steps; i++) {
pipe.runTransient(dt, id);
// Monitor progress every 15 minutes
if (i % 900 == 0 && i > 0) {
System.out.printf("Time: %.0f min, Slugs: %d, Outlet: %d%n",
i / 60.0,
pipe.getSlugTracker().getTotalSlugsGenerated(),
pipe.getOutletSlugCount());
}
}
// Final report
System.out.println(pipe.getSlugTrackingReport());
Both TwoFluidPipe and TransientPipe use the same slug tracking infrastructure, but predict different slug frequencies due to their underlying physical models:
| Aspect | TwoFluidPipe | TransientPipe |
|---|---|---|
| Physical Model | 7-equation two-fluid | 4-equation drift-flux |
| Holdup Prediction | Lower (mechanistic) | Higher (empirical slip) |
| Accumulation Rate | Slower | Faster |
| Slug Frequency | Lower | Higher (conservative) |
| Oil-Water Tracking | Separate phases | Combined liquid |
| Computation Time | ~3x slower | Faster |
Typical behavior comparison:
| Condition | TwoFluidPipe | TransientPipe |
|---|---|---|
| Avg liquid holdup | 0.25-0.35 | 0.90-0.95 |
| Zone fill rate | 6-15%/hour | 70-80% quickly |
| Time to first slug | ~2 hours | < 1 minute |
| Slug frequency | 1 per 1-2 hours | 2-3 per minute |
When to use each:
// Lower critical holdup → earlier slug initiation
pipe.getAccumulationTracker().setCriticalHoldup(0.20);
// The overflow threshold in LiquidAccumulationTracker determines
// when accumulated liquid is released as a slug
// (set to 20% by default for terrain-induced slugging)
A complete example demonstrating a slugging pipeline connected to a choke valve and separator with level control is available in:
Example file: examples/neqsim/process/pipeline/SlugPipelineToSeparatorExample.java
Test file: src/test/java/neqsim/process/equipment/pipeline/SlugPipelineToSeparatorTest.java
┌─────────────┐ ┌─────────────┐ ┌─────────┐ ┌───────────┐
│ Wellhead │────▶│ Flowline │────▶│ Choke │────▶│ Separator │
│ (Const P) │ │ TwoFluidPipe│ │ Valve │ │ (Level │
│ 80 bara │ │ 3 km │ │ │ │ Control) │
└─────────────┘ └─────────────┘ └─────────┘ └───────────┘
│ │
Low point Level
(Slugging) Controller
The TwoFluidPipe model produces realistic transient dynamics:
| Metric | Observed | Description |
|---|---|---|
| Outlet flow variation | 577% | Flow decreases from 8.0 to 0.46 kg/s during blowdown |
| Pressure range | 55-58 bara | Outlet pressure stabilizes to boundary value |
| Holdup variation | 0.3 → 0.006 | Pipeline drains liquid during transient |
// Create TwoFluidPipe with stream-connected inlet
TwoFluidPipe pipeline = new TwoFluidPipe("SubseaFlowline", pipeInlet);
pipeline.setLength(3000.0); // 3 km
pipeline.setDiameter(0.254); // 10 inch
pipeline.setNumberOfSections(30);
// Set outlet pressure boundary condition
pipeline.setOutletPressure(60.0, "bara");
// Terrain with low point for liquid accumulation
double[] elevations = new double[30];
for (int i = 0; i < 30; i++) {
double x = (i + 1.0) / 30.0;
if (x < 0.5) {
elevations[i] = -35.0 * x / 0.5; // Downhill to low point
} else {
elevations[i] = -35.0 + 85.0 * (x - 0.5) / 0.5; // Riser to +50m
}
}
pipeline.setElevationProfile(elevations);
// Choke valve between pipeline and separator
ThrottlingValve choke = new ThrottlingValve("Choke", pipeline.getOutletStream());
choke.setOutletPressure(55.0); // bara
// Separator with level control
Separator separator = new Separator("InletSeparator");
separator.addStream(choke.getOutletStream());
separator.setInternalDiameter(2.5);
separator.setSeparatorLength(8.0);
// Level controller on liquid outlet valve
ThrottlingValve liquidValve = new ThrottlingValve("LiquidValve",
separator.getLiquidOutStream());
LevelTransmitter levelTT = new LevelTransmitter("LT-100", separator);
ControllerDeviceBaseClass levelController = new ControllerDeviceBaseClass("LIC-100");
levelController.setTransmitter(levelTT);
levelController.setControllerSetPoint(0.50); // 50% level
levelController.setControllerParameters(1.5, 180.0, 15.0); // Kp, Ti, Td
liquidValve.setController(levelController);
// Build process system and run transient
ProcessSystem process = new ProcessSystem();
process.add(pipeInlet);
process.add(pipeline);
process.add(choke);
process.add(separator);
process.add(liquidValve);
process.run(); // Initial steady-state
// Transient simulation
UUID simId = UUID.randomUUID();
for (int step = 0; step < 150; step++) {
process.runTransient(2.0, simId);
double pipeOutFlow = pipeline.getOutletStream().getFlowRate("kg/sec");
double level = separator.getLiquidLevel();
// Track slug arrivals, level variations, etc.
}
The example simulates:
The TwoFluidPipe model has been validated against established correlations and published experimental data to ensure physically correct pressure drop predictions.
The validation test suite is implemented in TwoPhasePressureDropValidationTest.java and includes:
Comparison of TwoFluidPipe against Beggs & Brill (1973) test cases:
| Test Case | D (mm) | L (m) | B&B ΔP (bar) | TFP ΔP (bar) | Ratio |
|---|---|---|---|---|---|
| Horizontal Segregated | 100 | 500 | 0.844 | 0.587 | 0.70 |
| Horizontal Intermittent | 100 | 500 | 2.211 | 2.253 | 1.02 |
| Horizontal Distributed | 100 | 500 | 1.979 | 4.127 | 2.09 |
| Uphill 10° | 100 | 500 | 3.688 | 3.700 | 1.00 |
| Downhill 10° | 100 | 500 | -0.850 | -1.013 | 1.19 |
Key Observations:
The differences are expected because:
The model correctly captures the following physical behaviors:
| Physical Effect | Expected Behavior | Model Result |
|---|---|---|
| Pressure drop vs GLR | Increases with gas-liquid ratio | ✓ Verified |
| Uphill flow | Higher ΔP (gravity opposes) | ✓ Verified |
| Downhill flow | Negative ΔP (pressure gain) | ✓ Verified |
| Hydrostatic head | Proportional to sin(θ) | ✓ Verified |
| Friction loss | Increases with velocity² | ✓ Verified |
To run the validation tests:
# Run all two-phase pressure drop validation tests
./mvnw test -Dtest=TwoPhasePressureDropValidationTest
# Run specific validation test
./mvnw test -Dtest=TwoPhasePressureDropValidationTest#testTwoFluidPipeValidation
For applications where empirical accuracy is preferred over mechanistic modeling, NeqSim also provides PipeBeggsAndBrills which implements the original Beggs & Brill correlation with the Payne et al. (1979) corrections.
| Feature | TwoFluidPipe | PipeBeggsAndBrills |
|---|---|---|
| Approach | Mechanistic (conservation eqs) | Empirical (correlations) |
| Flow regimes | Computed from physics | Correlated flow map |
| Transient capability | Yes | Steady-state only |
| Heat transfer | Configurable | Built-in |
| Terrain effects | Elevation profile | Single angle |
| Best for | Transient, complex terrain | Quick steady-state |
The model includes comprehensive unit tests:
Total: 160+ tests
This document outlines recommendations for implementing a full multiphase 1D transient pipeline model in NeqSim, similar to commercial tools like OLGA and LedaFlow. The model would handle:
| Component | Status | Notes |
|---|---|---|
PipeBeggsAndBrills |
✅ | Steady-state multiphase, quasi-transient advection |
WaterHammerPipe |
✅ | Fast transients (MOC), single-phase |
| Thermodynamics | ✅ | Full EOS, flash calculations, physical properties |
| Flow regime maps | ✅ | Beggs & Brill flow pattern detection |
| Holdup correlations | ✅ | Beggs & Brill liquid holdup |
| Feature | Current | Required |
|---|---|---|
| Mass conservation | Quasi-steady | Full PDE |
| Momentum | Steady friction | Full transient + interfacial |
| Energy | Optional heat loss | Full enthalpy transport |
| Liquid accumulation | Not tracked | Dynamic holdup evolution |
| Slug tracking | Not modeled | Slug initiation, growth, dissipation |
| Phase slip | Correlations | Mechanistic drift-flux or two-fluid |
The two-fluid model solves separate conservation equations for each phase:
Gas Mass: $$\frac{\partial}{\partial t}(\alpha_g \rho_g A) + \frac{\partial}{\partial x}(\alpha_g \rho_g v_g A) = \Gamma_g$$
Liquid Mass: $$\frac{\partial}{\partial t}(\alpha_L \rho_L A) + \frac{\partial}{\partial x}(\alpha_L \rho_L v_L A) = \Gamma_L$$
Gas Momentum: $$\frac{\partial}{\partial t}(\alpha_g \rho_g v_g A) + \frac{\partial}{\partial x}(\alpha_g \rho_g v_g^2 A) = -\alpha_g A \frac{\partial P}{\partial x} - \tau_{wg} S_g - \tau_i S_i - \alpha_g \rho_g g A \sin\theta$$
Liquid Momentum: $$\frac{\partial}{\partial t}(\alpha_L \rho_L v_L A) + \frac{\partial}{\partial x}(\alpha_L \rho_L v_L^2 A) = -\alpha_L A \frac{\partial P}{\partial x} - \tau_{wL} S_L + \tau_i S_i - \alpha_L \rho_L g A \sin\theta$$
Mixture Energy: $$\frac{\partial}{\partial t}(E_{mix} A) + \frac{\partial}{\partial x}((E_{mix} + P) v_{mix} A) = Q_{wall} + W_{friction}$$
Where:
For less demanding applications, a drift-flux model combines phases:
Mixture Mass: $$\frac{\partial}{\partial t}(\rho_m A) + \frac{\partial}{\partial x}(\rho_m v_m A) = 0$$
Mixture Momentum: $$\frac{\partial}{\partial t}(\rho_m v_m A) + \frac{\partial}{\partial x}(\rho_m v_m^2 A + \Delta P_{slip}) = -A \frac{\partial P}{\partial x} - \tau_w S - \rho_m g A \sin\theta$$
Drift-Flux Relation: $$v_g = C_0 v_m + v_{drift}$$
Where $C_0$ is the distribution parameter and $v_{drift}$ is the drift velocity (from correlations).
src/main/java/neqsim/process/equipment/pipeline/
├── MultiphasePipe.java # Main transient solver
├── MultiphasePipeSection.java # Single pipe section state
├── transient/
│ ├── ConservationEquations.java # PDE discretization
│ ├── FluxCalculator.java # Numerical flux (AUSM, HLL, etc.)
│ ├── SourceTerms.java # Friction, gravity, mass transfer
│ ├── SlugTracker.java # Slug initiation and tracking
│ └── HoldupEvolution.java # Liquid accumulation dynamics
├── closure/
│ ├── FlowRegimeMap.java # Mechanistic flow regime
│ ├── InterfacialFriction.java # τ_i correlations
│ ├── WallFriction.java # τ_wg, τ_wL correlations
│ ├── DriftFluxParameters.java # C_0, v_drift correlations
│ └── EntrainmentDeposition.java # Droplet exchange
└── geometry/
├── PipeProfile.java # Elevation, diameter profile
└── PipeNetwork.java # Junctions, branches
public class MultiphasePipeSection {
// Primary variables (conservative)
private double pressure; // Pa
private double gasHoldup; // α_g (0-1)
private double oilHoldup; // α_o (0-1)
private double waterHoldup; // α_w (0-1)
private double gasVelocity; // m/s
private double liquidVelocity; // m/s (or separate oil/water)
private double temperature; // K
// Derived quantities
private double gasDensity;
private double liquidDensity;
private double mixtureVelocity;
private double liquidLevel; // For stratified flow
private FlowRegime flowRegime;
// Slug tracking
private boolean isInSlug;
private double slugFrontPosition;
private double slugTailPosition;
private double slugHoldup;
// Geometry
private double diameter;
private double area;
private double inclination; // radians
private double position; // m from inlet
}
public enum FlowRegime {
STRATIFIED_SMOOTH,
STRATIFIED_WAVY,
ANNULAR,
SLUG,
DISPERSED_BUBBLE,
BUBBLE,
CHURN,
MIST
}
public class MechanisticFlowRegime {
/**
* Determine flow regime using Taitel-Dukler or similar mechanistic model.
*/
public FlowRegime determine(double vsg, double vsl, double diameter,
double inclination, PhaseProperties gas,
PhaseProperties liquid) {
// Calculate dimensionless groups
double froude = calcFroudeNumber(vsg, vsl, diameter);
double lockhart = calcLockhartMartinelli(gas, liquid, vsg, vsl);
// Stratified stability (Kelvin-Helmholtz)
double criticalGasVelocity = calcKelvinHelmholtzLimit(
liquid.getDensity(), gas.getDensity(),
liquidLevel, diameter, inclination);
if (vsg < criticalGasVelocity && inclination < Math.toRadians(10)) {
// Check wavy vs smooth
if (isWavyTransition(vsg, liquid)) {
return FlowRegime.STRATIFIED_WAVY;
}
return FlowRegime.STRATIFIED_SMOOTH;
}
// Slug formation criterion
if (isSlugCondition(vsg, vsl, liquidLevel, diameter)) {
return FlowRegime.SLUG;
}
// Annular transition
if (vsg > calcAnnularTransition(diameter, gas, liquid)) {
return FlowRegime.ANNULAR;
}
// ... other transitions
return FlowRegime.INTERMITTENT;
}
/**
* Kelvin-Helmholtz instability criterion for stratified flow.
*/
private double calcKelvinHelmholtzLimit(double rhoL, double rhoG,
double hL, double D, double theta) {
double g = 9.81;
double aG = calcGasArea(hL, D);
double aL = calcLiquidArea(hL, D);
double dAL_dhL = calcDerivativeArea(hL, D);
// Taitel-Dukler criterion
double term1 = (rhoL - rhoG) * g * Math.cos(theta) * aG;
double term2 = rhoG * aL * dAL_dhL;
return Math.sqrt(term1 / term2);
}
}
public class SlugTracker {
private List<Slug> activeSlugs = new ArrayList<>();
private double minSlugLength; // Minimum stable slug length
private double slugInitiationVoid; // α_g threshold for slug formation
public class Slug {
double frontPosition; // m
double tailPosition; // m
double velocity; // m/s
double holdup; // liquid fraction in slug body
double bubbleHoldup; // gas fraction in slug bubble
double frequency; // slugs per unit time
boolean isTerrainInduced;
}
/**
* Check for slug initiation at each grid point.
*/
public void checkSlugInitiation(MultiphasePipeSection[] sections, double dt) {
for (int i = 1; i < sections.length - 1; i++) {
MultiphasePipeSection sec = sections[i];
// Terrain-induced slug: liquid accumulation at low point
if (isLowPoint(sections, i) && sec.getLiquidHoldup() > slugInitiationVoid) {
if (!sec.isInSlug() && sec.getGasVelocity() > getMinGasVelocityForSlug(sec)) {
initiateSlug(sections, i);
}
}
// Hydrodynamic slug: wave growth in stratified-wavy
if (sec.getFlowRegime() == FlowRegime.STRATIFIED_WAVY) {
if (isWaveBlockage(sec)) {
initiateSlug(sections, i);
}
}
}
}
/**
* Propagate existing slugs.
*/
public void propagateSlugs(MultiphasePipeSection[] sections, double dt) {
Iterator<Slug> iter = activeSlugs.iterator();
while (iter.hasNext()) {
Slug slug = iter.next();
// Slug front velocity (Bendiksen correlation)
double vFront = calcSlugFrontVelocity(slug, sections);
slug.frontPosition += vFront * dt;
// Slug tail velocity
double vTail = calcSlugTailVelocity(slug, sections);
slug.tailPosition += vTail * dt;
// Slug length
double length = slug.frontPosition - slug.tailPosition;
// Slug dissipation
if (length < minSlugLength || slug.frontPosition > getPipeLength()) {
iter.remove();
dissipateSlug(slug, sections);
}
// Update holdup in slug region
updateSlugHoldup(slug, sections);
}
}
/**
* Bendiksen (1984) slug front velocity.
*/
private double calcSlugFrontVelocity(Slug slug, MultiphasePipeSection[] sections) {
MultiphasePipeSection sec = getSectionAt(slug.frontPosition, sections);
double vm = sec.getMixtureVelocity();
double C = 1.2; // Distribution parameter
double vd = 0.35 * Math.sqrt(9.81 * sec.getDiameter()); // Drift velocity
return C * vm + vd;
}
}
public class LiquidAccumulation {
/**
* Track liquid inventory and low-point accumulation.
*/
public void updateAccumulation(MultiphasePipeSection[] sections,
double dt, double inletLiquidRate) {
// Identify low points in terrain profile
List<Integer> lowPoints = findLowPoints(sections);
for (int lowIdx : lowPoints) {
MultiphasePipeSection low = sections[lowIdx];
// Liquid drainage into low point
double drainageIn = calcDrainageRate(sections, lowIdx, -1); // From upstream
drainageIn += calcDrainageRate(sections, lowIdx, +1); // From downstream
// Liquid carryover out of low point
double carryover = calcCarryoverRate(low);
// Net accumulation
double netRate = drainageIn - carryover;
// Update holdup
double dHoldup = netRate * dt / (low.getArea() * getSegmentLength());
low.setLiquidHoldup(low.getLiquidHoldup() + dHoldup);
// Check for slug initiation if holdup exceeds critical
if (low.getLiquidHoldup() > getCriticalHoldup(low)) {
slugTracker.initiateSlug(sections, lowIdx);
}
}
}
/**
* Calculate liquid drainage rate into low point.
*/
private double calcDrainageRate(MultiphasePipeSection[] sections,
int lowIdx, int direction) {
int neighborIdx = lowIdx + direction;
if (neighborIdx < 0 || neighborIdx >= sections.length) return 0;
MultiphasePipeSection neighbor = sections[neighborIdx];
MultiphasePipeSection low = sections[lowIdx];
// Height difference drives drainage
double dz = neighbor.getElevation() - low.getElevation();
if (dz <= 0) return 0; // No drainage if neighbor is lower
// Stratified film drainage (Wallis falling film)
double holdup = neighbor.getLiquidHoldup();
double filmThickness = calcFilmThickness(holdup, neighbor.getDiameter());
double drainageVelocity = calcFilmDrainageVelocity(filmThickness,
neighbor.getInclination(), neighbor.getLiquidViscosity());
return holdup * neighbor.getArea() * drainageVelocity;
}
/**
* Calculate liquid carryover (entrainment by gas).
*/
private double calcCarryoverRate(MultiphasePipeSection section) {
// Critical gas velocity for liquid removal (Turner correlation)
double vgCrit = calcCriticalGasVelocity(section);
double vg = section.getGasVelocity();
if (vg < vgCrit) return 0;
// Carryover rate increases with excess gas velocity
double excessVelocity = vg - vgCrit;
double entrainmentFraction = calcEntrainmentFraction(excessVelocity, section);
return entrainmentFraction * section.getLiquidHoldup() *
section.getArea() * section.getLiquidVelocity();
}
}
public class MultiphaseFluxCalculator {
public enum FluxScheme {
AUSM_PLUS, // Advection Upstream Splitting Method
HLL, // Harten-Lax-van Leer
ROE, // Roe approximate Riemann solver
UPWIND // First-order upwind
}
/**
* Calculate numerical flux at cell interface using AUSM+ scheme.
* Good for multiphase flows with large density ratios.
*/
public double[] calcFluxAUSMPlus(double[] UL, double[] UR,
PhaseProperties propsL, PhaseProperties propsR) {
// Primitive variables
double rhoL = UL[0], rhoR = UR[0];
double vL = UL[1] / rhoL, vR = UR[1] / rhoR;
double pL = propsL.getPressure(), pR = propsR.getPressure();
double cL = propsL.getSoundSpeed(), cR = propsR.getSoundSpeed();
// Interface speed of sound
double cHalf = 0.5 * (cL + cR);
// Mach numbers
double ML = vL / cHalf;
double MR = vR / cHalf;
// Split Mach numbers (AUSM+ splitting functions)
double Mplus = calcMachPlus(ML);
double Mminus = calcMachMinus(MR);
double Mhalf = Mplus + Mminus;
// Split pressures
double Pplus = calcPressurePlus(ML) * pL;
double Pminus = calcPressureMinus(MR) * pR;
double Phalf = Pplus + Pminus;
// Convective flux
double[] flux = new double[3];
if (Mhalf >= 0) {
flux[0] = cHalf * Mhalf * rhoL;
flux[1] = cHalf * Mhalf * rhoL * vL + Phalf;
flux[2] = cHalf * Mhalf * rhoL * propsL.getEnthalpy();
} else {
flux[0] = cHalf * Mhalf * rhoR;
flux[1] = cHalf * Mhalf * rhoR * vR + Phalf;
flux[2] = cHalf * Mhalf * rhoR * propsR.getEnthalpy();
}
return flux;
}
/**
* MUSCL reconstruction for second-order accuracy.
*/
public double[] reconstructMUSCL(double[] U, int i, double[] dx) {
// Slope limiter (minmod, van Leer, etc.)
double slope = slopeLimiter(U[i-1], U[i], U[i+1], dx[i-1], dx[i]);
double[] UL = new double[U.length];
double[] UR = new double[U.length];
UL[i] = U[i] - 0.5 * slope * dx[i];
UR[i] = U[i] + 0.5 * slope * dx[i];
return new double[][] {UL, UR};
}
}
public class MultiphaseTimeIntegrator {
/**
* Explicit Runge-Kutta time stepping with CFL control.
*/
public void stepRK4(MultiphasePipeSection[] sections, double dt) {
int n = sections.length;
double[][] U = getConservativeVariables(sections);
// RK4 stages
double[][] k1 = calcRHS(sections, U);
double[][] U1 = addArrays(U, scaleArray(k1, 0.5 * dt));
double[][] k2 = calcRHS(sections, U1);
double[][] U2 = addArrays(U, scaleArray(k2, 0.5 * dt));
double[][] k3 = calcRHS(sections, U2);
double[][] U3 = addArrays(U, scaleArray(k3, dt));
double[][] k4 = calcRHS(sections, U3);
// Combine stages
for (int i = 0; i < n; i++) {
for (int j = 0; j < U[i].length; j++) {
U[i][j] += dt / 6.0 * (k1[i][j] + 2*k2[i][j] + 2*k3[i][j] + k4[i][j]);
}
}
setConservativeVariables(sections, U);
}
/**
* Calculate stable time step based on CFL condition.
*/
public double calcTimeStep(MultiphasePipeSection[] sections, double cflNumber) {
double dtMin = Double.MAX_VALUE;
for (MultiphasePipeSection sec : sections) {
// Wave speeds
double cGas = sec.getGasSoundSpeed();
double cLiq = sec.getLiquidSoundSpeed();
double vg = Math.abs(sec.getGasVelocity());
double vl = Math.abs(sec.getLiquidVelocity());
// Maximum characteristic speed
double maxSpeed = Math.max(vg + cGas, vl + cLiq);
// CFL condition
double dt = cflNumber * sec.getSegmentLength() / maxSpeed;
dtMin = Math.min(dtMin, dt);
}
return dtMin;
}
}
public class ThermodynamicCoupling {
/**
* Update phase properties using NeqSim flash calculations.
*/
public void updatePhaseProperties(MultiphasePipeSection section) {
// Clone and update thermodynamic system
SystemInterface system = section.getThermoSystem().clone();
system.setPressure(section.getPressure() / 1e5); // Convert to bar
system.setTemperature(section.getTemperature());
// Flash calculation
ThermodynamicOperations ops = new ThermodynamicOperations(system);
ops.TPflash();
system.initPhysicalProperties();
// Extract phase properties
if (system.hasPhaseType("gas")) {
PhaseInterface gas = system.getPhase("gas");
section.setGasDensity(gas.getDensity("kg/m3"));
section.setGasViscosity(gas.getViscosity("kg/msec"));
section.setGasSoundSpeed(gas.getSoundSpeed());
section.setGasEnthalpy(gas.getEnthalpy("J/mol"));
section.setGasMoleFraction(system.getMoleFraction(gas.getPhaseIndex()));
}
if (system.hasPhaseType("oil")) {
PhaseInterface oil = system.getPhase("oil");
section.setOilDensity(oil.getDensity("kg/m3"));
section.setOilViscosity(oil.getViscosity("kg/msec"));
// ... other properties
}
if (system.hasPhaseType("aqueous")) {
PhaseInterface water = system.getPhase("aqueous");
section.setWaterDensity(water.getDensity("kg/m3"));
// ... other properties
}
// Mass transfer rates (flashing/condensation)
section.setMassTransferRate(calcMassTransferRate(section, system));
}
/**
* Calculate mass transfer between phases (simplified).
*/
private double calcMassTransferRate(MultiphasePipeSection section,
SystemInterface system) {
// Departure from equilibrium
double pBubble = system.getBubblePointPressure();
double pActual = section.getPressure() / 1e5;
if (pActual < pBubble) {
// Flashing - liquid to gas
double dP = pBubble - pActual;
return section.getMassTransferCoefficient() * dP;
} else {
// Condensation - gas to liquid
double dP = pActual - pBubble;
return -section.getMassTransferCoefficient() * dP;
}
}
}
public class MultiphasePipe extends Pipeline {
private MultiphasePipeSection[] sections;
private SlugTracker slugTracker;
private LiquidAccumulation liquidAccumulation;
private MultiphaseFluxCalculator fluxCalculator;
private MultiphaseTimeIntegrator timeIntegrator;
private ThermodynamicCoupling thermoCoupling;
private double cflNumber = 0.5;
private int numberOfSections = 100;
private double totalLength;
private double[] elevationProfile;
private double[] diameterProfile;
public MultiphasePipe(String name, StreamInterface inStream) {
super(name, inStream);
slugTracker = new SlugTracker();
liquidAccumulation = new LiquidAccumulation();
fluxCalculator = new MultiphaseFluxCalculator();
timeIntegrator = new MultiphaseTimeIntegrator();
thermoCoupling = new ThermodynamicCoupling();
}
@Override
public void run(UUID id) {
// Initialize grid
initializeSections();
// Steady-state initialization
runSteadyState();
setCalculationIdentifier(id);
}
@Override
public void runTransient(double dt, UUID id) {
// Adaptive time stepping
double dtStable = timeIntegrator.calcTimeStep(sections, cflNumber);
double dtActual = Math.min(dt, dtStable);
int subSteps = (int) Math.ceil(dt / dtActual);
dtActual = dt / subSteps;
for (int step = 0; step < subSteps; step++) {
// 1. Update thermodynamic properties
for (MultiphasePipeSection sec : sections) {
thermoCoupling.updatePhaseProperties(sec);
}
// 2. Detect flow regimes
for (MultiphasePipeSection sec : sections) {
sec.setFlowRegime(flowRegimeMap.determine(sec));
}
// 3. Calculate fluxes and advance solution
timeIntegrator.stepRK4(sections, dtActual);
// 4. Track liquid accumulation
liquidAccumulation.updateAccumulation(sections, dtActual,
getInletLiquidRate());
// 5. Track and propagate slugs
slugTracker.checkSlugInitiation(sections, dtActual);
slugTracker.propagateSlugs(sections, dtActual);
// 6. Apply boundary conditions
applyBoundaryConditions();
}
// Update outlet stream
updateOutletStream();
setCalculationIdentifier(id);
}
/**
* Get liquid inventory in the pipeline.
*/
public double getLiquidInventory(String unit) {
double volume = 0;
for (MultiphasePipeSection sec : sections) {
volume += sec.getLiquidHoldup() * sec.getArea() * sec.getSegmentLength();
}
switch (unit.toLowerCase()) {
case "m3": return volume;
case "bbl": return volume * 6.28981;
case "l": return volume * 1000;
default: return volume;
}
}
/**
* Get slug statistics.
*/
public SlugStatistics getSlugStatistics() {
return slugTracker.getStatistics();
}
/**
* Get holdup profile.
*/
public double[] getHoldupProfile() {
double[] holdup = new double[sections.length];
for (int i = 0; i < sections.length; i++) {
holdup[i] = sections[i].getLiquidHoldup();
}
return holdup;
}
}
Scope:
Deliverables:
DriftFluxPipe classScope:
Deliverables:
TwoFluidPipe classSlugTracker with individual slug dynamicsScope:
Deliverables:
PipeNetwork classPigTracker class| Flow Regime | Correlation |
|---|---|
| Single-phase | Haaland/Colebrook |
| Stratified | Taitel-Dukler (separate phases) |
| Slug | Slug body + film friction |
| Annular | Core + film model |
| Model | Application |
|---|---|
| Taitel-Dukler | Stratified flow |
| Andritsos-Hanratty | Wavy stratified |
| Wallis | Annular film |
| Oliemans | Slug bubble zone |
| Model | Type |
|---|---|
| Taitel-Dukler | Mechanistic stratified |
| Gregory | Slug holdup |
| Beggs-Brill | Empirical (backup) |
| Bendiksen | Slug bubble holdup |
| Correlation | C_0 | v_drift |
|---|---|---|
| Zuber-Findlay (vertical) | 1.2 | 1.53(gσΔρ/ρ_L²)^0.25 |
| Bendiksen (horizontal) | 1.05 | 0.35√(gD) |
| Ferschneider | Variable | Inclination-dependent |
| Component | Cost | Optimization |
|---|---|---|
| Flash calculations | High | Tabulation, caching |
| Flow regime map | Medium | Skip if regime unchanged |
| Flux calculation | Low | Vectorization |
| Slug tracking | Low | Skip if no slugs |
Implementing a full multiphase transient model is a significant undertaking but highly valuable for:
The recommended approach is:
The existing NeqSim infrastructure (thermodynamics, physical properties, process equipment) provides an excellent foundation for this extension.
The TransientPipe class provides a 1D transient multiphase (gas-liquid) flow simulator for pipelines. It uses the drift-flux formulation combined with mechanistic flow regime detection to model complex phenomena like terrain-induced slugging, liquid accumulation at low points, and transient pressure wave propagation.
The model supports:
The core model uses the Zuber-Findlay drift-flux formulation:
v_G = C₀ · v_m + v_d
Where:
v_G = gas velocity (m/s)C₀ = distribution coefficient (typically 1.0-1.2)v_m = mixture velocity (m/s)v_d = drift velocity (m/s)The distribution coefficient C₀ and drift velocity v_d depend on the flow regime:
| Flow Regime | C₀ | Drift Velocity Correlation |
|---|---|---|
| Bubble | 1.2 | Harmathy (1960) |
| Slug | 1.05-1.2 (Fr dependent) | Bendiksen (1984) |
| Annular | 1.0 | Film drainage model |
| Stratified | Calculated from momentum balance | ~0 |
When both oil and aqueous (water) liquid phases are present, the model calculates volume-weighted average liquid properties:
ρ_L,avg = (V_oil / V_total) × ρ_oil + (V_water / V_total) × ρ_water
μ_L,avg = (V_oil / V_total) × μ_oil + (V_water / V_total) × μ_water
H_L,avg = (V_oil / V_total) × H_oil + (V_water / V_total) × H_water
c_L,avg = (V_oil / V_total) × c_oil + (V_water / V_total) × c_water
Where:
V_oil, V_water = volume of oil and water phases from thermodynamic flashV_total = V_oil + V_waterρ, μ, H, c = density, viscosity, enthalpy, and sound speedThis approach maintains the drift-flux framework while properly accounting for oil-water mixtures in the liquid phase. If only one liquid phase is present (oil OR water), that phase's properties are used directly.
The model supports two methods for flow regime detection:
Uses mechanistic criteria to determine the local flow pattern:
An alternative approach that selects the flow regime with the minimum slip ratio (closest to 1.0, i.e., homogeneous flow). This is based on the principle that the physical system naturally tends toward the flow pattern with minimum phase velocity difference.
FlowRegimeDetector detector = new FlowRegimeDetector();
// Enable minimum slip criterion
detector.setUseMinimumSlipCriterion(true);
// Or use the detection method enum
detector.setDetectionMethod(FlowRegimeDetector.DetectionMethod.MINIMUM_SLIP);
// Detect flow regime
FlowRegime regime = detector.detectFlowRegime(section);
The minimum slip criterion evaluates orientation-appropriate candidate regimes:
The model solves the conservation equations using:
Conservative variables:
U = [ρ_G·α_G, ρ_L·α_L, ρ_m·u, ρ_m·e]
import neqsim.process.equipment.pipeline.twophasepipe.TransientPipe;
import neqsim.process.equipment.stream.Stream;
import neqsim.thermo.system.SystemSrkEos;
// Create two-phase fluid
SystemInterface fluid = new SystemSrkEos(300, 50); // 300 K, 50 bar
fluid.addComponent("methane", 0.8);
fluid.addComponent("n-pentane", 0.2);
fluid.setMixingRule("classic");
fluid.setMultiPhaseCheck(true);
// Create inlet stream
Stream inlet = new Stream("inlet", fluid);
inlet.setFlowRate(5, "kg/sec");
inlet.run();
// Create transient pipe
TransientPipe pipe = new TransientPipe("Pipeline", inlet);
pipe.setLength(1000); // 1000 m
pipe.setDiameter(0.2); // 200 mm
pipe.setRoughness(0.00005); // 50 μm
pipe.setNumberOfSections(50); // 50 cells
pipe.setMaxSimulationTime(60); // 60 seconds
// Run simulation
pipe.run();
// Get results
double[] pressures = pipe.getPressureProfile();
double[] holdups = pipe.getLiquidHoldupProfile();
double[] gasVel = pipe.getGasVelocityProfile();
// Create pipe with terrain
TransientPipe pipe = new TransientPipe("TerrainPipe", inlet);
pipe.setLength(2000);
pipe.setDiameter(0.3);
pipe.setNumberOfSections(40);
pipe.setMaxSimulationTime(300);
// Define elevation profile with a low point
double[] elevations = new double[40];
for (int i = 0; i < 40; i++) {
double x = i * 50.0; // Position along pipe
if (x < 500) {
elevations[i] = 0;
} else if (x < 1000) {
elevations[i] = -20 * (x - 500) / 500; // Downhill to -20m
} else if (x < 1500) {
elevations[i] = -20 + 20 * (x - 1000) / 500; // Uphill
} else {
elevations[i] = 0;
}
}
pipe.setElevationProfile(elevations);
pipe.run();
// Check for liquid accumulation
var accumTracker = pipe.getAccumulationTracker();
for (var zone : accumTracker.getAccumulationZones()) {
System.out.println("Accumulation at position: " + zone.getPosition());
System.out.println("Accumulated volume: " + zone.getAccumulatedVolume() + " m³");
}
TransientPipe riser = new TransientPipe("Riser", inlet);
riser.setLength(200);
riser.setDiameter(0.15);
riser.setNumberOfSections(40);
// Vertical profile
double[] elevations = new double[40];
for (int i = 0; i < 40; i++) {
elevations[i] = i * 5; // 5m per section
}
riser.setElevationProfile(elevations);
riser.run();
// Significant pressure drop due to hydrostatic head
double[] P = riser.getPressureProfile();
// Create three-phase fluid
SystemInterface fluid = new SystemSrkEos(300, 50);
fluid.addComponent("methane", 0.40);
fluid.addComponent("propane", 0.10);
fluid.addComponent("n-heptane", 0.20);
fluid.addComponent("n-octane", 0.10);
fluid.addComponent("water", 0.20);
fluid.setMixingRule("classic");
fluid.setMultiPhaseCheck(true);
// Create stream and pipe
Stream inlet = new Stream("inlet", fluid);
inlet.setFlowRate(15, "kg/sec");
inlet.run();
TransientPipe pipe = new TransientPipe("ThreePhasePipe", inlet);
pipe.setLength(1000);
pipe.setDiameter(0.25);
pipe.setNumberOfSections(50);
pipe.run();
// The model automatically uses volume-weighted averaging for liquid properties
// when both oil and aqueous phases are present
double deltaP = P[0] - P[39];
System.out.println("Riser pressure drop: " + deltaP/1e5 + " bar");
| Method | Description | Default |
|---|---|---|
setLength(double) |
Total pipe length (m) | - |
setDiameter(double) |
Inner diameter (m) | - |
setRoughness(double) |
Wall roughness (m) | 0.0001 |
setNumberOfSections(int) |
Discretization cells | 50 |
setElevationProfile(double[]) |
Elevation at each node (m) | null (horizontal) |
setInclinationProfile(double[]) |
Inclination angles (rad) | null |
| Method | Description | Default |
|---|---|---|
setMaxSimulationTime(double) |
Total simulation time (s) | 3600 |
setCflNumber(double) |
CFL number (0.1-1.0) | 0.5 |
setThermodynamicUpdateInterval(int) |
Flash update frequency | 10 |
setUpdateThermodynamics(boolean) |
Enable/disable thermo updates | true |
// Available boundary condition types
pipe.setInletBoundaryCondition(BoundaryCondition.CONSTANT_FLOW);
pipe.setOutletBoundaryCondition(BoundaryCondition.CONSTANT_PRESSURE);
// Set boundary values
pipe.setInletMassFlow(5.0); // kg/s
pipe.setOutletPressure(30.0); // bara
| Type | Description |
|---|---|
CONSTANT_PRESSURE |
Fixed pressure boundary |
CONSTANT_FLOW |
Fixed mass flow rate |
CONSTANT_VELOCITY |
Fixed velocity |
CLOSED |
No-flow wall |
// Spatial profiles at end of simulation
double[] pressure = pipe.getPressureProfile(); // Pa
double[] temperature = pipe.getTemperatureProfile(); // K
double[] liquidHoldup = pipe.getLiquidHoldupProfile(); // fraction
double[] gasVelocity = pipe.getGasVelocityProfile(); // m/s
double[] liquidVelocity = pipe.getLiquidVelocityProfile(); // m/s
// Pressure history at all locations
double[][] pressureHistory = pipe.getPressureHistory();
// pressureHistory[time_index][position_index]
SlugTracker slugTracker = pipe.getSlugTracker();
int activeSlugCount = slugTracker.getSlugCount();
int totalGenerated = slugTracker.getTotalSlugsGenerated();
double avgLength = slugTracker.getAverageSlugLength();
double frequency = slugTracker.getSlugFrequency();
// Detailed statistics
String stats = slugTracker.getStatisticsString();
System.out.println(stats);
Note: Both TransientPipe (drift-flux) and TwoFluidPipe (two-fluid) use the same SlugTracker and LiquidAccumulationTracker components, but may predict different slug frequencies due to their underlying holdup models. See the Two-Fluid Model documentation for a detailed comparison.
LiquidAccumulationTracker tracker = pipe.getAccumulationTracker();
for (var zone : tracker.getAccumulationZones()) {
System.out.println("Zone position: " + zone.getPosition() + " m");
System.out.println("Accumulated volume: " + zone.getAccumulatedVolume() + " m³");
System.out.println("Current holdup: " + zone.getCurrentHoldup());
System.out.println("Is overflowing: " + zone.isOverflowing());
}
PipeSection[] sections = pipe.getSections();
FlowRegimeDetector detector = new FlowRegimeDetector();
for (PipeSection section : sections) {
FlowRegime regime = detector.detectFlowRegime(section);
System.out.println("Position " + section.getPosition() +
": " + regime);
}
DriftFluxModel model = new DriftFluxModel();
for (PipeSection section : sections) {
DriftFluxParameters params = model.calculateDriftFlux(section);
System.out.println("C0 = " + params.C0);
System.out.println("Drift velocity = " + params.driftVelocity + " m/s");
System.out.println("Void fraction = " + params.voidFraction);
System.out.println("Slip ratio = " + params.slipRatio);
}
PipeSection[] sections = pipe.getSections();
for (int i = 0; i < sections.length; i++) {
PipeSection s = sections[i];
System.out.printf("Section %d (x=%.1f m):%n", i, s.getPosition());
System.out.printf(" Pressure: %.2f bar%n", s.getPressure()/1e5);
System.out.printf(" Temperature: %.1f K%n", s.getTemperature());
System.out.printf(" Liquid holdup: %.3f%n", s.getLiquidHoldup());
System.out.printf(" Gas velocity: %.2f m/s%n", s.getGasVelocity());
System.out.printf(" Liquid velocity: %.2f m/s%n", s.getLiquidVelocity());
System.out.printf(" Flow regime: %s%n", s.getFlowRegime());
System.out.printf(" Is low point: %b%n", s.isLowPoint());
}
import neqsim.process.processmodel.ProcessSystem;
ProcessSystem process = new ProcessSystem();
// Add inlet stream
Stream inlet = new Stream("inlet", fluid);
inlet.setFlowRate(10, "kg/sec");
process.add(inlet);
// Add transient pipe
TransientPipe pipeline = new TransientPipe("MainPipeline", inlet);
pipeline.setLength(5000);
pipeline.setDiameter(0.4);
pipeline.setNumberOfSections(100);
pipeline.setMaxSimulationTime(600);
process.add(pipeline);
// Add downstream equipment
Stream outlet = pipeline.getOutletStream();
// ... add separators, compressors, etc.
process.run();
setThermodynamicUpdateInterval(20) reduces frequencysetUpdateThermodynamics(false)| Regime | Description | Typical Conditions |
|---|---|---|
SINGLE_PHASE_GAS |
Gas only | α_L < 0.001 |
SINGLE_PHASE_LIQUID |
Liquid only | α_G < 0.001 |
BUBBLE |
Discrete bubbles in liquid | Low gas velocity, vertical |
SLUG |
Alternating liquid slugs and gas bubbles | Moderate velocities |
STRATIFIED_SMOOTH |
Separated phases, smooth interface | Low velocities, horizontal |
STRATIFIED_WAVY |
Separated phases, wavy interface | Moderate velocities, horizontal |
ANNULAR |
Liquid film on wall, gas core | High gas velocity |
CHURN |
Chaotic, oscillating flow | High velocities, vertical |
Symptoms: NaN values, oscillations, crashes
Solutions:
pipe.setCflNumber(0.3)Solutions:
Possible causes:
Taitel, Y. and Dukler, A.E. (1976). "A Model for Predicting Flow Regime Transitions in Horizontal and Near Horizontal Gas-Liquid Flow." AIChE Journal, 22(1), 47-55.
Barnea, D. (1987). "A Unified Model for Predicting Flow-Pattern Transitions for the Whole Range of Pipe Inclinations." Int. J. Multiphase Flow, 13(1), 1-12.
Bendiksen, K.H. (1984). "An Experimental Investigation of the Motion of Long Bubbles in Inclined Tubes." Int. J. Multiphase Flow, 10(4), 467-483.
Zuber, N. and Findlay, J.A. (1965). "Average Volumetric Concentration in Two-Phase Flow Systems." J. Heat Transfer, 87(4), 453-468.
Harmathy, T.Z. (1960). "Velocity of Large Drops and Bubbles in Media of Infinite or Restricted Extent." AIChE Journal, 6(2), 281-288.
The TransientPipe model uses a different approach than empirical correlations like Beggs and Brill (1973). Understanding these differences helps in selecting the appropriate model for your application.
| Aspect | TransientPipe | Beggs and Brill |
|---|---|---|
| Approach | Mechanistic drift-flux with AUSM+ scheme | Empirical correlation from experiments |
| Basis | Conservation equations + closure relations | ~1500 experimental data points |
| Flow Regimes | Taitel-Dukler, Barnea criteria | Froude number based map |
| Transient | Full transient capability | Steady-state only |
| Terrain | Section-by-section integration | Overall correlation |
Comparison tests show significant differences between the models, which is expected given their fundamentally different approaches:
| Flow Condition | Typical Difference | Explanation |
|---|---|---|
| Single-phase gas, horizontal | 50-80% | Different friction correlations |
| Multiphase horizontal | 100-300% | Different holdup/slip models |
| Uphill flow (+10°) | 40-60% | Hydrostatic term treatment |
| Downhill flow (-10°) | 40-60% | Liquid drainage models differ |
| High velocity gas | 50-100% | Compressibility effects |
Use TransientPipe when:
Use Beggs and Brill when:
For critical applications, it is recommended to:
NeqSim includes comparison tests in TransientPipeVsBeggsAndBrillsComparisonTest.java:
// Example: Comparing models for horizontal multiphase flow
SystemInterface fluid = new SystemSrkEos(300.0, 50.0);
fluid.addComponent("methane", 0.8);
fluid.addComponent("n-pentane", 0.2);
fluid.setMixingRule("classic");
fluid.setMultiPhaseCheck(true);
// Setup Beggs and Brill
Stream bbStream = new Stream("BB_inlet", fluid);
bbStream.setFlowRate(2.0, "kg/sec");
bbStream.run();
PipeBeggsAndBrills bb = new PipeBeggsAndBrills("BeggsAndBrill", bbStream);
bb.setDiameter(0.2);
bb.setLength(500);
bb.setAngle(0);
bb.run();
double dpBeggsBrill = bb.getPressureDrop();
// Setup TransientPipe
Stream tpStream = new Stream("TP_inlet", fluid.clone());
tpStream.setFlowRate(2.0, "kg/sec");
tpStream.run();
TransientPipe tp = new TransientPipe("TransientPipe", tpStream);
tp.setLength(500);
tp.setDiameter(0.2);
tp.setNumberOfSections(25);
tp.setMaxSimulationTime(60);
tp.run();
double[] pressures = tp.getPressureProfile();
double dpTransient = (pressures[0] - pressures[pressures.length - 1]) / 1e5;
System.out.println("Beggs & Brill: " + dpBeggsBrill + " bar");
System.out.println("TransientPipe: " + dpTransient + " bar");
Beggs, H.D. and Brill, J.P. (1973). "A Study of Two-Phase Flow in Inclined Pipes." Journal of Petroleum Technology, 25(5), 607-617.
Ishii, M. and Hibiki, T. (2011). Thermo-Fluid Dynamics of Two-Phase Flow. 2nd ed. Springer.
This document outlines the development plan for enhancing the TwoPhasePipeFlowSystem to become a general-purpose two-phase mass and heat transfer pipeline simulation tool. The implementation is based on the non-equilibrium thermodynamics approach described in Solbraa (2002) and uses the Krishna-Standart film model for interphase mass transfer.
The TwoPhasePipeFlowSystem currently supports:
Priority: High
Allow users to select between different mass transfer models:
| Model | Description | Best For |
|---|---|---|
| Krishna-Standart Film | Multi-component diffusion with thermodynamic correction | General purpose, current default |
| Penetration Theory | Time-dependent diffusion into semi-infinite medium | Short contact times |
| Surface Renewal Theory | Statistical distribution of surface ages | Turbulent interfaces |
API Design:
public enum MassTransferModel {
KRISHNA_STANDART_FILM,
PENETRATION_THEORY,
SURFACE_RENEWAL
}
pipe.setMassTransferModel(MassTransferModel.KRISHNA_STANDART_FILM);
Priority: High
Add flow pattern-specific Sherwood number correlations:
| Flow Pattern | Correlation | Reference |
|---|---|---|
| Stratified | Sh = f(Re, Sc, geometry) | Solbraa (2002) |
| Annular | Sh = f(Re_film, Sc, wave amplitude) | Hewitt & Hall-Taylor |
| Slug | Sh_bubble + Sh_slug weighted | Fernandes et al. |
| Bubble | Sh = 2 + 0.6 Re^0.5 Sc^0.33 | Ranz-Marshall |
| Droplet | Sh = 2 + 0.6 Re^0.5 Sc^0.33 | Ranz-Marshall |
Priority: Medium
Track mass transfer for each component along the pipe:
double[] methaneProfile = pipe.getMassTransferProfile("methane");
double waterMassBalance = pipe.getComponentMassBalance("water");
Priority: High
The interfacial area per unit volume (a) is critical for mass transfer calculations: $$\dot{n}_i = k_L \cdot a \cdot \Delta C_i$$
| Flow Pattern | Interfacial Area Model | Key Parameters |
|---|---|---|
| Stratified | Flat interface: $a = \frac{S_i}{A}$ where $S_i$ = interface chord length | Liquid holdup, pipe diameter |
| Annular | Film interface: $a = \frac{4}{D} \cdot \frac{1}{1-\sqrt{1-\alpha_L}}$ | Film thickness, entrainment |
| Slug | $a = a_{Taylor} \cdot f_{bubble} + a_{slug} \cdot (1-f_{bubble})$ | Slug frequency, bubble length |
| Bubble | $a = \frac{6\alpha_G}{d_{32}}$ (Sauter mean diameter) | Bubble size distribution |
| Droplet | $a = \frac{6\alpha_L}{d_{32}}$ from Weber number | Droplet size, We number |
API Design:
public enum InterfacialAreaModel {
GEOMETRIC, // Based on flow geometry
EMPIRICAL_CORRELATION, // Literature correlations
USER_DEFINED // Custom model
}
pipe.setInterfacialAreaModel(InterfacialAreaModel.GEOMETRIC);
Implementation Details:
For bubble flow, use Hinze theory for maximum stable bubble size: $$d_{max} = 0.725 \left(\frac{\sigma}{\rho_L}\right)^{0.6} \epsilon^{-0.4}$$
For droplet flow, use critical Weber number: $$d_{max} = \frac{We_{crit} \cdot \sigma}{\rho_G \cdot u_G^2}$$
Priority: High
Support different thermal boundary conditions:
| Boundary Condition | Description | Use Case |
|---|---|---|
| Constant Wall Temperature | $T_{wall} = T_{const}$ | Isothermal pipes |
| Constant Heat Flux | $q'' = q''_{const}$ | Electric heating |
| Convective Boundary | $q'' = U_{overall}(T_{ambient} - T_{fluid})$ | Subsea pipelines |
API Design:
public enum WallHeatTransferModel {
CONSTANT_WALL_TEMPERATURE,
CONSTANT_HEAT_FLUX,
CONVECTIVE_BOUNDARY,
ADIABATIC
}
pipe.setWallHeatTransferModel(WallHeatTransferModel.CONVECTIVE_BOUNDARY);
pipe.setOverallHeatTransferCoefficient(10.0); // W/(m²·K)
Priority: Medium
Implement flow pattern-specific Nusselt number correlations:
| Flow Pattern | Correlation | Notes |
|---|---|---|
| Stratified | Separate gas/liquid Nu | Geometry-dependent |
| Annular | Nu = f(Re_film, Pr, roughness) | Film heat transfer |
| Slug | Weighted Nu_bubble + Nu_slug | Time-averaged |
| Bubble | Shah correlation | Enhanced mixing |
| Droplet | Dittus-Boelter for gas + droplet contribution | Mist flow |
Priority: Medium
Use rigorous enthalpy-based energy balance:
$$\frac{\partial(\rho H)}{\partial t} + \nabla \cdot (\rho \mathbf{u} H) = -\nabla \cdot \mathbf{q} + \dot{Q}_{wall} + \dot{Q}_{interphase}$$
Include:
Priority: Medium
Implement flow pattern transition criteria:
| Method | Description | Applicability |
|---|---|---|
| Baker Chart | Empirical map based on G_L, G_G | Horizontal pipes |
| Taitel-Dukler | Mechanistic model | Horizontal/inclined |
| Barnea | Extended Taitel-Dukler | All inclinations |
Transition Criteria:
API Design:
pipe.enableAutomaticFlowPatternDetection(true);
pipe.setFlowPatternModel(FlowPatternModel.TAITEL_DUKLER);
Priority: High
Add convenient methods for extracting simulation results:
// Get profiles along pipe
double[] temperatures = pipe.getTemperatureProfile();
double[] pressures = pipe.getPressureProfile(); // [bar]
double[] positions = pipe.getPositionProfile();
// Get composition profiles
double[][] gasComposition = pipe.getGasCompositionProfile();
double[][] liquidComposition = pipe.getLiquidCompositionProfile();
// Get flow properties
double[] gasVelocities = pipe.getVelocityProfile(0); // Gas phase
double[] liquidVelocities = pipe.getVelocityProfile(1); // Liquid phase
double[] voidFraction = pipe.getVoidFractionProfile();
// Export to CSV/JSON
pipe.exportResults("simulation_results.csv");
Priority: Low
Support chemical enhancement for reactive mass transfer:
$$N_A = E \cdot k_L \cdot (C_{A,i} - C_{A,bulk})$$
| System | Enhancement Factor Model |
|---|---|
| CO₂ + Amine | Danckwerts surface renewal |
| H₂S removal | Instantaneous reaction |
| SO₂ absorption | Film theory with reaction |
Priority: Low ✅ Already Implemented
NeqSim already has a comprehensive pipe wall and insulation infrastructure in the geometrydefinitions.internalgeometry.wall and geometrydefinitions.surrounding packages.
MaterialLayer.java - Single layer with thermal properties:
MaterialLayer layer = new MaterialLayer(PipeMaterial.POLYURETHANE_FOAM, 0.05); // 50mm insulation
double k = layer.getThermalConductivity(); // W/(m·K)
double R = layer.getThermalResistance(innerRadius, outerRadius); // For cylindrical geometry
PipeMaterial.java - Enum with 30+ predefined materials:
PipeWall.java - Multi-layer cylindrical heat transfer:
PipeWall wall = new PipeWall(innerDiameter);
wall.addLayer(new MaterialLayer(PipeMaterial.CARBON_STEEL, 0.01)); // 10mm steel
wall.addLayer(new MaterialLayer(PipeMaterial.POLYURETHANE_FOAM, 0.05)); // 50mm insulation
wall.addLayer(new MaterialLayer(PipeMaterial.POLYPROPYLENE, 0.003)); // 3mm coating
double U_inner = wall.getOverallHeatTransferCoefficient(); // W/(m²·K) based on inner surface
PipeSurroundingEnvironment.java - Factory methods for external conditions:
// Subsea pipeline
PipeSurroundingEnvironment env = PipeSurroundingEnvironment.subseaPipe(seawaterTemp, depth);
double h_ext = env.getExternalHeatTransferCoefficient(); // ~500 W/(m²·K) for seawater
// Buried onshore
PipeSurroundingEnvironment env = PipeSurroundingEnvironment.buriedPipe(soilTemp, burialDepth, soilType);
// Exposed to air
PipeSurroundingEnvironment env = PipeSurroundingEnvironment.exposedToAir(airTemp, windSpeed);
The FlowSystem base class already supports:
// Set wall heat transfer coefficients per leg
pipe.setLegWallHeatTransferCoefficients(new double[] {50.0, 50.0}); // U values
pipe.setLegOuterHeatTransferCoefficients(new double[] {500.0, 500.0}); // h_ext values
For advanced wall modeling, use PipeWall to calculate overall U and pass to the flow system.
Priority: Medium
Fluent builder pattern for easy setup:
TwoPhasePipeFlowSystem pipe = TwoPhasePipeFlowSystem.builder()
.withFluid(thermoSystem)
.withDiameter(0.1, "m")
.withLength(1000, "m")
.withNodes(100)
.withFlowPattern("stratified")
.withWallTemperature(278, "K")
.enableNonEquilibriumMassTransfer()
.enableNonEquilibriumHeatTransfer()
.build();
pipe.solve();
Priority: High
| Test Category | Validation Data |
|---|---|
| Mass transfer coefficients | Solbraa (2002) experimental data |
| Interfacial areas | Azzopardi correlations |
| Heat transfer | Gnielinski, Shah correlations |
| Pressure drop | Lockhart-Martinelli, Friedel |
| Flow pattern transitions | Taitel-Dukler maps |
Priority: Medium
Create Jupyter notebook examples:
Some advanced test scenarios are currently disabled pending solver optimization:
| Test | Status | Issue |
|---|---|---|
testCompleteLiquidEvaporationIn1kmPipe |
Disabled | Solver timeout - needs performance optimization |
testTransientWaterDryingInGasPipeline |
Disabled | Solver timeout - needs performance optimization |
testSubseaGasOilPipelineWithElevationProfile |
Disabled | Temperature calculation needs improvement |
testGasWithCondensationAlongPipeline |
Disabled | Phase transition solver needs optimization |
These tests represent advanced use cases that require further solver development to handle:
The steady-state solver (type 2) calculates mass transfer fluxes correctly at each node, but accumulated composition changes downstream may require more iterations or transient simulation.
| Component | File Path |
|---|---|
| Main class | src/main/java/neqsim/fluidmechanics/flowsystem/twophaseflowsystem/twophasepipeflowsystem/TwoPhasePipeFlowSystem.java |
| Builder | src/main/java/neqsim/fluidmechanics/flowsystem/twophaseflowsystem/twophasepipeflowsystem/TwoPhasePipeFlowSystemBuilder.java |
| Solver | src/main/java/neqsim/fluidmechanics/flowsolver/twophaseflowsolver/twophasepipeflowsolver/TwoPhaseFixedStaggeredGridSolver.java |
| Flow nodes | src/main/java/neqsim/fluidmechanics/flownode/twophasenode/twophasepipeflownode/ |
| Flow pattern enum | src/main/java/neqsim/fluidmechanics/flownode/FlowPattern.java |
| Flow pattern model | src/main/java/neqsim/fluidmechanics/flownode/FlowPatternModel.java |
| Flow pattern detector | src/main/java/neqsim/fluidmechanics/flownode/FlowPatternDetector.java |
| Wall heat transfer model | src/main/java/neqsim/fluidmechanics/flownode/WallHeatTransferModel.java |
| Interfacial area model | src/main/java/neqsim/fluidmechanics/flownode/InterfacialAreaModel.java |
| Interfacial area calculator | src/main/java/neqsim/fluidmechanics/flownode/InterfacialAreaCalculator.java |
| Mass transfer calculator | src/main/java/neqsim/fluidmechanics/flownode/MassTransferCoefficientCalculator.java |
| Fluid boundary | src/main/java/neqsim/fluidmechanics/flownode/fluidboundary/ |
| Tests | src/test/java/neqsim/fluidmechanics/flowsystem/twophaseflowsystem/twophasepipeflowsystem/ |
| Tests (flow node) | src/test/java/neqsim/fluidmechanics/flownode/ |
NeqSim supports dynamic (transient) simulation of pipelines using the PipeBeggsAndBrills class. This allows modeling of:
In steady-state mode, calling run() calculates the equilibrium pressure profile along the pipe assuming constant inlet conditions:
PipeBeggsAndBrills pipe = new PipeBeggsAndBrills("pipe", feed);
pipe.setLength(1000);
pipe.setDiameter(0.2);
pipe.run(); // Steady-state calculation
// Two calculation modes:
// 1. Forward (default): Given flow rate → calculate outlet pressure
// 2. Reverse: Given outlet pressure → calculate flow rate
pipe.setOutletPressure(45.0); // Switch to reverse mode
pipe.run();
double requiredFlow = pipe.getInletStream().getFlowRate("kg/hr");
In transient mode, the pipe remembers its internal state between time steps, allowing simulation of dynamic behavior:
pipe.setCalculateSteadyState(false); // Enable transient mode
for (int step = 0; step < 100; step++) {
pipe.runTransient(1.0, id); // 1 second time step
}
The transient model solves simplified forms of the mass and momentum conservation equations using a finite-difference approach:
Mass Conservation: $$\frac{\partial \rho}{\partial t} + \frac{\partial (\rho v)}{\partial x} = 0$$
Momentum (simplified): $$\frac{\partial P}{\partial x} = -\frac{dP}{dx}_{friction} - \rho g \sin\theta$$
The implementation uses a relaxation-based advection scheme:
$$\alpha = \min\left(1, \frac{\Delta t}{\tau}\right)$$
Where $\tau = \Delta x / v$ is the segment transit time.
For a property $\phi$ (pressure, temperature, flow):
$$\phi_{i+1}^{n+1} = \phi_{i+1}^{n} + \alpha \cdot (\phi_i^{n+1} - \phi_{i+1}^{n}) - \Delta P_{losses}$$
This gives physically realistic propagation delays.
// Create and run steady-state first
SystemInterface gas = new SystemSrkEos(298.15, 50.0);
gas.addComponent("methane", 50000, "kg/hr");
gas.setMixingRule(2);
Stream feed = new Stream("feed", gas);
feed.run();
PipeBeggsAndBrills pipeline = new PipeBeggsAndBrills("pipe", feed);
pipeline.setLength(1000);
pipeline.setDiameter(0.2);
pipeline.setPipeWallRoughness(4.6e-5);
pipeline.setNumberOfIncrements(20);
pipeline.run(); // Initialize with steady-state
// Switch to transient mode
pipeline.setCalculateSteadyState(false);
// Run transient simulation
UUID id = UUID.randomUUID();
double dt = 1.0; // 1 second time step
for (int step = 0; step < 100; step++) {
pipeline.runTransient(dt, id);
// Monitor outlet
double outletPressure = pipeline.getOutletPressure();
double outletFlow = pipeline.getOutletStream().getFlowRate("kg/hr");
}
// After running for some time, apply inlet pressure change
SystemInterface newGas = new SystemSrkEos(298.15, 55.0); // +5 bar
newGas.addComponent("methane", 50000, "kg/hr");
newGas.setMixingRule(2);
feed.setThermoSystem(newGas);
feed.run();
// Continue transient - disturbance will propagate
for (int step = 0; step < 200; step++) {
pipeline.runTransient(dt, id);
// ... monitor response
}
ProcessSystem process = new ProcessSystem();
process.add(feed);
process.add(valve);
process.add(pipeline);
process.add(separator);
// Initial steady-state
process.run();
// Configure for transient
valve.setCalculateSteadyState(false);
pipeline.setCalculateSteadyState(false);
process.setTimeStep(1.0);
// Run transient steps
for (int i = 0; i < 500; i++) {
if (i == 50) {
valve.setPercentValveOpening(80); // Change valve at t=50s
}
process.runTransient();
}
A common transient scenario is simulating the effect of closing a downstream valve or choke and observing how the pressure/flow disturbance propagates back through the pipeline.
// Setup: Source → Pipeline → Choke Valve → Separator
SystemInterface gas = new SystemSrkEos(298.15, 100.0);
gas.addComponent("methane", 1.0);
gas.setMixingRule("classic");
gas.setTotalFlowRate(50000, "kg/hr");
Stream source = new Stream("source", gas);
source.run();
// Pipeline
PipeBeggsAndBrills pipeline = new PipeBeggsAndBrills("pipeline", source);
pipeline.setLength(5000); // 5 km
pipeline.setDiameter(0.2); // 200 mm
pipeline.setNumberOfIncrements(50);
pipeline.run();
// Downstream choke valve
ThrottlingValve choke = new ThrottlingValve("choke", pipeline.getOutletStream());
choke.setOutletPressure(50.0); // 50 bara downstream
choke.run();
// Build process
ProcessSystem process = new ProcessSystem();
process.add(source);
process.add(pipeline);
process.add(choke);
process.run();
System.out.println("Initial steady-state:");
System.out.println(" Choke inlet P: " + pipeline.getOutletPressure() + " bara");
System.out.println(" Choke opening: " + choke.getPercentValveOpening() + "%");
System.out.println(" Flow rate: " + source.getFlowRate("kg/hr") + " kg/hr");
// Switch to transient
pipeline.setCalculateSteadyState(false);
choke.setCalculateSteadyState(false);
process.setTimeStep(1.0);
UUID id = UUID.randomUUID();
// Run transient with valve closure event
for (int step = 0; step < 300; step++) {
double t = step * 1.0; // seconds
// Gradual valve closure from t=50s to t=100s
if (t >= 50 && t <= 100) {
double closureFraction = (t - 50) / 50.0; // 0 to 1
double opening = 100.0 - closureFraction * 50.0; // 100% → 50%
choke.setPercentValveOpening(opening);
}
process.runTransient();
// Monitor pressure wave propagation
if (step % 10 == 0) {
System.out.printf("t=%3.0fs: P_pipe_out=%.2f bara, Choke=%.0f%%, Flow=%.0f kg/hr%n",
t, pipeline.getOutletPressure(),
choke.getPercentValveOpening(),
choke.getOutletStream().getFlowRate("kg/hr"));
}
}
When a downstream valve closes:
| Time | Pipeline Outlet | Flow Rate | Notes |
|---|---|---|---|
| 0s | 70 bara | 50,000 kg/hr | Initial steady-state |
| 50s | 70 bara | 50,000 kg/hr | Valve starts closing |
| 75s | 75 bara | 40,000 kg/hr | Pressure building |
| 100s | 82 bara | 30,000 kg/hr | Valve at 50% open |
| 200s | 85 bara | 28,000 kg/hr | New equilibrium |
Simulate rapid valve closure for ESD scenario:
// Fast valve closure (slam shut in 5 seconds)
for (int step = 0; step < 500; step++) {
double t = step * 0.1; // 100 ms time step for fast transient
// ESD triggered at t=10s
if (t >= 10 && t <= 15) {
double closureFraction = (t - 10) / 5.0;
choke.setPercentValveOpening(100.0 * (1 - closureFraction));
} else if (t > 15) {
choke.setPercentValveOpening(0.0); // Fully closed
}
process.runTransient();
// Log high-frequency data for pressure surge analysis
System.out.printf("%.1f, %.3f, %.1f%n",
t, pipeline.getOutletPressure(),
choke.getOutletStream().getFlowRate("kg/hr"));
}
For rapid valve closure (water hammer / pressure surge):
| Closure Time | Pressure Rise | Model Accuracy |
|---|---|---|
| > 2×L/c | Gradual | Good |
| ~ L/c | Moderate surge | Approximate |
| << L/c | Severe surge | Not supported |
Where L = pipe length, c = speed of sound (~400 m/s for gas, ~1200 m/s for liquid).
Note: The current transient model uses advection-based propagation (fluid velocity), not acoustic waves. For severe water hammer analysis, specialized transient software may be needed.
Water hammer (hydraulic shock) occurs when a valve closes rapidly, causing pressure waves to travel at the speed of sound through the fluid. The pressure surge can be calculated using the Joukowsky equation:
$$\Delta P = \rho \cdot c \cdot \Delta v$$
Where:
For water at 10 m/s suddenly stopped:
| Aspect | NeqSim Transient Model | True Water Hammer |
|---|---|---|
| Wave speed | Fluid velocity (1-20 m/s) | Speed of sound (400-1400 m/s) |
| Pressure peak | Gradual buildup | Sharp spike |
| Wave reflection | Not modeled | Multiple reflections |
| Timing | Minutes to equilibrate | Milliseconds for surge |
The advection-based model is suitable for:
✅ Slow valve operations (closure time > 2L/c)
✅ Production rate changes - Gradual flow adjustments
✅ Process upsets - Separator level changes, compressor trips
✅ Quasi-steady analysis - New equilibrium after disturbance
❌ Emergency shutdowns - Fast valve closure (<1 second)
❌ Pump trips - Sudden flow stoppage
❌ Check valve slam - Reverse flow closure
❌ Pipe stress analysis - Peak pressure for mechanical design
You can estimate the water hammer pressure surge separately:
// Calculate theoretical water hammer pressure rise
public static double joukowskyPressureSurge(double density, double soundSpeed,
double velocityChange) {
return density * soundSpeed * velocityChange; // Pa
}
// Example usage
double rho = 800; // kg/m³ (oil)
double c = 1100; // m/s (speed of sound in oil)
double dv = 5; // m/s (velocity before closure)
double surgePressure = joukowskyPressureSurge(rho, c, dv);
System.out.println("Max surge: " + surgePressure/1e5 + " bar");
// Output: Max surge: 44.0 bar
// Add to steady-state operating pressure for peak
double operatingPressure = 50.0; // bara
double peakPressure = operatingPressure + surgePressure/1e5;
System.out.println("Peak pressure: " + peakPressure + " bara");
// Output: Peak pressure: 94.0 bar
For gases, use NeqSim's thermodynamic properties:
SystemInterface gas = new SystemSrkEos(298.15, 50.0);
gas.addComponent("methane", 1.0);
gas.setMixingRule("classic");
gas.init(3);
gas.initPhysicalProperties();
double soundSpeed = gas.getSoundSpeed(); // m/s
System.out.println("Speed of sound: " + soundSpeed + " m/s");
// Typical: 400-450 m/s for natural gas
For detailed water hammer analysis, consider:
A proper water hammer model would require:
This is a potential future enhancement for NeqSim.
The reverse scenario - opening a choke to increase flow:
// Start with choke partially closed
choke.setPercentValveOpening(30.0);
process.run();
// Switch to transient
pipeline.setCalculateSteadyState(false);
process.setTimeStep(1.0);
// Gradually open choke
for (int step = 0; step < 200; step++) {
if (step >= 20 && step <= 70) {
double opening = 30.0 + (step - 20) * 1.4; // 30% → 100%
choke.setPercentValveOpening(Math.min(opening, 100.0));
}
process.runTransient();
}
When opening a valve:
| Pipe Length | Velocity | Transit Time | Recommended Δt |
|---|---|---|---|
| 100 m | 10 m/s | 10 s | 0.5-2 s |
| 1 km | 10 m/s | 100 s | 1-5 s |
| 10 km | 10 m/s | 1000 s | 5-20 s |
The time step should satisfy: $$\Delta t \leq \frac{\Delta x}{v}$$
Where $\Delta x = L / N_{increments}$
For fast transients (valve slam), use smaller time steps: $$\Delta t \leq \frac{\Delta x}{c}$$
Where $c$ is the speed of sound (~350-450 m/s for natural gas).
Based on validation tests:
| Mechanism | Propagation Speed | Notes |
|---|---|---|
| Mass flow | Fluid velocity | ~10-20 m/s (gas) |
| Pressure wave | ~0.4× transit time | Model uses advection |
| Temperature | Fluid velocity | Advective transport |
For a 1000m pipe with 12.5 m/s gas velocity:
// Pressure profile along pipe
List<Double> pressures = pipeline.getPressureProfile();
// Temperature profile
List<Double> temperatures = pipeline.getTemperatureProfile();
// Pressure drop per segment
List<Double> dpProfile = pipeline.getPressureDropProfile();
// Get segment-specific values
int segment = 10;
double p = pipeline.getSegmentPressure(segment);
double T = pipeline.getSegmentTemperature(segment);
double holdup = pipeline.getSegmentLiquidHoldup(segment);
Stream outlet = pipeline.getOutletStream();
double outP = outlet.getPressure("bara");
double outT = outlet.getTemperature("C");
double outFlow = outlet.getFlowRate("kg/hr");
Always run steady-state first to establish baseline:
pipeline.run(); // Steady-state
pipeline.setCalculateSteadyState(false); // Then switch
More segments = better resolution of waves:
pipeline.setNumberOfIncrements(20); // Minimum
pipeline.setNumberOfIncrements(50); // Better for long pipes
Check that outlet stabilizes after disturbances:
double prevP = 0;
for (int step = 0; step < 500; step++) {
pipeline.runTransient(dt, id);
double p = pipeline.getOutletPressure();
if (Math.abs(p - prevP) < 0.001) {
System.out.println("Converged at step " + step);
break;
}
prevP = p;
}
Transient should converge to same result as steady-state:
double steadyDp = ...; // From steady-state run
double transientDp = ...; // After convergence
assertTrue(Math.abs(transientDp - steadyDp) / steadyDp < 0.15);
For a step change in inlet conditions, expect:
Example for 1000m pipe with 12.5 m/s velocity:
Cause: Time step too large or flow rate too high Solution: Reduce time step or increase inlet pressure
Cause: Not enough transient steps Solution: Run more steps (at least 2× transit time)
Cause: Time step too small relative to physics Solution: Increase time step or reduce number of segments
Water hammer simulation is now available in NeqSim through the WaterHammerPipe class, which uses the Method of Characteristics (MOC) to simulate fast pressure transients. The recommended approach for water hammer analysis is:
WaterHammerPipe for fast transients - valve closures, pump trips, ESD eventsThe existing PipeBeggsAndBrills advection model remains valuable for slow transients, while WaterHammerPipe handles fast acoustic phenomena.
// Create water hammer pipe
WaterHammerPipe pipe = new WaterHammerPipe("pipe", feed);
pipe.setLength(1000);
pipe.setDiameter(0.2);
pipe.setNumberOfNodes(100);
pipe.setDownstreamBoundary(BoundaryType.VALVE);
pipe.run();
// Transient with valve closure
for (int step = 0; step < 1000; step++) {
if (step == 100) pipe.setValveOpening(0.0); // Slam shut
pipe.runTransient(0.001, id);
}
// Get maximum pressure surge
double maxP = pipe.getMaxPressure("bar");
For detailed implementation, see Water Hammer Implementation Guide.
START
│
▼
┌───────────────────┐
│ Is flow single │
│ phase (gas OR │──── YES ───┐
│ liquid only)? │ │
└────────┬──────────┘ │
│ ▼
NO ┌──────────────────┐
│ │ Is it primarily │
▼ │ gas? │
┌───────────────────┐ └────────┬─────────┘
│ PipeBeggsAndBrills│ │
│ (Multiphase) │ YES ◄──┴──► NO
└───────────────────┘ │ │
▼ ▼
AdiabaticPipe PipeBeggsAndBrills
(fast, accurate) (handles viscosity)
Use: AdiabaticPipe
AdiabaticPipe pipe = new AdiabaticPipe("transmission", feed);
pipe.setLength(100000); // 100 km
pipe.setDiameter(0.8); // 32 inch
pipe.setInletPressure(80); // bara
pipe.run();
Why? Fastest computation, accounts for gas compressibility, well-validated.
Use: PipeBeggsAndBrills
PipeBeggsAndBrills pipe = new PipeBeggsAndBrills("oil", feed);
pipe.setLength(5000);
pipe.setDiameter(0.3);
pipe.setPipeWallRoughness(4.6e-5);
pipe.setNumberOfIncrements(20);
pipe.run();
Why? Handles liquid accurately, proper friction for viscous fluids.
Use: PipeBeggsAndBrills
PipeBeggsAndBrills tubing = new PipeBeggsAndBrills("tubing", well);
tubing.setLength(3000); // 3 km vertical
tubing.setElevation(2900); // Almost vertical
tubing.setDiameter(0.0762); // 3 inch
tubing.setNumberOfIncrements(30);
tubing.run();
System.out.println("Flow regime: " + tubing.getFlowRegime());
System.out.println("Liquid holdup: " + tubing.getSegmentLiquidHoldup(30));
Why? Only model that handles multiphase + elevation properly.
Use: AdiabaticTwoPhasePipe (for quick estimates) or PipeBeggsAndBrills (for accuracy)
AdiabaticTwoPhasePipe pipe = new AdiabaticTwoPhasePipe("P-101", feed);
pipe.setLength(50);
pipe.setDiameter(0.1);
pipe.run();
Why? Fast, adequate accuracy for short pipes.
Use: PipeBeggsAndBrills
PipeBeggsAndBrills pipe = new PipeBeggsAndBrills("dynamic", feed);
pipe.setLength(1000);
pipe.setDiameter(0.2);
pipe.setNumberOfIncrements(20);
pipe.run();
pipe.setCalculateSteadyState(false);
for (int t = 0; t < 300; t++) {
pipe.runTransient(1.0, uuid);
}
Why? Only model with transient capability.
Use: PipeBeggsAndBrills with heat transfer
PipeBeggsAndBrills subsea = new PipeBeggsAndBrills("subsea", feed);
subsea.setLength(30000);
subsea.setDiameter(0.254);
subsea.setElevation(-500);
subsea.setRunAdiabatic(false);
subsea.setConstantSurfaceTemperature(277.15);
subsea.setHeatTransferCoefficient(5.0);
subsea.run();
Why? Handles elevation, multiphase, and heat transfer.
| Model | Relative Speed | Memory | Accuracy |
|---|---|---|---|
| AdiabaticPipe | ★★★★★ | Low | Good for gas |
| AdiabaticTwoPhasePipe | ★★★★☆ | Low | Moderate |
| PipeBeggsAndBrills | ★★★☆☆ | Medium | Best |
// Will give poor results for liquid
AdiabaticPipe pipe = new AdiabaticPipe("oil", liquidFeed);
PipeBeggsAndBrills pipe = new PipeBeggsAndBrills("oil", liquidFeed);
PipeBeggsAndBrills tubing = new PipeBeggsAndBrills("tubing", well);
tubing.setLength(3000);
// Missing: tubing.setElevation(3000); // Vertical well!
PipeBeggsAndBrills tubing = new PipeBeggsAndBrills("tubing", well);
tubing.setLength(3000);
tubing.setElevation(3000); // Important!
PipeBeggsAndBrills pipe = new PipeBeggsAndBrills("long", feed);
pipe.setLength(50000); // 50 km
pipe.setNumberOfIncrements(5); // Only 5 segments for 50 km!
pipe.setNumberOfIncrements(50); // 1 km per segment
pipe.setPipeWallRoughness(0.046); // This is 46 mm! Way too rough!
pipe.setPipeWallRoughness(4.6e-5); // 0.046 mm = 4.6×10⁻⁵ m
Before trusting results, verify:
NeqSim provides water hammer (hydraulic transient) simulation through the WaterHammerPipe class. This model uses the Method of Characteristics (MOC) to simulate fast pressure transients caused by:
Unlike the advection-based transient model in PipeBeggsAndBrills, WaterHammerPipe propagates pressure waves at the speed of sound, enabling accurate simulation of pressure surges.
import neqsim.process.equipment.pipeline.WaterHammerPipe;
import neqsim.process.equipment.pipeline.WaterHammerPipe.BoundaryType;
import neqsim.process.equipment.stream.Stream;
import neqsim.thermo.system.SystemSrkEos;
// Create fluid
SystemInterface water = new SystemSrkEos(298.15, 10.0);
water.addComponent("water", 1.0);
water.setMixingRule("classic");
water.setTotalFlowRate(100.0, "kg/hr");
Stream feed = new Stream("feed", water);
feed.run();
// Create water hammer pipe
WaterHammerPipe pipe = new WaterHammerPipe("pipeline", feed);
pipe.setLength(1000); // 1 km
pipe.setDiameter(0.2); // 200 mm
pipe.setNumberOfNodes(100); // Grid resolution
pipe.setDownstreamBoundary(BoundaryType.VALVE);
pipe.run(); // Initialize steady state
// Simulate valve closure
UUID id = UUID.randomUUID();
double dt = pipe.getMaxStableTimeStep();
for (int step = 0; step < 1000; step++) {
double t = step * dt;
// Close valve from t=0.1s to t=0.2s
if (t >= 0.1 && t <= 0.2) {
double tau = (t - 0.1) / 0.1;
pipe.setValveOpening(1.0 - tau); // 100% → 0%
}
pipe.runTransient(dt, id);
}
// Get maximum pressure surge
double maxPressure = pipe.getMaxPressure("bar");
System.out.println("Max surge pressure: " + maxPressure + " bar");
The MOC transforms the hyperbolic partial differential equations for 1D transient pipe flow into ordinary differential equations along characteristic lines.
Governing Equations:
Continuity: $$\frac{\partial H}{\partial t} + \frac{c^2}{gA}\frac{\partial Q}{\partial x} = 0$$
Momentum: $$\frac{\partial Q}{\partial t} + gA\frac{\partial H}{\partial x} + \frac{f}{2DA}Q|Q| = 0$$
Characteristic Lines:
Compatibility Equations:
Along C⁺: $H_P - H_A + B(Q_P - Q_A) + R \cdot Q_A|Q_A| = 0$
Along C⁻: $H_P - H_B - B(Q_P - Q_B) - R \cdot Q_B|Q_B| = 0$
Where:
The wave speed includes pipe elasticity using the Korteweg-Joukowsky formula:
$$c = \frac{c_{fluid}}{\sqrt{1 + \frac{K \cdot D}{E \cdot e}}}$$
Where:
// Wave speed is automatically calculated, but can be overridden
pipe.setPipeElasticModulus(200e9); // Steel
pipe.setWallThickness(0.01); // 10 mm
double waveSpeed = pipe.getWaveSpeed(); // After run()
The theoretical pressure surge for instantaneous velocity change:
$$\Delta P = \rho \cdot c \cdot \Delta v$$
// Calculate theoretical surge
double surgePa = pipe.calcJoukowskyPressureSurge(velocityChange);
double surgeBar = pipe.calcJoukowskyPressureSurge(velocityChange, "bar");
| Type | Description | Use Case |
|---|---|---|
RESERVOIR |
Constant pressure head | Upstream tank/reservoir |
VALVE |
Variable opening (0-1) | Downstream control valve |
CLOSED_END |
No flow (Q=0) | Dead end, closed valve |
CONSTANT_FLOW |
Fixed flow rate | Pump at constant speed |
// Upstream: constant pressure reservoir
pipe.setUpstreamBoundary(BoundaryType.RESERVOIR);
// Downstream: valve that can be opened/closed
pipe.setDownstreamBoundary(BoundaryType.VALVE);
pipe.setValveOpening(1.0); // Initially fully open
// During simulation, close the valve
pipe.setValveOpening(0.5); // 50% open
pipe.setValveOpening(0.0); // Fully closed
For numerical stability, the time step must satisfy:
$$\Delta t \leq \frac{\Delta x}{c}$$
Where Δx = length / (numberOfNodes - 1).
// Get maximum stable time step
double maxDt = pipe.getMaxStableTimeStep();
// Use smaller time step for safety
double dt = maxDt * 0.5;
The time for a pressure wave to travel the pipe length and back:
$$T_{round-trip} = \frac{2L}{c}$$
double roundTrip = pipe.getWaveRoundTripTime();
// For 1 km pipe with c=1000 m/s: roundTrip = 2 seconds
// Pressure along pipe (Pa)
double[] pressures = pipe.getPressureProfile();
// Pressure in bar
double[] pressuresBar = pipe.getPressureProfile("bar");
double[] velocities = pipe.getVelocityProfile(); // m/s
double[] flows = pipe.getFlowProfile(); // m³/s
double[] heads = pipe.getHeadProfile(); // m
Track maximum and minimum pressures during simulation:
double[] maxEnvelope = pipe.getMaxPressureEnvelope();
double[] minEnvelope = pipe.getMinPressureEnvelope();
double overallMax = pipe.getMaxPressure("bar");
double overallMin = pipe.getMinPressure("bar");
// Reset envelopes (e.g., after reaching steady state)
pipe.resetEnvelopes();
List<Double> pressureHistory = pipe.getPressureHistory(); // At outlet
List<Double> timeHistory = pipe.getTimeHistory();
double currentTime = pipe.getCurrentTime();
// Setup: 5 km oil pipeline
SystemInterface oil = new SystemSrkEos(298.15, 50.0);
oil.addComponent("nC10", 1.0);
oil.setMixingRule("classic");
oil.setTotalFlowRate(500000, "kg/hr");
Stream feed = new Stream("feed", oil);
feed.run();
WaterHammerPipe pipeline = new WaterHammerPipe("export pipeline", feed);
pipeline.setLength(5, "km");
pipeline.setDiameter(300, "mm");
pipeline.setNumberOfNodes(200);
pipeline.setDownstreamBoundary(BoundaryType.VALVE);
pipeline.run();
System.out.println("Wave speed: " + pipeline.getWaveSpeed() + " m/s");
System.out.println("Round-trip time: " + pipeline.getWaveRoundTripTime() + " s");
// Initial conditions
double initialPressure = pipeline.getMaxPressure("bar");
double velocity = pipeline.getVelocityProfile()[0];
// Simulate ESD - valve closes in 5 seconds
UUID id = UUID.randomUUID();
double dt = 0.01; // 10 ms time step
double closureTime = 5.0;
for (double t = 0; t < 30; t += dt) {
// Linear valve closure from t=0 to t=closureTime
if (t <= closureTime) {
pipeline.setValveOpening(1.0 - t / closureTime);
}
pipeline.runTransient(dt, id);
if (t % 1.0 < dt) {
System.out.printf("t=%.1fs: P_max=%.1f bar, valve=%.0f%%%n",
t, pipeline.getMaxPressure("bar"), pipeline.getValveOpening() * 100);
}
}
// Results
double maxSurge = pipeline.getMaxPressure("bar");
double minPressure = pipeline.getMinPressure("bar");
System.out.println("Initial pressure: " + initialPressure + " bar");
System.out.println("Maximum surge: " + maxSurge + " bar");
System.out.println("Minimum pressure: " + minPressure + " bar");
System.out.println("Surge increase: " + (maxSurge - initialPressure) + " bar");
// Compare with Joukowsky theoretical value
double joukowskySurge = pipeline.calcJoukowskyPressureSurge(velocity, "bar");
System.out.println("Joukowsky theoretical: " + joukowskySurge + " bar");
// Natural gas pipeline
SystemInterface gas = new SystemSrkEos(298.15, 70.0);
gas.addComponent("methane", 0.90);
gas.addComponent("ethane", 0.05);
gas.addComponent("propane", 0.03);
gas.addComponent("CO2", 0.02);
gas.setMixingRule("classic");
gas.setTotalFlowRate(1000000, "Sm3/day");
Stream gasFeed = new Stream("gas feed", gas);
gasFeed.run();
WaterHammerPipe gasPipe = new WaterHammerPipe("gas pipeline", gasFeed);
gasPipe.setLength(100, "km");
gasPipe.setDiameter(0.5); // 500 mm
gasPipe.setNumberOfNodes(500);
gasPipe.setDownstreamBoundary(BoundaryType.VALVE);
gasPipe.run();
// Gas has lower wave speed (~400 m/s) and lower density
// So pressure surges are typically smaller than for liquids
System.out.println("Gas wave speed: " + gasPipe.getWaveSpeed() + " m/s");
WaterHammerPipe(String name)
WaterHammerPipe(String name, StreamInterface inStream)
| Method | Description |
|---|---|
setLength(double meters) |
Pipe length in meters |
setLength(double value, String unit) |
Length with unit ("m", "km", "ft") |
setDiameter(double meters) |
Inside diameter in meters |
setDiameter(double value, String unit) |
Diameter with unit ("m", "mm", "in") |
setWallThickness(double meters) |
Pipe wall thickness |
setRoughness(double meters) |
Surface roughness |
setElevationChange(double meters) |
Outlet - inlet elevation |
setNumberOfNodes(int nodes) |
Computational grid size |
| Method | Description |
|---|---|
setPipeElasticModulus(double Pa) |
Pipe material modulus (default: 200 GPa) |
setWaveSpeed(double m_per_s) |
Override calculated wave speed |
| Method | Description |
|---|---|
setUpstreamBoundary(BoundaryType) |
Set upstream BC type |
setDownstreamBoundary(BoundaryType) |
Set downstream BC type |
setValveOpening(double fraction) |
Valve opening 0-1 |
getValveOpening() |
Current valve opening |
| Method | Description |
|---|---|
run(UUID id) |
Initialize steady state |
runTransient(double dt, UUID id) |
Run one time step |
getMaxStableTimeStep() |
Get Courant-limited time step |
setCourantNumber(double cn) |
Set Courant number (default: 1.0) |
reset() |
Reset to initial state |
resetEnvelopes() |
Reset min/max tracking |
| Method | Description |
|---|---|
getPressureProfile() |
Pressure array (Pa) |
getPressureProfile(String unit) |
Pressure in unit ("bar", "psi") |
getVelocityProfile() |
Velocity array (m/s) |
getFlowProfile() |
Flow rate array (m³/s) |
getHeadProfile() |
Piezometric head (m) |
getMaxPressureEnvelope() |
Max pressure at each node |
getMinPressureEnvelope() |
Min pressure at each node |
getMaxPressure(String unit) |
Overall maximum pressure |
getMinPressure(String unit) |
Overall minimum pressure |
getPressureHistory() |
Outlet pressure vs time |
getTimeHistory() |
Time values |
getCurrentTime() |
Current simulation time |
| Method | Description |
|---|---|
calcJoukowskyPressureSurge(double dv) |
Theoretical surge (Pa) |
calcJoukowskyPressureSurge(double dv, String unit) |
Surge in unit |
calcEffectiveWaveSpeed() |
Korteweg wave speed |
getWaveSpeed() |
Current wave speed (m/s) |
getWaveRoundTripTime() |
2L/c in seconds |
| Aspect | PipeBeggsAndBrills | WaterHammerPipe |
|---|---|---|
| Wave speed | Fluid velocity (~10-20 m/s) | Speed of sound (400-1500 m/s) |
| Time scale | Minutes to hours | Milliseconds to seconds |
| Use case | Slow transients, process upsets | Fast transients, valve slam |
| Physics | Advection | Acoustic waves |
| Two-phase | Full correlation | Single-phase (liquid or gas) |
| Heat transfer | Included | Not included |
When to use which:
Current implementation limitations:
Documentation for safety-related features and systems in NeqSim.
This folder contains guides for implementing safety systems in process simulations, including Emergency Shutdown (ESD), High Integrity Pressure Protection Systems (HIPPS), blowdown analysis, and alarm management.
| Document | Description |
|---|---|
| ESD_BLOWDOWN_SYSTEM.md | Complete ESD and blowdown system guide |
| PRESSURE_MONITORING_ESD.md | Pressure monitoring for ESD |
| Document | Description |
|---|---|
| HIPPS_SUMMARY.md | HIPPS overview and summary |
| hipps_implementation.md | HIPPS implementation details |
| hipps_safety_logic.md | HIPPS safety logic programming |
| Document | Description |
|---|---|
| INTEGRATED_SAFETY_SYSTEMS.md | Integrated safety systems overview |
| layered_safety_architecture.md | Layered safety architecture (defense in depth) |
| sis_logic_implementation.md | Safety Instrumented Systems (SIS) logic |
| integration_safety_chain_tests.md | Safety chain integration testing |
| SAFETY_SIMULATION_ROADMAP.md | Safety simulation development roadmap |
| Document | Description |
|---|---|
| fire_blowdown_capabilities.md | Fire case blowdown simulation |
| fire_heat_transfer_enhancements.md | Fire heat transfer modeling |
| Document | Description |
|---|---|
| psv_dynamic_sizing_example.md | Pressure Safety Valve dynamic sizing |
| rupture_disk_dynamic_behavior.md | Rupture disk dynamic behavior |
| Document | Description |
|---|---|
| alarm_system_guide.md | Alarm system implementation guide |
| alarm_triggered_logic_example.md | Alarm-triggered logic examples |
A comprehensive analysis of existing safety capabilities and a realistic implementation plan for enhancing NeqSim's safety simulation features.
NeqSim already has substantial safety infrastructure that covers approximately 90-95% of the proposed roadmap.
Recently Implemented (2024):
LeakModel - Choked/subsonic flow with time-series source termsSourceTermResult - Export to PHAST, FLACS, KFX, OpenFOAMInitiatingEvent enum - Standard safety scenario initiatorsBoundaryConditions - Environmental conditions with geographic presetsRiskModel - Monte Carlo, event trees, sensitivity analysis (tornado diagrams)RiskEvent / RiskResult - Probabilistic risk quantification with F-N curvesSafetyEnvelopeCalculator - Hydrate, wax, CO2, MDMT, phase envelope calculationSafetyEnvelope - P-T curve container with DCS/PI/Seeq exportRemaining Gaps:
| Feature | Status | Implementation |
|---|---|---|
SafetyScenario API |
✅ Exists | ProcessSafetyScenario with Builder pattern |
| Initiating events | ✅ Exists | Blocked outlets, utility loss, controller overrides |
| Custom manipulators | ✅ Exists | Lambda-based equipment manipulation |
| Scenario execution | ✅ Exists | ProcessSafetyAnalyzer.analyzeScenario() |
| Load case definition | ✅ Exists | ProcessSafetyLoadCase |
| Result repository | ✅ Exists | ProcessSafetyResultRepository |
Existing Code Location: neqsim.process.safety.*
// Current API
ProcessSafetyScenario scenario = ProcessSafetyScenario.builder()
.name("Compressor blowdown")
.blockedOutlet("V-101")
.utilityLoss("Cooling-Water")
.controllerSetPointOverride("PC-101", 50.0)
.customManipulator("SEP-001", eq -> eq.setRegulatorOutSignal(0.0))
.build();
ProcessSafetyAnalyzer analyzer = new ProcessSafetyAnalyzer(processSystem);
ProcessSafetyLoadCase result = analyzer.analyzeScenario(scenario);
Implemented Additions:
InitiatingEvent enum (ESD, PSV_LIFT, RUPTURE, LEAK_SMALL, LEAK_MEDIUM, LEAK_LARGE, FIRE_EXPOSURE, etc.)BoundaryConditions class (ambient temp, wind speed, humidity, stability class, presets for North Sea, Gulf of Mexico, etc.)Remaining Gaps:
| Feature | Status | Implementation |
|---|---|---|
| Time-dependent pressure | ✅ Complete | VesselDepressurization.runTransient() |
| Time-dependent temperature | ✅ Complete | Energy balance, adiabatic, isothermal modes |
| Phase split evolution | ✅ Complete | Two-phase support with separate wall temps |
| Choked vs non-choked flow | ✅ Complete | Critical pressure ratio calculation |
| Real-gas speed of sound | ✅ Complete | NeqSim thermodynamics |
| MDMT prediction | ✅ Complete | getMinimumWallTemperatureReached() |
| Blowdown duration | ✅ Complete | getTimeToReachPressure() |
| Two-phase formation timing | ✅ Complete | hasLiquidRainout() |
| Valve/piping temperature | ✅ Complete | TransientWallHeatTransfer |
| Fire case (API 521) | ✅ Complete | setFireCase(), heat flux models |
| Valve dynamics | ✅ Complete | setValveOpeningTime() |
| Hydrate risk | ✅ Complete | getHydrateFormationTemperature(), hasHydrateRisk() |
| CO2 freezing risk | ✅ Complete | getCO2FreezingTemperature(), hasCO2FreezingRisk() |
| Export to CSV/JSON | ✅ Complete | exportResultsToCSV(), exportResultsToJSON() |
Existing Code Location:
neqsim.process.equipment.tank.VesselDepressurization (~3000 lines)neqsim.process.util.fire.TransientWallHeatTransferneqsim.process.util.fire.VesselHeatTransferCalculator// Current API
VesselDepressurization vessel = new VesselDepressurization("Tank", feed);
vessel.setVolume(10.0);
vessel.setOrificeDiameter(0.03);
vessel.setCalculationType(CalculationType.ENERGY_BALANCE);
vessel.setFireCase(true, 100.0); // 100 kW/m² fire
vessel.runTransient(dt, uuid);
// Flow assurance
Map<String, String> risks = vessel.assessFlowAssuranceRisks();
Status: COMPLETE ✓
| Feature | Status | Implementation |
|---|---|---|
| SafetyValve class | ✅ Exists | SafetyValve extends ThrottlingValve |
| SafetyReliefValve | ✅ Exists | SafetyReliefValve with lift dynamics |
| Set pressure / blowdown | ✅ Exists | setPressureSpec(), setBlowdownPressure() |
| API 520 sizing | ✅ Exists | ReliefValveSizing.calculateRequiredArea() |
| API 521 fire heat input | ✅ Exists | calculateAPI521HeatInput() |
| Relieving scenarios | ✅ Exists | RelievingScenario class |
| Choked flow models | ✅ Exists | Critical flow calculations |
| Balanced-bellows support | ✅ Exists | isBalancedBellows parameter |
| Rupture disk combination | ✅ Exists | hasRuptureDisk parameter |
| Back pressure effects | ⚠️ Partial | Static calculation, needs dynamic |
| Reaction forces | ❌ Missing | Not implemented |
| Flare connection | ⚠️ Partial | Flare class exists, limited integration |
Existing Code Location:
neqsim.process.equipment.valve.SafetyValveneqsim.process.equipment.valve.SafetyReliefValveneqsim.process.util.fire.ReliefValveSizingGaps to Address:
| Feature | Status | Implementation |
|---|---|---|
| Leak model class | ✅ Complete | LeakModel with Builder pattern |
| Hole diameter specification | ✅ Complete | holeDiameter(double, String unit) |
| Mass flow vs time | ✅ Complete | calculateSourceTerm() returns time series |
| Gas/liquid split | ✅ Complete | Vapor fraction tracked at each timestep |
| Jet momentum | ✅ Complete | calculateJetMomentum() |
| Release temperature | ✅ Complete | Temperature tracked with isentropic expansion |
| Droplet size estimate | ✅ Complete | SMD via modified Weber number correlation |
| Export to PHAST/FLACS/KFX/OpenFOAM | ✅ Complete | All export methods implemented |
Implementation Location: neqsim.process.safety.release.*
LeakModel - Main leak/rupture model with choked/subsonic flowSourceTermResult - Time-series container with QRA tool exportReleaseOrientation - Enum for release directionsInitiatingEvent - Enum for scenario initiating events (in neqsim.process.safety)BoundaryConditions - Environmental conditions with presets (in neqsim.process.safety)// Example usage
LeakModel leak = LeakModel.builder()
.fluid(system)
.holeDiameter(25.0, "mm")
.vesselVolume(10.0)
.orientation(ReleaseOrientation.HORIZONTAL)
.scenarioName("HP Separator Leak")
.build();
SourceTermResult result = leak.calculateSourceTerm(600.0, 1.0); // 10 min, 1s step
result.exportToPHAST("leak_phast.csv");
result.exportToFLACS("leak_flacs.csv");
result.exportToKFX("leak_kfx.csv");
result.exportToOpenFOAM("/path/to/openfoam/case");
Original API design (now implemented):
// New package: neqsim.process.safety.release
public class LeakModel {
private double holeDiameter; // m
private String location;
private LeakOrientation orientation; // HORIZONTAL, VERTICAL_UP, VERTICAL_DOWN
public SourceTermResult calculateSourceTerm(SystemInterface system);
}
public class SourceTermResult {
private double[] time; // s
private double[] massFlowRate; // kg/s
private double[] temperature; // K
private double[] vaporFraction; // mol/mol
private double[] jetMomentum; // N
private double[] liquidDropletSize; // m (SMD)
// Export methods
public void exportToPHAST(String filename);
public void exportToFLACS(String filename);
public void exportToKFX(String filename);
public void exportToOpenFOAM(String directory);
}
| Feature | Status | Implementation |
|---|---|---|
| Monte Carlo analysis | ✅ Complete | RiskModel.runMonteCarloAnalysis(iterations) |
| Failure frequencies | ✅ Complete | RiskEvent with frequency and error factors |
| Conditional probabilities | ✅ Complete | RiskEvent with parent events and conditional probability |
| Event tree logic | ✅ Complete | RiskEvent.parentEvent() for event tree branching |
| Sensitivity analysis | ✅ Complete | RiskModel.runSensitivityAnalysis() with tornado diagrams |
| Risk quantification | ✅ Complete | RiskResult with category frequencies and F-N curves |
| Consequence categories | ✅ Complete | ConsequenceCategory enum (NEGLIGIBLE to CATASTROPHIC) |
| Export to CSV/JSON | ✅ Complete | RiskResult.exportToCSV(), exportToJSON() |
Implementation Location: neqsim.process.safety.risk.*
RiskEvent - Individual risk event with frequency, probability, consequence categoryRiskModel - Monte Carlo simulation and sensitivity analysis engineRiskResult - Results container with F-N curves and export methodsSensitivityResult - Tornado diagram data and sensitivity indices// Example usage
RiskModel model = new RiskModel("HP Separator Study");
model.setRandomSeed(42);
// Add initiating events with frequencies (per year)
model.addInitiatingEvent("Small Leak", 1e-3, ConsequenceCategory.MINOR);
model.addInitiatingEvent("Medium Leak", 1e-4, ConsequenceCategory.MODERATE);
model.addInitiatingEvent("Large Rupture", 1e-5, ConsequenceCategory.MAJOR);
// Or use builder pattern for event trees
RiskEvent fireEvent = RiskEvent.builder()
.name("Fire on Leak")
.parentEvent(leakEvent)
.conditionalProbability(0.1)
.consequenceCategory(ConsequenceCategory.MAJOR)
.build();
model.addEvent(fireEvent);
// Run Monte Carlo analysis
RiskResult result = model.runMonteCarloAnalysis(10000);
result.exportToCSV("risk_results.csv");
// Run sensitivity analysis (tornado diagram)
SensitivityResult sensitivity = model.runSensitivityAnalysis(0.1, 10.0);
sensitivity.exportToCSV("sensitivity.csv");
| Feature | Status | Implementation |
|---|---|---|
| Hydrate envelope | ✅ Complete | SafetyEnvelopeCalculator.calculateHydrateEnvelope() |
| Wax appearance | ✅ Complete | SafetyEnvelopeCalculator.calculateWaxEnvelope() |
| MDMT/Brittle fracture | ✅ Complete | SafetyEnvelopeCalculator.calculateMDMTEnvelope() |
| CO2 solid formation | ✅ Complete | SafetyEnvelopeCalculator.calculateCO2FreezingEnvelope() |
| Phase envelope | ✅ Complete | SafetyEnvelopeCalculator.calculatePhaseEnvelope() |
| Temperature interpolation | ✅ Complete | SafetyEnvelope.getTemperatureAtPressure() |
| Operating point check | ✅ Complete | SafetyEnvelope.isOperatingPointSafe() |
| Safety margin calc | ✅ Complete | SafetyEnvelope.calculateMarginToLimit() |
| Export to CSV/JSON | ✅ Complete | SafetyEnvelope.exportToCSV(), exportToJSON() |
| Export to PI/Seeq | ✅ Complete | SafetyEnvelope.exportToPIFormat(), exportToSeeq() |
Implementation Location: neqsim.process.safety.envelope.*
SafetyEnvelope - P-T curve container with interpolation and export methodsSafetyEnvelopeCalculator - Calculates hydrate, wax, CO2, MDMT, phase envelopesEnvelopeType - Enum for HYDRATE, WAX, CO2_FREEZING, MDMT, PHASE_ENVELOPE, BRITTLE_FRACTURE// Example usage
SystemInterface naturalGas = new SystemSrkEos(300.0, 50.0);
naturalGas.addComponent("methane", 0.85);
naturalGas.addComponent("ethane", 0.10);
naturalGas.addComponent("propane", 0.05);
naturalGas.setMixingRule("classic");
SafetyEnvelopeCalculator calc = new SafetyEnvelopeCalculator(naturalGas);
// Calculate safety envelopes
SafetyEnvelope hydrateEnv = calc.calculateHydrateEnvelope(1.0, 100.0, 20);
SafetyEnvelope co2Env = calc.calculateCO2FreezingEnvelope(10.0, 100.0, 10);
SafetyEnvelope mdmtEnv = calc.calculateMDMTEnvelope(1.0, 100.0, 300.0, 10);
// Check operating point safety
boolean safe = hydrateEnv.isOperatingPointSafe(50.0, 280.0);
double margin = hydrateEnv.calculateMarginToLimit(50.0, 280.0);
// Export for DCS/historian integration
hydrateEnv.exportToCSV("hydrate_envelope.csv");
hydrateEnv.exportToPIFormat("hydrate_pi.csv");
hydrateEnv.exportToSeeq("hydrate_seeq.json");
// Calculate all envelopes at once
SafetyEnvelope[] allEnvelopes = calc.calculateAllEnvelopes(1.0, 100.0, 20);
| Priority | Feature | Effort | Impact | Status |
|---|---|---|---|---|
| 1 | Leak/Release Source Term (3.4) | High | Critical | ❌ Not started |
| 2 | Safety Envelope Calculator (3.6) | Medium | High | ❌ Not started |
| 3 | Risk Model Framework (3.5) | High | High | ❌ Not started |
| 4 | PSV Back Pressure Dynamics (3.3) | Medium | Medium | ⚠️ Partial |
| 5 | Initiating Event Enum (3.1) | Low | Medium | ❌ Not started |
| 6 | Reaction Force Calculation (3.3) | Low | Low | ❌ Not started |
Timeline: 2-3 weeks
Create neqsim.process.safety.release package with:
LeakModel - Hole/rupture specificationSourceTermResult - Time-series release dataReleaseExporter - PHAST/FLACS/KFX/OpenFOAM exportVesselDepressurization for inventory trackingTimeline: 1-2 weeks
Create neqsim.process.safety.envelope package with:
SafetyEnvelopeCalculator - Compute P-T envelopesSafetyEnvelope - Data container with exportTimeline: 3-4 weeks
Create neqsim.process.safety.risk package with:
RiskModel - Event tree / fault tree basicsRiskEvent - Frequencies and probabilitiesMonteCarloRiskAnalysis - Uncertainty propagationProcessSafetyScenarioTimeline: 1-2 weeks
Enhance existing SafetyValve with:
neqsim.process.safety/
├── ProcessSafetyScenario.java ✅ Scenario definition
├── ProcessSafetyAnalyzer.java ✅ Scenario execution
├── ProcessSafetyLoadCase.java ✅ Results container
├── ProcessSafetyResultRepository.java ✅ Results storage
└── ProcessSafetyAnalysisSummary.java ✅ Summary report
neqsim.process.logic/
├── sis/
│ ├── SafetyInstrumentedFunction.java ✅ SIF with voting
│ ├── Detector.java ✅ Fire/gas detectors
│ └── VotingLogic.java ✅ 1oo1, 2oo3, etc.
├── hipps/ ✅ HIPPS logic
├── esd/ ✅ ESD sequences
├── shutdown/ ✅ Shutdown logic
└── voting/ ✅ Voting patterns
neqsim.process.equipment.valve/
├── SafetyValve.java ✅ PSV with hysteresis
├── SafetyReliefValve.java ✅ Relief valve dynamics
└── RelievingScenario.java ✅ Scenario definitions
neqsim.process.util.fire/
├── ReliefValveSizing.java ✅ API 520/521 sizing
├── FireHeatLoadCalculator.java ✅ API 521 heat input
├── VesselRuptureCalculator.java ✅ Von Mises stress
├── SeparatorFireExposure.java ✅ Fire case wrapper
├── TransientWallHeatTransfer.java ✅ Wall temperature
└── VesselHeatTransferCalculator.java ✅ Heat transfer
neqsim.process.equipment.tank/
└── VesselDepressurization.java ✅ Full blowdown (~3000 lines)
├── 5 calculation types
├── Two-phase support
├── Fire case (API 521)
├── Valve dynamics
├── Hydrate/CO2 risk
├── MDMT monitoring
├── Flare integration
└── CSV/JSON export
neqsim.process.equipment.flare/
└── Flare.java ✅ Flare equipment
public enum InitiatingEvent {
ESD("Emergency Shutdown"),
PSV_LIFT("Pressure Safety Valve Lift"),
RUPTURE("Vessel/Pipe Rupture"),
LEAK_SMALL("Small Leak (< 10mm)"),
LEAK_MEDIUM("Medium Leak (10-50mm)"),
LEAK_LARGE("Large Leak (> 50mm)"),
BLOCKED_OUTLET("Blocked Outlet"),
UTILITY_LOSS("Loss of Utility"),
FIRE_EXPOSURE("Fire Exposure"),
RUNAWAY_REACTION("Runaway Reaction");
private final String description;
// ...
}
public class BoundaryConditions implements Serializable {
private double ambientTemperature = 288.15; // K
private double windSpeed = 5.0; // m/s
private double relativeHumidity = 0.6; // fraction
private double solarRadiation = 0.0; // W/m²
// Builder pattern...
}
public double[][] calculateHydrateEnvelope(double pMin, double pMax, int points) {
double[][] envelope = new double[2][points];
double pStep = (pMax - pMin) / (points - 1);
for (int i = 0; i < points; i++) {
double p = pMin + i * pStep;
system.setPressure(p);
hydrateFormationTemperature();
envelope[0][i] = p;
envelope[1][i] = system.getTemperature();
}
return envelope;
}
NeqSim's safety simulation capabilities are more mature than initially apparent. The main gaps are:
The dynamic blowdown module (3.2) is fully complete with the VesselDepressurization class, including all requested features plus hydrate/CO2 risk assessment.
docs/fire_blowdown_capabilities.mddocs/hipps_implementation.mddocs/alarm_system_guide.mdnotebooks/VesselDepressurizationTutorial.ipynbNeqSim now implements a comprehensive defense-in-depth safety architecture with multiple independent protection layers. This document describes how HIPPS, fire/gas detection, and ESD systems work together to provide robust safety protection.
Safety protection follows the "onion model" with multiple layers:
┌─────────────────────────────────────────────────┐
│ 1. Process Control System (PCS) │ ← Normal operation
│ • Pressure controllers: 80-85% MAOP │
│ • Temperature controllers │
│ • Level controllers │
└──────────────────┬──────────────────────────────┘
│ If PCS fails ↓
┌─────────────────────────────────────────────────┐
│ 2. Basic Process Control Alarms (BPCS) │ ← Operator intervention
│ • High pressure alarm: 90% MAOP │
│ • High temperature alarm │
│ • Manual operator actions │
└──────────────────┬──────────────────────────────┘
│ If operator fails ↓
┌─────────────────────────────────────────────────┐
│ 3. HIPPS (High Integrity Pressure Protection) │ ← First SIS layer
│ • Activation: 90-95% MAOP │
│ • SIL: 2 or 3 │
│ • Response: <2 seconds │
│ • Action: Close isolation valve │
└──────────────────┬──────────────────────────────┘
│ If HIPPS fails ↓
┌─────────────────────────────────────────────────┐
│ 4. Fire & Gas Detection SIS │ ← Hazard detection
│ • Fire detectors: 2oo3 voting │
│ • Gas detectors: 2oo3 voting │
│ • SIL: 2 or 3 │
│ • Action: Activate ESD │
└──────────────────┬──────────────────────────────┘
│ If hazard detected ↓
┌─────────────────────────────────────────────────┐
│ 5. ESD (Emergency Shutdown) │ ← Emergency response
│ • Activation: 98% MAOP or hazard │
│ • SIL: 1 or 2 │
│ • Response: 2-10 seconds │
│ • Action: Full/partial shutdown │
└──────────────────┬──────────────────────────────┘
│ If ESD fails ↓
┌─────────────────────────────────────────────────┐
│ 6. Pressure Relief (PSV/Rupture Disk) │ ← Passive protection
│ • Activation: 100-110% MAOP │
│ • Mechanical device (fail-safe) │
│ • Action: Vent to flare/atmosphere │
└─────────────────────────────────────────────────┘
Purpose: Prevent overpressure before PSV activation
Key Features:
When It Activates:
Normal: 50 bara → Upset: 96 bara → HIPPS trips at 95 bara
Result: Isolation valve closes, prevents PSV lifting
Code Example:
HIPPSLogic hipps = new HIPPSLogic("HIPPS-101", VotingLogic.TWO_OUT_OF_THREE);
hipps.addPressureSensor(pt1);
hipps.addPressureSensor(pt2);
hipps.addPressureSensor(pt3);
hipps.setIsolationValve(isolationValve);
hipps.linkToEscalationLogic(esdLogic, 5.0);
Benefits:
Purpose: Detect hazardous conditions and initiate safe shutdown
Key Features:
When It Activates:
Fire: 2 of 3 detectors above 60°C → Fire SIF trips → ESD activated
Gas: 2 of 3 detectors above 25% LEL → Gas SIF trips → ESD activated
Code Example:
SafetyInstrumentedFunction fireSIF =
new SafetyInstrumentedFunction("Fire Detection", VotingLogic.TWO_OUT_OF_THREE);
fireSIF.addDetector(fireDetector1);
fireSIF.addDetector(fireDetector2);
fireSIF.addDetector(fireDetector3);
fireSIF.linkToLogic(esdLogic);
Benefits:
Purpose: Emergency shutdown of process facilities
Key Features:
ESD Levels:
ESD Level 1: Partial shutdown (specific area)
ESD Level 2: Full shutdown (entire facility)
ESD Level 3: Total evacuation + shutdown
Code Example:
ESDLogic esdLogic = new ESDLogic("ESD Level 1");
esdLogic.addAction(new TripValveAction(esdValve), 0.0); // Immediate
esdLogic.addAction(new ActivateBlowdownAction(bdValve), 0.5); // After 0.5s
esdLogic.addAction(new SetSplitterAction(splitter, [...]), 0.5); // After 1.0s
Benefits:
Scenario: Pressure protection with backup
// Create HIPPS (first line of defense)
HIPPSLogic hipps = new HIPPSLogic("HIPPS-101", VotingLogic.TWO_OUT_OF_THREE);
hipps.addPressureSensor(pt1); // 95 bara setpoint
hipps.addPressureSensor(pt2);
hipps.addPressureSensor(pt3);
hipps.setIsolationValve(hippsValve);
// Create ESD (backup)
ESDLogic esd = new ESDLogic("ESD Level 1");
esd.addAction(new TripValveAction(esdValve), 0.0);
// Link HIPPS to escalate to ESD after 5 seconds if pressure remains high
hipps.linkToEscalationLogic(esd, 5.0);
// Simulation
hipps.update(pressure, pressure, pressure);
hipps.execute(timeStep);
// Check status
if (hipps.isTripped() && !hipps.hasEscalated()) {
System.out.println("HIPPS controlling pressure");
} else if (hipps.hasEscalated()) {
System.out.println("HIPPS failed - ESD activated");
}
Flow:
t=0s: Pressure rises to 96 bara
t=0s: HIPPS trips (2/3 sensors above 95 bara)
t=0s: Isolation valve closes rapidly
t=0-5s: HIPPS monitors pressure
t=5s: If pressure still high → Escalate to ESD
t=5s: ESD valve trips → Full shutdown
Scenario: Fire detected in process area
// Create fire detection SIF
SafetyInstrumentedFunction fireSIF =
new SafetyInstrumentedFunction("Fire Detection", VotingLogic.TWO_OUT_OF_THREE);
fireSIF.addDetector(new Detector("FD-101", DetectorType.FIRE, AlarmLevel.HIGH, 60.0, "°C"));
fireSIF.addDetector(new Detector("FD-102", DetectorType.FIRE, AlarmLevel.HIGH, 60.0, "°C"));
fireSIF.addDetector(new Detector("FD-103", DetectorType.FIRE, AlarmLevel.HIGH, 60.0, "°C"));
// Create ESD logic
ESDLogic esd = new ESDLogic("Fire ESD");
esd.addAction(new TripValveAction(esdValve), 0.0);
esd.addAction(new ActivateBlowdownAction(blowdownValve), 0.5);
// Link fire SIF to ESD
fireSIF.linkToLogic(esd);
// Simulation
fireSIF.update(temp1, temp2, temp3);
// Check status
if (fireSIF.isTripped()) {
System.out.println("Fire detected - ESD activated");
}
Flow:
t=0s: Temperatures: FD-101=55°C, FD-102=65°C, FD-103=70°C
t=0s: Fire SIF evaluates: 2/3 detectors above 60°C → TRIP
t=0s: Fire SIF activates linked ESD logic
t=0s: ESD action 1: Trip ESD valve
t=0.5s: ESD action 2: Activate blowdown
Scenario: All safety layers configured
// Layer 1: HIPPS for pressure protection
HIPPSLogic hipps = new HIPPSLogic("HIPPS-101", VotingLogic.TWO_OUT_OF_THREE);
hipps.addPressureSensor(pt1); // 95 bara
hipps.addPressureSensor(pt2);
hipps.addPressureSensor(pt3);
hipps.setIsolationValve(hippsValve);
// Layer 2: Fire detection
SafetyInstrumentedFunction fireSIF =
new SafetyInstrumentedFunction("Fire SIF", VotingLogic.TWO_OUT_OF_THREE);
fireSIF.addDetector(fd1); // 60°C
fireSIF.addDetector(fd2);
fireSIF.addDetector(fd3);
// Layer 3: Gas detection
SafetyInstrumentedFunction gasSIF =
new SafetyInstrumentedFunction("Gas SIF", VotingLogic.TWO_OUT_OF_THREE);
gasSIF.addDetector(gd1); // 25% LEL
gasSIF.addDetector(gd2);
gasSIF.addDetector(gd3);
// Layer 4: ESD (final layer)
ESDLogic esd = new ESDLogic("ESD Level 1");
esd.addAction(new TripValveAction(esdValve), 0.0);
esd.addAction(new ActivateBlowdownAction(blowdownValve), 0.5);
// Integrate layers
hipps.linkToEscalationLogic(esd, 5.0);
fireSIF.linkToLogic(esd);
gasSIF.linkToLogic(esd);
// Simulation
hipps.update(pressure, pressure, pressure);
fireSIF.update(temp1, temp2, temp3);
gasSIF.update(gas1, gas2, gas3);
hipps.execute(timeStep);
// Any layer can trigger ESD
if (hipps.hasEscalated() || fireSIF.isTripped() || gasSIF.isTripped()) {
System.out.println("ESD activated from safety layer");
}
| Condition | HIPPS | Fire SIF | Gas SIF | ESD | Result |
|---|---|---|---|---|---|
| Normal operation | ✗ | ✗ | ✗ | ✗ | All systems idle |
| Pressure 96 bara | ✓ | ✗ | ✗ | ✗ | HIPPS isolates, ESD standby |
| Fire detected | ✗ | ✓ | ✗ | ✓ | Fire SIF triggers ESD |
| Gas leak | ✗ | ✗ | ✓ | ✓ | Gas SIF triggers ESD |
| HIPPS fails | ✓ | ✗ | ✗ | ✓ | HIPPS escalates to ESD |
| Fire + Gas | ✗ | ✓ | ✓ | ✓ | Both SIFs trigger ESD |
| All hazards | ✓ | ✓ | ✓ | ✓ | Multiple layers activated |
Process Control: 80-85% MAOP (PID controller setpoint)
BPCS High Alarm: 90% MAOP (operator warning)
HIPPS Activation: 95% MAOP (first SIS layer)
ESD Activation: 98% MAOP (backup SIS layer)
PSV Set Pressure: 100% MAOP (passive protection)
Example for 100 bara MAOP:
| Application | Criticality | Availability Need | Recommended Voting |
|---|---|---|---|
| HIPPS | High | High | 2oo3 (SIL 3) |
| Fire Detection | High | Medium | 2oo3 (SIL 2/3) |
| Gas Detection | High | Medium | 2oo3 (SIL 2/3) |
| ESD | Medium | Medium | 1oo2 or 2oo3 (SIL 1/2) |
| System | Target Response | Typical |
|---|---|---|
| HIPPS | <2 seconds | 1-2 seconds |
| Fire Detection | <5 seconds | 2-5 seconds |
| Gas Detection | <10 seconds | 5-10 seconds |
| ESD | <10 seconds | 5-10 seconds |
| Component | Test Type | Frequency | Bypass Allowed |
|---|---|---|---|
| Pressure transmitters | Calibration | Annual | Yes (1 at a time) |
| Fire detectors | Functional test | 6 months | Yes (1 at a time) |
| Gas detectors | Calibration | 6 months | Yes (1 at a time) |
| HIPPS valve | Partial stroke | Quarterly | No |
| HIPPS valve | Full stroke | Annual | Yes (with backup) |
| ESD valve | Partial stroke | Quarterly | No |
| ESD valve | Full stroke | Annual | Yes (with backup) |
// Bypass detector for maintenance (max 1 at a time)
Detector pt1 = hipps.getPressureSensor(0);
pt1.setBypass(true);
// Check bypass status
for (Detector sensor : hipps.getPressureSensors()) {
if (sensor.isBypassed()) {
System.out.println("WARNING: " + sensor.getName() + " bypassed");
}
}
// Verify bypass constraint
if (bypassCount > maxAllowed) {
System.out.println("ERROR: Too many sensors bypassed");
}
All implemented safety systems comply with IEC 61511:
Documentation for safety systems modeling in NeqSim.
Location: neqsim.process.equipment.safety, neqsim.process.safety
NeqSim provides equipment and logic for modeling process safety systems:
import neqsim.process.equipment.valve.SafetyValve;
SafetyValve psv = new SafetyValve("PSV-100", vessel);
psv.setOpeningPressure(95.0, "barg"); // Set pressure
psv.setFullOpenPressure(100.0, "barg"); // Overpressure
psv.setBlowdownPressure(85.0, "barg"); // Reseating pressure
import neqsim.process.equipment.valve.RuptureDisk;
RuptureDisk disk = new RuptureDisk("RD-100", vessel);
disk.setBurstPressure(110.0, "barg");
disk.setDiameter(150.0, "mm");
import neqsim.process.safety.ESDController;
ESDController esd = new ESDController("ESD-1");
// Add trip conditions
esd.addHighPressureTrip(separator, 100.0, "barg");
esd.addLowPressureTrip(separator, 5.0, "barg");
esd.addHighLevelTrip(separator, 0.9);
esd.addLowLevelTrip(separator, 0.1);
// Add shutdown actions
esd.addShutdownValve(inletValve);
esd.addShutdownValve(outletValve);
| Level | Description | Actions |
|---|---|---|
| ESD-0 | Total shutdown | Full plant shutdown |
| ESD-1 | Process shutdown | Process area isolation |
| ESD-2 | Unit shutdown | Single unit isolation |
| ESD-3 | Equipment shutdown | Single equipment stop |
import neqsim.process.equipment.valve.BlowdownValve;
BlowdownValve blowdown = new BlowdownValve("BDV-100", vessel);
blowdown.setDownstreamPressure(1.0, "barg"); // Flare pressure
blowdown.setOrificeSize(100.0, "mm");
// Run depressuring transient
for (double t = 0; t < 900; t += 1.0) {
blowdown.runTransient();
double P = vessel.getPressure("barg");
double T = vessel.getTemperature("C");
if (P < 7.0) { // 15 minute rule target
System.out.println("Reached target at " + t + " seconds");
break;
}
}
// Calculate heat input from fire
double wettedArea = 50.0; // m²
double Q = 43200 * Math.pow(wettedArea, 0.82); // API 521 formula
vessel.setHeatInput(Q, "W");
// Calculate required relief rate
double reliefRate = psv.getReliefRate("kg/hr");
// API 520 sizing
double area = psv.getRequiredOrificeArea("mm2");
String orifice = psv.getAPIOrificeLetter();
System.out.println("Required area: " + area + " mm²");
System.out.println("API orifice: " + orifice);
| Scenario | Description |
|---|---|
| Blocked outlet | Outlet valve closed |
| Fire case | External fire exposure |
| Tube rupture | Heat exchanger tube failure |
| Power failure | Loss of cooling/control |
| Thermal relief | Liquid expansion |
High Integrity Pressure Protection System.
import neqsim.process.safety.HIPPS;
HIPPS hipps = new HIPPS("HIPPS-1");
// Add sensors (2oo3 voting)
hipps.addPressureSensor(pt1);
hipps.addPressureSensor(pt2);
hipps.addPressureSensor(pt3);
// Set trip point
hipps.setTripPressure(95.0, "barg");
// Add final elements
hipps.addIsolationValve(sdv1);
hipps.addIsolationValve(sdv2);
// Set voting logic
hipps.setVotingLogic("2oo3"); // 2 out of 3
ProcessSystem process = new ProcessSystem();
// Process equipment
Stream feed = new Stream("Feed", feedFluid);
process.add(feed);
Separator separator = new Separator("V-100", feed);
separator.setVolume(10.0, "m3");
process.add(separator);
// PSV on separator
SafetyValve psv = new SafetyValve("PSV-100", separator);
psv.setOpeningPressure(100.0, "barg");
psv.setDischargeStream(flareStream);
process.add(psv);
// Blowdown valve
BlowdownValve bdv = new BlowdownValve("BDV-100", separator);
bdv.setDownstreamPressure(1.0, "barg");
process.add(bdv);
// ESD controller
ESDController esd = new ESDController("ESD");
esd.addHighPressureTrip(separator, 95.0, "barg");
esd.addShutdownAction(() -> {
inletValve.close();
bdv.open();
});
process.add(esd);
// Run simulation
process.run();
// Simulate fire scenario
separator.setHeatInput(500.0, "kW");
for (double t = 0; t < 3600; t += 1.0) {
process.runTransient();
// Check ESD status
if (esd.isTripped()) {
System.out.println("ESD activated at " + t + " s");
}
}
The NeqSim alarm system provides a comprehensive framework for monitoring process variables and managing alarm states throughout the lifecycle of process operations. This guide demonstrates how to configure and handle alarms in a consistent and easy way.
Configure alarms using the fluent builder pattern:
AlarmConfig pressureAlarmConfig = AlarmConfig.builder()
.lowLowLimit(10.0) // LOLO alarm threshold
.lowLimit(20.0) // LO alarm threshold
.highLimit(80.0) // HI alarm threshold
.highHighLimit(90.0) // HIHI alarm threshold
.deadband(2.0) // Deadband to prevent chattering
.delay(3.0) // Time delay before activation (seconds)
.unit("bara") // Engineering unit
.build();
Four standard alarm levels aligned with ISA-18.2:
Centralized coordinator for all process alarms:
ProcessAlarmManager alarmManager = new ProcessAlarmManager();
// Register measurement devices
alarmManager.register(pressureTransmitter);
alarmManager.register(temperatureTransmitter);
alarmManager.register(flowTransmitter);
// Evaluate alarms during simulation
double measuredValue = transmitter.getMeasuredValue();
List<AlarmEvent> events = alarmManager.evaluateMeasurement(
transmitter, measuredValue, dt, currentTime);
// Acknowledge all active alarms
List<AlarmEvent> ackEvents = alarmManager.acknowledgeAll(currentTime);
// Get active alarms
List<AlarmStatusSnapshot> activeAlarms = alarmManager.getActiveAlarms();
// Get complete alarm history
List<AlarmEvent> history = alarmManager.getHistory();
PressureTransmitter pt = new PressureTransmitter("PT-101", stream);
AlarmConfig pressureAlarms = AlarmConfig.builder()
.highLimit(55.0) // HI: 55 bara
.highHighLimit(58.0) // HIHI: 58 bara
.deadband(1.0) // 1 bara deadband
.delay(2.0) // 2 second delay
.unit("bara")
.build();
pt.setAlarmConfig(pressureAlarms);
alarmManager.register(pt);
TemperatureTransmitter tt = new TemperatureTransmitter("TT-101", stream);
AlarmConfig tempAlarms = AlarmConfig.builder()
.highLimit(45.0) // HI: 45°C
.highHighLimit(60.0) // HIHI: 60°C
.deadband(2.0) // 2°C deadband
.delay(5.0) // 5 second delay (slower response)
.unit("C")
.build();
tt.setAlarmConfig(tempAlarms);
alarmManager.register(tt);
FlowTransmitter ft = new FlowTransmitter("FT-201", stream);
AlarmConfig flowAlarms = AlarmConfig.builder()
.lowLowLimit(100.0) // LOLO: 100 kg/hr
.lowLimit(500.0) // LO: 500 kg/hr
.highLimit(20000.0) // HI: 20000 kg/hr
.deadband(50.0) // 50 kg/hr deadband
.delay(3.0) // 3 second delay
.unit("kg/hr")
.build();
ft.setAlarmConfig(flowAlarms);
alarmManager.register(ft);
LevelTransmitter lt = new LevelTransmitter("LT-101", separator);
AlarmConfig levelAlarms = AlarmConfig.builder()
.lowLowLimit(15.0) // LOLO: 15%
.lowLimit(30.0) // LO: 30%
.highLimit(75.0) // HI: 75%
.highHighLimit(90.0) // HIHI: 90%
.deadband(2.0) // 2% deadband
.delay(4.0) // 4 second delay
.unit("%")
.build();
lt.setAlarmConfig(levelAlarms);
alarmManager.register(lt);
Three types of alarm events:
List<AlarmEvent> events = alarmManager.evaluateMeasurement(
transmitter, measuredValue, dt, time);
for (AlarmEvent event : events) {
switch (event.getType()) {
case ACTIVATED:
System.out.println("⚠ ALARM: " + event.getSource() +
" " + event.getLevel() +
" at " + event.getValue());
// Trigger operator notification
// Log to SCADA system
break;
case CLEARED:
System.out.println("✓ CLEARED: " + event.getSource() +
" " + event.getLevel());
// Log clearance
break;
case ACKNOWLEDGED:
System.out.println("✋ ACKNOWLEDGED: " + event.getSource());
// Update alarm display
break;
}
}
List<AlarmStatusSnapshot> activeAlarms = alarmManager.getActiveAlarms();
System.out.println("Active Alarms: " + activeAlarms.size());
for (AlarmStatusSnapshot alarm : activeAlarms) {
String status = alarm.isAcknowledged() ? "[ACK]" : "[NEW]";
System.out.println(status + " " +
alarm.getLevel() + " - " +
alarm.getSource() + ": " +
alarm.getValue());
}
List<AlarmEvent> history = alarmManager.getHistory();
// Count events by type
long activations = history.stream()
.filter(e -> e.getType() == AlarmEventType.ACTIVATED)
.count();
long clearances = history.stream()
.filter(e -> e.getType() == AlarmEventType.CLEARED)
.count();
long acknowledged = history.stream()
.filter(e -> e.getType() == AlarmEventType.ACKNOWLEDGED)
.count();
Alarms can be integrated with ESD logic to trigger automatic actions:
// Monitor pressure alarms
List<AlarmEvent> events = alarmManager.evaluateMeasurement(
pressureTransmitter, pressure, dt, time);
// Check for HIHI alarm activation
for (AlarmEvent event : events) {
if (event.getType() == AlarmEventType.ACTIVATED &&
event.getLevel() == AlarmLevel.HIHI) {
// Trigger ESD logic
esdLogic.activate();
System.out.println("ESD triggered by HIHI pressure alarm");
}
}
See ProcessLogicWithAlarmsExample.java for a complete demonstration showing:
┌─────────────────────────────────────────────────────────────┐
│ ProcessAlarmManager │
│ - Centralized alarm coordination │
│ - Alarm history tracking │
│ - Active alarm monitoring │
└────────────────────┬────────────────────────────────────────┘
│
┌────────────┼────────────┬────────────┐
│ │ │ │
┌───────▼──────┐ ┌──▼──────┐ ┌──▼──────┐ ┌──▼──────┐
│ Pressure TX │ │ Temp TX │ │ Flow TX │ │Level TX │
├──────────────┤ ├─────────┤ ├─────────┤ ├─────────┤
│ AlarmConfig │ │AlarmCfg │ │AlarmCfg │ │AlarmCfg │
│ - HI: 55 │ │- HI: 45 │ │- LO:500 │ │- HI: 75 │
│ - HIHI: 58 │ │-HIHI:60 │ │-LOLO:100│ │-HIHI:90 │
│ - Deadband:1 │ │-Dband:2 │ │-HI:20000│ │- LO: 30 │
│ - Delay: 2s │ │-Delay:5s│ │-Dband:50│ │-LOLO:15 │
└──────────────┘ └─────────┘ └─────────┘ └─────────┘
│ │ │ │
└────────────┴────────────┴────────────┘
│
┌────────────▼────────────────┐
│ AlarmEvent Stream │
│ - ACTIVATED │
│ - CLEARED │
│ - ACKNOWLEDGED │
└────────────┬────────────────┘
│
┌────────────▼────────────────┐
│ Integration Points │
│ - ESD Logic Triggers │
│ - SCADA Display │
│ - Historian Logging │
│ - Operator Notifications │
└─────────────────────────────┘
The NeqSim alarm system provides:
✓ Consistent Configuration: Builder pattern for all alarm types
✓ Flexible Limits: Support for LOLO, LO, HI, HIHI levels
✓ Smart Behavior: Deadband and delay to prevent nuisance alarms
✓ Centralized Management: ProcessAlarmManager for system-wide coordination
✓ Event Tracking: Complete lifecycle from activation to acknowledgement
✓ Safety Integration: Seamless connection to ESD and control logic
This framework enables reliable process monitoring with minimal code complexity while maintaining industrial alarm management best practices.
ProcessLogicAlarmIntegratedExample.java demonstrates a complete, production-ready integration of the NeqSim alarm system with process control and safety logic. This example shows how alarms can trigger automatic control actions, safety responses, and emergency shutdown sequences in a layered protection architecture.
The example implements a comprehensive 5-layer safety system:
Layer 1 (Alarms - SIL-0):
├─ HI/LO Alarms → Operator notification and manual intervention
│
Layer 2 (Alarms + Control - SIL-1):
├─ HIHI/LOLO Alarms → Automatic control responses (valve throttling, etc.)
│
Layer 3 (HIPPS - SIL-2):
├─ Independent fast-acting pressure protection
├─ 2oo3 voting logic on pressure transmitters
├─ Triggered by PT-HIPPS HIHI alarms (59 bara)
│
Layer 4 (ESD - SIL-2):
├─ Emergency shutdown system
├─ Triggered by PT-ESD-001 HIHI alarm (60 bara) or manual button
├─ Full isolation and blowdown sequence
│
Layer 5 (PSV - Mechanical):
└─ Pressure safety valve (65 bara set pressure)
AlarmConfig pressureAlarmConfig = AlarmConfig.builder()
.highLimit(53.0) // HI: Operator notification
.highHighLimit(56.0) // HIHI: Auto throttle valve
.deadband(0.5)
.delay(1.0)
.unit("bara")
.build();
Alarm Actions:
AlarmConfig temperatureAlarmConfig = AlarmConfig.builder()
.highLimit(40.0) // HI: Operator notification
.highHighLimit(55.0) // HIHI: Trigger cooling
.deadband(2.0)
.delay(3.0)
.unit("C")
.build();
Alarm Actions:
AlarmConfig flowAlarmConfig = AlarmConfig.builder()
.lowLimit(100.0) // LO: Operator notification
.lowLowLimit(50.0) // LOLO: Trigger shutdown
.highLimit(2000.0) // HI: High flow warning
.deadband(10.0)
.delay(5.0)
.unit("m3/hr")
.build();
Alarm Actions:
AlarmConfig levelAlarmConfig = AlarmConfig.builder()
.lowLowLimit(20.0) // LOLO: Emergency shutdown
.lowLimit(30.0) // LO: Operator notification
.highLimit(70.0) // HI: High level warning
.highHighLimit(85.0) // HIHI: Critical high level
.deadband(2.0)
.delay(2.0)
.unit("%")
.build();
Alarm Actions:
AlarmConfig hippsAlarmConfig = AlarmConfig.builder()
.highHighLimit(59.0) // Immediate HIPPS closure
.deadband(0.2) // Minimal deadband
.delay(0.0) // No delay - safety critical
.unit("bara")
.build();
2oo3 Voting Logic:
AlarmConfig esdAlarmConfig = AlarmConfig.builder()
.highHighLimit(60.0) // Full ESD sequence
.deadband(0.5)
.delay(0.0) // Immediate ESD trigger
.unit("bara")
.build();
The example runs six comprehensive scenarios demonstrating alarm-triggered logic:
// 1. Build process system
ProcessSystem processSystem = buildProcessSystem();
// 2. Create alarm manager
ProcessAlarmManager alarmManager = new ProcessAlarmManager();
// 3. Setup instrumentation with alarms
InstrumentationSetup instruments =
setupInstrumentationWithAlarms(processSystem, alarmManager);
// 4. Setup process logic
ProcessLogicSetup logicSetup = setupProcessLogic(processSystem, instruments);
// 5. Run scenarios
runAlarmTriggeredScenarios(runner, alarmManager, instruments,
logicSetup, processSystem);
private static List<AlarmEvent> evaluateAndDisplayAlarms(
ProcessAlarmManager alarmManager,
InstrumentationSetup instruments,
ProcessSystem system,
double dt) {
List<AlarmEvent> allEvents = new ArrayList<>();
// Run process to get current values
system.run();
// Evaluate each measurement device
double sepPressure = instruments.separatorPT.getMeasuredValue();
allEvents.addAll(alarmManager.evaluateMeasurement(
instruments.separatorPT, sepPressure, dt, simulationTime));
// ... evaluate other transmitters ...
return allEvents;
}
private static void handlePressureHIHIAlarm(List<AlarmEvent> events,
ProcessSystem system,
ProcessAlarmManager alarmManager) {
for (AlarmEvent event : events) {
if (event.getType() == AlarmEventType.ACTIVATED &&
event.getLevel() == AlarmLevel.HIHI &&
event.getSource().equals("PT-101")) {
// Automatic control response
ControlValve inletValve =
(ControlValve) system.getUnit("Inlet Control Valve");
inletValve.setPercentValveOpening(50.0);
// Run system with new valve position
system.run();
// Acknowledge alarm after action
alarmManager.acknowledgeAll(simulationTime);
}
}
}
private static void handleHIPPSAlarm(List<AlarmEvent> events,
ESDLogic hippsLogic,
ProcessAlarmManager alarmManager) {
for (AlarmEvent event : events) {
if (event.getType() == AlarmEventType.ACTIVATED &&
event.getLevel() == AlarmLevel.HIHI &&
event.getSource().startsWith("PT-HIPPS")) {
// Activate HIPPS logic
hippsLogic.activate();
alarmManager.acknowledgeAll(simulationTime);
break; // Only need one HIPPS transmitter to trigger
}
}
}
The example generates comprehensive reports:
Shows currently active alarms with acknowledgement status:
┌─────────────────────────────────────────────────────────┐
│ ALARM STATUS: After HIHI Alarm + Auto Control │
├─────────────────────────────────────────────────────────┤
│ Active Alarms: 1 │
├─────────────────────────────────────────────────────────┤
│ [ACK] HIHI - PT-101 : 57.00 │
└─────────────────────────────────────────────────────────┘
Shows all alarm events with timestamps:
╔════════════════════════════════════════════════════════════════╗
║ ALARM HISTORY REPORT ║
╠════════════════════════════════════════════════════════════════╣
║ Total Events: 12 ║
╠════════════════════════════════════════════════════════════════╣
║ Recent Events (last 10): ║
║ ⚠ 30.0s ACTIVATED PT-101 HI 53.50 ║
║ ⚠ 35.0s ACTIVATED PT-101 HIHI 57.00 ║
║ ✋ 35.5s ACKNOWLEDGED PT-101 HIHI 57.00 ║
║ ... ║
╚════════════════════════════════════════════════════════════════╝
Aggregated statistics by type and level:
╔════════════════════════════════════════════════════════════════╗
║ ALARM STATISTICS ║
╠════════════════════════════════════════════════════════════════╣
║ Total Activations: 8 ║
║ Total Clearances: 3 ║
║ Total Acknowledgements: 5 ║
║ ║
║ By Level: ║
║ HIHI (Critical High): 4 ║
║ HI (High): 2 ║
║ LO (Low): 1 ║
║ LOLO (Critical Low): 1 ║
╚════════════════════════════════════════════════════════════════╝
// Monitor for HIHI alarm
if (alarm.getLevel() == AlarmLevel.HIHI) {
// Implement automatic control response
valve.setPercentValveOpening(safeValue);
system.run();
alarmManager.acknowledgeAll(time);
}
// Monitor for safety-critical alarm
if (alarm.getLevel() == AlarmLevel.HIHI &&
alarm.getSource().equals("PT-ESD-001")) {
// Activate safety logic
esdLogic.activate();
}
// Evaluate alarms
List<AlarmEvent> events = evaluateAlarms();
// Process events
for (AlarmEvent event : events) {
if (event.getType() == AlarmEventType.ACTIVATED) {
// Log alarm activation
logger.logAlarm(event);
// Notify operator
operatorPanel.displayAlarm(event);
}
}
// Acknowledge after operator review or automatic action
alarmManager.acknowledgeAll(currentTime);
Layered Protection: Multiple independent protection layers from alarms to mechanical safety devices
Appropriate Delays:
Deadband Configuration:
Alarm Actions:
Acknowledgement:
Comprehensive Logging:
# Compile
mvn compile
# Run
mvn exec:java -Dexec.mainClass="neqsim.process.util.example.ProcessLogicAlarmIntegratedExample"
✅ Consistent Framework: All alarms configured using the same AlarmConfig builder pattern
✅ Flexible Triggering: Alarms can trigger operator notifications, control actions, or safety logic
✅ Centralized Management: ProcessAlarmManager coordinates all process alarms
✅ Safety Integration: Seamless connection between alarms and SIL-rated safety systems
✅ Production-Ready: Complete with logging, statistics, and acknowledgement workflows
✅ ISA-18.2 Aligned: Four standard alarm levels (LOLO, LO, HI, HIHI)
This example provides a complete template for implementing alarm-triggered process control and safety logic in industrial applications using NeqSim.
This implementation demonstrates a comprehensive Emergency Shutdown (ESD) system with fire alarm voting logic in NeqSim. The system showcases how multiple fire detectors can be used in a voting configuration to prevent spurious trips while ensuring safety when multiple alarms confirm a fire event.
Location: src/main/java/neqsim/process/measurementdevice/FireDetector.java
A new binary sensor for fire detection with features:
Example Usage:
FireDetector fireDetector = new FireDetector("FD-101", "Separator Area - North");
fireDetector.setDetectionThreshold(0.5);
fireDetector.setDetectionDelay(1.0);
// Configure alarm
AlarmConfig alarmConfig = AlarmConfig.builder()
.highLimit(0.5)
.delay(1.0)
.unit("binary")
.build();
fireDetector.setAlarmConfig(alarmConfig);
// Detect fire
fireDetector.detectFire();
// Check status
if (fireDetector.isFireDetected()) {
System.out.println("Fire detected!");
}
Requires both fire detectors to activate before triggering ESD. Provides:
Any two of three detectors trigger ESD. Provides:
Implementation:
// Count active alarms
int activeAlarms = (fireDetector1.isFireDetected() ? 1 : 0)
+ (fireDetector2.isFireDetected() ? 1 : 0)
+ (fireDetector3.isFireDetected() ? 1 : 0);
// Apply voting logic
boolean esdShouldActivate = (activeAlarms >= 2);
if (esdShouldActivate && !bdValve.isActivated()) {
bdValve.activate();
gasSplitter.setSplitFactors(new double[] {0.0, 1.0}); // Redirect to blowdown
}
The test demonstrates a realistic ESD scenario:
Phase 1: Normal Operation (t=0-5s)
Phase 2: First Fire Alarm (t=5s)
Phase 3: Second Fire Alarm (t=10s)
Phase 4: Blowdown with Emissions Tracking (t=10-20s)
System Configuration:
Simulation Results (20-second blowdown):
Time (s) | FD-101 | FD-102 | Alarms | BD Open (%) | BD Flow (kg/hr) | Flare Heat (MW) | CO2 Rate (kg/s) | Cumul Heat (GJ) | Cumul CO2 (kg)
---------|--------|--------|--------|-------------|-----------------|-----------------|-----------------|-----------------|----------------
0.0 | FIRE | FIRE | 2 | 0.0 | 817,553 | 11,657 | 626.4 | 11.66 | 626.4
2.0 | FIRE | FIRE | 2 | 0.0 | 814,203 | 11,610 | 623.8 | 34.90 | 1875.3
10.0 | FIRE | FIRE | 2 | 20.0 | 803,959 | 11,464 | 616.0 | 127.08 | 6828.3
14.0 | FIRE | FIRE | 2 | 100.0 | 800,259 | 11,411 | 613.1 | 172.80 | 9285.0
20.0 | FIRE | FIRE | 2 | 100.0 | 795,957 | 11,349 | 609.8 | 241.04 | 12951.8
Final Summary:
Tests voting combinations:
Key Safety Feature: BD valve remains activated even when alarms clear, requiring manual reset to prevent automatic system restoration during emergency.
┌──────────────────────────────────────────────────────────────┐
│ ESD FIRE ALARM SYSTEM │
└──────────────────────────────────────────────────────────────┘
Fire Detectors: Voting Logic:
┌────────────┐ ┌─────────────────┐
│ FD-101 │─────┐ │ Count Active │
│ (North) │ │ │ Alarms >= 2? │──► ESD Trigger
└────────────┘ ├──────────►│ │
│ └─────────────────┘
┌────────────┐ │
│ FD-102 │─────┤
│ (South) │ │
└────────────┘ │
│
┌────────────┐ │
│ FD-103 │─────┘
│ (East) │ [Optional - for 2-out-of-3]
└────────────┘
Process Flow:
┌────────────┐ ┌──────────┐ ┌─────────────┐
│ Separator │────►│ Splitter │────►│ To Process │
│ 50 bara │ └──────────┘ └─────────────┘
└────────────┘ │
│ (ESD redirects flow)
▼
┌─────────────┐
│ BD Valve │
│ (opens 5s) │
└─────────────┘
│
▼
┌─────────────┐
│ Orifice │ (flow control)
└─────────────┘
│
▼
┌─────────────┐
│ Flare │ ◄── Heat & CO2
│ 1.5 bara │ Calculations
└─────────────┘
The flare tracks cumulative values during blowdown:
Heat Release:
Q = LCV × Flow_rateCO2 Emissions:
CO2 = Σ(moles_C × MW_CO2)Gas Burned:
Run all ESD fire alarm tests:
mvnw test -Dtest=ESDFireAlarmSystemTest
Run specific test:
mvnw test -Dtest=ESDFireAlarmSystemTest#testESDWithTwoFireAlarmVoting
Compatible Equipment:
Separator - Source vessel for blowdownSplitter - Flow routing between process and blowdownBlowdownValve - ESD-activated valve with transient openingOrifice - ISO 5167 flow restrictionFlare - Combustion and emissions trackingPushButton - Manual ESD activation (can be combined with fire alarms)Alarm System Integration:
FireDetector extends MeasurementDeviceBaseClassAlarmConfig, AlarmState, AlarmLevelThis implementation demonstrates concepts used in:
Potential additions:
docs/ESD_BLOWDOWN_SYSTEM.mddocs/wiki/process_control.mddocs/wiki/test-overview.mdsrc/main/java/neqsim/process/alarm/src/main/java/neqsim/process/measurementdevice/FireDetector.javasrc/test/java/neqsim/process/equipment/valve/ESDFireAlarmSystemTest.javadocs/wiki/esd_fire_alarm_system.mdBlowdownValve - ESD-activated valveFlare - Emissions calculationsAlarmConfig - Alarm configurationOrifice - Flow controlSeparator - Dynamic vessel simulation/**
* Custom voting logic class for ESD systems.
*/
public class ESDVotingLogic {
private final List<FireDetector> detectors;
private final int requiredAlarms;
private final BlowdownValve bdValve;
public ESDVotingLogic(BlowdownValve bdValve, int requiredAlarms,
FireDetector... detectors) {
this.bdValve = bdValve;
this.requiredAlarms = requiredAlarms;
this.detectors = Arrays.asList(detectors);
}
public void evaluate() {
int activeAlarms = (int) detectors.stream()
.filter(FireDetector::isFireDetected)
.count();
if (activeAlarms >= requiredAlarms && !bdValve.isActivated()) {
bdValve.activate();
System.out.println("ESD ACTIVATED: " + activeAlarms +
" of " + detectors.size() + " alarms active");
}
}
public boolean isESDActive() {
return bdValve.isActivated();
}
}
// Usage:
ESDVotingLogic esdLogic = new ESDVotingLogic(bdValve, 2,
fireDetector1,
fireDetector2,
fireDetector3);
// In simulation loop:
esdLogic.evaluate();
Author: ESOL
Date: November 2025
Version: 1.0
This example demonstrates how to perform a dynamic safety calculation for sizing a pressure safety valve (PSV) using NeqSim's transient simulation capabilities. The scenario simulates a blocked outlet condition where a pressure control valve suddenly closes, causing pressure to rise in a separator until the PSV opens to prevent overpressure.
The simulation models a sudden blocked outlet scenario:
// Create gas system
SystemInterface feedFluid = new SystemSrkEos(273.15 + 40.0, 50.0);
feedFluid.addComponent("nitrogen", 1.0);
feedFluid.addComponent("methane", 85.0);
// ... additional components
// Create separator
Separator separator = new Separator("HP Separator", feedStream);
separator.setCalculateSteadyState(false); // Enable dynamic mode
// Split gas to PCV and PSV
Splitter gasSplitter = new Splitter("Gas Splitter", separator.getGasOutStream(), 2);
gasSplitter.setSplitFactors(new double[] {0.999, 0.001});
gasSplitter.setCalculateSteadyState(false);
The SafetyValve class now automatically controls its opening based on inlet pressure during dynamic simulations. When runTransient() is called, the valve:
// PSV configured with set and full open pressures
SafetyValve pressureSafetyValve = new SafetyValve("PSV-001", stream);
pressureSafetyValve.setPressureSpec(55.0); // Set pressure
pressureSafetyValve.setFullOpenPressure(60.5); // Full open pressure
pressureSafetyValve.setCalculateSteadyState(false); // Enable dynamic mode
// PSV automatically calculates opening in runTransient()
// No manual opening calculation needed!
double dt = 0.5; // Time step in seconds
for (int i = 0; i < numSteps; i++) {
currentTime = i * dt;
// Simulate blocked outlet at t=50s
if (currentTime >= 50.0 && currentTime < 51.0) {
pressureControlValve.setPercentValveOpening(1.0);
}
// Run transient calculations
// PSV automatically adjusts its opening based on inlet pressure
separator.runTransient(dt, id);
gasSplitter.runTransient(dt, id);
pressureControlValve.runTransient(dt, id);
pressureSafetyValve.runTransient(dt, id); // Automatic PSV control
}
The test validates several critical aspects:
The example is implemented as a JUnit test:
mvnw test -Dtest=SafetyValveDynamicSizingTest
You can modify the following parameters to study different scenarios:
SystemInterface setupsetInternalDiameter(), setSeparatorLength()setPressureSpec()setFullOpenPressure()setCv() - size the valve appropriatelydt variable - smaller for better accuracycurrentTimeThis example demonstrates how to perform a dynamic safety calculation for a pressure safety valve (PSV) sizing using NeqSim's transient simulation capabilities.
A high-pressure separator operates at ~50 bara with gas output flowing through a splitter:
The PSV implements realistic hysteresis (blowdown) behavior to prevent valve chattering:
Key Point: Once the PSV opens, it does NOT close immediately when pressure drops below the set pressure. It stays open until pressure drops to the blowdown/reseat pressure. This prevents rapid cycling (chattering) that could damage the valve.
// Setup equipment
Separator separator = new Separator("HP Separator", feedStream);
Splitter gasSplitter = new Splitter("Gas Splitter", separator.getGasOutStream(), 2);
ThrottlingValve pressureControlValve = new ThrottlingValve("PCV-001", gasSplitter.getSplitStream(0));
SafetyValve pressureSafetyValve = new SafetyValve("PSV-001", gasSplitter.getSplitStream(1));
// Configure PSV with automatic opening
pressureSafetyValve.setPressureSpec(55.0); // Set pressure (bara)
pressureSafetyValve.setFullOpenPressure(60.5); // Full open at 110% of set
// Blowdown is automatically set to 7% (reseat at 51.15 bara)
// Alternative: Explicitly set blowdown percentage
pressureSafetyValve.setBlowdown(10.0); // 10% blowdown for liquid service
// Dynamic simulation loop
for (int i = 0; i < numSteps; i++) {
currentTime = i * dt;
// Simulate events (blockage, recovery, etc.)
if (currentTime >= 50.0 && currentTime < 51.0) {
pressureControlValve.setPercentValveOpening(1.0); // Block outlet
}
if (currentTime >= 200.0 && currentTime < 201.0) {
pressureControlValve.setPercentValveOpening(50.0); // Recover
}
// Run transient calculations
// PSV opening is calculated automatically based on inlet pressure
separator.runTransient(dt, id);
gasSplitter.runTransient(dt, id);
pressureControlValve.runTransient(dt, id);
pressureSafetyValve.runTransient(dt, id); // Automatic PSV control with hysteresis
}
The SafetyValve.runTransient() method automatically:
From the test simulation with 5000 kg/hr feed:
| Parameter | Value |
|---|---|
| Feed flow rate | 5000 kg/hr |
| PSV set pressure | 55.0 bara |
| PSV full open pressure | 60.5 bara |
| PSV blowdown pressure | 51.15 bara (7% blowdown) |
| Maximum separator pressure | 58.69 bara |
| Maximum PSV relief flow | 6086 kg/hr |
| PSV opening at max pressure | 67.1% |
PSV prevents catastrophic overpressure: Maximum pressure (58.69 bara) is well below the full open pressure (60.5 bara), demonstrating effective pressure control.
Adequate relief capacity: PSV relieves 6086 kg/hr, which exceeds the feed rate (5000 kg/hr), ensuring the valve can handle the relief scenario.
Hysteresis prevents chattering:
Smooth pressure control: The automatic PSV control provides smooth pressure regulation during both pressure buildup and recovery phases.
Always use dynamic mode: Set setCalculateSteadyState(false) for all equipment in transient simulations
Size PSV conservatively: Ensure PSV can handle at least 100% of the feed flow rate
Set appropriate blowdown: Use 7-10% for gas, 10-20% for liquid service to prevent chattering
Use unique UUID: Create one UUID per simulation run to track transient state correctly
Choose appropriate time step: 0.5 seconds provides good resolution for PSV dynamics
Monitor key parameters: Track separator pressure, valve openings, and flow rates throughout the simulation
SafetyValveDynamicSizingTest.javasrc/main/java/neqsim/process/equipment/valve/SafetyValve.javaThe PSDValve (Process Shutdown Valve) is a safety isolation valve that automatically closes when a High-High (HIHI) pressure alarm is triggered. It provides emergency shutdown protection by monitoring a pressure transmitter and rapidly closing to prevent overpressure conditions from propagating through the process.
// Create pressure transmitter monitoring separator inlet
PressureTransmitter PT = new PressureTransmitter("PT-101", separatorInlet);
// Configure HIHI alarm at 55 bara with 1 bara deadband and 0.5 second delay
AlarmConfig alarmConfig = AlarmConfig.builder()
.highHighLimit(55.0) // HIHI trip point
.deadband(1.0) // Alarm clears at 54 bara
.delay(0.5) // 0.5 second confirmation delay
.unit("bara")
.build();
PT.setAlarmConfig(alarmConfig);
// Create PSD valve on separator inlet
PSDValve psdValve = new PSDValve("PSD-101", feedStream);
psdValve.setPercentValveOpening(100.0); // Start fully open
psdValve.setCv(150.0); // Sizing coefficient
psdValve.setClosureTime(2.0); // 2 seconds fast closure
// Link to pressure transmitter
psdValve.linkToPressureTransmitter(PT);
double time = 0.0;
double dt = 1.0; // 1 second time step
while (time < simulationTime) {
// Update pressure measurement
double measuredPressure = psdValve.getOutletStream().getPressure("bara");
PT.evaluateAlarm(measuredPressure, dt, time);
// Run equipment transient calculations
psdValve.runTransient(dt, UUID.randomUUID());
separator.runTransient(dt, UUID.randomUUID());
// Check if valve has tripped
if (psdValve.hasTripped()) {
System.out.println("PSD VALVE TRIPPED - Emergency shutdown activated!");
// Implement emergency response...
}
time += dt;
}
// After alarm clears and situation is safe
if (psdValve.hasTripped()) {
psdValve.reset(); // Clear trip state
psdValve.setPercentValveOpening(100.0); // Manually reopen valve
}
import neqsim.process.equipment.valve.PSDValve;
import neqsim.process.equipment.separator.Separator;
import neqsim.process.equipment.stream.Stream;
import neqsim.process.alarm.AlarmConfig;
import neqsim.process.measurementdevice.PressureTransmitter;
import neqsim.thermo.system.SystemInterface;
import neqsim.thermo.system.SystemSrkEos;
import java.util.UUID;
public class PSDValveExample {
public static void main(String[] args) {
// Create natural gas mixture at 50 bara, 40°C
SystemInterface fluid = new SystemSrkEos(273.15 + 40, 50.0);
fluid.addComponent("nitrogen", 0.5);
fluid.addComponent("CO2", 1.0);
fluid.addComponent("methane", 85.0);
fluid.addComponent("ethane", 8.0);
fluid.addComponent("propane", 4.0);
fluid.addComponent("i-butane", 0.75);
fluid.addComponent("n-butane", 0.75);
fluid.setMixingRule(2);
// Create feed stream
Stream feedStream = new Stream("Feed", fluid);
feedStream.setFlowRate(5000.0, "kg/hr");
feedStream.setTemperature(40.0, "C");
feedStream.setPressure(50.0, "bara");
feedStream.run();
// Create PSD valve on separator inlet
PSDValve psdValve = new PSDValve("PSD-101", feedStream);
psdValve.setPercentValveOpening(100.0);
psdValve.setCv(150.0);
psdValve.setClosureTime(2.0); // 2 second fast closure
psdValve.run();
// Create pressure transmitter monitoring PSD outlet (separator inlet)
PressureTransmitter PT = new PressureTransmitter("PT-101",
psdValve.getOutletStream());
// Configure HIHI alarm
AlarmConfig alarmConfig = AlarmConfig.builder()
.highHighLimit(55.0) // PAHH at 55 bara
.deadband(1.0)
.delay(0.5)
.unit("bara")
.build();
PT.setAlarmConfig(alarmConfig);
// Link PSD valve to pressure transmitter
psdValve.linkToPressureTransmitter(PT);
// Create separator
Separator separator = new Separator("Separator", psdValve.getOutletStream());
separator.setInternalDiameter(1.5);
separator.setSeparatorLength(4.0);
separator.run();
// Dynamic simulation
double time = 0.0;
double dt = 1.0;
System.out.println("=== PSD VALVE PROTECTION SYSTEM ===");
System.out.println("HIHI setpoint: 55.0 bara");
System.out.println("PSD closure time: 2.0 seconds\n");
for (int i = 0; i < 100; i++) {
// Run transient calculations
psdValve.runTransient(dt, UUID.randomUUID());
separator.runTransient(dt, UUID.randomUUID());
time += dt;
// Evaluate alarm
double pressure = psdValve.getOutletStream().getPressure("bara");
PT.evaluateAlarm(pressure, dt, time);
// Status reporting
if (i % 10 == 0 || psdValve.hasTripped()) {
String alarmState = "NONE";
if (PT.getAlarmState().isActive()) {
alarmState = PT.getAlarmState().getActiveLevel().toString();
}
System.out.printf("Time: %5.0f s | Pressure: %5.2f bara | " +
"Alarm: %4s | PSD: %5.1f %% | Tripped: %3s%n",
time, pressure, alarmState,
psdValve.getPercentValveOpening(),
psdValve.hasTripped() ? "YES" : "NO");
}
if (psdValve.hasTripped()) {
System.out.println("\n*** EMERGENCY SHUTDOWN ACTIVATED ***");
break;
}
}
}
}
The PSD valve relies on NeqSim's alarm system. Key parameters:
| Parameter | Description | Typical Value |
|---|---|---|
highHighLimit |
HIHI trip pressure | 110% of MAWP |
highLimit |
High alarm (warning only) | 105% of MAWP |
deadband |
Hysteresis to prevent chattering | 1-2% of setpoint |
delay |
Confirmation time before alarm | 0.1-2.0 seconds |
unit |
Engineering unit | "bara", "barg", "psia" |
PSDValve(String name)
PSDValve(String name, StreamInterface inletStream)
void linkToPressureTransmitter(MeasurementDeviceInterface transmitter)
void setClosureTime(double closureTime) // seconds
void setTripEnabled(boolean enabled)
void setCv(double Cv) // Valve sizing coefficient
boolean hasTripped()
boolean isTripEnabled()
double getClosureTime()
MeasurementDeviceInterface getPressureTransmitter()
void reset() // Clear trip state
void setPercentValveOpening(double opening) // 0-100%
@Override
void runTransient(double dt, UUID id)
| Feature | PSD Valve | Safety Valve |
|---|---|---|
| Activation | HIHI alarm signal | Pressure overcomes spring |
| Response Time | 1-5 seconds (fast) | Milliseconds (immediate) |
| Reopening | Manual reset required | Automatic reseating |
| Primary Use | Isolation/shutdown | Pressure relief |
| Flow Direction | Stops inlet flow | Vents to atmosphere/flare |
| Typical Location | Inlet to equipment | Top of vessel/equipment |
| Failure Mode | Should fail closed | Must fail open |
The PSD valve integrates with NeqSim's process safety features:
This document explains the rupture disk implementation in NeqSim, demonstrating the key difference between rupture disks and pressure safety valves (PSVs).
A rupture disk (also called a bursting disc) is a non-reclosing pressure relief device that:
This is fundamentally different from a safety valve which:
Rupture disks are typically used for:
RuptureDisk disk = new RuptureDisk("RD-001", inletStream);
disk.setBurstPressure(55.0); // bara - disk ruptures at this pressure
disk.setFullOpenPressure(57.75); // bara - fully open (typically 5% above burst)
disk.setOutletPressure(1.0, "bara");
disk.setCv(150.0);
disk.setCalculateSteadyState(false);
| Parameter | Description | Typical Value |
|---|---|---|
| Burst Pressure | Pressure at which disk ruptures | Set by design |
| Full Open Pressure | Pressure for 100% opening | 105-110% of burst |
| Cv | Flow coefficient | Sized for relief scenario |
The rupture disk automatically:
hasRuptured() flagPressure rises → Opens at 55 bara → Relieves pressure
Pressure drops → Stays open until 51.15 bara (blowdown)
Pressure below blowdown → Closes → Can reopen if needed
Pressure rises → Bursts at 55 bara → Relieves pressure
Pressure drops → STAYS 100% OPEN
Pressure at any level → STAYS 100% OPEN (one-time device)
// Setup separator with gas splitter
Separator separator = new Separator("HP Separator", feedStream);
Splitter gasSplitter = new Splitter("Gas Splitter", separator.getGasOutStream(), 2);
// Normal operation path
ThrottlingValve pcv = new ThrottlingValve("PCV-001", gasSplitter.getSplitStream(0));
pcv.setPercentValveOpening(50.0);
// Emergency relief path
RuptureDisk disk = new RuptureDisk("RD-001", gasSplitter.getSplitStream(1));
disk.setBurstPressure(55.0);
disk.setFullOpenPressure(57.75);
// Dynamic simulation
UUID id = UUID.randomUUID();
for (int i = 0; i < numSteps; i++) {
double time = i * dt;
// Simulate PCV blockage at t=50s
if (time >= 50.0 && time < 51.0) {
pcv.setPercentValveOpening(1.0);
}
// Simulate PCV recovery at t=200s
if (time >= 200.0 && time < 201.0) {
pcv.setPercentValveOpening(50.0);
}
// Run transient - disk bursts automatically
separator.runTransient(dt, id);
gasSplitter.runTransient(dt, id);
pcv.runTransient(dt, id);
disk.runTransient(dt, id); // Automatic rupture control
}
From RuptureDiskDynamicTest:
Time: 0-120s: Normal operation, disk closed, pressure below 55 bara
Time: ~130s: Disk ruptures at 55 bara
Time: 140-200s: Pressure controlled at ~53 bara, disk 100% open
Time: 200-300s: PCV reopens, pressure drops to 30 bara
→ Disk STILL 100% open!
| Metric | Value |
|---|---|
| Burst pressure | 55.0 bara |
| Max pressure | 55.35 bara |
| Max relief flow | 5950 kg/hr |
| Final pressure | 30.5 bara |
| Final disk opening | 100% |
Critical Behavior: Disk remained fully open even though pressure dropped 24.5 bara below the burst pressure!
For simulation purposes, you can reset a ruptured disk:
disk.reset(); // Simulates disk replacement
// Disk is now unruptured and closed
// In reality, you would physically replace the disk
reset() method in simulations to test multiple scenariosRuptureDisk.javaRuptureDiskDynamicTest.javapsv_dynamic_sizing_example.mdA complete HIPPS (High Integrity Pressure Protection System) implementation has been added to NeqSim for safety simulation and analysis. HIPPS is a Safety Instrumented System (SIS) that prevents overpressure by shutting down the source of pressure before it reaches unsafe levels, providing an alternative or complement to traditional pressure relief devices (PSVs/rupture disks).
File: src/main/java/neqsim/process/equipment/valve/HIPPSValve.java
ThrottlingValveKey Features:
File: src/test/java/neqsim/process/equipment/valve/HIPPSValveTest.java
Test Coverage:
File: docs/hipps_implementation.md
Documentation Includes:
File: src/main/java/neqsim/process/util/example/HIPPSExample.java
Example Features:
ThrottlingValve (base)
└── HIPPSValve (new)
HIPPSValve
├── MeasurementDeviceInterface (pressure transmitters)
├── AlarmState (HIHI alarm monitoring)
├── ProcessEquipmentBaseClass (standard equipment interface)
└── Serializable (state persistence)
public enum VotingLogic {
ONE_OUT_OF_ONE("1oo1"),
ONE_OUT_OF_TWO("1oo2"),
TWO_OUT_OF_TWO("2oo2"),
TWO_OUT_OF_THREE("2oo3"), // Recommended for SIL 2/3
TWO_OUT_OF_FOUR("2oo4")
}
// Create HIPPS valve
HIPPSValve hipps = new HIPPSValve("HIPPS-XV-001", feedStream);
// Add redundant transmitters
hipps.addPressureTransmitter(PT1);
hipps.addPressureTransmitter(PT2);
hipps.addPressureTransmitter(PT3);
// Configure for SIL 3
hipps.setVotingLogic(HIPPSValve.VotingLogic.TWO_OUT_OF_THREE);
hipps.setSILRating(3);
hipps.setClosureTime(3.0); // 3 seconds
// In transient simulation
PT1.evaluateAlarm(pressure, dt, time);
PT2.evaluateAlarm(pressure, dt, time);
PT3.evaluateAlarm(pressure, dt, time);
hipps.runTransient(dt, UUID.randomUUID());
if (hipps.hasTripped()) {
System.out.println("HIPPS activated - overpressure prevented");
}
| Aspect | HIPPS | PSV |
|---|---|---|
| Action | Stops flow (isolation) | Relieves pressure (venting) |
| Trip Point | Below MAWP (e.g., 90%) | At/above MAWP |
| Emissions | Prevents flaring | Releases to flare |
| SIL Rating | SIL 2 or SIL 3 | Mechanical (non-SIL) |
| Response | 2-5 seconds | Instantaneous |
| Redundancy | Multiple transmitters | Single device |
| Testing | Partial stroke, proof tests | Periodic inspection |
# Windows (cmd)
.\mvnw test -Dtest=HIPPSValveTest
# Windows (PowerShell)
.\mvnw.cmd test -Dtest=HIPPSValveTest
# Linux/Mac
./mvnw test -Dtest=HIPPSValveTest
# Compile and run
.\mvnw exec:java -Dexec.mainClass="neqsim.process.util.example.HIPPSExample"
// HIPPS uses existing alarm infrastructure
AlarmConfig hippsAlarm = AlarmConfig.builder()
.highHighLimit(90.0)
.deadband(2.0)
.delay(0.5)
.unit("bara")
.build();
PT.setAlarmConfig(hippsAlarm);
// HIPPS participates in transient calculations
hipps.runTransient(dt, UUID.randomUUID());
// Create redundant transmitters
PressureTransmitter PT1 = new PressureTransmitter("PT-A", stream);
PressureTransmitter PT2 = new PressureTransmitter("PT-B", stream);
PressureTransmitter PT3 = new PressureTransmitter("PT-C", stream);
// Configure alarms at trip point
AlarmConfig alarm = AlarmConfig.builder()
.highHighLimit(tripPoint)
.deadband(2.0)
.delay(0.5)
.unit("bara")
.build();
// Create HIPPS with voting
HIPPSValve hipps = new HIPPSValve("HIPPS-XV-001", stream);
hipps.addPressureTransmitter(PT1);
hipps.addPressureTransmitter(PT2);
hipps.addPressureTransmitter(PT3);
hipps.setVotingLogic(HIPPSValve.VotingLogic.TWO_OUT_OF_THREE);
hipps.setSILRating(3);
// PSV provides backup protection
SafetyValve psv = new SafetyValve("PSV-001", stream);
psv.setPressureSpec(mawp); // Set at MAWP
for (double time = 0; time < totalTime; time += dt) {
// Update process conditions
// ...
// Evaluate alarms
PT1.evaluateAlarm(pressure, dt, time);
PT2.evaluateAlarm(pressure, dt, time);
PT3.evaluateAlarm(pressure, dt, time);
// Run HIPPS
hipps.runTransient(dt, UUID.randomUUID());
// Check protection status
if (hipps.hasTripped()) {
// HIPPS activated - analyze response
}
}
// Get comprehensive diagnostics
System.out.println(hipps.getDiagnostics());
// Verify safety objectives
boolean preventedPsvLift = !psv.getPercentValveOpening() > 0;
boolean belowMAWP = maxPressure < mawp;
boolean trippedCorrectly = hipps.hasTripped();
| Application | Recommended Voting | SIL Level |
|---|---|---|
| Low risk, simple | 1oo1 | SIL 1 |
| Medium risk | 1oo2 or 2oo3 | SIL 2 |
| High risk, critical | 2oo3 | SIL 3 |
The HIPPS implementation in NeqSim provides comprehensive capabilities for safety simulation and analysis:
✅ Complete SIS modeling with voting logic and redundancy ✅ Realistic transient behavior including closure dynamics ✅ SIL-rated configuration (SIL 1, 2, 3) per industry standards ✅ Comprehensive testing support (partial stroke, proof tests) ✅ Integration with existing safety systems (PSV, PSD, alarms) ✅ Extensive documentation and working examples ✅ Production-ready code with full test coverage
Key Advantage: HIPPS prevents overpressure before it occurs, eliminating flaring and protecting equipment, while PSVs relieve pressure after it exceeds safe limits. For safety-critical applications, HIPPS + PSV provides robust defense-in-depth protection.
Implementation follows NeqSim architecture patterns and coding standards for process safety simulation, consistent with existing ESD, PSD, and safety valve implementations.
HIPPS is a Safety Instrumented System (SIS) designed to prevent overpressure by shutting down the source of pressure rather than relieving it through pressure safety valves (PSVs) or rupture disks. This document describes the HIPPS implementation in NeqSim and provides guidance for safety simulations.
High Integrity Pressure Protection System (HIPPS) is an automated safety system that:
| Aspect | HIPPS | PSV (Pressure Safety Valve) |
|---|---|---|
| Action | Stops flow (isolation) | Relieves pressure (venting) |
| Trip Point | Below MAWP (e.g., 90%) | At or above MAWP |
| Environmental Impact | Prevents flaring | Releases to flare |
| Safety Rating | SIL 2 or SIL 3 | Mechanical (non-SIL) |
| Testing | Partial stroke, proof tests | Periodic inspection |
| Response Time | 2-5 seconds typical | Instantaneous (spring-loaded) |
| Redundancy | Multiple transmitters, voting logic | Single device |
| Failure Mode | Fail-safe (close) or diagnosed | Fail-safe (open) |
| Cost | Higher initial cost | Lower initial cost |
| Application | Subsea, closed systems | General overpressure protection |
Location: src/main/java/neqsim/process/equipment/valve/HIPPSValve.java
Key Features:
HIPPS uses redundant pressure transmitters with voting logic to prevent spurious trips while maintaining safety:
// Create high-pressure feed stream
SystemInterface fluid = new SystemSrkEos(298.15, 100.0);
fluid.addComponent("methane", 85.0);
fluid.addComponent("ethane", 10.0);
fluid.addComponent("propane", 5.0);
fluid.setMixingRule("classic");
fluid.createDatabase(true);
Stream feedStream = new Stream("HP Feed", fluid);
feedStream.setFlowRate(20000.0, "kg/hr");
feedStream.setPressure(100.0, "bara");
feedStream.setTemperature(40.0, "C");
feedStream.run();
// Create three redundant pressure transmitters
PressureTransmitter PT1 = new PressureTransmitter("PT-101A", feedStream);
PressureTransmitter PT2 = new PressureTransmitter("PT-101B", feedStream);
PressureTransmitter PT3 = new PressureTransmitter("PT-101C", feedStream);
// Configure HIHI alarm at 90 bara (below 100 bara MAWP)
AlarmConfig hippsAlarm = AlarmConfig.builder()
.highHighLimit(90.0) // HIPPS trips at 90% of MAWP
.deadband(2.0)
.delay(0.5) // 500ms confirmation delay
.unit("bara")
.build();
PT1.setAlarmConfig(hippsAlarm);
PT2.setAlarmConfig(hippsAlarm);
PT3.setAlarmConfig(hippsAlarm);
// Create HIPPS valve with 2oo3 voting (SIL 3)
HIPPSValve hippsValve = new HIPPSValve("HIPPS-XV-001", feedStream);
hippsValve.addPressureTransmitter(PT1);
hippsValve.addPressureTransmitter(PT2);
hippsValve.addPressureTransmitter(PT3);
hippsValve.setVotingLogic(HIPPSValve.VotingLogic.TWO_OUT_OF_THREE);
hippsValve.setClosureTime(3.0); // 3 second SIL-rated actuator
hippsValve.setSILRating(3);
hippsValve.setProofTestInterval(8760.0); // Annual proof test
// Add to process system
ProcessSystem process = new ProcessSystem();
process.add(hippsValve);
// Transient simulation with pressure ramp
double timeStep = 0.5; // seconds
double totalTime = 30.0;
for (double time = 0; time <= totalTime; time += timeStep) {
// Update process conditions (e.g., blocked outlet scenario)
if (time > 5.0) {
// Simulate pressure buildup
double pressure = 80.0 + (time - 5.0) * 2.0; // 2 bara/sec ramp
feedStream.setPressure(pressure, "bara");
feedStream.run();
}
// Evaluate alarms on all transmitters
double currentPressure = feedStream.getPressure("bara");
PT1.evaluateAlarm(currentPressure, timeStep, time);
PT2.evaluateAlarm(currentPressure, timeStep, time);
PT3.evaluateAlarm(currentPressure, timeStep, time);
// Run HIPPS transient calculation
hippsValve.runTransient(timeStep, UUID.randomUUID());
// Check HIPPS status
if (hippsValve.hasTripped()) {
System.out.println("HIPPS activated at t=" + time + "s, P=" + currentPressure + " bara");
System.out.println("Active transmitters: " + hippsValve.getActiveTransmitterCount());
break;
}
// Continue processing downstream equipment...
}
// HIPPS provides primary protection, PSV is backup
// Create HIPPS (trips at 90 bara)
HIPPSValve hippsValve = new HIPPSValve("HIPPS-XV-001", feedStream);
// ... configure as in Example 1 ...
// Create PSV as backup (set at 100 bara MAWP)
SafetyValve psv = new SafetyValve("PSV-001", feedStream);
psv.setPressureSpec(100.0); // PSV set pressure at MAWP
psv.setFullOpenPressure(110.0); // Full open at 10% overpressure
psv.setBlowdown(7.0); // 7% blowdown
// In normal operation:
// 1. Pressure rises due to upset condition
// 2. HIPPS trips at 90 bara (prevents further pressure rise)
// 3. PSV never lifts because HIPPS stopped the overpressure
// 4. No flaring or emissions
// In HIPPS failure scenario:
// 1. Pressure continues to rise
// 2. PSV lifts at 100 bara (backup protection)
// 3. System is protected, but gas is flared
// Simulate a transmitter failure during operation
HIPPSValve hippsValve = new HIPPSValve("HIPPS-XV-001", feedStream);
hippsValve.setVotingLogic(HIPPSValve.VotingLogic.TWO_OUT_OF_THREE);
PressureTransmitter PT1 = new PressureTransmitter("PT-101A", feedStream);
PressureTransmitter PT2 = new PressureTransmitter("PT-101B", feedStream);
PressureTransmitter PT3 = new PressureTransmitter("PT-101C", feedStream);
hippsValve.addPressureTransmitter(PT1);
hippsValve.addPressureTransmitter(PT2);
hippsValve.addPressureTransmitter(PT3);
// During operation, PT2 fails (diagnosed and bypassed)
hippsValve.removePressureTransmitter(PT2);
// Change voting to 1oo2 for continued operation
hippsValve.setVotingLogic(HIPPSValve.VotingLogic.ONE_OUT_OF_TWO);
// System continues operating with degraded redundancy
// Schedule maintenance to repair PT2 and restore 2oo3 voting
// Perform partial stroke test (required for SIL validation)
HIPPSValve hippsValve = new HIPPSValve("HIPPS-XV-001", feedStream);
// During normal operation, perform 15% stroke test
hippsValve.performPartialStrokeTest(0.15); // 15% stroke
// Simulation of partial stroke test
double testDuration = 5.0; // seconds
double timeStep = 0.1;
for (double time = 0; time < testDuration; time += timeStep) {
hippsValve.runTransient(timeStep, UUID.randomUUID());
if (hippsValve.isPartialStrokeTestActive()) {
System.out.println("Test in progress: Opening = " +
hippsValve.getPercentValveOpening() + "%");
}
}
// Valve returns to 100% open after test
// Test validates valve can move (demonstrates functional operation)
HIPPS response time includes:
// Model realistic closure time
hippsValve.setClosureTime(3.0); // 3 seconds typical for SIL-rated ball valve
// Account for alarm confirmation delay
AlarmConfig alarm = AlarmConfig.builder()
.highHighLimit(90.0)
.delay(0.5) // 500 ms confirmation delay
.build();
HIPPS trip point should be:
// Example: MAWP = 100 bara
// Normal operation = 70-80 bara
// HIPPS trip = 90 bara (10% margin below MAWP)
// PSV set = 100 bara (at MAWP)
double mawp = 100.0;
double hippsTrip = mawp * 0.90; // 90% of MAWP
Model both success and failure scenarios:
// Scenario 1: HIPPS successful operation
// - Transmitters detect overpressure
// - Voting logic triggers
// - Valve closes in 3 seconds
// - Pressure stabilizes below MAWP
// Scenario 2: HIPPS spurious trip
hippsValve.recordSpuriousTrip();
// - Production lost
// - Economic impact
// - Need to restart system
// Scenario 3: HIPPS failure to close
hippsValve.setTripEnabled(false); // Simulate failure
// - PSV must provide protection
// - Flaring occurs
// - Verify PSV capacity adequate
// HIPPS should be independent of process control system
// But can provide signals for:
// - Alarm annunciation
// - Automatic process shutdown
// - Data logging
if (hippsValve.hasTripped()) {
// Trigger alarms
// Shut down feed pumps/compressors
// Log event for investigation
System.out.println(hippsValve.getDiagnostics());
}
// Track proof test intervals for SIL validation
hippsValve.setProofTestInterval(8760.0); // Annual proof test
// During operation
if (hippsValve.isProofTestDue()) {
// Schedule maintenance
// Perform full functional test
// Document results
hippsValve.performProofTest(); // Reset timer
}
[Platform] --100 bara--> [Subsea Pipeline] ---> [HIPPS] --50 bara--> [Receiving Platform]
[Compressor] --> [HIPPS] --> [Valve] --> [Process]
[Storage] --liquid--> [HIPPS] ---> [Isolated Section] ---> [Valve]
// Basic status
System.out.println(hippsValve.toString());
// Comprehensive diagnostics
System.out.println(hippsValve.getDiagnostics());
// Key metrics
int activeTx = hippsValve.getActiveTransmitterCount();
boolean tripped = hippsValve.hasTripped();
int spurious = hippsValve.getSpuriousTripCount();
double lastTrip = hippsValve.getLastTripTime();
boolean testDue = hippsValve.isProofTestDue();
=== HIPPS DIAGNOSTICS ===
System: HIPPS-XV-001
SIL Rating: SIL 3
Configuration: 2oo3 voting
Closure Time: 3.0 s
Transmitter Status:
PT-1: ALARM (92.50 bara)
PT-2: ALARM (92.45 bara)
PT-3: OK (89.80 bara)
Operational History:
Total Trips: 1
Spurious Trips: 0
Last Trip: 15.5 s
Runtime: 120.0 s
Maintenance:
Proof Test Interval: 8760 hrs
Time Since Proof Test: 450.5 hrs
Status: OK
Comprehensive test suite located at:
src/test/java/neqsim/process/equipment/valve/HIPPSValveTest.java
Tests cover:
Run tests:
mvnw test -Dtest=HIPPSValveTest
| SIL Level | PFD (Probability of Failure on Demand) | Typical Application |
|---|---|---|
| SIL 1 | 10⁻¹ to 10⁻² | Low risk, 1oo1 voting |
| SIL 2 | 10⁻² to 10⁻³ | Medium risk, 1oo2 or 2oo3 voting |
| SIL 3 | 10⁻³ to 10⁻⁴ | High risk, 2oo3 voting |
HIPPS in NeqSim provides:
Key Advantage: HIPPS prevents overpressure before it occurs, eliminating flaring and protecting equipment, while PSVs relieve pressure after it exceeds safe limits.
For safety-critical applications, HIPPS + PSV provides defense-in-depth protection strategy.
Implementation follows NeqSim architecture patterns and coding standards for process safety simulation.
High Integrity Pressure Protection System (HIPPS) is a Safety Instrumented System (SIS) designed to prevent overpressure in process equipment by rapidly closing isolation valves when pressure exceeds safe limits. HIPPS acts as the first line of defense before pressure relief devices (PSVs) or Emergency Shutdown (ESD) systems are activated.
HIPPS is an automated safety system that:
| Feature | HIPPS | ESD |
|---|---|---|
| Activation Pressure | 90-95% MAOP | 98-99% MAOP |
| Primary Function | Prevent overpressure | Emergency shutdown |
| Response Time | <2 seconds | 2-10 seconds |
| Scope | Local pressure protection | Full facility shutdown |
| SIL Level | SIL 2 or SIL 3 | SIL 1 or SIL 2 |
| Typical Voting | 2oo3 | 1oo2 or 2oo3 |
A typical pressure safety system has multiple layers:
HIPPSLogic (implements ProcessLogic)
├── VotingLogic (enum: 1oo1, 1oo2, 2oo2, 2oo3, 2oo4, 3oo4)
├── List<Detector> (pressure transmitters)
├── ThrottlingValve (isolation valve)
└── ProcessLogic (escalation logic - typically ESD)
// Create HIPPS with 2oo3 voting (SIL 3)
HIPPSLogic hipps = new HIPPSLogic("HIPPS-101", VotingLogic.TWO_OUT_OF_THREE);
// Add pressure transmitters
double hippsSetpoint = 95.0; // 95 bara (95% of 100 bara MAOP)
Detector pt1 = new Detector("PT-101A", DetectorType.PRESSURE,
AlarmLevel.HIGH_HIGH, hippsSetpoint, "bara");
Detector pt2 = new Detector("PT-101B", DetectorType.PRESSURE,
AlarmLevel.HIGH_HIGH, hippsSetpoint, "bara");
Detector pt3 = new Detector("PT-101C", DetectorType.PRESSURE,
AlarmLevel.HIGH_HIGH, hippsSetpoint, "bara");
hipps.addPressureSensor(pt1);
hipps.addPressureSensor(pt2);
hipps.addPressureSensor(pt3);
// Set isolation valve
ThrottlingValve isolationValve = new ThrottlingValve("HIPPS-Isolation-Valve", stream);
hipps.setIsolationValve(isolationValve);
// Create ESD logic as backup
ESDLogic esdLogic = new ESDLogic("ESD Level 1");
esdLogic.addAction(new TripValveAction(esdValve), 0.0);
// Link HIPPS to escalate to ESD after 5 seconds if pressure remains high
hipps.linkToEscalationLogic(esdLogic, 5.0);
// In transient simulation
for (double time = 0; time < totalTime; time += timeStep) {
// Run process equipment
stream.run();
isolationValve.run();
// Get pressure from process
double pressure = stream.getPressure();
// Update HIPPS (all three sensors)
hipps.update(pressure, pressure, pressure);
// Execute HIPPS logic (checks for escalation)
hipps.execute(timeStep);
// Check status
if (hipps.isTripped()) {
System.out.println("HIPPS ACTIVATED: Isolation valve closed");
}
if (hipps.hasEscalated()) {
System.out.println("ESCALATED TO ESD: HIPPS failed to control pressure");
}
}
| Pattern | Description | Spurious Trip Rate | Safety Integrity | Typical Use |
|---|---|---|---|---|
| 1oo1 | 1 out of 1 must trip | High | Low | Low criticality |
| 1oo2 | 1 out of 2 must trip | Medium | Medium | Standard applications |
| 2oo2 | 2 out of 2 must trip | Very Low | Low | High availability needed |
| 2oo3 | 2 out of 3 must trip | Low | High | HIPPS standard (SIL 3) |
| 2oo4 | 2 out of 4 must trip | Very Low | High | Critical applications |
| 3oo4 | 3 out of 4 must trip | Low | Very High | Ultra-high reliability |
The 2oo3 voting pattern is the industry standard for HIPPS because:
// Bypass one sensor for maintenance (max 1 allowed)
Detector pt1 = hipps.getPressureSensor(0);
pt1.setBypass(true);
// Set maximum bypassed sensors (default is 1)
hipps.setMaxBypassedSensors(1);
// Set target closure time (default 2 seconds)
hipps.setValveClosureTime(1.5); // Very fast closure
// Override HIPPS (inhibit trip function)
// WARNING: Requires management approval and risk assessment
hipps.setOverride(true);
// Check override status
if (hipps.isOverridden()) {
System.out.println("WARNING: HIPPS is overridden");
}
// Reset HIPPS after pressure returns to normal
if (hipps.reset()) {
System.out.println("HIPPS reset successful");
} else {
System.out.println("Cannot reset - pressure still high");
}
HIPPS typically requires SIL 2 or SIL 3 per IEC 61511:
| Failure Mode | Detection | Mitigation |
|---|---|---|
| Sensor failure | Self-diagnostics, voting | 2oo3 voting, regular testing |
| Valve failure to close | Position feedback, escalation | ESD backup, proof testing |
| Logic solver failure | Watchdog, self-test | Redundant processors |
| Common cause failure | Design diversity | Different sensor technologies |
// Trip statistics
double tripTime = hipps.getTimeSinceTrip();
boolean hasEscalated = hipps.hasEscalated();
// Sensor status
int trippedCount = 0;
int bypassedCount = 0;
for (Detector sensor : hipps.getPressureSensors()) {
if (sensor.isTripped()) trippedCount++;
if (sensor.isBypassed()) bypassedCount++;
}
Pressure: 50 bara → 96 bara (exceeds 95 bara setpoint)
Result:
- 3/3 sensors trip
- 2oo3 voting satisfied
- Isolation valve closes in <2 seconds
- Pressure controlled
- ESD not activated
Pressure: 50 bara → 96 bara
Sensor Status:
- PT-101A: TRIPPED
- PT-101B: TRIPPED
- PT-101C: FAULTY (not counted)
Result:
- 2/2 active sensors trip
- 2oo3 voting satisfied (excludes faulty sensor)
- HIPPS activates successfully
Pressure: 50 bara → 96 bara
HIPPS: Trips and closes valve
Pressure: Remains at 96 bara (valve failure)
Time: 5 seconds elapsed
Result:
- HIPPS escalation timer expires
- ESD activated automatically
- Full shutdown initiated
Sensor Status:
- PT-101A: BYPASSED (maintenance)
- PT-101B: NORMAL
- PT-101C: NORMAL
Pressure: 50 bara → 96 bara
Result:
- PT-101B: TRIPPED
- PT-101C: TRIPPED
- 2/2 active sensors trip
- 2oo3 voting satisfied
- HIPPS activates successfully with 1 sensor bypassed
This implementation adds a complete Emergency Shutdown (ESD) blowdown system to NeqSim, including:
Location: src/main/java/neqsim/process/equipment/valve/BlowdownValve.java
Key Features:
Usage Example:
// Create blowdown valve
BlowdownValve bdValve = new BlowdownValve("BD-101", blowdownStream);
bdValve.setOpeningTime(5.0); // 5 seconds to fully open
// Activate in emergency
bdValve.activate();
// Check status
if (bdValve.isActivated()) {
System.out.println("Blowdown in progress");
}
Location: src/main/java/neqsim/process/measurementdevice/PushButton.java
Key Features:
Usage Example:
// Create push button linked to BD valve
PushButton esdButton = new PushButton("ESD-PB-101", bdValve);
// Operator pushes button in emergency
esdButton.push(); // Automatically activates linked BD valve
// Check button state
if (esdButton.isPushed()) {
System.out.println("ESD button pushed - blowdown active");
}
// Reset button (valve stays activated for safety)
esdButton.reset();
Location: src/main/java/neqsim/process/equipment/diffpressure/Orifice.java
Key Features:
Transient Behavior: In dynamic/transient mode, the orifice acts as a pressure-driven flow restriction device. Unlike steady-state mode where flow may be specified upstream, in transient mode the orifice calculates and determines the actual flow based on the pressure differential:
Flow Equation (ISO 5167):
m = A × C × ε × √(2ρΔP / (1 - β⁴))
Where:
Usage Example:
// Create orifice with ISO 5167 parameters
// Orifice(name, pipeDiameter, orificeDiameter, P_upstream_ref, P_downstream, dischargeCoeff)
Orifice bdOrifice = new Orifice("BD-Orifice",
0.45, // Pipe diameter (m)
0.18, // Orifice diameter (m)
60.0, // Upstream reference pressure (bara)
1.5, // Downstream pressure - flare header (bara)
0.61); // Discharge coefficient
// In transient simulation, the orifice automatically calculates flow
bdOrifice.runTransient(timeStep, uuid);
// As separator pressure drops, orifice flow decreases
// Example: ΔP 22.5→4.2 bar causes flow to drop 29,110→7,756 kg/hr (73% reduction)
Important Notes:
pressureDownstream parameter sets the boundary pressure (e.g., flare header at 1.5 bara)Location: src/test/java/neqsim/process/equipment/valve/BlowdownValveESDSystemTest.java
Test Coverage:
Key Scenario:
Location: src/main/java/neqsim/process/util/example/ESDBlowdownSystemExample.java
A standalone runnable example showing:
Separator (50 bara)
|
v
Gas Splitter
|
+----> Process Stream (normal operation)
|
+----> Blowdown Stream
|
v
BD Valve (BD-101) <--- ESD Push Button (ESD-PB-101)
|
v
BD Orifice - ISO 5167 pressure-driven flow
| Flow = f(√ΔP) - decreases as P₁ drops
| Controls depressurization rate
v
Flare Header (1.5 bara)
|
v
Flare - Combusts blowdown gas
In transient simulations, the orifice flow is pressure-driven following ISO 5167:
Flow Rate ∝ √(ΔP)
Example from ESD Simulation:
This demonstrates realistic depressurization behavior where:
Realistic Depressurization Profiles:
PSV/Rupture Disc Scenarios:
mvn exec:java -Dexec.mainClass="neqsim.process.util.example.ESDBlowdownSystemExample"
mvn test -Dtest=BlowdownValveESDSystemTest
The system will show:
╔════════════════════════════════════════════════════════════════╗
║ EMERGENCY SHUTDOWN (ESD) BLOWDOWN SYSTEM TEST ║
╚════════════════════════════════════════════════════════════════╝
═══ SYSTEM CONFIGURATION ═══
Separator operating pressure: 50.0 bara
Gas flow rate: 10000.0 kg/hr
Blowdown valve: BD-101 (normally closed)
ESD Push Button: ESD-PB-101 (linked to BD-101)
BD Orifice Cv: 150.0
Flare header pressure: 1.5 bara
BD valve opening time: 5.0 seconds
>>> OPERATOR PUSHES ESD BUTTON - BLOWDOWN INITIATED <<<
Time (s) | Sep Press | Process Flow | BD Flow | BD Opening | Flare Flow | Heat Release
| (bara) | (kg/hr) | (kg/hr) | (%) | (kg/hr) | (MW)
---------|-----------|--------------|------------|------------|------------|-------------
10.0 | 50.00 | 0.0 | 10000.0 | 0.0 | 0.0 | 0.00
12.0 | 50.00 | 0.0 | 10000.0 | 40.0 | 4000.0 | 29.45
14.0 | 50.00 | 0.0 | 10000.0 | 80.0 | 8000.0 | 58.91
16.0 | 50.00 | 0.0 | 10000.0 | 100.0 | 10000.0 | 73.63
═══ BLOWDOWN SUMMARY ═══
Maximum blowdown flow: 10000.0 kg/hr
Total gas blown down: 138.9 kg
Total heat released: 3.21 GJ
Total CO2 emissions: 382.5 kg
✓ ESD push button successfully triggered blowdown
✓ BD valve automatically activated by push button
✓ Controlled depressurization through BD orifice
MeasurementDeviceBaseClassAlarmConfig)All tests are in BlowdownValveESDSystemTest.java:
Orifice Flow Validation: The tests verify that orifice flow correctly responds to pressure changes:
Run all tests:
mvn test -Dtest=BlowdownValveESDSystemTest
Potential additions:
Implementation follows NeqSim architecture patterns and coding standards.
The ESD blowdown system now includes comprehensive pressure monitoring during transient calculations to verify that the separator pressure is properly released via the blowdown valve.
The system tracks separator pressure at every time step:
Automated checks verify:
// Track pressure during blowdown
double initialPressure = separator.getGasOutStream().getPressure("bara");
double minPressure = initialPressure;
double maxPressure = initialPressure;
for (double time = 0.0; time <= totalTime; time += timeStep) {
// Run equipment in transient mode
separator.run();
bdValve.runTransient(timeStep, uuid);
// ... other equipment
// Monitor separator pressure
double currentPressure = separator.getGasOutStream().getPressure("bara");
minPressure = Math.min(minPressure, currentPressure);
maxPressure = Math.max(maxPressure, currentPressure);
System.out.printf("Time: %.1fs, Pressure: %.2f bara%n", time, currentPressure);
}
// Verify pressure relief
System.out.printf("Pressure drop: %.2f bar (%.1f%% reduction)%n",
initialPressure - finalPressure,
100.0 * (initialPressure - finalPressure) / initialPressure);
The new test testPressureReliefViaBlowdown() demonstrates:
Scenario:
Output:
═══ PRESSURE PROFILE ═══
Initial pressure: 60.00 bara
Maximum pressure reached: 65.23 bara
Pressure at ESD activation: 65.23 bara
Final pressure: 52.18 bara
Pressure rise before ESD: 5.23 bar (8.7% increase)
Pressure drop after ESD: 13.05 bar (20.0% reduction)
✓ Pressure buildup detected: 60.00 bara → 65.23 bara
✓ ESD successfully activated at 65.23 bara
✓ Pressure relieved to 52.18 bara (20.0% reduction)
double sepPressure = separator.getPressure("bara");
// or
double sepPressure = separator.getGasOutStream().getPressure("bara");
double bdFlow = blowdownStream.getFlowRate("kg/hr");
double opening = bdValve.getPercentValveOpening(); // 0-100%
boolean isOpen = bdValve.getPercentValveOpening() > 90.0;
double heatRelease = flare.getHeatDuty("MW");
double totalGas = flare.getCumulativeGasBurned("kg");
// Initialize tracking variables
double initialPressure = separator.getPressure("bara");
double maxPressure = initialPressure;
double finalPressure = initialPressure;
// Time loop
double timeStep = 0.5; // seconds
for (double time = 0.0; time <= simulationTime; time += timeStep) {
// 1. Update process conditions (if needed)
if (time >= eventTime) {
esdButton.push(); // Trigger ESD
splitter.setSplitFactors(new double[] {0.0, 1.0});
}
// 2. Run all equipment
feedStream.run();
separator.run();
// ... other steady-state equipment
// 3. Run transient equipment
bdValve.runTransient(timeStep, UUID.randomUUID());
// ... more equipment
flare.run();
flare.updateCumulative(timeStep); // Track cumulative values
// 4. Monitor and record
double currentPressure = separator.getPressure("bara");
maxPressure = Math.max(maxPressure, currentPressure);
finalPressure = currentPressure;
// 5. Output (optional)
System.out.printf("t=%.1fs: P=%.2f bara, BD=%.1f%%, Flow=%.1f kg/hr%n",
time, currentPressure, bdValve.getPercentValveOpening(),
blowdownStream.getFlowRate("kg/hr"));
}
// 6. Verify results
if (finalPressure < initialPressure) {
System.out.println("✓ Pressure successfully relieved");
}
// Pressure should decrease after blowdown
assertTrue(finalPressure < pressureAtEsdActivation,
"Pressure should decrease after ESD activation");
// Valve should be activated
assertTrue(bdValve.isActivated(), "BD valve should be activated");
// Gas should flow to flare
assertTrue(flare.getCumulativeGasBurned("kg") > 0,
"Gas should flow to flare");
// Valve should be fully open
assertTrue(bdValve.getPercentValveOpening() > 90.0,
"BD valve should be nearly fully open");
The system provides detailed output showing:
mvn exec:java -Dexec.mainClass="neqsim.process.util.example.ESDBlowdownSystemExample"
# Run all blowdown tests
mvn test -Dtest=BlowdownValveESDSystemTest
# Run specific pressure relief test
mvn test -Dtest=BlowdownValveESDSystemTest#testPressureReliefViaBlowdown
When the blowdown system is working correctly, you should observe:
If pressure is not relieved:
bdValve.isActivated()bdValve.getPercentValveOpening()blowdownStream.getFlowRate("kg/hr")splitter.setSplitFactors(...)bdOrifice.getOutletPressure()The pressure monitoring integrates with:
The enhanced ESD blowdown system provides:
This ensures that the blowdown valve correctly relieves pressure when activated, protecting equipment from overpressure conditions.
This note summarizes how to extend NeqSim blowdown calculations with rigorous fire exposure models.
FireHeatTransferCalculator utility to compute inner/outer wall temperatures with
separate film coefficients for wetted and unwetted regions. The calculator solves a 1-D thermal
resistance network so that external fire temperatures and internal boiling/convective coefficients
can be applied consistently. Wetted and dry areas can be pulled directly from a Separator via
getWettedArea() / getUnwettedArea(), so dynamic level changes during blowdown feed into the
fire heat flux sizing automatically.SurfaceTemperatureResult object reports heat flux and both metal surface temperatures so
material-strength checks can be driven by actual wall metal temperature.FireHeatLoadCalculator.api521PoolFireHeatLoad(wettedArea, F) gives
the classic correlation in Watts using the 6.19e6·F·A^0.82 metric form.FireHeatLoadCalculator.generalizedStefanBoltzmannHeatFlux(...)
provides radiative heat flux using emissivity and view/configuration factors. Combine with
convective terms for jet fires or shielding adjustments.SeparatorFireExposure.evaluate(config, flare, distanceM) can fold in the
actual flare heat duty and radiant fraction (via Flare.estimateRadiationHeatFlux) at a specified
horizontal distance so you can compare environmental fire sizing vs. radiation from the burning
gas itself.VesselRuptureCalculator.vonMisesStress(P, r, t) to derive von Mises stress from hoop and
axial components for thin-walled vessels.ruptureMargin or
isRuptureLikely to flag impending rupture. Feeding these checks with the metal temperatures from
the heat-transfer calculator allows temperature-dependent strength curves to be implemented later.These utilities are designed to plug into existing blowdown scenarios and flare models so that
transient depressurization can be tracked alongside external fire loads and structural integrity.
The helper SeparatorFireExposure.evaluate(...) (or separator.evaluateFireExposure(...)) wraps
area lookup, heat-load estimation, wall temperatures, and rupture checks into a single call so the
fire calculations can be dropped into a process simulation loop without hand-wiring each piece.
If you want the separator inventory to warm up from the calculated fire load, set the duty on the
separator (separator.setDuty(fireResult.totalFireHeat())) and call separator.runTransient(...)
so the energy balance absorbs that heat during the timestep.
The runnable SeparatorFireDepressurizationExample (src/main/java/neqsim/process/util/example/SeparatorFireDepressurizationExample.java)
illustrates how to couple a separator depressurization to the flare with the fire utilities:
BlowdownValve + Orifice feeding a FlareFireHeatTransferCalculatorVesselRuptureCalculatorseparator.setDuty(fireResult.totalFireHeat())To run the illustration:
mvn -pl . -Dexec.mainClass="neqsim.process.util.example.SeparatorFireDepressurizationExample" exec:java
The output prints separator pressure, flow to flare, wall temperatures, rupture margin, and fire heat metrics at each timestep so the fire impact on depressurization can be reviewed end-to-end.
This note summarizes the current fire, heat-transfer, and structural integrity helpers available in NeqSim and how to apply them in process simulations.
FireHeatLoadCalculator.api521PoolFireHeatLoad(wettedArea, F) returns the classic total heat input (W) using wetted area and environment factor.FireHeatLoadCalculator.generalizedStefanBoltzmannHeatFlux(...) calculates radiative heat flux (W/m²) from emissivity, view/configuration factor, and flame temperature so callers can compose pool or jet fire scenarios and include shielding/angle effects.FireHeatTransferCalculator solves a steady 1-D wall model for wetted and unwetted regions using caller-supplied internal/external film coefficients.SurfaceTemperatureResult reports inner/outer metal temperatures and heat flux for each region so process models can track thermal response during depressurization.VesselRuptureCalculator provides thin-wall von-Mises stress plus helpers to compute rupture margin or boolean likelihood when allowable tensile strength is supplied (optionally temperature-dependent when paired with wall temperatures from the heat-transfer step).getWettedArea(), getUnwettedArea()), allowing fire heat loads to follow liquid level during blowdown.SeparatorFireExposure.evaluate(...) (also available via separator.evaluateFireExposure(...)) wraps area lookup, heat-load estimation, wall temperatures, and rupture checks into a single call to simplify use inside dynamic process simulations.separator.setDuty(fireResult.totalFireHeat()); the separator’s runTransient call will consume that duty in its energy balance so the process temperature responds to fire loading without manual temperature edits.SeparatorFireDepressurizationExample demonstrates end-to-end use by routing a separator blowdown to a flare while evaluating fire loads, wall temperatures, and rupture margin over time.The IntegratedSafetySystemExample demonstrates a comprehensive safety system implementation for process facilities, incorporating multiple layers of protection following the principles of Safety Instrumented Systems (SIS) and Defense in Depth.
The example implements a complete safety architecture with four distinct layers:
┌─────────────────────────────────────┐
│ 1. High Pressure Alarm (SIL-1) │ ← 55.0 bara
│ ├─ Operator intervention │
│ └─ Alarms and warnings │
├─────────────────────────────────────┤
│ 2. ESD System (SIL-2) │ ← 58.0 bara
│ ├─ Emergency shutdown │
│ ├─ Blowdown activation │
│ └─ Fire detection response │
├─────────────────────────────────────┤
│ 3. HIPPS (SIL-3) │ ← 60.0 bara
│ ├─ High integrity protection │
│ ├─ Fast-acting valve closure │
│ └─ Redundant pressure monitoring │
├─────────────────────────────────────┤
│ 4. PSV (Mechanical) │ ← 65.0 bara
│ └─ Final mechanical relief │
└─────────────────────────────────────┘
Purpose: Prevent overpressure by rapidly closing inlet valve before pressure reaches dangerous levels.
Safety Integrity Level: SIL-3 (PFD: 0.0001-0.001)
Features:
Implementation:
HIPPSController hippsController =
new HIPPSController("HIPPS-Logic-001", hippsPT1, hippsPT2, hippsValve);
// 2oo2 voting for higher integrity
if (p1 >= HIPPS_ACTIVATION_PRESSURE && p2 >= HIPPS_ACTIVATION_PRESSURE) {
hippsValve.setPercentValveOpening(0.0); // Close immediately
}
Purpose: Shut down process and activate blowdown when emergency conditions are detected.
Safety Integrity Level: SIL-2 (PFD: 0.001-0.01)
Activation Conditions:
Actions on Activation:
Implementation:
ESDController esdController =
new ESDController("ESD-Logic-201", separatorPT, separatorTT,
esdButton, esdInletValve, bdValve);
// Multiple trigger conditions
if (pressure >= HIGH_HIGH_PRESSURE_ALARM ||
temperature >= FIRE_DETECTION_TEMPERATURE ||
manualESD.isPushed()) {
esdValve.setPercentValveOpening(0.0);
blowdownValve.activate();
}
Purpose: Detect fire conditions and trigger ESD.
Configuration:
Implementation:
FireDetectionSystem fireSystem =
new FireDetectionSystem(
new TemperatureTransmitter[] {fireTT1, fireTT2, fireTT3},
2 // voting threshold
);
Purpose: Rapidly depressurize equipment during emergency situations.
Features:
Implementation:
BlowdownValve bdValve = new BlowdownValve("BD-301", blowdownStream);
bdValve.setOpeningTime(5.0);
bdValve.setCv(250.0);
Purpose: Final mechanical protection layer - relieves pressure if all other systems fail.
Characteristics:
Implementation:
SafetyValve psv = new SafetyValve("PSV-401", separatorGasOut);
psv.setPressureSpec(65.0);
psv.setFullOpenPressure(67.0);
psv.setBlowdown(7.0);
Purpose: Safely combust and dispose of emergency relief gases.
Features:
The example demonstrates four operational scenarios:
Expected Behavior:
Expected Behavior:
Expected Behavior:
Expected Behavior:
| SIL Level | PFD Range | RRF Range | Implementation |
|---|---|---|---|
| SIL-3 | 10⁻⁴ to 10⁻³ | 10,000 to 1,000 | HIPPS with 2oo2 voting |
| SIL-2 | 10⁻³ to 10⁻² | 1,000 to 100 | ESD with redundant sensors |
| SIL-1 | 10⁻² to 10⁻¹ | 100 to 10 | Alarms with operator action |
PFD: Probability of Failure on Demand
RRF: Risk Reduction Factor
2oo2 (HIPPS - SIL-3):
2oo3 (Fire Detection):
Multiple independent protection layers ensure safety even if individual layers fail.
// Run the integrated safety system example
java neqsim.process.util.example.IntegratedSafetySystemExample
// Expected output:
// - System configuration summary
// - Four safety scenarios with detailed monitoring
// - Verification of each protection layer
HIPPS status: NORMAL
ESD status: NORMAL
Fire detection: NORMAL
PSV status: CLOSED
>>> HIPPS ACTIVATED (SIL-3) - Both pressure sensors confirm <<<
HIPPS Valve: Closing from 100% to 0%
Separator pressure: Controlled below 60 bara
>>> ESD ACTIVATED (SIL-2) - Manual Push Button <<<
ESD inlet valve: Closing to 0%
BD valve: Opening to 100%
Blowdown flow: Increasing to flare
Separator pressure: Decreasing
Sep P > 65.0 bara
PSV status: RELIEVING
PSV Flow: High flow to flare
The example tracks and reports:
ThrottlingValve - Control valves and isolation valvesBlowdownValve - Emergency depressurization valveSafetyValve - Pressure relief valve with hysteresisPressureTransmitter - Pressure measurement devicesTemperatureTransmitter - Temperature measurement devicesPushButton - Manual activation deviceSeparator - Process vessel with transient capabilityFlare - Flare system with emission trackingControllerDeviceBaseClass - Base for custom controllersThis example can be extended to include:
Implemented a comprehensive Safety Instrumented System (SIS) framework for NeqSim following IEC 61511 standards, enabling realistic fire and gas detection with voting logic and automatic ESD triggering.
neqsim.process.logic.sis)Represents standard voting patterns for redundant sensors:
VotingLogic voting = VotingLogic.TWO_OUT_OF_THREE;
boolean shouldTrip = voting.evaluate(trippedCount); // true if ≥2 tripped
neqsim.process.logic.sis)Represents fire, gas, or process detectors with:
Detector fireDetector = new Detector("FD-101", DetectorType.FIRE,
AlarmLevel.HIGH, 60.0, "°C");
fireDetector.update(temperatureValue); // Evaluates trip condition
if (fireDetector.isTripped()) {
// Detector has tripped
}
neqsim.process.logic.sis)Complete SIF implementation following IEC 61511 architecture:
Key Features:
// Create fire SIF with 2oo3 voting
SafetyInstrumentedFunction fireSIF =
new SafetyInstrumentedFunction("Fire Detection SIF", VotingLogic.TWO_OUT_OF_THREE);
// Add 3 detectors
fireSIF.addDetector(new Detector("FD-101", DetectorType.FIRE, AlarmLevel.HIGH, 60.0, "°C"));
fireSIF.addDetector(new Detector("FD-102", DetectorType.FIRE, AlarmLevel.HIGH, 60.0, "°C"));
fireSIF.addDetector(new Detector("FD-103", DetectorType.FIRE, AlarmLevel.HIGH, 60.0, "°C"));
// Link to ESD logic
fireSIF.linkToLogic(esdLogic);
// Update detector values
fireSIF.update(temp1, temp2, temp3);
// Check if SIF tripped (2 of 3 detectors exceeded setpoint)
if (fireSIF.isTripped()) {
// ESD logic automatically activated
}
Redundancy
Bypass Management
Fault Handling
Reset Logic
The FireGasSISExample demonstrates a complete safety system:
┌─────────────────────────────────┐
│ Fire Detection (2oo3) │
│ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │FD-101│ │FD-102│ │FD-103│ │
│ └──┬──┘ └──┬──┘ └──┬──┘ │
│ └───────┴───────┘ │
│ 2/3 must trip │
└──────────────┬──────────────────┘
│
├─────────┐
│ │
┌──────────────▼───────────────────┐
│ Gas Detection (2oo3) │
│ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │GD-101│ │GD-102│ │GD-103│ │
│ └──┬──┘ └──┬──┘ └──┬──┘ │
│ └───────┴───────┘ │
│ 2/3 must trip │
└──────────────┬───────────────────┘
│
▼
┌────────────────┐
│ ESD Logic │
│ 1. Trip ESD │
│ 2. Open BD │
│ 3. Redirect │
└────────────────┘
The SIS components integrate seamlessly with the existing process logic framework:
// Create SIF
SafetyInstrumentedFunction fireSIF =
new SafetyInstrumentedFunction("Fire SIF", VotingLogic.TWO_OUT_OF_THREE);
fireSIF.addDetector(fireDetector1);
fireSIF.addDetector(fireDetector2);
fireSIF.addDetector(fireDetector3);
// Create ESD logic with actions
ESDLogic esdLogic = new ESDLogic("ESD Level 1");
esdLogic.addAction(new TripValveAction(esdValve), 0.0);
esdLogic.addAction(new ActivateBlowdownAction(bdValve), 0.5);
esdLogic.addAction(new SetSplitterAction(splitter, factors), 0.0);
// Link SIF to ESD logic
fireSIF.linkToLogic(esdLogic);
// Push button can also trigger ESD
PushButton esdButton = new PushButton("ESD-PB-101");
esdButton.linkToLogic(esdLogic);
// In simulation loop:
fireSIF.update(temp1, temp2, temp3); // Automatic evaluation
// or
esdButton.push(); // Manual trigger
| Pattern | Spurious Trip Rate | Safety Integrity | Availability | Common Use |
|---|---|---|---|---|
| 1oo1 | High | Low | Low | Low criticality |
| 1oo2 | Low | Medium | High | Balance needed |
| 2oo2 | Very Low | Low | Medium | Rare |
| 2oo3 | Low | High | High | Standard choice |
| 2oo4 | Very Low | High | Very High | Critical systems |
VotingLogic.java - Voting pattern enumerationDetector.java - Fire/gas/process detectorSafetyInstrumentedFunction.java - Complete SIF implementationFireGasSISExample.java - Comprehensive demonstrationSCENARIO 3: FIRE DETECTED - 2oo3 VOTING SATISFIED
>>> FD-101 and FD-102 detect fire <<<
Fire Detection SIF [2oo3] - TRIPPED (2/3 tripped)
FD-101: TRIPPED
FD-102: TRIPPED
FD-103: NORMAL
SIF Status: TRIPPED - ESD ACTIVATED
ESD Logic: ESD Level 1 - RUNNING (Step 1/3: Trip ESD valve ESD-XV-101)
Time (s) | Fire SIF | Gas SIF | ESD Step | ESD Valve (%) | BD Valve (%)
---------|----------|----------|----------|---------------|-------------
0.0 | TRIPPED | NORMAL | Step 1/3 | 80.0 | 0.0
1.0 | TRIPPED | NORMAL | Step 1/3 | 60.0 | 0.0
...
5.0 | TRIPPED | NORMAL | Step 2/3 | 0.0 | 0.0
...
✓ SIF architecture (sensor → logic → final element) ✓ Voting logic patterns ✓ Bypass management ✓ Proof test considerations
✓ Safety integrity levels ✓ Systematic failure prevention ✓ Diagnostic coverage
✓ Safety instrumented systems ✓ Safety lifecycle management ✓ SIS design requirements
The SIS logic framework provides NeqSim with industry-standard fire and gas detection capabilities, enabling realistic simulation of safety-critical process control systems. The 2oo3 voting implementation balances safety integrity with operational availability, making it suitable for modeling high-reliability applications.
Combined with the process logic framework, NeqSim now supports comprehensive modeling of:
All following recognized international standards for process safety instrumentation.
This document describes a critical safety scenario where an inlet choke valve (throttle valve) suddenly fails open to 100%, causing rapid pressure rise in downstream equipment. A Process Shutdown (PSD) valve monitors the pressure and automatically closes when a High-High (HIHI) alarm is triggered, protecting the system from overpressure.
// High pressure feed at 100 bara
SystemInterface feedGas = new SystemSrkEos(273.15 + 40.0, 100.0);
feedGas.addComponent("methane", 85.0);
feedGas.addComponent("ethane", 8.0);
// ... add other components
feedGas.setMixingRule("classic");
Stream feedStream = new Stream("High Pressure Feed", feedGas);
feedStream.setFlowRate(5000.0, "kg/hr");
feedStream.setPressure(100.0, "bara");
// Choke valve - normally at 30% opening
ThrottlingValve chokeValve = new ThrottlingValve("Inlet Choke Valve", feedStream);
chokeValve.setPercentValveOpening(30.0);
chokeValve.setOutletPressure(50.0);
Stream chokeOutlet = new Stream("Choke Outlet", chokeValve.getOutletStream());
// PSD valve for protection
PSDValve psdValve = new PSDValve("PSD Inlet Protection", chokeOutlet);
psdValve.setPercentValveOpening(100.0);
psdValve.setClosureTime(2.0); // Fast closure
// Pressure transmitter with HIHI alarm
PressureTransmitter pressureTransmitter = new PressureTransmitter(
"Separator Inlet PT", chokeOutlet);
AlarmConfig alarmConfig = AlarmConfig.builder()
.highHighLimit(55.0)
.highLimit(52.0)
.deadband(0.5)
.delay(1.0)
.unit("bara")
.build();
pressureTransmitter.setAlarmConfig(alarmConfig);
// Link PSD valve to pressure transmitter
psdValve.linkToPressureTransmitter(pressureTransmitter);
// Run initial steady state
feedStream.run();
chokeValve.run();
psdValve.run();
// FAILURE EVENT: Choke valve fails open
chokeValve.setPercentValveOpening(100.0);
// Simulate dynamic response
double timeStep = 0.5; // 0.5 second time steps
for (double time = 0.0; time <= simulationTime; time += timeStep) {
// Run process
feedStream.run();
chokeValve.run();
chokeOutlet.run();
// Evaluate alarm
pressureTransmitter.evaluateAlarm(
chokeOutlet.getPressure("bara"), timeStep, time);
// Run PSD transient behavior
psdValve.runTransient(timeStep, UUID.randomUUID());
// Check if PSD tripped
if (psdValve.hasTripped()) {
System.out.println("PSD valve tripped at " + time + " s");
break;
}
}
===== CHOKE COLLAPSE SCENARIO =====
Initial Configuration:
Feed pressure: 100.0 bara
Choke opening: 30.0% (normal operation)
PSD opening: 100.0% (normal operation)
PSD HIHI setpoint: 55.0 bara
Time (s) | Choke Opening | Pressure (bara) | Alarm State | PSD Opening | PSD Tripped
---------|---------------|-----------------|-------------|-------------|------------
0.0 | 100.0% | 50.00 | NONE | 100.0% | NO
0.5 | 100.0% | 51.00 | NONE | 100.0% | NO
1.0 | 100.0% | 52.00 | NONE | 100.0% | NO
1.5 | 100.0% | 53.00 | HI | 100.0% | NO
2.0 | 100.0% | 54.00 | HI | 100.0% | NO
2.5 | 100.0% | 55.00 | HI | 100.0% | NO
3.0 | 100.0% | 56.00 | HIHI | 0.0% | YES
Results:
===== CHOKE REPAIR AND PSD RESET TEST =====
Step 1: Simulating choke collapse...
PSD tripped at 56.0 bara
Step 2: Repairing choke valve (returning to 30% opening)...
Choke repaired and pressure returned to 50 bara
Step 3: Attempting to open PSD valve while still tripped...
✓ PSD correctly prevents opening while tripped
Step 4: Resetting PSD valve...
PSD valve reset complete
Step 5: Opening PSD valve to resume operation...
✓ PSD successfully opened to 100.0%
===== RESET TEST SUMMARY =====
✓ Choke collapse triggered PSD trip
✓ Choke repaired (returned to 30% opening)
✓ PSD prevented opening while tripped
✓ PSD reset successful
✓ System ready to resume normal operation
| Layer | Device | Setpoint | Action | Response Time |
|---|---|---|---|---|
| 1 | HI Alarm | 52 bara | Operator notification | Immediate |
| 2 | HIHI Alarm | 55 bara | Triggers PSD closure | 1 second delay |
| 3 | PSD Valve | On HIHI | Closes to 0% | 2 seconds |
AlarmConfig alarmConfig = AlarmConfig.builder()
.highHighLimit(55.0) // Trip setpoint
.highLimit(52.0) // Early warning
.deadband(0.5) // Prevent chattering
.delay(1.0) // Confirmation time
.unit("bara")
.build();
This scenario complements other safety devices in NeqSim:
| Feature | PSD Valve | Safety Valve (PSV) | Rupture Disk |
|---|---|---|---|
| Activation | HIHI alarm | Set pressure | Burst pressure |
| Response | Fast closure | Opens to relieve | Bursts open |
| Reset | Manual | Self-resetting | Requires replacement |
| Use Case | Process shutdown | Overpressure relief | Last-resort protection |
| Typical Setpoint | 55 bara (HIHI) | 55 bara (set) | 65 bara (burst) |
For complete system protection, use all three in series:
// High pressure feed from wellhead
Stream wellheadStream = new Stream("Wellhead", highPressureGas);
wellheadStream.setPressure(100.0, "bara");
// Choke valve for flow control
ThrottlingValve chokeValve = new ThrottlingValve("Production Choke", wellheadStream);
chokeValve.setPercentValveOpening(30.0);
// PSD valve for emergency isolation
PSDValve psdValve = new PSDValve("ESD Inlet", chokeValve.getOutletStream());
psdValve.linkToPressureTransmitter(pressureTransmitter);
// Production separator
Separator separator = new Separator("Production Sep", psdValve.getOutletStream());
separator.setInternalDiameter(1.5);
separator.setSeparatorLength(4.0);
// PSV for overpressure relief
SafetyValve psv = new SafetyValve("PSV-101", separator.getGasOutStream());
psv.setSetPressure(55.0, "bara");
// Rupture disk as last resort
RuptureDisk ruptureDisk = new RuptureDisk("RD-101", separator.getGasOutStream());
ruptureDisk.setBurstPressure(65.0, "bara");
Complete test implementation: neqsim.process.equipment.valve.ChokeCollapsePSDProtectionTest
Run tests:
mvn test -Dtest=ChokeCollapsePSDProtectionTest
This repository now includes an integration test that links alarms, HIPPS isolation, and ESD depressurization logic against dynamic equipment models during a transient upset. The goal is to verify that layered safety functions respond coherently when feed pressure surges beyond high-high limits.
HIPPS Isolation Valve closed in under two seconds.ESD Level 1, closing inlet valves, opening the blowdown valve, and routing gas to the flare.Execute the JUnit test directly:
mvn -q -Dtest=IntegratedSafetyChainTransientTest test
The test lives at src/test/java/neqsim/process/util/scenario/IntegratedSafetyChainTransientTest.java
and uses ProcessScenarioRunner to coordinate logic execution with the process model.
This document describes the automatic safety scenario generation infrastructure added to NeqSim.
Process safety analysis requires systematic evaluation of failure modes. The AutomaticScenarioGenerator class automatically identifies potential failures from process topology and generates scenarios for what-if analysis.
ProcessSystem process = new ProcessSystem();
// ... configure process with valves, compressors, coolers, etc. ...
AutomaticScenarioGenerator generator = new AutomaticScenarioGenerator(process);
// Enable specific failure modes
generator.addFailureModes(
FailureMode.COOLING_LOSS,
FailureMode.VALVE_STUCK_CLOSED,
FailureMode.COMPRESSOR_TRIP
);
// Generate single-failure scenarios
List<ProcessSafetyScenario> singleFailures = generator.generateSingleFailures();
// Generate combination scenarios (up to 2 simultaneous failures)
List<ProcessSafetyScenario> combinations = generator.generateCombinations(2);
// Quick scenario generation
List<ProcessSafetyScenario> scenarios = process.generateSafetyScenarios();
// With combination depth
List<ProcessSafetyScenario> combinations = process.generateCombinationScenarios(2);
| Mode | Description | HAZOP Deviation | Applicable To |
|---|---|---|---|
COOLING_LOSS |
Complete loss of cooling | No flow | Coolers |
HEATING_LOSS |
Complete loss of heating | No flow | Heaters |
VALVE_STUCK_CLOSED |
Valve stuck closed | No flow | Valves |
VALVE_STUCK_OPEN |
Valve stuck open | More flow | Valves |
VALVE_CONTROL_FAILURE |
Control valve failure | Other | Control valves |
COMPRESSOR_TRIP |
Compressor emergency stop | No flow | Compressors |
PUMP_TRIP |
Pump emergency stop | No flow | Pumps |
BLOCKED_OUTLET |
Downstream blockage | No flow | Separators |
POWER_FAILURE |
Loss of electrical power | Other | All electrical |
INSTRUMENT_FAILURE |
Instrument/control failure | Other | All |
EXTERNAL_FIRE |
Fire exposure | High temperature | All |
LOSS_OF_CONTAINMENT |
Leak or rupture | Less pressure | All |
| Deviation | Description |
|---|---|
NO_FLOW |
Complete loss of flow |
LESS_FLOW |
Reduced flow rate |
MORE_FLOW |
Increased flow rate |
REVERSE_FLOW |
Flow in wrong direction |
HIGH_PRESSURE |
Pressure above normal |
LOW_PRESSURE / LESS_PRESSURE |
Pressure below normal |
HIGH_TEMPERATURE |
Temperature above normal |
LOW_TEMPERATURE |
Temperature below normal |
HIGH_LEVEL |
Liquid level too high |
LOW_LEVEL |
Liquid level too low |
CONTAMINATION |
Unwanted substance present |
CORROSION |
Material degradation |
// Generate scenarios
List<ProcessSafetyScenario> scenarios = generator.generateSingleFailures();
for (ProcessSafetyScenario scenario : scenarios) {
// Create a copy of the process for each scenario
ProcessSystem copy = process.copy();
// Apply the scenario
scenario.applyTo(copy);
// Run simulation
try {
copy.run();
// Analyze results
analyzeScenarioResults(scenario, copy);
} catch (Exception e) {
// Scenario caused simulation failure - important finding!
recordFailedScenario(scenario, e);
}
}
The generator provides built-in scenario execution capabilities:
// Run all single-failure scenarios automatically
List<ScenarioRunResult> results = generator.runAllSingleFailures();
// Or run specific scenarios
List<ProcessSafetyScenario> scenarios = generator.generateSingleFailures();
List<ScenarioRunResult> results = generator.runScenarios(scenarios);
// Get execution summary
String summary = generator.summarizeResults(results);
System.out.println(summary);
Each result contains:
| Field | Type | Description |
|---|---|---|
scenario |
ProcessSafetyScenario | The scenario that was run |
successful |
boolean | Whether simulation completed without error |
errorMessage |
String | Error message if failed (null otherwise) |
resultValues |
Map |
Key results: pressures, temperatures, levels |
executionTimeMs |
long | Execution time in milliseconds |
=== Scenario Execution Summary ===
Total scenarios: 12
Successful: 10 (83.3%)
Failed: 2 (16.7%)
Failed scenarios:
- COOLING_LOSS on Cooler-1: Simulation did not converge
- COMPRESSOR_TRIP on MainCompressor: Exceeded iteration limit
String summary = generator.getFailureModeSummary();
System.out.println(summary);
// Output:
// Failure Mode Analysis Summary
// =============================
// Total potential failures: 42
//
// By Equipment Type:
// ThrottlingValve: 12 failure modes
// Compressor: 8 failure modes
// Cooler: 6 failure modes
// Separator: 8 failure modes
// Pump: 8 failure modes
The AutomaticScenarioGenerator complements the existing safety framework:
The pvtsimulation package provides tools for simulating standard PVT laboratory experiments used in reservoir fluid characterization.
Location: neqsim.pvtsimulation
Purpose:
pvtsimulation/
├── simulation/ # PVT experiment simulations
│ ├── BasePVTsimulation.java # Base class
│ ├── SimulationInterface.java # Interface
│ │
│ ├── SaturationPressure.java # Bubble/dew point
│ ├── SaturationTemperature.java # Saturation temperature
│ │
│ ├── ConstantMassExpansion.java # CME experiment
│ ├── ConstantVolumeDepletion.java # CVD experiment
│ ├── DifferentialLiberation.java # DL experiment
│ │
│ ├── SeparatorTest.java # Single separator
│ ├── MultiStageSeparatorTest.java # Multi-stage separation
│ │
│ ├── SwellingTest.java # Gas injection swelling
│ ├── SlimTubeSim.java # Slim tube MMP
│ ├── MMPCalculator.java # Minimum miscibility pressure
│ │
│ ├── ViscositySim.java # Viscosity vs pressure
│ ├── ViscosityWaxOilSim.java # Wax oil viscosity
│ ├── DensitySim.java # Density vs pressure
│ ├── WaxFractionSim.java # Wax precipitation
│ └── GOR.java # Gas-oil ratio
│
├── regression/ # Parameter fitting
│ └── PVTRegression.java
│
├── modeltuning/ # Model tuning
│ └── ModelTuning.java
│
├── reservoirproperties/ # Reservoir calculations
│ └── ReservoirProperties.java
│
├── util/ # Utilities
│ ├── parameterfitting/ # Parameter fitting utilities
│ │ ├── AsphalteneOnsetFunction.java
│ │ └── AsphalteneOnsetFitting.java
│ └── PVTUtil.java
│
└── flowassurance/ # Flow assurance analysis
├── AsphalteneStabilityAnalyzer.java
├── DeBoerAsphalteneScreening.java
└── AsphalteneMethodComparison.java
| Package | Documentation | Description |
|---|---|---|
flowassurance |
flowassurance/ | Asphaltene stability, De Boer screening, CPA onset calculations |
Calculate bubble point or dew point pressure.
import neqsim.pvtsimulation.simulation.SaturationPressure;
SystemInterface oil = new SystemSrkEos(373.15, 200.0);
oil.addComponent("nitrogen", 0.5);
oil.addComponent("CO2", 2.0);
oil.addComponent("methane", 40.0);
oil.addComponent("ethane", 8.0);
oil.addComponent("propane", 5.0);
oil.addComponent("n-pentane", 3.0);
oil.addComponent("n-heptane", 20.0);
oil.addComponent("n-C10", 21.5);
oil.setMixingRule("classic");
SaturationPressure satPres = new SaturationPressure(oil);
satPres.setTemperature(373.15, "K");
satPres.run();
System.out.println("Saturation pressure: " + satPres.getSaturationPressure() + " bar");
Simulates isothermal expansion of reservoir fluid, measuring relative volume.
import neqsim.pvtsimulation.simulation.ConstantMassExpansion;
ConstantMassExpansion cme = new ConstantMassExpansion(oil);
cme.setTemperature(373.15, "K");
// Set pressure steps
double[] pressures = {300, 250, 200, 180, 160, 140, 120, 100, 80, 60, 40};
cme.setPressures(pressures, "bara");
cme.run();
// Get results
double[] relativeVolumes = cme.getRelativeVolume();
double[] liquidVolumes = cme.getLiquidRelativeVolume();
double[] Yvalues = cme.getYfactor();
double[] densities = cme.getDensity();
double[] Zfactors = cme.getZfactor();
// Print results
System.out.println("P (bar)\tVrel\tVliq\tY\tDensity\tZ");
for (int i = 0; i < pressures.length; i++) {
System.out.printf("%.1f\t%.4f\t%.4f\t%.4f\t%.2f\t%.4f%n",
pressures[i], relativeVolumes[i], liquidVolumes[i],
Yvalues[i], densities[i], Zfactors[i]);
}
CME Output Properties:
Simulates gas condensate depletion at constant volume.
import neqsim.pvtsimulation.simulation.ConstantVolumeDepletion;
SystemInterface condensate = new SystemPrEos(373.15, 400.0);
// Add components...
ConstantVolumeDepletion cvd = new ConstantVolumeDepletion(condensate);
cvd.setTemperature(373.15, "K");
double[] pressures = {400, 350, 300, 250, 200, 150, 100, 50};
cvd.setPressures(pressures, "bara");
cvd.run();
// Results
double[] liquidDropout = cvd.getLiquidVolumeFraction();
double[] cumulativeGasProduced = cvd.getCumulativeGasProduced();
double[] Zfactors = cvd.getZfactor();
double[] Bg = cvd.getBg();
CVD Output Properties:
Black oil experiment - gas released at each pressure step.
import neqsim.pvtsimulation.simulation.DifferentialLiberation;
DifferentialLiberation dl = new DifferentialLiberation(oil);
dl.setTemperature(373.15, "K");
double[] pressures = {300, 250, 200, 150, 100, 50, 1.01325};
dl.setPressures(pressures, "bara");
dl.run();
// Results
double[] Rs = dl.getRs(); // Solution GOR
double[] Bo = dl.getBo(); // Oil FVF
double[] oilDensity = dl.getOilDensity();
double[] oilViscosity = dl.getOilViscosity();
double[] gasDensity = dl.getGasDensity();
double[] gasGravity = dl.getGasGravity();
double[] Bg = dl.getBg();
System.out.println("P (bar)\tRs (Sm3/Sm3)\tBo\tρ_oil (kg/m3)");
for (int i = 0; i < pressures.length; i++) {
System.out.printf("%.1f\t%.2f\t\t%.4f\t%.2f%n",
pressures[i], Rs[i], Bo[i], oilDensity[i]);
}
DL Output Properties:
Model surface separation conditions.
import neqsim.pvtsimulation.simulation.SeparatorTest;
SeparatorTest sepTest = new SeparatorTest(oil);
// Define separator stages
double[] temperatures = {323.15, 288.15}; // K
double[] pressures = {50.0, 1.01325}; // bar
sepTest.setSeparatorConditions(temperatures, pressures);
sepTest.run();
// Results
double GOR = sepTest.getGOR();
double Bo = sepTest.getBo();
double oilDensity = sepTest.getOilDensity();
double oilMW = sepTest.getOilMolarMass();
System.out.println("GOR: " + GOR + " Sm3/Sm3");
System.out.println("Bo: " + Bo);
System.out.println("Stock tank oil density: " + oilDensity + " kg/m3");
import neqsim.pvtsimulation.simulation.MultiStageSeparatorTest;
MultiStageSeparatorTest mst = new MultiStageSeparatorTest(oil);
// Configure stages
mst.addSeparator(50.0, 323.15); // HP separator
mst.addSeparator(10.0, 308.15); // LP separator
mst.addSeparator(1.01325, 288.15); // Stock tank
mst.run();
double[] stageGORs = mst.getStageGOR();
double totalGOR = mst.getTotalGOR();
Gas injection swelling experiment.
import neqsim.pvtsimulation.simulation.SwellingTest;
SystemInterface injectionGas = new SystemSrkEos(373.15, 200.0);
injectionGas.addComponent("CO2", 1.0);
injectionGas.setMixingRule("classic");
SwellingTest swelling = new SwellingTest(oil);
swelling.setInjectionGas(injectionGas);
swelling.setTemperature(373.15, "K");
// Injection amounts (moles per mole original oil)
double[] injectionAmounts = {0.0, 0.1, 0.2, 0.3, 0.4, 0.5};
swelling.setInjectionAmounts(injectionAmounts);
swelling.run();
double[] saturationPressures = swelling.getSaturationPressures();
double[] swellingFactors = swelling.getSwellingFactors();
double[] oilDensities = swelling.getOilDensities();
Minimum miscibility pressure via slim tube simulation.
import neqsim.pvtsimulation.simulation.MMPCalculator;
MMPCalculator mmp = new MMPCalculator(oil, injectionGas);
mmp.setTemperature(373.15, "K");
mmp.run();
double minimumMiscibilityPressure = mmp.getMMP();
System.out.println("MMP: " + minimumMiscibilityPressure + " bar");
import neqsim.pvtsimulation.simulation.ViscositySim;
ViscositySim viscSim = new ViscositySim(oil);
viscSim.setTemperature(373.15, "K");
double[] pressures = {400, 300, 200, 150, 100, 50};
viscSim.setPressures(pressures, "bara");
viscSim.run();
double[] oilViscosities = viscSim.getOilViscosity();
double[] gasViscosities = viscSim.getGasViscosity();
import neqsim.pvtsimulation.regression.PVTRegression;
// Set up experiment with measured data
ConstantMassExpansion cme = new ConstantMassExpansion(oil);
cme.setExperimentalData(measuredPressures, measuredRelativeVolumes);
// Create regression
PVTRegression regression = new PVTRegression(oil);
regression.addExperiment(cme);
// Select parameters to tune
regression.tuneParameter("C7+", "Tc", 0.95, 1.05); // ±5%
regression.tuneParameter("C7+", "Pc", 0.95, 1.05);
regression.tuneParameter("C7+", "acf", 0.90, 1.10);
// Run regression
regression.run();
// Get tuned fluid
SystemInterface tunedOil = regression.getTunedSystem();
// Create reservoir oil
SystemInterface oil = new SystemSrkEos(373.15, 300.0);
oil.addComponent("nitrogen", 0.5);
oil.addComponent("CO2", 2.1);
oil.addComponent("methane", 35.2);
oil.addComponent("ethane", 7.8);
oil.addComponent("propane", 5.4);
oil.addComponent("i-butane", 1.2);
oil.addComponent("n-butane", 3.1);
oil.addComponent("i-pentane", 1.5);
oil.addComponent("n-pentane", 2.1);
oil.addComponent("n-hexane", 3.5);
oil.addTBPfraction("C7", 8.2, 96.0 / 1000.0, 0.738);
oil.addTBPfraction("C10", 12.4, 134.0 / 1000.0, 0.785);
oil.addTBPfraction("C15", 8.5, 206.0 / 1000.0, 0.835);
oil.addTBPfraction("C20+", 8.5, 450.0 / 1000.0, 0.920);
oil.setMixingRule("classic");
oil.useVolumeCorrection(true);
double reservoirTemp = 373.15; // K
// 1. Saturation Pressure
SaturationPressure sat = new SaturationPressure(oil);
sat.setTemperature(reservoirTemp, "K");
sat.run();
double Psat = sat.getSaturationPressure();
System.out.println("Bubble point: " + Psat + " bar");
// 2. CME
ConstantMassExpansion cme = new ConstantMassExpansion(oil.clone());
cme.setTemperature(reservoirTemp, "K");
double[] cmePressures = generatePressureRange(Psat + 50, 50, 15);
cme.setPressures(cmePressures, "bara");
cme.run();
// 3. Differential Liberation
DifferentialLiberation dl = new DifferentialLiberation(oil.clone());
dl.setTemperature(reservoirTemp, "K");
double[] dlPressures = generatePressureRange(Psat, 1.01325, 10);
dl.setPressures(dlPressures, "bara");
dl.run();
// 4. Separator Test
SeparatorTest sep = new SeparatorTest(oil.clone());
sep.setSeparatorConditions(
new double[]{323.15, 288.15}, // Temperatures
new double[]{30.0, 1.01325} // Pressures
);
sep.run();
// Print summary
System.out.println("\n=== PVT Summary ===");
System.out.println("Bubble point: " + Psat + " bar");
System.out.println("GOR: " + sep.getGOR() + " Sm3/Sm3");
System.out.println("Bo at Psat: " + dl.getBo()[0]);
System.out.println("Oil density at STC: " + sep.getOilDensity() + " kg/m3");
This guide covers PVT simulation workflows in NeqSim, backed by regression tests under src/test/java/neqsim/pvtsimulation/simulation. Use these tested setups to reproduce experiments in your own studies.
CVD simulation maintains reservoir volume constant while reducing pressure, measuring the liquid dropout from gas condensate reservoirs.
import neqsim.pvtsimulation.simulation.ConstantVolumeDepletion;
import neqsim.thermo.system.SystemSrkEos;
// Create fluid with TBP fractions
SystemSrkEos fluid = new SystemSrkEos(97.5 + 273.15, 300.0); // T(K), P(bara)
fluid.addComponent("nitrogen", 0.34);
fluid.addComponent("CO2", 3.59);
fluid.addComponent("methane", 67.42);
fluid.addComponent("ethane", 9.02);
fluid.addComponent("propane", 4.31);
fluid.addComponent("i-butane", 0.93);
fluid.addComponent("n-butane", 1.71);
fluid.addComponent("i-pentane", 0.74);
fluid.addComponent("n-pentane", 0.85);
fluid.addTBPfraction("C6", 1.38, 86.0 / 1000, 0.664);
fluid.addTBPfraction("C7", 1.57, 96.0 / 1000, 0.738);
fluid.addTBPfraction("C8", 1.73, 107.0 / 1000, 0.765);
fluid.addTBPfraction("C9", 1.40, 121.0 / 1000, 0.781);
fluid.addTBPfraction("C10+", 5.01, 230.0 / 1000, 0.820);
fluid.setMixingRule("classic");
fluid.useVolumeCorrection(true);
fluid.init(0);
fluid.init(1);
// Configure CVD simulation
ConstantVolumeDepletion cvd = new ConstantVolumeDepletion(fluid);
cvd.setTemperature(97.5, "C");
cvd.setPressures(new double[] {300, 250, 200, 150, 100, 50}); // bara
// Run simulation
cvd.runCalc();
// Get results
double[] relativeVolume = cvd.getRelativeVolume();
double[] liquidVolumeFraction = cvd.getLiquidVolume();
double[] Zgas = cvd.getZgas();
SystemInterface with EOS and add components/TBP fractionsConstantVolumeDepletionsetTemperature(...), setPressures(...), and runCalc()setExperimentalData(...)getRelativeVolume(), getLiquidVolume(), getZgas()DL simulation removes liberated gas at each pressure step, measuring oil shrinkage and gas evolution - essential for black oil PVT tables.
import neqsim.pvtsimulation.simulation.DifferentialLiberation;
import neqsim.thermo.system.SystemSrkEos;
// Create rich oil system with TBP characterization
SystemSrkEos fluid = new SystemSrkEos(97.5 + 273.15, 250.0);
fluid.addComponent("nitrogen", 0.5);
fluid.addComponent("CO2", 2.1);
fluid.addComponent("methane", 45.0);
fluid.addComponent("ethane", 7.5);
fluid.addComponent("propane", 5.2);
fluid.addComponent("i-butane", 1.1);
fluid.addComponent("n-butane", 2.8);
fluid.addComponent("i-pentane", 1.4);
fluid.addComponent("n-pentane", 1.9);
fluid.addTBPfraction("C6", 2.5, 86.0 / 1000, 0.685);
fluid.addTBPfraction("C7", 4.2, 96.0 / 1000, 0.755);
fluid.addTBPfraction("C8", 3.8, 107.0 / 1000, 0.775);
fluid.addTBPfraction("C9", 3.2, 121.0 / 1000, 0.790);
fluid.addTBPfraction("C10+", 18.8, 350.0 / 1000, 0.880);
fluid.setMixingRule("classic");
fluid.useVolumeCorrection(true);
fluid.init(0);
fluid.init(1);
// Configure DL simulation
DifferentialLiberation dl = new DifferentialLiberation(fluid);
dl.setTemperature(97.5, "C");
dl.setPressures(new double[] {250, 225, 200, 175, 150, 125, 100, 75, 50, 25, 1});
// Run simulation
dl.runCalc();
// Get results
double[] Bo = dl.getBo(); // Oil formation volume factor
double[] Rs = dl.getRs(); // Solution gas-oil ratio (Sm3/Sm3)
double[] Bg = dl.getBg(); // Gas formation volume factor
double[] oilDensity = dl.getOilDensity();
| Property | Description | Expected Trend |
|---|---|---|
| Bo | Oil formation volume factor (Vres/Vstock) | Decreases from ~1.7 to ~1.05 as pressure drops |
| Rs | Solution gas-oil ratio | Decreases to zero at final stage |
| Bg | Gas formation volume factor | Increases as gas expands at lower pressure |
| Oil Density | Density of remaining oil | Increases as light components liberate |
CCE measures PV behavior without removing any material - used to determine bubble/dew point pressure.
import neqsim.pvtsimulation.simulation.ConstantMassExpansion;
// After creating and initializing fluid...
ConstantMassExpansion cce = new ConstantMassExpansion(fluid);
cce.setTemperature(100.0, "C");
// Run to find saturation pressure
cce.runCalc();
double saturationPressure = cce.getSaturationPressure();
double[] relativeVolume = cce.getRelativeVolume();
double[] Ytfactor = cce.getYfactor();
Quick calculation of bubble or dew point pressure:
import neqsim.thermodynamicoperations.ThermodynamicOperations;
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
// For bubble point (oil system)
ops.calcBubblePoint();
double bubbleP = fluid.getPressure("bara");
// For dew point (gas condensate)
ops.calcDewPoint();
double dewP = fluid.getPressure("bara");
Minimum miscibility pressure (MMP) determination:
import neqsim.pvtsimulation.simulation.SlimTubeSim;
// Create injection gas
SystemSrkEos injectionGas = new SystemSrkEos(373.15, 200.0);
injectionGas.addComponent("CO2", 1.0);
injectionGas.setMixingRule("classic");
// Configure slim tube
SlimTubeSim slimTube = new SlimTubeSim(reservoirFluid, injectionGas);
slimTube.setTemperature(100.0, "C");
slimTube.setPressures(new double[] {150, 200, 250, 300, 350, 400});
slimTube.runCalc();
double[] recovery = slimTube.getOilRecovery();
init(0) and init(1) before creating PVT simulationssetTemperature() on each simulation to avoid state carryoveruseVolumeCorrection(true) for better liquid density predictionssetExperimentalData() methods for regressionThis document describes the complete workflow for generating and tuning fluid models from laboratory PVT data in NeqSim.
A standard PVT laboratory report contains the following sections. Below are example data tables showing the format typically received from labs like Core Lab, Schlumberger, or Intertek.
Well Name: A-1
Sample Type: Bottom Hole Sample (BHS)
Sampling Depth: 2850 m TVD
Sampling Date: 2024-06-15
Reservoir Pressure: 285 bara
Reservoir Temp: 98 °C (371.15 K)
Saturation Pressure: 248 bara (Bubble Point)
| Component | Mol % | MW (g/mol) | Density (g/cm³) |
|---|---|---|---|
| N₂ | 0.34 | 28.01 | - |
| CO₂ | 3.53 | 44.01 | - |
| H₂S | 0.00 | 34.08 | - |
| C₁ | 70.78 | 16.04 | - |
| C₂ | 8.94 | 30.07 | - |
| C₃ | 5.05 | 44.10 | - |
| i-C₄ | 0.85 | 58.12 | - |
| n-C₄ | 1.68 | 58.12 | - |
| i-C₅ | 0.62 | 72.15 | - |
| n-C₅ | 0.79 | 72.15 | - |
| C₆ | 0.83 | 86.18 | 0.664 |
| C₇ | 1.06 | 92.2 | 0.7324 |
| C₈ | 1.06 | 104.6 | 0.7602 |
| C₉ | 0.79 | 119.1 | 0.7677 |
| C₁₀ | 0.57 | 133.0 | 0.790 |
| C₁₁ | 0.38 | 147.0 | 0.795 |
| C₁₂+ | 2.73 | 263.0 | 0.854 |
| Total | 100.00 |
| Pressure (bara) | Relative Volume | Y-Factor | Oil Density (kg/m³) | Oil Viscosity (cP) |
|---|---|---|---|---|
| 350 | 0.9521 | - | 612.3 | 0.285 |
| 300 | 0.9712 | - | 625.1 | 0.312 |
| 280 | 0.9845 | - | 632.8 | 0.328 |
| 248 (Psat) | 1.0000 | - | 645.2 | 0.352 |
| 220 | 1.0523 | 2.145 | 658.4 | 0.385 |
| 200 | 1.1124 | 2.287 | 670.1 | 0.412 |
| 180 | 1.1892 | 2.456 | 682.5 | 0.445 |
| 150 | 1.3245 | 2.712 | 698.2 | 0.498 |
| 120 | 1.5421 | 3.024 | 715.8 | 0.562 |
| 100 | 1.7856 | 3.312 | 728.4 | 0.615 |
| Pressure (bara) | Rs (Sm³/Sm³) | Bo (m³/Sm³) | Oil Density (kg/m³) | Oil Viscosity (cP) | Gas Z-factor | Gas Gravity |
|---|---|---|---|---|---|---|
| 248 (Psat) | 152.3 | 1.4521 | 645.2 | 0.352 | - | - |
| 220 | 138.5 | 1.4012 | 658.4 | 0.385 | 0.862 | 0.745 |
| 200 | 125.2 | 1.3654 | 670.1 | 0.412 | 0.851 | 0.768 |
| 180 | 112.8 | 1.3285 | 682.5 | 0.445 | 0.838 | 0.792 |
| 150 | 94.5 | 1.2756 | 698.2 | 0.498 | 0.815 | 0.825 |
| 120 | 75.2 | 1.2198 | 715.8 | 0.562 | 0.792 | 0.861 |
| 100 | 61.8 | 1.1812 | 728.4 | 0.615 | 0.775 | 0.892 |
| 80 | 48.2 | 1.1425 | 742.1 | 0.685 | 0.758 | 0.928 |
| 50 | 28.5 | 1.0912 | 762.5 | 0.812 | 0.732 | 0.975 |
| 20 | 8.5 | 1.0385 | 785.2 | 1.025 | 0.712 | 1.045 |
| 1.01 (STO) | 0.0 | 1.0000 | 825.4 | 2.850 | - | - |
| Pressure (bara) | Liquid Dropout (%) | Z-factor | Cumulative Gas Produced (%) | Liquid Density (kg/m³) |
|---|---|---|---|---|
| 285 (Pdew) | 0.0 | 0.892 | 0.0 | - |
| 260 | 4.2 | 0.875 | 8.5 | 612.5 |
| 230 | 8.5 | 0.856 | 18.2 | 628.4 |
| 200 | 12.8 | 0.838 | 28.5 | 645.2 |
| 170 | 15.2 | 0.821 | 39.8 | 658.7 |
| 140 | 14.5 | 0.805 | 51.2 | 672.1 |
| 110 | 12.1 | 0.792 | 62.8 | 685.4 |
| 80 | 8.8 | 0.781 | 74.5 | 698.2 |
| 50 | 5.2 | 0.772 | 86.2 | 712.5 |
Test Conditions:
| Stage | Pressure (bara) | Temperature (°C) |
|---|---|---|
| 1 (HP Sep) | 45.0 | 45.0 |
| 2 (LP Sep) | 8.0 | 35.0 |
| 3 (Stock Tank) | 1.01 | 15.0 |
Results:
| Property | Stage 1 | Stage 2 | Stage 3 | Total |
|---|---|---|---|---|
| GOR (Sm³/Sm³) | 95.2 | 18.5 | 8.2 | 121.9 |
| Gas Gravity | 0.752 | 0.985 | 1.245 | 0.812 |
| Oil Density (kg/m³) | 712.5 | 758.2 | 825.4 | - |
| Oil Viscosity (cP) | 0.52 | 0.85 | 2.45 | - |
Stock Tank Oil Properties:
| Pressure (bara) | Temperature (°C) | Oil Viscosity (cP) | Gas Viscosity (cP) |
|---|---|---|---|
| 285 | 98 | 0.285 | 0.0185 |
| 248 | 98 | 0.352 | 0.0178 |
| 200 | 98 | 0.412 | 0.0165 |
| 150 | 98 | 0.498 | 0.0148 |
| 100 | 98 | 0.615 | 0.0132 |
| 248 | 80 | 0.425 | 0.0168 |
| 248 | 60 | 0.585 | 0.0155 |
| Cumulative CO₂ Injected (mol%) | Saturation Pressure (bara) | Relative Swelling Factor | Oil Density (kg/m³) |
|---|---|---|---|
| 0.0 | 248.0 | 1.000 | 645.2 |
| 5.0 | 268.5 | 1.025 | 638.4 |
| 10.0 | 292.1 | 1.052 | 630.1 |
| 15.0 | 318.4 | 1.082 | 620.5 |
| 20.0 | 348.2 | 1.115 | 608.2 |
| 25.0 | 382.5 | 1.152 | 594.8 |
| 30.0 | 421.8 | 1.195 | 578.5 |
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Lab PVT Data │───▶│ Initial Fluid │───▶│ EOS Regression │───▶│ Export Model │
│ (CCE,DLE,CVD) │ │ Characterization│ │ (Parameter Fit)│ │ (E300, CSV) │
└─────────────────┘ └─────────────────┘ └─────────────────┘ └─────────────────┘
Start with laboratory-reported composition and characterize heavy fractions:
import neqsim.thermo.system.SystemSrkEos;
import neqsim.thermo.system.SystemInterface;
// Create fluid at reservoir conditions
SystemInterface fluid = new SystemSrkEos(373.15, 250.0); // T(K), P(bar)
// Add defined components (from lab composition)
fluid.addComponent("nitrogen", 0.34);
fluid.addComponent("CO2", 3.53);
fluid.addComponent("methane", 70.78);
fluid.addComponent("ethane", 8.94);
fluid.addComponent("propane", 5.05);
fluid.addComponent("i-butane", 0.85);
fluid.addComponent("n-butane", 1.68);
fluid.addComponent("i-pentane", 0.62);
fluid.addComponent("n-pentane", 0.79);
fluid.addComponent("n-hexane", 0.83);
// Add C7+ fractions (TBP cuts from lab)
fluid.addTBPfraction("C7", 1.06, 92.2/1000.0, 0.7324); // name, mole%, MW(kg/mol), SG
fluid.addTBPfraction("C8", 1.06, 104.6/1000.0, 0.7602);
fluid.addTBPfraction("C9", 0.79, 119.1/1000.0, 0.7677);
fluid.addTBPfraction("C10", 0.57, 133.0/1000.0, 0.79);
// Add plus fraction
fluid.addPlusFraction("C20+", 2.11, 381.0/1000.0, 0.88);
// Characterize plus fraction into pseudo-components
fluid.getCharacterization().characterisePlusFraction();
// Initialize database and set mixing rule
fluid.createDatabase(true);
fluid.setMixingRule(2); // Classic mixing rule
Import experimental data from CCE, DLE, CVD, and separator tests:
import neqsim.pvtsimulation.regression.PVTRegression;
import neqsim.pvtsimulation.regression.RegressionParameter;
PVTRegression regression = new PVTRegression(fluid);
// CCE (Constant Composition Expansion) data
double[] ccePressures = {400, 350, 300, 280, 260, 240, 220, 200, 180}; // bara
double[] cceRelVol = {0.95, 0.97, 0.99, 1.00, 1.02, 1.05, 1.10, 1.18, 1.30};
double cceTemperature = 373.15; // K
regression.addCCEData(ccePressures, cceRelVol, cceTemperature);
// DLE (Differential Liberation) data
double[] dlePressures = {280, 240, 200, 160, 120, 80, 40, 1.01325}; // bara
double[] dleRs = {150, 130, 110, 85, 60, 35, 15, 0}; // Sm³/Sm³
double[] dleBo = {1.45, 1.40, 1.35, 1.28, 1.20, 1.12, 1.06, 1.02}; // m³/Sm³
double[] dleOilDensity = {650, 680, 710, 740, 770, 800, 830, 850}; // kg/m³
double dleTemperature = 373.15; // K
regression.addDLEData(dlePressures, dleRs, dleBo, dleOilDensity, dleTemperature);
// CVD (Constant Volume Depletion) data - for gas condensates
double[] cvdPressures = {350, 300, 250, 200, 150, 100}; // bara
double[] cvdLiquidDropout = {0, 5, 12, 18, 15, 10}; // volume %
double[] cvdZFactor = {0.85, 0.82, 0.80, 0.78, 0.77, 0.76};
double cvdTemperature = 373.15; // K
regression.addCVDData(cvdPressures, cvdLiquidDropout, cvdZFactor, cvdTemperature);
// Separator test data
regression.addSeparatorData(
125.0, // GOR (Sm³/Sm³)
1.35, // Bo
35.0, // API gravity
50.0, // separator pressure (bar)
313.15, // separator temperature (K)
373.15 // reservoir temperature (K)
);
Select which EOS parameters to tune:
// Binary Interaction Parameters (BIPs)
regression.addRegressionParameter(RegressionParameter.BIP_METHANE_C7PLUS);
regression.addRegressionParameter(RegressionParameter.BIP_C2C6_C7PLUS);
regression.addRegressionParameter(RegressionParameter.BIP_CO2_HC);
// Critical property multipliers for C7+ pseudo-components
regression.addRegressionParameter(RegressionParameter.TC_MULTIPLIER_C7PLUS);
regression.addRegressionParameter(RegressionParameter.PC_MULTIPLIER_C7PLUS);
regression.addRegressionParameter(RegressionParameter.OMEGA_MULTIPLIER_C7PLUS);
// Volume shift for density matching
regression.addRegressionParameter(RegressionParameter.VOLUME_SHIFT_C7PLUS);
// Viscosity parameters (if tuning viscosity)
regression.addRegressionParameter(RegressionParameter.VISCOSITY_LBC_MULTIPLIER);
regression.addRegressionParameter(RegressionParameter.VISCOSITY_PEDERSEN_ALPHA);
// Custom bounds (optional)
regression.addRegressionParameter(
RegressionParameter.BIP_METHANE_C7PLUS,
0.0, // lower bound
0.15, // upper bound
0.05 // initial guess
);
// Set experiment weights (optional)
regression.setExperimentWeight(ExperimentType.CCE, 1.0);
regression.setExperimentWeight(ExperimentType.DLE, 1.5); // Higher weight for DLE
regression.setExperimentWeight(ExperimentType.SEPARATOR, 1.0);
// Optimization settings
regression.setMaxIterations(200);
regression.setTolerance(1e-8);
regression.setVerbose(true);
Execute the optimization:
import neqsim.pvtsimulation.regression.RegressionResult;
RegressionResult result = regression.runRegression();
// Get the tuned fluid
SystemInterface tunedFluid = result.getTunedFluid();
// Check results
System.out.println("Final chi-square: " + result.getChiSquare());
System.out.println("Optimized parameters:");
double[] params = result.getOptimizedParameters();
for (int i = 0; i < params.length; i++) {
System.out.println(" Parameter " + i + ": " + params[i]);
}
Generate comparison reports:
import neqsim.pvtsimulation.util.PVTReportGenerator;
import neqsim.pvtsimulation.simulation.*;
// Run PVT simulations with tuned fluid
ConstantMassExpansion cce = new ConstantMassExpansion(tunedFluid);
cce.setTemperature(373.15);
cce.setPressures(ccePressures);
cce.runCalc();
DifferentialLiberation dle = new DifferentialLiberation(tunedFluid);
dle.setTemperature(373.15);
dle.setPressures(dlePressures);
dle.runCalc();
// Create report generator
PVTReportGenerator report = new PVTReportGenerator(tunedFluid);
report.setProjectInfo("Field X Development", "Well A-1 Sample");
report.setReservoirConditions(250.0, 100.0); // P(bar), T(°C)
// Add simulation results
report.addCCE(cce);
report.addDLE(dle);
// Add lab data for comparison
for (int i = 0; i < ccePressures.length; i++) {
report.addLabCCEData(ccePressures[i], "RelVol", cceRelVol[i], "");
}
for (int i = 0; i < dlePressures.length; i++) {
report.addLabDLEData(dlePressures[i], "Bo", dleBo[i], "m³/Sm³");
report.addLabDLEData(dlePressures[i], "Rs", dleRs[i], "Sm³/Sm³");
}
// Generate comparison with statistics
String comparison = report.generateLabComparison();
System.out.println(comparison);
// Output includes AAD (Average Absolute Deviation) and ARE (Average Relative Error)
// Generate full Markdown report
String fullReport = report.generateMarkdownReport();
Export the tuned fluid model to Eclipse E300 format:
import neqsim.blackoil.io.EclipseEOSExporter;
import java.nio.file.Path;
// Simple export with default settings
EclipseEOSExporter.toFile(tunedFluid, Path.of("PVT_TUNED.INC"));
// Export with custom configuration
EclipseEOSExporter.ExportConfig config = new EclipseEOSExporter.ExportConfig()
.setUnits(EclipseEOSExporter.Units.FIELD) // METRIC or FIELD
.setReferenceTemperature(373.15) // Reservoir temp (K)
.setIncludePVTO(true) // Live oil table
.setIncludePVTG(true) // Wet gas table
.setIncludePVTW(true) // Water properties
.setIncludeDensity(true) // Stock tank densities
.setComment("Tuned to Well A-1 PVT data");
EclipseEOSExporter.toFile(tunedFluid, Path.of("PVT_FIELD.INC"), config);
// Or get as string for inspection
String eclipseContent = EclipseEOSExporter.toString(tunedFluid, config);
System.out.println(eclipseContent);
The exporter produces standard Eclipse keywords:
| Unit System | Pressure | Density | GOR/CGR | Viscosity |
|---|---|---|---|---|
| METRIC | bar | kg/m³ | Sm³/Sm³ | mPa·s |
| FIELD | psia | lb/ft³ | scf/stb | cp |
In addition to black-oil PVT tables, you can export the full compositional EOS model to Eclipse E300 format. This preserves all component properties and binary interaction coefficients:
import neqsim.thermo.util.readwrite.EclipseFluidReadWrite;
// Export tuned fluid to E300 compositional format
EclipseFluidReadWrite.write(tunedFluid, "TUNED_FLUID.e300", 100.0); // reservoir temp in °C
// Or get as string for inspection
String e300Content = EclipseFluidReadWrite.toE300String(tunedFluid, 100.0);
System.out.println(e300Content);
The E300 compositional file includes:
| Keyword | Description |
|---|---|
NCOMPS |
Number of components |
EOS |
Equation of state (SRK, PR) |
RTEMP |
Reservoir temperature |
CNAMES |
Component names |
TCRIT |
Critical temperatures (K) |
PCRIT |
Critical pressures (bar) |
ACF |
Acentric factors |
MW |
Molecular weights (g/mol) |
TBOIL |
Normal boiling points (K) |
VCRIT |
Critical volumes (m³/kmol) |
SSHIFT |
Volume translation parameters |
PARACHOR |
Parachor values for IFT |
ZI |
Mole fractions |
BIC |
Binary interaction coefficients |
// Read the E300 file back into a NeqSim fluid
SystemInterface importedFluid = EclipseFluidReadWrite.read("TUNED_FLUID.e300");
// Set conditions and run flash
importedFluid.setPressure(200.0, "bara");
importedFluid.setTemperature(100.0, "C");
ThermodynamicOperations ops = new ThermodynamicOperations(importedFluid);
ops.TPflash();
Generate CSV files for spreadsheet analysis or other simulators:
// CCE data
String cceCSV = report.generateCCECSV();
// DLE data
String dleCSV = report.generateDLECSV();
// CVD data
String cvdCSV = report.generateCVDCSV();
// Viscosity data
String viscCSV = report.generateViscosityCSV();
// Density data
String densCSV = report.generateDensityCSV();
// Swelling test
String swellCSV = report.generateSwellingCSV();
// GOR data
String gorCSV = report.generateGORCSV();
// MMP data
String mmpCSV = report.generateMMPCSV();
| Parameter | Description | Typical Bounds |
|---|---|---|
BIP_METHANE_C7PLUS |
BIP between CH₄ and C7+ | 0.0 - 0.10 |
BIP_C2C6_C7PLUS |
BIP between C2-C6 and C7+ | 0.0 - 0.05 |
BIP_CO2_HC |
BIP between CO₂ and hydrocarbons | 0.08 - 0.18 |
BIP_N2_HC |
BIP between N₂ and hydrocarbons | 0.02 - 0.12 |
VOLUME_SHIFT_C7PLUS |
Volume shift multiplier for C7+ | 0.8 - 1.2 |
TC_MULTIPLIER_C7PLUS |
Critical temperature multiplier | 0.95 - 1.05 |
PC_MULTIPLIER_C7PLUS |
Critical pressure multiplier | 0.95 - 1.05 |
OMEGA_MULTIPLIER_C7PLUS |
Acentric factor multiplier | 0.90 - 1.10 |
PLUS_MOLAR_MASS_MULTIPLIER |
Plus fraction MW adjustment | 0.90 - 1.10 |
GAMMA_ALPHA |
Whitson gamma distribution shape | 0.5 - 4.0 |
GAMMA_ETA |
Whitson gamma min MW (η) | 75.0 - 95.0 |
VISCOSITY_LBC_MULTIPLIER |
LBC viscosity correlation factor | 0.8 - 1.5 |
VISCOSITY_PEDERSEN_ALPHA |
Pedersen viscosity parameter | 0.5 - 2.0 |
Find optimal separator conditions:
import neqsim.pvtsimulation.simulation.MultiStageSeparatorTest;
MultiStageSeparatorTest sepTest = new MultiStageSeparatorTest(tunedFluid);
sepTest.setReservoirConditions(250.0, 373.15); // P(bar), T(K)
// Add separator stages
sepTest.addSeparatorStage(50.0, 40.0, "HP Separator"); // P(bar), T(°C)
sepTest.addSeparatorStage(10.0, 30.0, "LP Separator");
sepTest.addSeparatorStage(1.01325, 15.0, "Stock Tank");
// Run simulation
sepTest.run();
// Optimize first stage pressure/temperature
MultiStageSeparatorTest.OptimizationResult optResult =
sepTest.optimizeFirstStageSeparator(
5.0, 80.0, 16, // pressure: min, max, steps
20.0, 60.0, 9 // temperature: min, max, steps
);
System.out.println("Optimal P: " + optResult.getOptimalPressure() + " bara");
System.out.println("Optimal T: " + optResult.getOptimalTemperature() + " °C");
System.out.println("Max Recovery: " + optResult.getMaximumOilRecovery());
System.out.println("GOR at optimum: " + optResult.getGorAtOptimum() + " Sm³/Sm³");
See PVTRegressionTest.java for working examples.
ThermodynamicOperationsTest exercises NeqSim's property flash API across PT/TP orderings, unit handling, and online composition updates. This guide distills the tested patterns so you can set up property flashes with confidence.
testFlash creates a binary methane/ethane SRK system and runs both flash(FlashType.PT, P, T, unitP, unitT) and flash(FlashType.TP, T, P, unitT, unitP) on the same state, then compares all returned properties for equality.【F:src/test/java/neqsim/thermodynamicoperations/ThermodynamicOperationsTest.java†L25-L48】 The test verifies that property flashes are order-invariant when pressure and temperature units are supplied explicitly.
Takeaway: You can interchange PT and TP flash modes without changing results, as long as you reinitialize (init(2) and initPhysicalProperties()) before reading properties.
testFluidDefined builds several SRK air mixtures and calls propertyFlash with streaming pressure/temperature vectors and optional online compositions to mimic live analyzers.【F:src/test/java/neqsim/thermodynamicoperations/ThermodynamicOperationsTest.java†L50-L119】 The test first omits init(0) to prove the API returns a descriptive error ("Sum of fractions must be approximately to 1 or 100..."), then reinitializes and confirms all calculation errors are null.
Setup checklist from the test:
init(0) before requesting properties to normalize molar fractions.FlashMode 1, 2, or 3; any other mode yields the explicit error asserted in testNeqSimPython.【F:src/test/java/neqsim/thermodynamicoperations/ThermodynamicOperationsTest.java†L120-L152】testNeqSimPython and testNeqSimPython2 illustrate how property flashes can be called from foreign interfaces (e.g., Python bindings) while maintaining result integrity.【F:src/test/java/neqsim/thermodynamicoperations/ThermodynamicOperationsTest.java†L120-L209】 The tests check that:
CalculationResult objects are stable under equality and hashCode() comparisons.FlashMode inputs return clear error strings without throwing exceptions.SystemProperties.getPropertyNames() even when only single-point requests are made.When wrapping the API externally, mirror these assertions to guard against transport or serialization errors.
The WhitsonPVTReader class enables NeqSim to read PVT parameter files exported from Whitson+ and similar PVT software, creating fully configured fluid systems with accurate thermodynamic properties.
When working with PVT characterization software like Whitson+, you can export equation of state parameters and component properties to a tab-separated file format. The WhitsonPVTReader parses these files and creates NeqSim SystemInterface objects with:
The Whitson PVT parameter file is a tab-separated text file with three main sections:
Key-value pairs defining global EOS and model parameters:
Parameter Value
Name Predictive_EOS_Parameters
EOS Type PR
LBC P0 0.1023
LBC P1 0.023364
LBC P2 0.058533
LBC P3 -0.037734245
LBC P4 0.00839916
LBC F0 0.1
C7+ Gamma Shape 0.677652
C7+ Gamma Bound 94.9981
Omega A, ΩA 0.457236
Omega B, ΩB 0.0777961
Component properties with a header row and units row:
Component MW Pc Tc AF, ω Volume Shift, s ZcVisc VcVisc Vc Zc Pchor SG Tb LMW
- - bara C - - - m3/kmol m3/kmol - - - C -
CO2 44.01000 73.74000 30.97000 0.225000 0.001910 0.274330 0.09407 0.09407 0.274330 80.00 0.76193 -88.266
N2 28.01400 33.98000 -146.95000 0.037000 -0.167580 0.291780 0.09010 0.09010 0.291780 59.10 0.28339 -195.903
C1 16.04300 45.99000 -82.59000 0.011000 -0.149960 0.286200 0.09860 0.09860 0.286200 71.00 0.14609 -161.593
...
Column definitions:
| Column | Description | Units |
|---|---|---|
| Component | Component name | - |
| MW | Molecular weight | g/mol |
| Pc | Critical pressure | bara |
| Tc | Critical temperature | °C |
| AF, ω | Acentric factor | - |
| Volume Shift, s | Peneloux volume shift | - |
| ZcVisc | Critical compressibility for viscosity | - |
| VcVisc | Critical volume for viscosity | m³/kmol |
| Vc | Critical volume | m³/kmol |
| Zc | Critical compressibility | - |
| Pchor | Parachor | - |
| SG | Specific gravity | - |
| Tb | Normal boiling point | °C |
| LMW | Lumped molecular weight (optional) | g/mol |
Full symmetric matrix of binary interaction parameters:
BIPS CO2 N2 C1 C2 C3 ...
CO2 0.00 0.00 0.11 0.13 0.13 ...
N2 0.00 0.00 0.03 0.01 0.09 ...
C1 0.11 0.03 0.00 0.00 0.00 ...
...
Read a parameter file and create a fluid with equal molar composition:
import neqsim.thermo.util.readwrite.WhitsonPVTReader;
import neqsim.thermo.system.SystemInterface;
// Read parameter file
SystemInterface fluid = WhitsonPVTReader.read("path/to/volveparam.txt");
// Set conditions
fluid.setTemperature(373.15); // 100°C in Kelvin
fluid.setPressure(200.0); // 200 bar
// Run flash calculation
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
// Get properties
double density = fluid.getDensity("kg/m3");
double viscosity = fluid.getViscosity("cP");
Specify molar composition matching the component order in the file:
// Define composition (must match number of components in file)
double[] composition = {
0.02, // CO2
0.01, // N2
0.70, // C1
0.10, // C2
0.05, // C3
0.08, // C7
0.04 // C10
};
SystemInterface fluid = WhitsonPVTReader.read("path/to/volveparam.txt", composition);
The reader provides getters for extracted parameters:
WhitsonPVTReader reader = new WhitsonPVTReader();
reader.parseFile("path/to/volveparam.txt");
// Get LBC viscosity parameters
double[] lbcParams = reader.getLBCParameters();
// Returns: [P0, P1, P2, P3, P4, F0]
// Get C7+ gamma distribution parameters
double[] gammaParams = reader.getGammaParameters();
// Returns: [shape, bound]
// Get Omega parameters
double omegaA = reader.getOmegaA();
double omegaB = reader.getOmegaB();
// Get component information
int numComponents = reader.getNumberOfComponents();
List<String> names = reader.getComponentNames();
The reader automatically maps Whitson component names to NeqSim standard names:
| Whitson Name | NeqSim Name |
|---|---|
| C1 | methane |
| C2 | ethane |
| C3 | propane |
| i-C4 | i-butane |
| n-C4 | n-butane |
| i-C5 | i-pentane |
| n-C5 | n-pentane |
| NEO-C5 | 22-dim-C3 |
| C6 | n-hexane |
| N2 | nitrogen |
| CO2 | CO2 |
| H2S | H2S |
| H2O | water |
C7+ fractions (C7, C8, C9, ..., C36+) are added as pseudo-components with the suffix _PC.
| File Value | NeqSim Class |
|---|---|
| PR | SystemPrEos |
| SRK | SystemSrkEos |
| PR78 | SystemPrEos1978 |
This reader enables seamless integration between Whitson+ PVT characterization and NeqSim process simulation:
WhitsonPVTReader to create a NeqSim fluidThis workflow ensures consistency between PVT modeling and process simulation, using the same EOS parameters throughout.
Once a fluid is created from a Whitson file, you can run standard PVT simulations:
import neqsim.pvtsimulation.simulation.*;
import neqsim.pvtsimulation.util.PVTReportGenerator;
// Create fluid from Whitson file
double[] composition = {0.02, 0.01, 0.70, 0.10, 0.05, 0.08, 0.04};
SystemInterface fluid = WhitsonPVTReader.read("volveparam.txt", composition);
// Initialize
fluid.setTemperature(373.15); // 100°C
fluid.setPressure(300.0);
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
fluid.initPhysicalProperties();
// Saturation pressure (dew point for gas condensate)
SaturationPressure satPres = new SaturationPressure(fluid);
satPres.setTemperature(100.0, "C");
satPres.run();
double psat = satPres.getSaturationPressure();
// Constant Composition Expansion (CCE)
ConstantMassExpansion cce = new ConstantMassExpansion(fluid);
cce.setTemperature(100.0, "C");
cce.setPressures(new double[]{300, 250, 200, 150, 100});
cce.runCalc();
double[] relVol = cce.getRelativeVolume();
// Multi-Stage Separator Test
MultiStageSeparatorTest sepTest = new MultiStageSeparatorTest(fluid);
sepTest.setReservoirConditions(300.0, 100.0);
sepTest.addSeparatorStage(50.0, 40.0, "HP Separator");
sepTest.addSeparatorStage(10.0, 30.0, "LP Separator");
sepTest.addSeparatorStage(1.01325, 15.0, "Stock Tank");
sepTest.run();
double gor = sepTest.getTotalGOR(); // Sm³/Sm³
double bo = sepTest.getBo(); // m³/Sm³
double apiGravity = sepTest.getStockTankAPIGravity();
double stockTankDensity = sepTest.getStockTankOilDensity(); // kg/m³
// Get oil properties at each separator stage
for (var stage : sepTest.getStageResults()) {
double oilDensity = stage.getOilDensity(); // kg/m³
double oilViscosity = stage.getOilViscosity(); // cP
}
// Generate PVT Report
PVTReportGenerator report = new PVTReportGenerator(fluid);
report.setProjectInfo("My Project", "Gas Condensate Sample")
.setReservoirConditions(300.0, 100.0)
.setSaturationPressure(psat, false) // false = dew point
.addCCE(cce)
.addSeparatorTest(sepTest);
String markdownReport = report.generateMarkdownReport();
System.out.println(markdownReport);
The reader automatically applies the LBC viscosity model with the parameters from the Whitson file (P0-P4). The viscosity is calculated using:
// Viscosity is automatically calculated with LBC model
fluid.initPhysicalProperties();
// Gas phase viscosity
double gasViscosity = fluid.getPhase("gas").getPhysicalProperties().getViscosity();
double gasViscosityCP = gasViscosity * 1000; // Convert Pa·s to cP
// Oil phase viscosity (if oil phase exists)
if (fluid.hasPhaseType("oil")) {
double oilViscosity = fluid.getPhase("oil").getPhysicalProperties().getViscosity();
double oilViscosityCP = oilViscosity * 1000; // Convert Pa·s to cP
}
You can also use ViscositySim to calculate viscosity at multiple pressures:
import neqsim.pvtsimulation.simulation.ViscositySim;
ViscositySim viscSim = new ViscositySim(fluid);
double[] pressures = {300.0, 250.0, 200.0, 150.0, 100.0};
double[] temperatures = new double[pressures.length];
Arrays.fill(temperatures, 373.15); // 100°C in Kelvin
viscSim.setTemperaturesAndPressures(temperatures, pressures);
viscSim.runCalc();
double[] gasViscosity = viscSim.getGasViscosity(); // Pa·s (multiply by 1000 for cP)
double[] oilViscosity = viscSim.getOilViscosity(); // Pa·s (multiply by 1000 for cP)
A typical PVT report for a gas condensate fluid created from a Whitson parameter file:
# PVT Study Report
## Project Information
| Property | Value |
|----------|-------|
| Project | Whitson PVT Reader Test |
| Fluid Name | Test Gas Condensate |
| Report Date | 2025-12-15 12:59 |
## Reservoir Conditions
| Property | Value | Unit |
|----------|-------|------|
| Reservoir Pressure | 300.0 | bara |
| Reservoir Temperature | 100.0 | °C |
| Dew Point Pressure | 248.41 | bara |
## Fluid Composition
| Component | Mole Fraction | MW (g/mol) |
|-----------|--------------|------------|
| CO2 | 0.020000 | 44.01 |
| nitrogen | 0.010000 | 28.01 |
| methane | 0.700000 | 16.04 |
| ethane | 0.100000 | 30.07 |
| propane | 0.050000 | 44.10 |
| C7_PC | 0.080000 | 97.63 |
| C10_PC | 0.040000 | 134.47 |
## Constant Composition Expansion (CCE)
| Pressure (bara) | Rel. Volume | Y-Factor | Density (kg/m³) |
|-----------------|-------------|----------|-----------------|
| 298.1 | 0.8332 | - | 332.0 |
| 273.2 | 0.8703 | - | 318.3 |
| 248.4 | 0.9163 | - | 302.8 |
| 223.6 | 1.0042 | 1.1212 | 234.5 |
| 198.7 | 1.1188 | 1.0966 | 193.6 |
| 173.9 | 1.2725 | 1.0707 | 160.5 |
## Separator Test
=== Multi-Stage Separator Test Results ===
Reservoir Conditions: P = 300.0 bara, T = 100.0 °C
Number of Stages: 3
Stage-by-Stage Results:
| Stage | P (bara) | T (°C) | GOR (Sm³/Sm³) | Cum GOR (Sm³/Sm³) |
|-------|----------|--------|---------------|-------------------|
| HP Separator | 50.0 | 40.0 | 1096.9 | 1096.9 |
| LP Separator | 10.0 | 30.0 | 53.9 | 1150.8 |
| Stock Tank | 1.0 | 15.0 | 23.6 | 1174.4 |
Overall Results:
Total GOR: 1174.4 Sm³/Sm³
Bo (FVF): 5.0880 rm³/Sm³
Stock Tank Density: 751.5 kg/m³
API Gravity: 56.6 °API
## Quality Metrics
---
*Report generated by NeqSim PVT Report Generator*
| Pressure (bara) | Viscosity (cP) |
|---|---|
| 300.0 | 0.0387 |
| 250.0 | 0.0340 |
| 200.0 | 0.0227 |
| 150.0 | 0.0178 |
| 100.0 | 0.0151 |
| Pressure (bara) | Density (kg/m³) |
|---|---|
| 300.0 | 333.04 |
| 250.0 | 303.87 |
| 200.0 | 195.47 |
| 150.0 | 132.69 |
| 100.0 | 82.11 |
| Stage | P (bara) | T (°C) | Oil Density (kg/m³) | Oil Viscosity (cP) |
|---|---|---|---|---|
| HP Separator | 50.0 | 40.0 | - | 0.6315 |
| LP Separator | 10.0 | 30.0 | 675.35 | 0.6143 |
| Stock Tank | 1.0 | 15.0 | 723.65 | 0.6643 |
For gas condensates, an oil (condensate) phase forms below the dew point pressure. The properties are:
| Pressure (bara) | Density (kg/m³) | Viscosity (cP) |
|---|---|---|
| 200.0 | 482.81 | 0.0666 |
| 150.0 | 544.85 | 0.0903 |
Note: Oil phase only exists below the dew point pressure (248.4 bara for this fluid).
| Property | Value | Unit |
|---|---|---|
| Dew Point Pressure | 248.41 | bara |
| GOR | 1174.4 | Sm³/Sm³ |
| CGR | 5.4 | bbl/MMscf |
| Bo | 5.0880 | m³/Sm³ |
| Stock Tank API | 56.6 | °API |
| Stock Tank Density | 751.5 | kg/m³ |
| Molar Mass | 30.79 | g/mol |
The Solution Gas-Water Ratio (Rsw) represents the volume of gas dissolved in water at reservoir conditions, expressed as standard cubic meters of gas per standard cubic meter of water (Sm³/Sm³). This property is essential for:
The SolutionGasWaterRatio class in NeqSim provides three calculation methods with varying levels of complexity and accuracy.
Gas solubility in water is governed by Henry's Law at low pressures:
$$x_g = \frac{P_g}{H}$$
where:
At higher pressures, deviations from Henry's Law become significant, and equation of state methods are required.
| Condition | Rsw (Sm³/Sm³) |
|---|---|
| Shallow reservoir (50 bar, 50°C) | 0.5 - 1.0 |
| Medium depth (150 bar, 80°C) | 1.5 - 3.0 |
| Deep reservoir (300 bar, 120°C) | 3.0 - 6.0 |
| CO₂-rich gas (100 bar, 50°C) | 5.0 - 15.0 |
Best for: Quick estimates, pure methane systems, engineering screening
The McCain correlation is based on the Culberson-McKetta experimental data (1951) with coefficients from McCain (1990).
For pure water: $$R_{sw,pure} = A + B \cdot P + C \cdot P^2$$
where coefficients A, B, C are temperature-dependent polynomials:
$$A = 8.15839 - 6.12265 \times 10^{-2}T + 1.91663 \times 10^{-4}T^2 - 2.1654 \times 10^{-7}T^3$$
$$B = 1.01021 \times 10^{-2} - 7.44241 \times 10^{-5}T + 3.05553 \times 10^{-7}T^2 - 2.94883 \times 10^{-10}T^3$$
$$C = (-9.02505 + 0.130237T - 8.53425 \times 10^{-4}T^2 + 2.34122 \times 10^{-6}T^3 - 2.37049 \times 10^{-9}T^4) \times 10^{-7}$$
where T is in °F and P is in psia.
$$R_{sw,brine} = R_{sw,pure} \times 10^{-C_s \cdot S}$$
where:
| Parameter | Range |
|---|---|
| Temperature | 60-350°F (15-177°C) |
| Pressure | 14.7-10,000 psia (1-690 bar) |
| Salinity | 0-30 wt% NaCl |
| Gas type | Methane (use with caution for other gases) |
Best for: Multi-component gas mixtures, moderate salinity, process simulation
Uses the modified Peng-Robinson equation of state with Søreide-Whitson alpha function and mixing rules specifically developed for hydrocarbon-water systems.
The Søreide-Whitson mixing rule (mixing rule 11 in NeqSim) uses:
$$a_m = \sum_i \sum_j x_i x_j \sqrt{a_i a_j}(1 - k_{ij})$$
with special binary interaction parameters for water-hydrocarbon pairs that account for salinity.
| Parameter | Range |
|---|---|
| Temperature | 273-473 K (0-200°C) |
| Pressure | 1-1000 bar |
| Salinity | 0-6 mol/kg (0-26 wt% NaCl) |
| Gas type | Any hydrocarbon mixture with CO₂, N₂, H₂S |
Best for: High-accuracy predictions, electrolyte systems, research applications
Uses the Cubic-Plus-Association (CPA) equation of state with electrolyte extensions for rigorous modeling of ion-water-gas interactions.
$$P = \frac{RT}{V_m - b} - \frac{a}{V_m(V_m + b)} - \frac{1}{2}\frac{RT}{V_m}\left(1 + \rho\frac{\partial \ln g}{\partial \rho}\right)\sum_A x_A(1 - X_A)$$
where the last term accounts for hydrogen bonding associations.
| Parameter | Range |
|---|---|
| Temperature | 273-473 K (0-200°C) |
| Pressure | 1-1000 bar |
| Salinity | 0-6 mol/kg NaCl equivalent |
| Gas type | Any composition |
import neqsim.thermo.system.SystemInterface;
import neqsim.thermo.system.SystemSrkCPAstatoil;
import neqsim.pvtsimulation.simulation.SolutionGasWaterRatio;
// Create gas system
SystemInterface gas = new SystemSrkCPAstatoil(350.0, 100.0); // 350 K, 100 bar
gas.addComponent("methane", 0.95);
gas.addComponent("CO2", 0.05);
gas.setMixingRule(10);
// Create Rsw calculator
SolutionGasWaterRatio rswCalc = new SolutionGasWaterRatio(gas);
// Set conditions
double[] temperatures = {350.0, 360.0, 370.0}; // K
double[] pressures = {100.0, 150.0, 200.0}; // bar
rswCalc.setTemperaturesAndPressures(temperatures, pressures);
// Set salinity (pure water)
rswCalc.setSalinity(0.0);
// Use McCain method
rswCalc.setCalculationMethod(SolutionGasWaterRatio.CalculationMethod.MCCAIN);
rswCalc.runCalc();
// Get results
double[] rsw = rswCalc.getRsw();
for (int i = 0; i < rsw.length; i++) {
System.out.printf("T=%.1f K, P=%.1f bar: Rsw = %.4f Sm³/Sm³%n",
temperatures[i], pressures[i], rsw[i]);
}
// Create gas with multiple components
SystemInterface gas = new SystemSrkCPAstatoil(373.15, 200.0);
gas.addComponent("methane", 0.85);
gas.addComponent("ethane", 0.05);
gas.addComponent("propane", 0.03);
gas.addComponent("CO2", 0.05);
gas.addComponent("nitrogen", 0.02);
gas.setMixingRule(10);
SolutionGasWaterRatio rswCalc = new SolutionGasWaterRatio(gas);
rswCalc.setTemperaturesAndPressures(new double[]{373.15}, new double[]{200.0});
// Use Søreide-Whitson method
rswCalc.setCalculationMethod(SolutionGasWaterRatio.CalculationMethod.SOREIDE_WHITSON);
rswCalc.runCalc();
System.out.printf("Rsw (Søreide-Whitson) = %.4f Sm³/Sm³%n", rswCalc.getRsw(0));
SolutionGasWaterRatio rswCalc = new SolutionGasWaterRatio(gas);
rswCalc.setTemperaturesAndPressures(new double[]{350.0}, new double[]{100.0});
// Compare pure water vs seawater vs formation water
double[] salinities = {0.0, 3.5, 10.0}; // wt% NaCl
String[] waterTypes = {"Pure water", "Seawater", "Formation water"};
rswCalc.setCalculationMethod(SolutionGasWaterRatio.CalculationMethod.ELECTROLYTE_CPA);
for (int i = 0; i < salinities.length; i++) {
rswCalc.setSalinity(salinities[i], "wt%");
rswCalc.runCalc();
System.out.printf("%s (%.1f wt%% NaCl): Rsw = %.4f Sm³/Sm³%n",
waterTypes[i], salinities[i], rswCalc.getRsw(0));
}
The class supports multiple salinity units:
// Set salinity in different units
rswCalc.setSalinity(0.5); // Default: molality (mol NaCl / kg water)
rswCalc.setSalinity(3.5, "wt%"); // Weight percent NaCl
rswCalc.setSalinity(35000, "ppm"); // Parts per million
| Scenario | Recommended Method | Reason |
|---|---|---|
| Quick screening | McCain | Fast, simple |
| Pure methane system | McCain | Optimized for CH₄ |
| Multi-component gas | Søreide-Whitson | Accounts for composition |
| CO₂-rich gas (>20% CO₂) | Søreide-Whitson or CPA | McCain underestimates |
| High salinity (>5 wt%) | Electrolyte CPA | Best ion modeling |
| Research/validation | Electrolyte CPA | Most rigorous |
| Process simulation | Søreide-Whitson | Good balance |
| T (°C) | P (bar) | Literature (Sm³/Sm³) | McCain | Søreide-Whitson | CPA |
|---|---|---|---|---|---|
| 25 | 100 | 1.8-2.2 | 2.20 | 1.02 | 2.52 |
| 50 | 100 | 1.5-1.8 | 1.78 | 1.03 | 1.94 |
| 75 | 100 | 1.4-1.7 | 1.57 | 1.13 | 1.65 |
| 100 | 100 | 1.5-1.8 | 1.59 | 1.31 | 1.52 |
The salting-out coefficient ($k_s$) represents the reduction in solubility per unit salinity:
$$\log_{10}\left(\frac{R_{sw,brine}}{R_{sw,pure}}\right) = -k_s \cdot m_{salt}$$
| Method | Typical $k_s$ (L/mol) |
|---|---|
| McCain | 0.10-0.15 |
| Søreide-Whitson | 0.12-0.18 |
| Electrolyte CPA | 0.10-0.16 |
| Experimental (Duan & Mao, 2006) | 0.11-0.14 |
public SolutionGasWaterRatio(SystemInterface inputSystem)
Creates a new Rsw calculator using the given thermodynamic system as the gas composition source.
| Method | Description |
|---|---|
setCalculationMethod(CalculationMethod method) |
Set calculation method (MCCAIN, SOREIDE_WHITSON, ELECTROLYTE_CPA) |
setSalinity(double salinity) |
Set salinity in mol/kg water |
setSalinity(double salinity, String unit) |
Set salinity with unit ("wt%", "ppm") |
setTemperaturesAndPressures(double[] T, double[] P) |
Set calculation conditions |
runCalc() |
Execute calculation |
getRsw() |
Get array of calculated Rsw values |
getRsw(int index) |
Get Rsw at specific index |
calculateRsw(double T, double P) |
Single-point calculation |
public enum CalculationMethod {
MCCAIN, // Empirical correlation (fast)
SOREIDE_WHITSON, // Modified PR-EoS (recommended)
ELECTROLYTE_CPA // CPA with electrolytes (most accurate)
}
Culberson, O.L. and McKetta, J.J. (1951). "Phase Equilibria in Hydrocarbon-Water Systems III - The Solubility of Methane in Water at Pressures to 10,000 psia." Journal of Petroleum Technology, 3(08), 223-226.
McCain, W.D. Jr. (1990). The Properties of Petroleum Fluids, 2nd Edition. PennWell Publishing Company.
Søreide, I. and Whitson, C.H. (1992). "Peng-Robinson Predictions for Hydrocarbons, CO₂, N₂, and H₂S with Pure Water and NaCl Brine." Fluid Phase Equilibria, 77, 217-240.
Duan, Z. and Mao, S. (2006). "A Thermodynamic Model for Calculating Methane Solubility, Density and Gas Phase Composition of Methane-Bearing Aqueous Fluids from 273 to 523 K and from 1 to 2000 bar." Geochimica et Cosmochimica Acta, 70(13), 3369-3386.
Haghighi, H., Chapoy, A., and Tohidi, B. (2009). "Methane and Water Phase Equilibria in the Presence of Single and Mixed Electrolyte Solutions Using the Cubic-Plus-Association Equation of State." Oil & Gas Science and Technology, 64(2), 141-154.
The blackoil package provides black oil model capabilities for reservoir engineering applications, including PVT table handling and flash calculations.
Location: neqsim.blackoil
Purpose:
blackoil/
├── BlackOilFlash.java # Black oil flash calculator
├── BlackOilFlashResult.java # Flash results container
├── BlackOilPVTTable.java # PVT table storage
├── BlackOilConverter.java # Compositional to black oil conversion
├── SystemBlackOil.java # Black oil system representation
│
└── io/ # I/O utilities
└── BlackOilTableExporter.java
The black oil model describes reservoir fluids using three pseudo-components:
| Property | Symbol | Description |
|---|---|---|
| Solution GOR | Rs | Gas dissolved in oil (Sm³/Sm³) |
| Oil FVF | Bo | Oil formation volume factor |
| Gas FVF | Bg | Gas formation volume factor |
| Water FVF | Bw | Water formation volume factor |
| Oil-in-Gas ratio | Rv | Oil vaporized in gas (Sm³/Sm³) |
| Oil viscosity | μo | Dynamic viscosity of oil |
| Gas viscosity | μg | Dynamic viscosity of gas |
| Water viscosity | μw | Dynamic viscosity of water |
$$R_s = \gamma_g \left( \frac{P}{18.2} \cdot 10^{0.0125 \cdot API - 0.00091 \cdot T} \right)^{1.2048}$$
$$B_o = 1 + C_1 R_s + C_2 (T - 60) \left( \frac{API}{\gamma_{g,100}} \right) + C_3 R_s (T - 60) \left( \frac{API}{\gamma_{g,100}} \right)$$
import neqsim.blackoil.BlackOilPVTTable;
// Create PVT table
BlackOilPVTTable pvtTable = new BlackOilPVTTable();
// Set pressure points
double[] pressures = {50, 100, 150, 200, 250, 300};
pvtTable.setPressures(pressures);
// Set properties at each pressure
pvtTable.setRs(new double[]{80, 100, 120, 140, 160, 180});
pvtTable.setBo(new double[]{1.25, 1.30, 1.35, 1.40, 1.45, 1.50});
pvtTable.setBg(new double[]{0.010, 0.008, 0.006, 0.005, 0.004, 0.003});
pvtTable.setMuO(new double[]{1.5, 1.2, 1.0, 0.9, 0.8, 0.7});
pvtTable.setMuG(new double[]{0.015, 0.016, 0.017, 0.018, 0.019, 0.020});
// Get properties at any pressure
double P = 175.0; // bar
double Rs = pvtTable.Rs(P);
double Bo = pvtTable.Bo(P);
double Bg = pvtTable.Bg(P);
double muO = pvtTable.mu_o(P);
double muG = pvtTable.mu_g(P);
import neqsim.blackoil.BlackOilFlash;
import neqsim.blackoil.BlackOilFlashResult;
// Create flash calculator
double rho_o_sc = 850.0; // Oil density at SC, kg/m³
double rho_g_sc = 0.85; // Gas density at SC, kg/m³
double rho_w_sc = 1000.0; // Water density at SC, kg/m³
BlackOilFlash flash = new BlackOilFlash(pvtTable, rho_o_sc, rho_g_sc, rho_w_sc);
// Perform flash at reservoir conditions
double P = 200.0; // bar
double T = 373.15; // K (not used in simple model)
double Otot_std = 1000.0; // Stock tank oil, Sm³
double Gtot_std = 150000.0; // Total gas, Sm³
double W_std = 500.0; // Water, Sm³
BlackOilFlashResult result = flash.flash(P, T, Otot_std, Gtot_std, W_std);
// Phase volumes at reservoir conditions
double V_oil = result.V_o; // Oil volume, m³
double V_gas = result.V_g; // Gas volume, m³
double V_water = result.V_w; // Water volume, m³
// Phase properties
double rho_oil = result.rho_o; // Oil density, kg/m³
double rho_gas = result.rho_g; // Gas density, kg/m³
double rho_water = result.rho_w; // Water density, kg/m³
double mu_oil = result.mu_o; // Oil viscosity, cP
double mu_gas = result.mu_g; // Gas viscosity, cP
double mu_water = result.mu_w; // Water viscosity, cP
// PVT properties used
double Rs = result.Rs; // Solution GOR
double Bo = result.Bo; // Oil FVF
double Bg = result.Bg; // Gas FVF
double Bw = result.Bw; // Water FVF
Generate black oil tables from compositional EoS model.
import neqsim.blackoil.BlackOilConverter;
// Create compositional oil
SystemInterface oil = new SystemPrEos(373.15, 300.0);
oil.addComponent("nitrogen", 0.5);
oil.addComponent("CO2", 2.0);
oil.addComponent("methane", 35.0);
oil.addComponent("ethane", 8.0);
oil.addComponent("propane", 5.0);
oil.addComponent("n-butane", 3.0);
oil.addComponent("n-pentane", 2.5);
oil.addComponent("n-hexane", 3.0);
oil.addTBPfraction("C7+", 41.0, 220.0/1000.0, 0.85);
oil.setMixingRule("classic");
oil.useVolumeCorrection(true);
// Convert to black oil
BlackOilConverter converter = new BlackOilConverter(oil);
converter.setTemperature(373.15, "K");
// Define separator conditions
converter.setSeparatorTemperature(288.15, "K");
converter.setSeparatorPressure(1.01325, "bara");
// Generate table
double[] pressures = {1, 50, 100, 150, 200, 250, 300};
converter.setPressures(pressures, "bara");
converter.run();
// Get black oil table
BlackOilPVTTable boTable = converter.getBlackOilTable();
import neqsim.blackoil.io.BlackOilTableExporter;
BlackOilTableExporter exporter = new BlackOilTableExporter(boTable);
exporter.setFormat("ECLIPSE");
exporter.exportToFile("PVTO.inc");
PVTO
-- Rs P Bo viscosity
50.0 50.0 1.250 1.50
100.0 1.248 1.55
150.0 1.246 1.60 /
100.0 100.0 1.350 1.20
150.0 1.347 1.25
200.0 1.345 1.30 /
/
import neqsim.blackoil.*;
import neqsim.pvtsimulation.simulation.*;
// Step 1: Create compositional model
SystemInterface oil = new SystemPrEos(373.15, 250.0);
oil.addComponent("nitrogen", 0.3);
oil.addComponent("CO2", 1.5);
oil.addComponent("methane", 40.0);
oil.addComponent("ethane", 7.0);
oil.addComponent("propane", 4.5);
oil.addComponent("i-butane", 1.0);
oil.addComponent("n-butane", 2.5);
oil.addComponent("i-pentane", 1.2);
oil.addComponent("n-pentane", 1.8);
oil.addComponent("n-hexane", 2.5);
oil.addTBPfraction("C7-C10", 15.0, 120.0/1000.0, 0.78);
oil.addTBPfraction("C11-C15", 10.0, 180.0/1000.0, 0.82);
oil.addTBPfraction("C16+", 12.7, 350.0/1000.0, 0.90);
oil.setMixingRule("classic");
oil.useVolumeCorrection(true);
// Step 2: Run differential liberation
DifferentialLiberation dl = new DifferentialLiberation(oil);
dl.setTemperature(373.15, "K");
double[] pressures = {250, 200, 150, 100, 75, 50, 25, 1.01325};
dl.setPressures(pressures, "bara");
dl.run();
// Step 3: Create black oil table
BlackOilPVTTable boTable = new BlackOilPVTTable();
boTable.setPressures(pressures);
boTable.setRs(dl.getRs());
boTable.setBo(dl.getBo());
boTable.setMuO(dl.getOilViscosity());
// Add gas properties
boTable.setBg(dl.getBg());
boTable.setMuG(dl.getGasViscosity());
// Step 4: Separator test for stock tank conditions
SeparatorTest sep = new SeparatorTest(oil.clone());
sep.setSeparatorConditions(
new double[]{323.15, 288.15},
new double[]{30.0, 1.01325}
);
sep.run();
double rho_o_sc = sep.getOilDensity();
// Step 5: Create flash calculator
BlackOilFlash boFlash = new BlackOilFlash(boTable, rho_o_sc, 0.85, 1000.0);
// Step 6: Calculate at different conditions
System.out.println("P (bar)\tRs\tBo\tρ_oil\tμ_oil");
for (double P : new double[]{50, 100, 150, 200}) {
BlackOilFlashResult r = boFlash.flash(P, 373.15, 1000.0, 150000.0, 0.0);
System.out.printf("%.0f\t%.1f\t%.4f\t%.1f\t%.3f%n",
P, r.Rs, r.Bo, r.rho_o, r.mu_o);
}
The black-oil flash workflow is exercised in SystemBlackOilTest, which demonstrates both Eclipse deck import and direct tabular setup before running a flash. The following notes pair the tested setup with the theory it relies on so you can reproduce the flow efficiently.
testBasicFlash constructs a minimal Eclipse deck with metric units, PVTO/PVTG/PVTW tables, and standard-condition densities, then feeds it to EclipseBlackOilImporter.fromFile(...).【F:src/test/java/neqsim/blackoil/SystemBlackOilTest.java†L31-L74】 The importer returns a BlackOilPVTTable and an initialized SystemBlackOil instance whose bubblepoint is read directly from the tables.
Key steps mirrored from the test:
SystemBlackOil.setStdTotals) before calling flash().The flash solves phase-split mass balance for three pseudo-phases using deck-derived formation volume factors (B_o, B_g, B_w), dissolved gas–oil ratio (R_s), and vaporized oil–gas ratio (R_v). Reservoir volumes are calculated as
[ V_{res} = B_x \times V_{std} ]
for each phase (x\in{o,g,w}), with viscosities pulled directly from the tables.
testDirectPVTTable shows how to bypass deck parsing by interpolating PVTO/PVTG/PVTW data onto a merged pressure grid, then wrapping it in BlackOilPVTTable.Record entries.【F:src/test/java/neqsim/blackoil/SystemBlackOilTest.java†L77-L133】 The resulting table is flashed the same way as the imported one.
When scripting your own tests:
Pb) inside the pressure grid so gas liberation follows expected two-phase behavior.flash(), validate density, viscosity, and reservoir volume signs as sanity checks, then compare against lab or simulator references.This document describes NeqSim's black oil implementation and the ability to export PVT data to reservoir simulators like Eclipse and CMG.
NeqSim provides a complete workflow for converting compositional EOS (Equation of State) fluid models to black oil PVT tables suitable for reservoir simulation. The neqsim.blackoil package includes:
The BlackOilConverter class performs flash calculations at multiple pressures to generate black oil properties:
import neqsim.blackoil.BlackOilConverter;
import neqsim.blackoil.BlackOilPVTTable;
import neqsim.thermo.system.SystemInterface;
import neqsim.thermo.system.SystemSrkEos;
// Create a compositional fluid
SystemInterface fluid = new SystemSrkEos(373.15, 200.0); // 100°C, 200 bar
fluid.addComponent("nitrogen", 0.01);
fluid.addComponent("CO2", 0.02);
fluid.addComponent("methane", 0.50);
fluid.addComponent("ethane", 0.08);
fluid.addComponent("propane", 0.05);
fluid.addComponent("n-butane", 0.03);
fluid.addComponent("n-pentane", 0.02);
fluid.addComponent("n-hexane", 0.02);
fluid.addComponent("n-heptane", 0.07);
fluid.addComponent("water", 0.20);
fluid.setMixingRule("classic");
// Define pressure grid for PVT table
double[] pressures = {20, 50, 100, 150, 200, 250, 300, 350, 400}; // bar
// Convert to black oil
double Tref = 373.15; // Reference temperature (K)
double Pstd = 1.01325; // Standard pressure (bar)
double Tstd = 288.15; // Standard temperature (K)
BlackOilPVTTable pvtTable = BlackOilConverter.convert(fluid, Tref, pressures, Pstd, Tstd);
The BlackOilPVTTable contains records with the following properties:
| Property | Symbol | Unit | Description |
|---|---|---|---|
| Pressure | p | bar | Reservoir pressure |
| Solution GOR | Rs | Sm³/Sm³ | Gas dissolved in oil at standard conditions |
| Oil FVF | Bo | Rm³/Sm³ | Oil formation volume factor |
| Oil viscosity | μo | Pa·s | Oil dynamic viscosity |
| Gas FVF | Bg | Rm³/Sm³ | Gas formation volume factor |
| Gas viscosity | μg | Pa·s | Gas dynamic viscosity |
| Vaporized OGR | Rv | Sm³/Sm³ | Oil vaporized in gas (for volatile oil/gas condensate) |
| Water FVF | Bw | Rm³/Sm³ | Water formation volume factor |
| Water viscosity | μw | Pa·s | Water dynamic viscosity |
The EclipseEOSExporter generates PVT include files compatible with Schlumberger Eclipse reservoir simulator.
import neqsim.blackoil.io.EclipseEOSExporter;
import java.nio.file.Path;
// Export with default settings (METRIC units)
EclipseEOSExporter.toFile(fluid, Path.of("PVT.INC"));
// Or get as string
String eclipseOutput = EclipseEOSExporter.toString(fluid);
import neqsim.blackoil.io.EclipseEOSExporter;
import neqsim.blackoil.io.EclipseEOSExporter.ExportConfig;
import neqsim.blackoil.io.EclipseEOSExporter.Units;
ExportConfig config = new ExportConfig()
.setUnits(Units.FIELD) // METRIC or FIELD
.setIncludeHeader(true) // Include header comments
.setComment("Generated for Field X, Well A-1") // Custom comment
.setPressureGrid(new double[]{50, 100, 150, 200, 250, 300, 350, 400})
.setIncludePVTO(true) // Include live oil table
.setIncludePVTG(true) // Include wet gas table
.setIncludePVTW(true) // Include water properties
.setIncludeDensity(true); // Include DENSITY keyword
String output = EclipseEOSExporter.toString(fluid, config);
// If you already have a BlackOilPVTTable
double rhoOilSc = 820.0; // Oil density at standard conditions (kg/m³)
double rhoGasSc = 1.2; // Gas density at standard conditions (kg/m³)
double rhoWaterSc = 1000.0; // Water density at standard conditions (kg/m³)
EclipseEOSExporter.toFile(pvtTable, rhoOilSc, rhoGasSc, rhoWaterSc, Path.of("PVT.INC"));
The exporter generates the following Eclipse keywords:
DENSITY
-- Oil Gas Water
820.0 1.200000 1000.0 /
PVTO
-- Rs P Bo mu_o
50.0 100.0 1.250 0.00080
150.0 1.220 0.00085
200.0 1.200 0.00090 /
80.0 150.0 1.350 0.00070
200.0 1.320 0.00075 /
/
PVTG
-- Pg Rv Bg mu_g
100.0 0.0 0.0120 1.50E-05
0.0001 0.0122 1.52E-05 /
150.0 0.0 0.0080 1.80E-05 /
/
PVTW
-- Pref Bw Cw mu_w Cv
200.0 1.020 4.5E-05 0.00050 0.0 /
| Property | METRIC | FIELD |
|---|---|---|
| Pressure | bar (BARSA) | psia (PSIA) |
| Density | kg/m³ | lb/ft³ |
| FVF | rm³/sm³ | rb/stb |
| GOR | sm³/sm³ | scf/stb |
| Viscosity | cP | cP |
The CMGEOSExporter generates PVT data files compatible with CMG reservoir simulators (IMEX, GEM, STARS).
import neqsim.blackoil.io.CMGEOSExporter;
import java.nio.file.Path;
// Export with default settings (IMEX, SI units)
CMGEOSExporter.toFile(fluid, Path.of("PVT.DAT"));
// Or get as string
String cmgOutput = CMGEOSExporter.toString(fluid);
import neqsim.blackoil.io.CMGEOSExporter;
import neqsim.blackoil.io.CMGEOSExporter.ExportConfig;
import neqsim.blackoil.io.CMGEOSExporter.Simulator;
import neqsim.blackoil.io.CMGEOSExporter.Units;
ExportConfig config = new ExportConfig()
.setSimulator(Simulator.IMEX) // IMEX, GEM, or STARS
.setUnits(Units.FIELD) // SI or FIELD
.setModelName("RESERVOIR_FLUID_MODEL") // Model identifier
.setComment("PVT model for Field X") // Custom comment
.setPressureGrid(new double[]{50, 100, 150, 200, 250, 300});
CMGEOSExporter.toFile(fluid, Path.of("PVT.DAT"), config);
| Simulator | Type | Description |
|---|---|---|
| IMEX | Black Oil | Implicit-Explicit black oil simulator |
| GEM | Compositional | Generalized equation-of-state model |
| STARS | Thermal | Steam, thermal, and advanced processes |
** ============================================================
** Generated by NeqSim - Black Oil PVT Export
** Simulator: IMEX
** Units: SI
** ============================================================
*MODEL *BLACKOIL
*DENSITY *OIL 820.0
*DENSITY *GAS 1.2
*DENSITY *WATER 1000.0
*BOTOIL
** P(kPa) Rs(m3/m3) Bo(m3/m3) mu_o(cP)
10000.0 50.0 1.250 0.80
15000.0 80.0 1.350 0.70
20000.0 100.0 1.420 0.65
*BOTGAS
** P(kPa) Bg(m3/m3) mu_g(cP)
10000.0 0.0120 0.015
15000.0 0.0080 0.018
20000.0 0.0060 0.021
| Property | SI | FIELD |
|---|---|---|
| Pressure | kPa | psia |
| Density | kg/m³ | lb/ft³ |
| FVF | m³/m³ | bbl/bbl |
| GOR | m³/m³ | scf/bbl |
| Viscosity | cP | cP |
import neqsim.blackoil.BlackOilConverter;
import neqsim.blackoil.BlackOilPVTTable;
import neqsim.blackoil.io.EclipseEOSExporter;
import neqsim.blackoil.io.CMGEOSExporter;
import neqsim.thermo.system.SystemSrkEos;
import java.nio.file.Path;
public class PVTExportExample {
public static void main(String[] args) {
// 1. Create compositional fluid model
var fluid = new SystemSrkEos(373.15, 250.0);
fluid.addComponent("methane", 0.60);
fluid.addComponent("ethane", 0.08);
fluid.addComponent("propane", 0.05);
fluid.addComponent("n-butane", 0.03);
fluid.addComponent("n-pentane", 0.02);
fluid.addComponent("n-heptane", 0.12);
fluid.addComponent("water", 0.10);
fluid.setMixingRule("classic");
// 2. Export to Eclipse (METRIC units)
var eclipseConfig = new EclipseEOSExporter.ExportConfig()
.setUnits(EclipseEOSExporter.Units.METRIC)
.setComment("Light oil reservoir - Block A");
EclipseEOSExporter.toFile(fluid, Path.of("eclipse_pvt.inc"), eclipseConfig);
// 3. Export to CMG IMEX (FIELD units)
var cmgConfig = new CMGEOSExporter.ExportConfig()
.setSimulator(CMGEOSExporter.Simulator.IMEX)
.setUnits(CMGEOSExporter.Units.FIELD)
.setModelName("BLOCK_A_FLUID");
CMGEOSExporter.toFile(fluid, Path.of("cmg_pvt.dat"), cmgConfig);
System.out.println("PVT files exported successfully!");
}
}
In addition to black-oil PVT tables, NeqSim can export the full compositional EOS model to Eclipse E300 format. This is useful when you need to preserve all EOS parameters for compositional reservoir simulation.
The Eclipse E300 format is a proprietary format originated by Schlumberger for their Eclipse compositional reservoir simulator. While there is no public formal specification, the format is widely used and supported by:
The NeqSim E300 export is compatible with files generated by PVTsim Nova and produces output that can be read by Eclipse 300 and OPM Flow.
import neqsim.thermo.util.readwrite.EclipseFluidReadWrite;
// Export fluid to E300 compositional format
EclipseFluidReadWrite.write(fluid, "RESERVOIR_FLUID.e300", 100.0); // 100°C reservoir temp
// Get as string for inspection
String e300Content = EclipseFluidReadWrite.toE300String(fluid, 100.0);
The exported E300 file contains all EOS parameters needed for compositional simulation:
| Keyword | Description | Units |
|---|---|---|
METRIC |
Units system (always METRIC) | - |
NCOMPS |
Number of components | - |
EOS |
Equation of state type | SRK/PR |
PRCORR |
Peng-Robinson correction (for PR EOS only) | - |
RTEMP |
Reservoir temperature | °C |
STCOND |
Standard conditions | °C, bara |
CNAMES |
Component names | - |
TCRIT |
Critical temperatures | K |
PCRIT |
Critical pressures | bar |
ACF |
Acentric factors | - |
OMEGAA |
EOS parameter a (0.45724 for PR, 0.42748 for SRK) | - |
OMEGAB |
EOS parameter b (0.07780 for PR, 0.08664 for SRK) | - |
MW |
Molecular weights | g/mol |
TBOIL |
Normal boiling points | K |
VCRIT |
Critical volumes | m³/kmol |
ZCRIT |
Critical Z-factors | - |
SSHIFT |
Volume translation | - |
PARACHOR |
Parachor values | - |
ZI |
Mole fractions | - |
BIC |
Binary interaction coefficients | - |
import neqsim.thermo.util.readwrite.EclipseFluidReadWrite;
import neqsim.thermo.system.SystemInterface;
import neqsim.thermodynamicoperations.ThermodynamicOperations;
// Create and tune fluid
SystemInterface tunedFluid = /* ... your tuned fluid ... */;
// Export to E300 file
EclipseFluidReadWrite.write(tunedFluid, "TUNED.e300", 100.0);
// Read it back (e.g., in another session or application)
SystemInterface importedFluid = EclipseFluidReadWrite.read("TUNED.e300");
// Use the imported fluid
importedFluid.setPressure(200.0, "bara");
importedFluid.setTemperature(100.0, "C");
new ThermodynamicOperations(importedFluid).TPflash();
Some E300 files include additional keywords that are handled by NeqSim:
| Keyword | Description | Status in NeqSim |
|---|---|---|
BICS |
BIC at surface conditions | Supported (read and write) |
SSHIFTS |
Volume shift at surface conditions | Supported (read and write) |
LBCCOEF |
Lohrenz-Bray-Clark viscosity coefficients | Supported (read and write) |
PEDERSEN |
Pedersen viscosity correlation flag | Supported (read and write) |
NeqSim supports reading and writing the LBCCOEF keyword for the Lohrenz-Bray-Clark (LBC) viscosity correlation:
LBCCOEF
0.1084806 -0.0295031 0.1130421 -0.0553108 0.0093324 /
When reading an E300 file with LBCCOEF:
When writing an E300 file:
The PEDERSEN keyword is a flag indicating the Pedersen (PFCT) corresponding-states viscosity correlation:
PEDERSEN
When reading: NeqSim applies the PFCT viscosity model to all phases.
When writing: If PFCT viscosity model is active, the PEDERSEN keyword is output.
These are written automatically when exporting E300 files.
SystemPrEos, etc.) export with PR and include the PRCORR keyword.from jpype import JClass
EclipseFluidReadWrite = JClass('neqsim.thermo.util.readwrite.EclipseFluidReadWrite')
# Export to E300 file
EclipseFluidReadWrite.write(fluid, "tuned_fluid.e300", 100.0)
# Read back
imported_fluid = EclipseFluidReadWrite.read("tuned_fluid.e300")
The export functionality enables seamless integration with PVT software like whitsonPVT:
// After EOS tuning and characterization
SystemInterface tunedFluid = /* ... tuned fluid model ... */;
// Export for reservoir simulation workflow
EclipseEOSExporter.toFile(tunedFluid, Path.of("TUNED_PVT.INC"));
| Method | Description |
|---|---|
read(String) |
Read E300 file into NeqSim fluid |
write(SystemInterface, String) |
Write fluid to E300 file |
write(SystemInterface, String, double) |
Write with reservoir temp (°C) |
toE300String(SystemInterface) |
Export to E300 format string |
toE300String(SystemInterface, double) |
Export with reservoir temp |
| Method | Description |
|---|---|
toString(SystemInterface) |
Export fluid to Eclipse format string |
toString(SystemInterface, ExportConfig) |
Export with configuration |
toString(BlackOilPVTTable, rhoO, rhoG, rhoW) |
Export PVT table |
toFile(SystemInterface, Path) |
Write to file |
toFile(SystemInterface, Path, ExportConfig) |
Write with configuration |
| Method | Description |
|---|---|
toString(SystemInterface) |
Export fluid to CMG format string |
toString(SystemInterface, ExportConfig) |
Export with configuration |
toString(BlackOilPVTTable, rhoO, rhoG, rhoW) |
Export PVT table |
toFile(SystemInterface, Path) |
Write to file |
toFile(SystemInterface, Path, ExportConfig) |
Write with configuration |
Flow assurance is the discipline ensuring that hydrocarbon fluids can be produced, transported, and processed safely and economically throughout the life of a field. NeqSim provides comprehensive tools for predicting and managing flow assurance challenges.
Flow assurance encompasses the prevention and remediation of:
| Topic | Description |
|---|---|
| Asphaltene Modeling | Overview of asphaltene stability analysis |
| CPA-Based Asphaltene Calculations | Thermodynamic onset pressure/temperature |
| De Boer Asphaltene Screening | Empirical screening correlation |
| Asphaltene Parameter Fitting | Tuning CPA parameters to experimental data |
| Asphaltene Method Comparison | Comparing CPA vs De Boer approaches |
| Asphaltene Model Validation | Validation against SPE-24987 field data |
| Class | Package | Purpose |
|---|---|---|
AsphalteneCharacterization |
neqsim.thermo.characterization |
SARA-based characterization |
AsphalteneStabilityAnalyzer |
neqsim.pvtsimulation.flowassurance |
High-level CPA analysis API |
DeBoerAsphalteneScreening |
neqsim.pvtsimulation.flowassurance |
Empirical De Boer screening |
AsphalteneMethodComparison |
neqsim.pvtsimulation.flowassurance |
Compare multiple methods |
AsphalteneOnsetPressureFlash |
neqsim.thermodynamicoperations |
Onset pressure calculation |
AsphalteneOnsetTemperatureFlash |
neqsim.thermodynamicoperations |
Onset temperature calculation |
AsphalteneOnsetFitting |
neqsim.pvtsimulation.util.parameterfitting |
Fit CPA parameters to experimental onset data |
AsphalteneOnsetFunction |
neqsim.pvtsimulation.util.parameterfitting |
Levenberg-Marquardt function for fitting |
import neqsim.pvtsimulation.flowassurance.DeBoerAsphalteneScreening;
// Create screening for specific conditions
DeBoerAsphalteneScreening screening = new DeBoerAsphalteneScreening(
350.0, // Reservoir pressure [bar]
150.0, // Bubble point pressure [bar]
750.0 // In-situ oil density [kg/m³]
);
// Get risk assessment
String risk = screening.evaluateRisk();
double riskIndex = screening.calculateRiskIndex();
System.out.println("Asphaltene Risk: " + risk);
System.out.println("Risk Index: " + riskIndex);
import neqsim.thermo.system.SystemSrkCPAstatoil;
import neqsim.thermodynamicoperations.ThermodynamicOperations;
// Create CPA fluid with asphaltene
SystemSrkCPAstatoil fluid = new SystemSrkCPAstatoil(373.15, 200.0);
fluid.addComponent("methane", 0.5);
fluid.addComponent("n-heptane", 0.45);
fluid.addComponent("asphaltene", 0.05);
fluid.setMixingRule("classic");
// Calculate onset pressure
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.asphalteneOnsetPressure();
double onsetPressure = fluid.getPressure();
System.out.println("Onset Pressure: " + onsetPressure + " bar");
import neqsim.pvtsimulation.util.parameterfitting.AsphalteneOnsetFitting;
// Create fitter with your fluid system
AsphalteneOnsetFitting fitter = new AsphalteneOnsetFitting(fluid);
// Add experimental onset points (T in Kelvin, P in bar)
fitter.addOnsetPoint(353.15, 350.0); // 80°C
fitter.addOnsetPoint(373.15, 320.0); // 100°C
fitter.addOnsetPoint(393.15, 280.0); // 120°C
// Set initial parameter guesses and run fitting
fitter.setInitialGuess(3500.0, 0.005); // epsilon/R, kappa
fitter.solve();
// Get fitted CPA parameters
double epsilonR = fitter.getFittedAssociationEnergy();
double kappa = fitter.getFittedAssociationVolume();
NeqSim includes a pre-defined asphaltene pseudo-component with CPA parameters suitable for typical asphaltene modeling:
| Property | Value | Unit |
|---|---|---|
| Molecular Weight | 750 | g/mol |
| Critical Temperature | 1049.85 | K |
| Critical Pressure | 8.0 | bar |
| Acentric Factor | 1.5 | - |
| Association Energy (ε/R) | 3500 | K |
| Association Volume (κ) | 0.005 | - |
These parameters can be tuned to match specific experimental data using the AsphalteneOnsetFitting class.
When asphaltene precipitates, NeqSim identifies it using the dedicated PhaseType.ASPHALTENE enum value. This enables:
fluid.hasPhaseType("asphaltene") to detect precipitationimport neqsim.thermo.phase.PhaseType;
// After flash calculation
if (fluid.hasPhaseType(PhaseType.ASPHALTENE)) {
PhaseInterface asphaltene = fluid.getPhaseOfType("asphaltene");
System.out.println("Asphaltene density: " + asphaltene.getDensity("kg/m3") + " kg/m³");
System.out.println("Asphaltene fraction: " + (asphaltene.getBeta() * 100) + "%");
}
See Asphaltene Modeling for more details on PhaseType.ASPHALTENE.
Asphaltenes are the heaviest and most polar fraction of crude oil, defined operationally as the fraction soluble in aromatic solvents (toluene) but insoluble in paraffinic solvents (n-heptane or n-pentane). Asphaltene precipitation during production can cause:
NeqSim provides multiple approaches to assess asphaltene stability, ranging from simple empirical correlations to rigorous thermodynamic modeling.
| Property | Typical Range | Notes |
|---|---|---|
| Molecular Weight | 500 - 10,000 g/mol | Polydisperse distribution |
| H/C Ratio | 0.9 - 1.2 | Lower than other oil fractions |
| Heteroatom Content | 0.5 - 10 wt% | N, S, O, metals (V, Ni) |
| Aromaticity | 40 - 60% | Polyaromatic core structures |
Asphaltenes exist in crude oil as colloidal particles stabilized by resins (maltenes). The Colloidal Instability Index (CII) quantifies this balance:
$$ \text{CII} = \frac{\text{Saturates} + \text{Asphaltenes}}{\text{Aromatics} + \text{Resins}} $$
Where:
Another stability indicator:
$$ \text{R/A} = \frac{\text{Resins wt\%}}{\text{Asphaltenes wt\%}} $$
SARA fractionation separates crude oil into four pseudo-component groups:
| Fraction | Description | Typical Range |
|---|---|---|
| Saturates | Alkanes and cycloalkanes | 40-80% |
| Aromatics | Mono/poly-aromatic hydrocarbons | 10-35% |
| Resins | Polar, heteroatom-containing | 5-25% |
| Asphaltenes | Heaviest polar fraction | 0-15% |
import neqsim.thermo.characterization.AsphalteneCharacterization;
// Create characterization from SARA analysis
AsphalteneCharacterization sara = new AsphalteneCharacterization(
0.45, // Saturates weight fraction
0.30, // Aromatics weight fraction
0.20, // Resins weight fraction
0.05 // Asphaltenes weight fraction
);
// Get stability indicators
double cii = sara.getColloidalInstabilityIndex();
double ra = sara.getResinToAsphalteneRatio();
String stability = sara.evaluateStability();
System.out.println("CII: " + cii);
System.out.println("R/A Ratio: " + ra);
System.out.println("Stability: " + stability);
Asphaltene precipitation occurs when the solubility parameter of the oil phase changes sufficiently to destabilize the asphaltene colloids:
As pressure drops below the bubble point:
Fast, conservative screening based on field correlations. See De Boer Screening.
Advantages:
Limitations:
Rigorous equation of state approach. See CPA Calculations.
Advantages:
PhaseType.ASPHALTENE for accurate phase identificationLimitations:
NeqSim uses a dedicated PhaseType.ASPHALTENE enum value to distinguish precipitated asphaltenes from other solid phases (wax, hydrate). This enables:
fluid.hasPhaseType("asphaltene")import neqsim.thermo.phase.PhaseType;
// Check for asphaltene precipitation
if (fluid.hasPhaseType(PhaseType.ASPHALTENE)) {
PhaseInterface asphaltene = fluid.getPhaseOfType("asphaltene");
// Access asphaltene phase properties
double density = asphaltene.getDensity("kg/m3"); // ~1150 kg/m³
double Cp = asphaltene.getCp("kJ/kgK"); // ~0.9 kJ/kgK
double viscosity = asphaltene.getViscosity("Pa*s"); // ~10,000 Pa·s
double thermalCond = asphaltene.getThermalConductivity("W/mK"); // ~0.20 W/mK
double soundSpeed = asphaltene.getSoundSpeed("m/s"); // ~1745 m/s
}
A simpler approach using classical cubic equations of state (SRK/PR) without association terms, developed by K.S. Pedersen and presented at GOTECH Dubai 2025.
Reference: Pedersen, K.S. (2025). "The Mechanisms Behind Asphaltene Precipitation – Successfully Handled by a Classical Cubic Equation of State." SPE-224534-MS, GOTECH, Dubai.
The key insight from Pedersen's work is that asphaltene precipitation can be successfully modeled as a liquid-liquid phase split using classical cubic equations of state. The approach treats asphaltene as a heavy pseudo-component characterized using the same correlations as C7+ fractions.
Critical Property Correlations:
$$T_c = a_0 + a_1 \ln(M) + a_2 M + \frac{a_3}{M}$$
$$\ln(P_c) = b_0 + b_1 \rho^{0.25} + \frac{b_2}{M} + \frac{b_3}{M^2}$$
$$\omega = c_0 + c_1 \ln(M) + c_2 \rho + c_3 M$$
Where:
import neqsim.thermo.characterization.PedersenAsphalteneCharacterization;
import neqsim.thermo.system.SystemSrkEos;
import neqsim.thermodynamicoperations.ThermodynamicOperations;
// Create SRK system (classical cubic EOS)
SystemSrkEos fluid = new SystemSrkEos(373.15, 200.0);
fluid.addComponent("methane", 0.40);
fluid.addComponent("n-heptane", 0.45);
fluid.addComponent("nC20", 0.10);
// Create Pedersen asphaltene characterization
PedersenAsphalteneCharacterization asphChar = new PedersenAsphalteneCharacterization();
asphChar.setAsphalteneMW(750.0); // Molecular weight (g/mol)
asphChar.setAsphalteneDensity(1.10); // Density (g/cm³)
asphChar.addAsphalteneToSystem(fluid, 0.05); // Add 0.05 mol
// IMPORTANT: Set mixing rule AFTER adding all components
fluid.setMixingRule("classic");
fluid.init(0);
// Print characterization results
System.out.println(asphChar.toString());
For realistic oil systems, combine asphaltene characterization with TBP (True Boiling Point) fraction characterization:
import neqsim.thermo.characterization.PedersenAsphalteneCharacterization;
import neqsim.thermo.system.SystemSrkEos;
import neqsim.thermodynamicoperations.ThermodynamicOperations;
// Create system and set Pedersen TBP model
SystemInterface oil = new SystemSrkEos(373.15, 200.0);
oil.getCharacterization().setTBPModel("PedersenSRK");
// Add light components (defined compounds)
oil.addComponent("nitrogen", 0.005);
oil.addComponent("CO2", 0.02);
oil.addComponent("methane", 0.35);
oil.addComponent("ethane", 0.08);
oil.addComponent("propane", 0.05);
oil.addComponent("i-butane", 0.01);
oil.addComponent("n-butane", 0.02);
oil.addComponent("i-pentane", 0.015);
oil.addComponent("n-pentane", 0.015);
oil.addComponent("n-hexane", 0.02);
// Add C7+ fractions as TBP pseudo-components
// Parameters: name, moles, MW (kg/mol), density (g/cm³)
oil.addTBPfraction("C7", 0.10, 96.0 / 1000.0, 0.738);
oil.addTBPfraction("C8", 0.08, 107.0 / 1000.0, 0.765);
oil.addTBPfraction("C9", 0.06, 121.0 / 1000.0, 0.781);
oil.addTBPfraction("C10", 0.04, 134.0 / 1000.0, 0.792);
oil.addTBPfraction("C11-C15", 0.06, 180.0 / 1000.0, 0.825);
oil.addTBPfraction("C16-C20", 0.03, 260.0 / 1000.0, 0.865);
oil.addTBPfraction("C21+", 0.02, 450.0 / 1000.0, 0.920);
// Add asphaltene using Pedersen characterization
PedersenAsphalteneCharacterization asphChar = new PedersenAsphalteneCharacterization();
asphChar.setAsphalteneMW(850.0);
asphChar.setAsphalteneDensity(1.12);
asphChar.addAsphalteneToSystem(oil, 0.015);
// Set mixing rule and initialize
oil.setMixingRule("classic");
oil.init(0);
// Perform flash calculation
ThermodynamicOperations ops = new ThermodynamicOperations(oil);
ops.TPflash();
oil.prettyPrint();
The class provides tuning multipliers for fitting to experimental onset pressure data:
// Create characterization
PedersenAsphalteneCharacterization asphChar = new PedersenAsphalteneCharacterization();
asphChar.setAsphalteneMW(750.0);
asphChar.setAsphalteneDensity(1.10);
// Apply tuning multipliers to match experimental onset pressure
asphChar.setTcMultiplier(1.03); // Increase Tc by 3%
asphChar.setPcMultiplier(0.97); // Decrease Pc by 3%
asphChar.setOmegaMultiplier(1.05); // Increase ω by 5%
// Or use convenience method for all at once
asphChar.setTuningParameters(1.03, 0.97, 1.05);
// Characterize and add to system
asphChar.characterize();
asphChar.addAsphalteneToSystem(fluid, 0.05);
// Reset to defaults if needed
asphChar.resetTuningParameters();
Tuning Guidelines:
| Parameter | Effect on Onset Pressure | Typical Range |
|---|---|---|
tcMultiplier |
Higher Tc → lower onset pressure | 0.95 - 1.10 |
pcMultiplier |
Higher Pc → higher onset pressure | 0.85 - 1.15 |
omegaMultiplier |
Higher ω → complex effects | 0.90 - 1.20 |
For heavy oils, represent asphaltene as multiple pseudo-components with distributed MW:
// Add distributed asphaltene (3 pseudo-components)
asphChar.setAsphalteneMW(1000.0); // Average MW
asphChar.setAsphalteneDensity(1.15);
asphChar.addDistributedAsphaltene(heavyOil, 0.08, 3);
// This creates 3 components: Asph_1_PC, Asph_2_PC, Asph_3_PC
// with MW ranging from 0.5x to 2x the average MW
Based on the Pedersen correlations, asphaltene components show these typical properties:
| Property | Light Asphaltene (MW=500) | Medium (MW=750) | Heavy (MW=1500) |
|---|---|---|---|
| Tc | 950-1000 K | 990-1010 K | 1040-1060 K |
| Pc | 17-19 bar | 15-17 bar | 14-16 bar |
| ω | 0.9-1.0 | 0.9-1.0 | 1.2-1.4 |
| Tb | 700-800 K | 800-850 K | 900-950 K |
Advantages:
Limitations:
Use De Boer for initial screening, then CPA or Pedersen method for detailed analysis of flagged cases. See Method Comparison.
AsphalteneOnsetFitting to match measured onset data┌─────────────────────────────────────────────────────────────────┐
│ ASPHALTENE ANALYSIS WORKFLOW │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Step 1: DE BOER SCREENING │
│ • Fast empirical screening │
│ • Input: P_res, P_bub, density │
│ • Output: Risk category (NO/SLIGHT/MODERATE/SEVERE) │
└─────────────────────────────────────────────────────────────────┘
│
┌───────────────┴───────────────┐
│ │
▼ ▼
┌──────────────┐ ┌──────────────────────┐
│ LOW RISK │ │ ELEVATED RISK │
│ • Monitor │ │ • Proceed to CPA │
│ • Document │ │ analysis │
└──────────────┘ └──────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Step 2: CPA THERMODYNAMIC ANALYSIS │
│ • Create fluid with asphaltene component │
│ • Calculate onset pressure/temperature │
│ • Generate precipitation envelope │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Step 3: LABORATORY VALIDATION │
│ • Measure AOP at multiple temperatures │
│ • SARA analysis for composition │
│ • HPM microscopy for onset detection │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Step 4: PARAMETER TUNING (AsphalteneOnsetFitting) │
│ • Fit CPA parameters to lab data │
│ • Validate predictions at other conditions │
│ • Document fitted parameters for field use │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Step 5: FIELD APPLICATION │
│ • Predict onset during depletion/injection │
│ • Design mitigation strategies │
│ • Establish monitoring program │
└─────────────────────────────────────────────────────────────────┘
De Boer, R.B., et al. (1995). "Screening of Crude Oils for Asphalt Precipitation: Theory, Practice, and the Selection of Inhibitors." SPE Production & Facilities. SPE-24987-PA
Mullins, O.C. (2010). "The Modified Yen Model." Energy & Fuels, 24(4), 2179-2207.
Leontaritis, K.J., and Mansoori, G.A. (1988). "Asphaltene Deposition: A Survey of Field Experiences and Research Approaches." Journal of Petroleum Science and Engineering.
Victorov, A.I., and Firoozabadi, A. (1996). "Thermodynamic Micellization Model of Asphaltene Precipitation from Petroleum Fluids." AIChE Journal.
Kontogeorgis, G.M., and Folas, G.K. (2010). "Thermodynamic Models for Industrial Applications." Wiley.
Li, Z., and Firoozabadi, A. (2010). "Modeling Asphaltene Precipitation by n-Alkanes from Heavy Oils and Bitumens Using Cubic-Plus-Association Equation of State." Energy & Fuels, 24, 1106-1113.
Vargas, F.M., et al. (2009). "Development of a General Method for Modeling Asphaltene Stability." Energy & Fuels, 23, 1140-1146.
Pedersen, K.S. (2025). "The Mechanisms Behind Asphaltene Precipitation – Successfully Handled by a Classical Cubic Equation of State." SPE-224534-MS, GOTECH, Dubai.
Pedersen, K.S., Christensen, P.L. (2007). "Phase Behavior of Petroleum Reservoir Fluids." CRC Press.
Pedersen, K.S., Fredenslund, A., Thomassen, P. (1989). "Properties of Oils and Natural Gases." Gulf Publishing.
The Cubic Plus Association (CPA) equation of state extends classical cubic equations (SRK or PR) with an association term to handle self-associating and cross-associating compounds. This makes CPA particularly suitable for asphaltene modeling, where polar interactions and molecular aggregation are important.
The CPA pressure equation combines cubic and association contributions:
$$ P = P_{\text{cubic}} + P_{\text{assoc}} $$
Where:
Asphaltenes self-associate through:
CPA captures these through association parameters:
Asphaltene precipitation is modeled as a solid phase formation. At the onset conditions, the fugacity of asphaltene in the liquid phase equals that in the solid phase:
$$ f_{\text{asph}}^{L}(T, P, x) = f_{\text{asph}}^{S}(T, P) $$
import neqsim.thermo.system.SystemSrkCPAstatoil;
import neqsim.thermodynamicoperations.ThermodynamicOperations;
// Create CPA fluid system
SystemSrkCPAstatoil fluid = new SystemSrkCPAstatoil(373.15, 250.0);
fluid.addComponent("methane", 0.40);
fluid.addComponent("propane", 0.10);
fluid.addComponent("n-heptane", 0.45);
fluid.addComponent("asphaltene", 0.05);
fluid.setMixingRule("classic");
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
// Calculate asphaltene onset pressure
// Searches from current pressure down to find precipitation
ops.asphalteneOnsetPressure();
double onsetP = fluid.getPressure();
// Calculate asphaltene onset temperature at fixed pressure
fluid.setPressure(200.0);
ops.asphalteneOnsetTemperature();
double onsetT = fluid.getTemperature();
Direct flash operation for onset pressure calculation:
import neqsim.thermodynamicoperations.flashops.saturationops.AsphalteneOnsetPressureFlash;
// Create flash operation
AsphalteneOnsetPressureFlash flash = new AsphalteneOnsetPressureFlash(fluid);
// Configure search range
flash.setMaxPressure(400.0); // Start pressure [bar]
flash.setMinPressure(1.0); // End pressure [bar]
flash.setPressureStep(10.0); // Coarse search step [bar]
flash.setTolerance(0.1); // Bisection tolerance [bar]
// Run calculation
flash.run();
// Get results
if (flash.isOnsetFound()) {
double onsetPressure = flash.getOnsetPressure();
System.out.println("Onset Pressure: " + onsetPressure + " bar");
} else {
System.out.println("No onset found in search range");
}
Temperature-based onset calculation:
import neqsim.thermodynamicoperations.flashops.saturationops.AsphalteneOnsetTemperatureFlash;
// Create flash operation at fixed pressure
AsphalteneOnsetTemperatureFlash flash = new AsphalteneOnsetTemperatureFlash(fluid);
// Configure search range
flash.setMinTemperature(273.15); // 0°C
flash.setMaxTemperature(473.15); // 200°C
flash.setTemperatureStep(5.0); // Search step [K]
flash.setTolerance(0.1); // Tolerance [K]
// Run calculation
flash.run();
if (flash.isOnsetFound()) {
double onsetTemp = flash.getOnsetTemperature();
System.out.println("Onset Temperature: " + (onsetTemp - 273.15) + " °C");
}
High-level API combining multiple analysis methods:
import neqsim.pvtsimulation.flowassurance.AsphalteneStabilityAnalyzer;
// Create analyzer with fluid
AsphalteneStabilityAnalyzer analyzer = new AsphalteneStabilityAnalyzer(fluid);
// Configure conditions
analyzer.setReservoirPressure(350.0); // bar
analyzer.setReservoirTemperature(373.15); // K
analyzer.setBubblePointPressure(150.0); // bar
// Quick screening (De Boer)
String screening = analyzer.deBoerScreening();
System.out.println("De Boer: " + screening);
// Thermodynamic onset pressure
double onsetP = analyzer.calculateOnsetPressure();
System.out.println("CPA Onset Pressure: " + onsetP + " bar");
// Comprehensive assessment
String fullReport = analyzer.comprehensiveAssessment();
System.out.println(fullReport);
// Use SystemSrkCPAstatoil for asphaltene calculations
SystemSrkCPAstatoil fluid = new SystemSrkCPAstatoil(T_kelvin, P_bar);
// Light ends
fluid.addComponent("methane", 0.40);
fluid.addComponent("ethane", 0.08);
fluid.addComponent("propane", 0.05);
// Intermediates
fluid.addComponent("n-heptane", 0.30);
fluid.addComponent("n-decane", 0.10);
// Asphaltene pseudo-component
fluid.addComponent("asphaltene", 0.05);
// Set mixing rule (required for CPA)
fluid.setMixingRule("classic");
For tuned asphaltene parameters:
// After adding components, modify asphaltene properties
int aspIndex = fluid.getComponentIndex("asphaltene");
// Adjust molecular weight if needed
fluid.getComponent(aspIndex).setMolarMass(750.0 / 1000.0); // kg/mol
// Adjust critical properties (affects solubility)
fluid.getComponent(aspIndex).setTC(900.0); // Critical temperature [K]
fluid.getComponent(aspIndex).setPC(15.0); // Critical pressure [bar]
Map the full precipitation boundary in P-T space:
import neqsim.pvtsimulation.flowassurance.AsphalteneStabilityAnalyzer;
import java.util.Map;
AsphalteneStabilityAnalyzer analyzer = new AsphalteneStabilityAnalyzer(fluid);
// Generate envelope from Tmin to Tmax
double[][] envelope = analyzer.generatePrecipitationEnvelope(
280.0, // Min temperature [K]
400.0, // Max temperature [K]
10.0 // Temperature step [K]
);
// envelope[0] = temperatures
// envelope[1] = onset pressures
for (int i = 0; i < envelope[0].length; i++) {
double T_C = envelope[0][i] - 273.15;
double P_bar = envelope[1][i];
System.out.printf("T = %.1f°C, P_onset = %.1f bar%n", T_C, P_bar);
}
P_start (e.g., 400 bar)
|
v
[Coarse search: decrease by step size]
|
v
Solid phase appears? --> No --> Continue decreasing
|
Yes
v
[Bisection between last two points]
|
v
Converged to tolerance --> Report onset pressure
NeqSim uses PhaseType.ASPHALTENE to distinguish asphaltene precipitation from other solid phases (wax, hydrate). This enables accurate phase identification in multi-phase flash calculations.
import neqsim.thermo.phase.PhaseType;
// After flash calculation - check for asphaltene precipitation
// Method 1: Using PhaseType enum (recommended)
if (fluid.hasPhaseType(PhaseType.ASPHALTENE)) {
PhaseInterface asphaltene = fluid.getPhaseOfType("asphaltene");
double precipitatedFraction = asphaltene.getBeta();
System.out.println("Asphaltene precipitated: " + (precipitatedFraction * 100) + "%");
}
// Method 2: Using string-based lookup
if (fluid.hasPhaseType("asphaltene")) {
PhaseInterface asphaltene = fluid.getPhaseOfType("asphaltene");
// Access asphaltene phase properties
double density = asphaltene.getDensity("kg/m3"); // ~1150 kg/m³
double viscosity = asphaltene.getViscosity("Pa*s"); // ~10,000 Pa·s
double thermalCond = asphaltene.getThermalConductivity("W/mK"); // ~0.20 W/mK
}
// Method 3: Legacy approach (also checks for generic solid)
private boolean hasAsphaltenePhase(SystemInterface fluid) {
// Check for specific asphaltene phase type
if (fluid.hasPhaseType("asphaltene")) {
return true;
}
// Fallback: check solid phase for asphaltene component
if (fluid.hasPhaseType("solid")) {
PhaseInterface solid = fluid.getPhaseOfType("solid");
for (int i = 0; i < solid.getNumberOfComponents(); i++) {
if (solid.getComponent(i).getComponentName().toLowerCase().contains("asphaltene")) {
return true;
}
}
}
return false;
}
When asphaltene precipitates, it forms a distinct phase with PhaseType.ASPHALTENE. The physical properties are calculated using literature-based correlations:
| Property | Value | Unit | Notes |
|---|---|---|---|
| Density | ~1150 | kg/m³ | Literature-based, temperature-independent |
| Heat Capacity (Cp) | ~0.9 | kJ/kgK | EOS-based calculation |
| Thermal Conductivity | 0.20 | W/mK | Typical for organic solids |
| Viscosity | ~10,000 | Pa·s | Arrhenius correlation at 350K |
| Speed of Sound | ~1745 | m/s | EOS-based calculation |
## Tuning to Experimental Data
### Automated Parameter Fitting with AsphalteneOnsetFitting
NeqSim provides the `AsphalteneOnsetFitting` class to automatically tune CPA asphaltene parameters to match experimental onset pressure measurements using the Levenberg-Marquardt optimization algorithm.
```java
import neqsim.pvtsimulation.util.parameterfitting.AsphalteneOnsetFitting;
import neqsim.thermo.system.SystemSrkCPAstatoil;
// Step 1: Create fluid system with asphaltene
SystemSrkCPAstatoil fluid = new SystemSrkCPAstatoil(373.15, 200.0);
fluid.addComponent("methane", 0.30);
fluid.addTBPfraction("C7", 0.30, 0.100, 0.75);
fluid.addComponent("asphaltene", 0.05);
fluid.setMixingRule("classic");
// Step 2: Create fitter and add experimental onset data
AsphalteneOnsetFitting fitter = new AsphalteneOnsetFitting(fluid);
fitter.addOnsetPoint(353.15, 350.0); // T=80°C, P_onset=350 bar
fitter.addOnsetPoint(373.15, 320.0); // T=100°C, P_onset=320 bar
fitter.addOnsetPoint(393.15, 280.0); // T=120°C, P_onset=280 bar
// Step 3: Set initial parameter guesses
fitter.setInitialGuess(3500.0, 0.005); // epsilon/R=3500K, kappa=0.005
// Step 4: Configure pressure search range (optional)
fitter.setPressureRange(500.0, 10.0, 10.0);
// Step 5: Run fitting
boolean success = fitter.solve();
// Step 6: Get fitted parameters
if (success) {
double epsilonR = fitter.getFittedAssociationEnergy();
double kappa = fitter.getFittedAssociationVolume();
System.out.println("Fitted ε/R: " + epsilonR + " K");
System.out.println("Fitted κ: " + kappa);
// Step 7: Predict onset at new conditions
double onsetP = fitter.calculateOnsetPressure(400.0); // T=400K
System.out.println("Predicted onset at 400K: " + onsetP + " bar");
}
Based on literature (Li & Firoozabadi, Vargas et al.):
| Parameter | Typical Range | Units | Notes |
|---|---|---|---|
| Molar Mass | 500 - 1500 | g/mol | Polydisperse distribution |
| Association Energy (ε/R) | 2500 - 4500 | K | Controls aggregation strength |
| Association Volume (κ) | 0.001 - 0.05 | - | Probability of association |
| Association Scheme | 1A (single site) | - | Mimics π-π stacking |
| Critical Temperature (Tc) | 700 - 900 | K | Affects phase behavior |
| Critical Pressure (Pc) | 5 - 15 | bar | Affects phase behavior |
| Acentric Factor (ω) | 1.0 - 2.0 | - | Shape factor |
| Oil Type | API Gravity | ε/R [K] | κ |
|---|---|---|---|
| Light oils | >35° | 3000 | 0.01 |
| Medium oils | 25-35° | 3500 | 0.005 |
| Heavy oils | <25° | 4000 | 0.003 |
The fitter supports different parameter types:
import neqsim.pvtsimulation.util.parameterfitting.AsphalteneOnsetFunction.FittingParameterType;
// Fit only association energy
fitter.setParameterType(FittingParameterType.ASSOCIATION_ENERGY);
fitter.setInitialGuess(3500.0);
// Fit only association volume
fitter.setParameterType(FittingParameterType.ASSOCIATION_VOLUME);
fitter.setInitialGuess(0.005);
// Fit both association parameters (default)
fitter.setParameterType(FittingParameterType.ASSOCIATION_PARAMETERS);
fitter.setInitialGuess(3500.0, 0.005);
// Fit binary interaction parameter
fitter.setParameterType(FittingParameterType.BINARY_INTERACTION);
fitter.setInitialGuess(0.0);
// Fit molar mass
fitter.setParameterType(FittingParameterType.MOLAR_MASS);
fitter.setInitialGuess(750.0);
For manual tuning without the fitter:
// Target: Match experimental AOP of 180 bar
// Strategy 1: Adjust asphaltene critical properties
// Higher Tc/Pc = lower solubility = higher onset pressure
// Strategy 2: Adjust association parameters
// Stronger association = earlier precipitation
// Strategy 3: Adjust binary interaction parameters
fluid.setMixingRule("classic"); // k_ij values
Key parameters affecting onset pressure:
| Operation | Typical Time | Notes |
|---|---|---|
| Single flash | 10-100 ms | Depends on composition complexity |
| Onset pressure | 1-5 seconds | Multiple flashes + bisection |
| Full envelope | 10-60 seconds | Multiple onset calculations |
// Reduce search range if approximate onset known
flash.setMaxPressure(250.0); // Instead of 400
flash.setMinPressure(100.0); // Instead of 1
// Use larger tolerance for initial screening
flash.setTolerance(1.0); // Instead of 0.1
// Larger steps for coarse search
flash.setPressureStep(20.0); // Instead of 10
import neqsim.thermo.system.SystemSrkCPAstatoil;
import neqsim.thermodynamicoperations.ThermodynamicOperations;
import neqsim.pvtsimulation.flowassurance.AsphalteneStabilityAnalyzer;
// 1. Create and characterize fluid
SystemSrkCPAstatoil fluid = new SystemSrkCPAstatoil(373.15, 350.0);
fluid.addComponent("methane", 0.35);
fluid.addComponent("ethane", 0.08);
fluid.addComponent("propane", 0.05);
fluid.addComponent("n-heptane", 0.40);
fluid.addComponent("n-decane", 0.07);
fluid.addComponent("asphaltene", 0.05);
fluid.setMixingRule("classic");
fluid.init(0);
fluid.init(1);
// 2. Create analyzer
AsphalteneStabilityAnalyzer analyzer = new AsphalteneStabilityAnalyzer(fluid);
analyzer.setReservoirPressure(350.0);
analyzer.setReservoirTemperature(373.15);
analyzer.setBubblePointPressure(150.0);
// 3. Quick screening
System.out.println("=== De Boer Screening ===");
System.out.println(analyzer.deBoerScreening());
// 4. Calculate onset pressure
System.out.println("\n=== CPA Onset Pressure ===");
double onsetP = analyzer.calculateOnsetPressure();
System.out.println("Onset Pressure: " + onsetP + " bar");
// 5. Generate envelope
System.out.println("\n=== Precipitation Envelope ===");
double[][] envelope = analyzer.generatePrecipitationEnvelope(300.0, 400.0, 20.0);
for (int i = 0; i < envelope[0].length; i++) {
System.out.printf("T = %.1f K, P_onset = %.1f bar%n",
envelope[0][i], envelope[1][i]);
}
// 6. Full report
System.out.println("\n=== Comprehensive Assessment ===");
System.out.println(analyzer.comprehensiveAssessment());
The De Boer correlation is an empirical screening method developed from field observations of asphaltene problems in producing oil fields. It provides a quick, conservative assessment of asphaltene precipitation risk without requiring detailed thermodynamic modeling.
De Boer et al. (1995) analyzed field data and found that asphaltene problems correlate with two parameters:
$$ \Delta P = P_{\text{reservoir}} - P_{\text{bubble}} $$
The plot divides the $\Delta P$ vs $\rho$ space into risk zones:
Undersaturation ΔP [bar]
^
400 | SEVERE |
| PROBLEM |
300 | | MODERATE
| ───────────── | PROBLEM
200 | |
| SLIGHT |
100 | PROBLEM |
| ───────────────┼───────────────
0 | NO PROBLEM |
+─────────────────┼───────────────> ρ [kg/m³]
700 800
The correlation uses empirical boundaries:
| Zone | Description | Boundary |
|---|---|---|
| No Problem | Low risk | $\Delta P < f_1(\rho)$ |
| Slight Problem | Minor risk | $f_1(\rho) < \Delta P < f_2(\rho)$ |
| Moderate Problem | Moderate risk | $f_2(\rho) < \Delta P < f_3(\rho)$ |
| Severe Problem | High risk | $\Delta P > f_3(\rho)$ |
Where $f_i(\rho)$ are linear functions of density.
High undersaturation + Low density = Severe risk
Low undersaturation + High density = Low risk
import neqsim.pvtsimulation.flowassurance.DeBoerAsphalteneScreening;
// Create screening with reservoir data
DeBoerAsphalteneScreening screening = new DeBoerAsphalteneScreening(
350.0, // Reservoir pressure [bar]
150.0, // Bubble point pressure [bar]
720.0 // In-situ oil density [kg/m³]
);
// Get risk assessment
String riskLevel = screening.evaluateRisk();
System.out.println("Risk Level: " + riskLevel);
The method returns one of four risk categories:
public enum RiskLevel {
NO_PROBLEM, // No action needed
SLIGHT_PROBLEM, // Monitor during production
MODERATE_PROBLEM, // Consider mitigation
SEVERE_PROBLEM // Mitigation required
}
// Usage
String risk = screening.evaluateRisk();
switch (risk) {
case "NO_PROBLEM":
System.out.println("No asphaltene issues expected");
break;
case "SLIGHT_PROBLEM":
System.out.println("Minor issues possible - monitor");
break;
case "MODERATE_PROBLEM":
System.out.println("Significant risk - plan mitigation");
break;
case "SEVERE_PROBLEM":
System.out.println("High risk - mitigation required");
break;
}
For more nuanced assessment:
double riskIndex = screening.calculateRiskIndex();
The risk index interpretation:
| Index Value | Interpretation |
|---|---|
| < 0 | Stable, no precipitation expected |
| 0 - 0.3 | Low risk |
| 0.3 - 0.7 | Moderate risk |
| > 0.7 | High risk, likely problems |
String report = screening.performScreening();
System.out.println(report);
Output:
De Boer Asphaltene Screening Results
====================================
Reservoir Pressure: 350.0 bar
Bubble Point Pressure: 150.0 bar
Undersaturation: 200.0 bar
In-situ Density: 720.0 kg/m³
Risk Assessment: MODERATE_PROBLEM
Risk Index: 0.55
Recommendation: Moderate asphaltene risk.
Consider preventive measures and monitoring plan.
Evaluate risk over a range of conditions:
import neqsim.pvtsimulation.flowassurance.DeBoerAsphalteneScreening;
// Base case
double bubblePoint = 150.0;
double density = 720.0;
// Pressure depletion scenario
System.out.println("Pressure Depletion Analysis:");
for (double pRes = 400.0; pRes >= 160.0; pRes -= 20.0) {
DeBoerAsphalteneScreening screening =
new DeBoerAsphalteneScreening(pRes, bubblePoint, density);
double deltaP = pRes - bubblePoint;
String risk = screening.evaluateRisk();
System.out.printf("P_res = %.0f bar, ΔP = %.0f bar: %s%n",
pRes, deltaP, risk);
}
Create data for visualization:
double[][] plotData = screening.generatePlotData(
650.0, // Min density [kg/m³]
850.0, // Max density [kg/m³]
25.0 // Density step [kg/m³]
);
// plotData[0] = density values
// plotData[1] = undersaturation values for "slight problem" boundary
// plotData[2] = undersaturation values for "moderate problem" boundary
// plotData[3] = undersaturation values for "severe problem" boundary
// Export for plotting
System.out.println("Density,Slight,Moderate,Severe");
for (int i = 0; i < plotData[0].length; i++) {
System.out.printf("%.1f,%.1f,%.1f,%.1f%n",
plotData[0][i], plotData[1][i], plotData[2][i], plotData[3][i]);
}
Screen multiple samples:
// Sample data: [reservoir P, bubble P, density]
double[][] samples = {
{350.0, 150.0, 720.0}, // Sample A
{280.0, 100.0, 780.0}, // Sample B
{400.0, 200.0, 690.0}, // Sample C
{300.0, 180.0, 750.0} // Sample D
};
String[] sampleNames = {"A", "B", "C", "D"};
System.out.println("Sample | ΔP [bar] | ρ [kg/m³] | Risk Level");
System.out.println("-------+----------+-----------+----------------");
for (int i = 0; i < samples.length; i++) {
DeBoerAsphalteneScreening screening = new DeBoerAsphalteneScreening(
samples[i][0], samples[i][1], samples[i][2]
);
double deltaP = samples[i][0] - samples[i][1];
String risk = screening.evaluateRisk();
System.out.printf(" %s | %5.0f | %5.0f | %s%n",
sampleNames[i], deltaP, samples[i][2], risk);
}
| Parameter | Unit | How to Obtain |
|---|---|---|
| Reservoir Pressure | bar | Formation test (RFT/MDT) |
| Bubble Point Pressure | bar | PVT lab test or correlation |
| In-situ Density | kg/m³ | PVT lab or flash calculation |
If density at reservoir conditions is not available:
import neqsim.thermo.system.SystemSrkEos;
import neqsim.thermodynamicoperations.ThermodynamicOperations;
// Create fluid from composition
SystemSrkEos fluid = new SystemSrkEos(373.15, 350.0); // Reservoir T, P
fluid.addComponent("methane", 0.40);
fluid.addComponent("n-heptane", 0.55);
fluid.addComponent("n-decane", 0.05);
fluid.setMixingRule("classic");
// Flash to get density
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
// Get liquid density
double density = fluid.getPhase("oil").getDensity("kg/m3");
System.out.println("In-situ density: " + density + " kg/m³");
Use thermodynamic modeling (CPA) when:
De Boer was validated against:
The correlation correctly predicted:
The method is intentionally conservative:
import neqsim.pvtsimulation.flowassurance.DeBoerAsphalteneScreening;
// Field data for multiple reservoirs
String[] reservoirs = {"Alpha", "Beta", "Gamma", "Delta"};
double[] pRes = {380.0, 320.0, 290.0, 410.0}; // Reservoir pressure [bar]
double[] pBub = {180.0, 150.0, 120.0, 220.0}; // Bubble point [bar]
double[] rho = {710.0, 750.0, 800.0, 680.0}; // In-situ density [kg/m³]
System.out.println("Field Development Asphaltene Screening");
System.out.println("======================================\n");
int highRiskCount = 0;
for (int i = 0; i < reservoirs.length; i++) {
DeBoerAsphalteneScreening screening =
new DeBoerAsphalteneScreening(pRes[i], pBub[i], rho[i]);
String risk = screening.evaluateRisk();
double riskIndex = screening.calculateRiskIndex();
System.out.printf("Reservoir %s:%n", reservoirs[i]);
System.out.printf(" P_res = %.0f bar, P_bub = %.0f bar, ρ = %.0f kg/m³%n",
pRes[i], pBub[i], rho[i]);
System.out.printf(" Risk: %s (index: %.2f)%n%n", risk, riskIndex);
if (risk.equals("SEVERE_PROBLEM") || risk.equals("MODERATE_PROBLEM")) {
highRiskCount++;
}
}
System.out.printf("Summary: %d of %d reservoirs require further analysis%n",
highRiskCount, reservoirs.length);
De Boer, R.B., Leerlooyer, K., Eigner, M.R.P., and van Bergen, A.R.D. (1995). "Screening of Crude Oils for Asphalt Precipitation: Theory, Practice, and the Selection of Inhibitors." SPE Production & Facilities, 10(1), 55-61.
Hammami, A., and Ratulowski, J. (2007). "Precipitation and Deposition of Asphaltenes in Production Systems: A Flow Assurance Overview." In Asphaltenes, Heavy Oils, and Petroleomics, Springer.
Akbarzadeh, K., et al. (2007). "Asphaltenes—Problematic but Rich in Potential." Oilfield Review, 19(2), 22-43.
NeqSim provides two complementary approaches for asphaltene stability analysis:
This document explains when to use each method and how to compare their results.
| Aspect | De Boer | CPA |
|---|---|---|
| Speed | Milliseconds | Seconds to minutes |
| Input Required | P_res, P_bub, ρ | Full composition + properties |
| Output | Risk category | Onset P, T, amounts |
| Accuracy | Conservative screening | Predictive (if tuned) |
| Composition Effects | No | Yes |
| Temperature Effects | No | Yes |
| Injection Effects | No | Yes |
✅ Early field screening with limited data
✅ Quick portfolio risk ranking
✅ Conservative go/no-go decisions
✅ Baseline risk communication
✅ Detailed well/facility design
✅ Operating envelope definition
✅ Gas injection impact assessment
✅ Inhibitor effectiveness evaluation
✅ Field development planning (screen then analyze)
✅ Validating thermodynamic model predictions
✅ Communicating risk to non-technical stakeholders
✅ Comprehensive flow assurance studies
import neqsim.pvtsimulation.flowassurance.AsphalteneMethodComparison;
import neqsim.thermo.system.SystemSrkCPAstatoil;
// Create CPA fluid
SystemSrkCPAstatoil fluid = new SystemSrkCPAstatoil(373.15, 350.0);
fluid.addComponent("methane", 0.40);
fluid.addComponent("n-heptane", 0.50);
fluid.addComponent("asphaltene", 0.10);
fluid.setMixingRule("classic");
fluid.init(0);
fluid.init(1);
// Create comparison
AsphalteneMethodComparison comparison = new AsphalteneMethodComparison(fluid);
comparison.setReservoirPressure(350.0);
comparison.setBubblePointPressure(150.0);
comparison.setInSituDensity(720.0);
// Run comparison
String report = comparison.runComparison();
System.out.println(report);
=================================================
ASPHALTENE STABILITY: METHOD COMPARISON REPORT
=================================================
FLUID INFORMATION
-----------------
Temperature: 100.0 °C
Pressure: 350.0 bar
Number of Components: 3
DE BOER SCREENING RESULTS
-------------------------
Undersaturation: 200.0 bar
In-situ Density: 720.0 kg/m³
Risk Level: MODERATE_PROBLEM
Risk Index: 0.55
CPA THERMODYNAMIC RESULTS
-------------------------
Asphaltene Onset Pressure: 185.0 bar
Above Bubble Point: Yes
Precipitation Expected: Yes
Onset Margin: 35.0 bar above bubble point
COMPARISON SUMMARY
------------------
De Boer Risk: MODERATE
CPA Prediction: Precipitation at 185 bar
Agreement: CONSISTENT
Both methods indicate asphaltene precipitation
risk during normal production.
RECOMMENDATIONS
---------------
1. Consider asphaltene inhibitor injection
2. Design for periodic wellbore cleanout
3. Monitor production for pressure decline
4. Validate with laboratory AOP test
For rapid assessment:
String summary = comparison.getQuickSummary();
System.out.println(summary);
Output:
De Boer: MODERATE_PROBLEM | CPA Onset: 185 bar | Agreement: CONSISTENT
| De Boer | CPA | Interpretation |
|---|---|---|
| NO_PROBLEM | No onset found | Low risk, minimal mitigation |
| SLIGHT_PROBLEM | Onset near P_bub | Monitor during late life |
| MODERATE_PROBLEM | Onset > P_bub | Active mitigation needed |
| SEVERE_PROBLEM | Onset >> P_bub | Significant risk, design for it |
| De Boer | CPA | Possible Causes |
|---|---|---|
| HIGH | No onset | De Boer conservative; unusual composition |
| LOW | Onset found | Light oil with high asphaltene; check composition |
When results disagree:
// Step 1: De Boer screening of all fluids
List<String> needsDetailedAnalysis = new ArrayList<>();
for (FluidSample sample : allSamples) {
DeBoerAsphalteneScreening screening = new DeBoerAsphalteneScreening(
sample.getReservoirPressure(),
sample.getBubblePoint(),
sample.getInSituDensity()
);
String risk = screening.evaluateRisk();
if (!risk.equals("NO_PROBLEM")) {
needsDetailedAnalysis.add(sample.getName());
}
}
// Step 2: CPA analysis only for flagged samples
for (String sampleName : needsDetailedAnalysis) {
// Create detailed CPA model
SystemSrkCPAstatoil fluid = createCPAFluid(sampleName);
AsphalteneStabilityAnalyzer analyzer =
new AsphalteneStabilityAnalyzer(fluid);
double onsetP = analyzer.calculateOnsetPressure();
System.out.printf("Sample %s: Onset at %.1f bar%n",
sampleName, onsetP);
}
// Use CPA to define safe operating window
AsphalteneStabilityAnalyzer analyzer = new AsphalteneStabilityAnalyzer(fluid);
// Generate precipitation boundary
double[][] envelope = analyzer.generatePrecipitationEnvelope(
280.0, 420.0, 10.0 // Temperature range [K]
);
// Define operating envelope with safety margin
double safetyMargin = 20.0; // bar below onset
System.out.println("Safe Operating Envelope:");
System.out.println("T [°C], Max P [bar]");
for (int i = 0; i < envelope[0].length; i++) {
double T_C = envelope[0][i] - 273.15;
double maxSafeP = envelope[1][i] - safetyMargin;
System.out.printf("%.0f, %.1f%n", T_C, maxSafeP);
}
// Base fluid
SystemSrkCPAstatoil baseFluid = createFluid("FieldA");
// Blend fluid
SystemSrkCPAstatoil blendFluid = createFluid("FieldB");
// Test blend ratios
double[] blendRatios = {0.0, 0.25, 0.50, 0.75, 1.0};
System.out.println("Blend Compatibility Study");
System.out.println("-------------------------");
for (double ratio : blendRatios) {
SystemSrkCPAstatoil mixed = blendFluids(baseFluid, blendFluid, ratio);
AsphalteneStabilityAnalyzer analyzer =
new AsphalteneStabilityAnalyzer(mixed);
double onsetP = analyzer.calculateOnsetPressure();
String deBoer = analyzer.deBoerScreening();
System.out.printf("%.0f%% Field B: Onset = %.1f bar, De Boer = %s%n",
ratio * 100, onsetP, deBoer);
}
For CPA model tuning:
| Test | Purpose | Priority |
|---|---|---|
| AOP Test | Onset pressure at reservoir T | Required |
| HPM Analysis | Onset detection and quantification | Recommended |
| SARA Analysis | Composition for characterization | Required |
| Titration | Onset with n-heptane at ambient | Optional |
| Density | For De Boer screening | Required |
NeqSim provides the AsphalteneOnsetFitting class for automated CPA parameter tuning:
import neqsim.pvtsimulation.util.parameterfitting.AsphalteneOnsetFitting;
// Create fitter with your fluid system
AsphalteneOnsetFitting fitter = new AsphalteneOnsetFitting(fluid);
// Add experimental AOP data from lab tests
fitter.addOnsetPointCelsius(80.0, 350.0); // 80°C, 350 bar
fitter.addOnsetPointCelsius(100.0, 320.0); // 100°C, 320 bar
fitter.addOnsetPointCelsius(120.0, 280.0); // 120°C, 280 bar
// Set initial parameter guesses based on oil type
// Light oils: epsilon/R=3000, kappa=0.01
// Heavy oils: epsilon/R=4000, kappa=0.003
fitter.setInitialGuess(3500.0, 0.005);
// Run Levenberg-Marquardt optimization
boolean success = fitter.solve();
if (success) {
// Get fitted parameters
double epsilonR = fitter.getFittedAssociationEnergy();
double kappa = fitter.getFittedAssociationVolume();
System.out.printf("Fitted ε/R: %.1f K%n", epsilonR);
System.out.printf("Fitted κ: %.4f%n", kappa);
// Predict onset at new temperature
double predictedAOP = fitter.calculateOnsetPressure(393.15); // 120°C
System.out.printf("Predicted AOP at 120°C: %.1f bar%n", predictedAOP);
}
For manual iteration without the automated fitter:
// Experimental AOP
double measuredAOP = 195.0; // bar at 100°C
// Initial CPA prediction
AsphalteneStabilityAnalyzer analyzer =
new AsphalteneStabilityAnalyzer(fluid);
double predictedAOP = analyzer.calculateOnsetPressure();
// Calculate error
double error = predictedAOP - measuredAOP;
System.out.printf("Initial error: %.1f bar%n", error);
// Adjust asphaltene parameters to match
// Options: molecular weight, critical properties, kij values
// Iterate until error < tolerance
| Operation | Time | Notes |
|---|---|---|
| De Boer screening | < 1 ms | Single correlation evaluation |
| De Boer batch (1000 samples) | < 100 ms | Highly parallelizable |
| CPA single flash | 10-100 ms | Depends on composition |
| CPA onset pressure | 1-5 s | Multiple flashes + bisection |
| CPA full envelope | 10-60 s | Many onset calculations |
| Parameter fitting (3 points) | 10-60 s | Depends on convergence |
| Full comparison report | 2-10 s | Both methods + formatting |
// For real-time monitoring: Use De Boer only
if (isRealTimeMonitoring) {
DeBoerAsphalteneScreening screening =
new DeBoerAsphalteneScreening(pRes, pBub, density);
return screening.calculateRiskIndex();
}
// For batch screening: Use De Boer first
if (numberOfSamples > 100) {
// Screen with De Boer
List<Sample> flagged = deBoerScreen(samples);
// CPA only on flagged samples
for (Sample s : flagged) {
runCPAAnalysis(s);
}
}
✅ Start with De Boer for initial assessment
✅ Validate CPA with experimental data before design
✅ Use both for comprehensive studies
✅ Document assumptions in both methods
✅ Apply safety margins to predicted onset
❌ Don't use CPA without tuning for critical decisions
❌ Don't ignore De Boer warnings even if CPA looks safe
❌ Don't over-interpret small onset pressure differences
❌ Don't extrapolate beyond validated conditions
import neqsim.pvtsimulation.flowassurance.*;
import neqsim.thermo.system.SystemSrkCPAstatoil;
public class AsphalteneStudy {
public static void main(String[] args) {
// Create realistic oil composition
SystemSrkCPAstatoil fluid = new SystemSrkCPAstatoil(373.15, 350.0);
// Light ends
fluid.addComponent("nitrogen", 0.01);
fluid.addComponent("CO2", 0.02);
fluid.addComponent("methane", 0.35);
fluid.addComponent("ethane", 0.08);
fluid.addComponent("propane", 0.05);
// Intermediates
fluid.addComponent("n-butane", 0.03);
fluid.addComponent("n-pentane", 0.03);
fluid.addComponent("n-hexane", 0.05);
fluid.addComponent("n-heptane", 0.15);
fluid.addComponent("n-decane", 0.10);
// Heavy ends
fluid.addComponent("n-C15", 0.08);
fluid.addComponent("asphaltene", 0.05);
fluid.setMixingRule("classic");
fluid.init(0);
fluid.init(1);
// Reservoir conditions
double pRes = 350.0; // bar
double pBub = 150.0; // bar
double density = 720.0; // kg/m³
// Run full comparison
AsphalteneMethodComparison comparison =
new AsphalteneMethodComparison(fluid);
comparison.setReservoirPressure(pRes);
comparison.setBubblePointPressure(pBub);
comparison.setInSituDensity(density);
// Generate reports
System.out.println(comparison.runComparison());
System.out.println("\n" + StringUtils.repeat("=", 50));
System.out.println("QUICK REFERENCE");
System.out.println(StringUtils.repeat("=", 50));
System.out.println(comparison.getQuickSummary());
}
}
The AsphalteneOnsetFitting class provides automated CPA parameter tuning using the Levenberg-Marquardt optimization algorithm. This enables matching CPA model predictions to experimental asphaltene onset pressure (AOP) measurements.
CPA parameters for asphaltenes (association energy ε/R and volume κ) vary between crude oils and must be tuned to experimental data for accurate predictions:
| Parameter | Effect on Onset Pressure |
|---|---|
| Higher ε/R | Earlier precipitation (higher onset P) |
| Higher κ | Stronger aggregation tendency |
| Higher MW | Reduced solubility |
Default parameters provide reasonable estimates, but tuning to measured AOP data improves prediction accuracy significantly.
import neqsim.pvtsimulation.util.parameterfitting.AsphalteneOnsetFitting;
import neqsim.thermo.system.SystemSrkCPAstatoil;
// 1. Create fluid with asphaltene
SystemSrkCPAstatoil fluid = new SystemSrkCPAstatoil(373.15, 200.0);
fluid.addComponent("methane", 0.30);
fluid.addTBPfraction("C7", 0.50, 0.150, 0.80);
fluid.addComponent("asphaltene", 0.05);
fluid.setMixingRule("classic");
// 2. Create fitter and add experimental data
AsphalteneOnsetFitting fitter = new AsphalteneOnsetFitting(fluid);
fitter.addOnsetPointCelsius(80.0, 350.0); // T=80°C, P=350 bar
fitter.addOnsetPointCelsius(100.0, 320.0); // T=100°C, P=320 bar
fitter.addOnsetPointCelsius(120.0, 280.0); // T=120°C, P=280 bar
// 3. Set initial guesses and run fitting
fitter.setInitialGuess(3500.0, 0.005); // ε/R [K], κ [-]
boolean success = fitter.solve();
// 4. Get results
if (success) {
System.out.println("Fitted ε/R: " + fitter.getFittedAssociationEnergy() + " K");
System.out.println("Fitted κ: " + fitter.getFittedAssociationVolume());
}
// Create fitter from existing fluid system
AsphalteneOnsetFitting fitter = new AsphalteneOnsetFitting(fluid);
The fluid system must:
SystemSrkCPAstatoil)// Add point with temperature in Kelvin
fitter.addOnsetPoint(373.15, 320.0); // T [K], P [bar]
// Add point with temperature in Celsius (convenience method)
fitter.addOnsetPointCelsius(100.0, 320.0); // T [°C], P [bar]
// Add point with measurement uncertainty
fitter.addOnsetPoint(373.15, 320.0, 10.0); // T [K], P [bar], stdDev [bar]
// Clear all data points
fitter.clearData();
Recommended: Use at least 3 data points at different temperatures for robust fitting.
// Two parameters: association energy and volume
fitter.setInitialGuess(3500.0, 0.005); // ε/R [K], κ [-]
// Single parameter (when fitting only one)
fitter.setInitialGuess(3500.0);
| Oil Type | API Gravity | ε/R [K] | κ |
|---|---|---|---|
| Light oil | >35° | 2500-3500 | 0.005-0.015 |
| Medium oil | 25-35° | 3000-4000 | 0.003-0.008 |
| Heavy oil | <25° | 3500-4500 | 0.002-0.005 |
Control which parameters are fitted:
import neqsim.pvtsimulation.util.parameterfitting.AsphalteneOnsetFunction.FittingParameterType;
// Fit both association parameters (default)
fitter.setParameterType(FittingParameterType.ASSOCIATION_PARAMETERS);
fitter.setInitialGuess(3500.0, 0.005);
// Fit only association energy
fitter.setParameterType(FittingParameterType.ASSOCIATION_ENERGY);
fitter.setInitialGuess(3500.0);
// Fit only association volume
fitter.setParameterType(FittingParameterType.ASSOCIATION_VOLUME);
fitter.setInitialGuess(0.005);
// Fit binary interaction parameter
fitter.setParameterType(FittingParameterType.BINARY_INTERACTION);
fitter.setInitialGuess(0.0);
// Fit molar mass
fitter.setParameterType(FittingParameterType.MOLAR_MASS);
fitter.setInitialGuess(750.0);
// Set pressure range for onset calculation
fitter.setPressureRange(
500.0, // Start pressure [bar] - search starts here
10.0, // Min pressure [bar] - search stops here
10.0 // Pressure step [bar] - coarse search step
);
boolean success = fitter.solve();
if (success) {
// Fitting converged
double[] params = fitter.getFittedParameters();
} else {
// Fitting failed - check initial guesses or data quality
}
// Get fitted parameters
double epsilonR = fitter.getFittedAssociationEnergy();
double kappa = fitter.getFittedAssociationVolume();
// Get all fitted parameters as array
double[] params = fitter.getFittedParameters();
// Check if fitting was successful
boolean success = fitter.isSolved();
// Calculate onset pressure at a new temperature
double onsetP = fitter.calculateOnsetPressure(393.15); // T [K]
// Get the tuned fluid system
SystemInterface tunedFluid = fitter.getTunedSystem();
The fitting uses the Levenberg-Marquardt algorithm to minimize:
$$ \chi^2 = \sum_{i=1}^{n} \frac{(P_{\text{calc},i} - P_{\text{exp},i})^2}{\sigma_i^2} $$
Where:
For each temperature, the onset pressure is found by:
✅ Use at least 3 onset points at different temperatures
✅ Include temperature range spanning expected field conditions
✅ Use reliable measurement techniques (HPM, light scattering)
✅ Specify measurement uncertainty for weighted fitting
✅ Start with literature values for similar oils
✅ Use the "heavy" guess for conservative prediction
✅ Try multiple initial guesses if fitting fails
✅ Check that fitted parameters are physically reasonable
✅ Validate predictions at conditions not used in fitting
✅ Compare with De Boer screening for consistency
Possible causes:
Solutions:
fluid.setMixingRule("classic")Expected ranges:
If outside range:
Possible causes:
Solutions:
import neqsim.pvtsimulation.util.parameterfitting.AsphalteneOnsetFitting;
import neqsim.thermo.system.SystemSrkCPAstatoil;
public class AsphalteneParameterTuning {
public static void main(String[] args) {
// Create realistic oil composition
SystemSrkCPAstatoil fluid = new SystemSrkCPAstatoil(373.15, 350.0);
fluid.addComponent("methane", 0.35);
fluid.addComponent("ethane", 0.08);
fluid.addComponent("propane", 0.05);
fluid.addTBPfraction("C7", 0.35, 0.100, 0.75);
fluid.addTBPfraction("C15", 0.12, 0.200, 0.85);
fluid.addComponent("asphaltene", 0.05);
fluid.setMixingRule("classic");
fluid.init(0);
// Create fitter
AsphalteneOnsetFitting fitter = new AsphalteneOnsetFitting(fluid);
// Add experimental AOP data from lab tests
// (Temperature in Celsius, Pressure in bar)
fitter.addOnsetPointCelsius(60.0, 380.0); // Cool conditions
fitter.addOnsetPointCelsius(80.0, 350.0); // Intermediate
fitter.addOnsetPointCelsius(100.0, 320.0); // Reservoir T
fitter.addOnsetPointCelsius(120.0, 280.0); // Hot conditions
// Set initial guesses (medium oil)
fitter.setInitialGuess(3500.0, 0.005);
// Configure pressure search range
fitter.setPressureRange(500.0, 10.0, 10.0);
// Run fitting
System.out.println("Starting parameter fitting...");
boolean success = fitter.solve();
if (success) {
System.out.println("\n=== FITTING RESULTS ===");
System.out.printf("Fitted ε/R: %.1f K%n", fitter.getFittedAssociationEnergy());
System.out.printf("Fitted κ: %.5f%n", fitter.getFittedAssociationVolume());
// Validate: predict onset at test temperatures
System.out.println("\n=== VALIDATION ===");
double[] testTemps = {333.15, 353.15, 373.15, 393.15}; // 60-120°C
double[] measuredP = {380.0, 350.0, 320.0, 280.0};
for (int i = 0; i < testTemps.length; i++) {
double predP = fitter.calculateOnsetPressure(testTemps[i]);
double error = predP - measuredP[i];
System.out.printf("T = %.0f°C: Measured = %.0f bar, Predicted = %.1f bar (Δ = %.1f)%n",
testTemps[i] - 273.15, measuredP[i], predP, error);
}
// Predict at new conditions
System.out.println("\n=== PREDICTIONS ===");
double newOnset = fitter.calculateOnsetPressure(413.15); // 140°C
System.out.printf("Predicted AOP at 140°C: %.1f bar%n", newOnset);
} else {
System.out.println("Fitting failed - try different initial guesses");
}
}
}
Li, Z., and Firoozabadi, A. (2010). "Modeling Asphaltene Precipitation by n-Alkanes from Heavy Oils and Bitumens Using Cubic-Plus-Association Equation of State." Energy & Fuels, 24, 1106-1113.
Vargas, F.M., Gonzalez, D.L., Hirasaki, G.J., and Chapman, W.G. (2009). "Modeling Asphaltene Phase Behavior in Crude Oil Systems Using the Perturbed Chain Form of the Statistical Associating Fluid Theory (PC-SAFT) Equation of State." Energy & Fuels, 23, 1140-1146.
Gonzalez, D.L., Ting, P.D., Hirasaki, G.J., and Chapman, W.G. (2005). "Prediction of Asphaltene Instability under Gas Injection with the PC-SAFT Equation of State." Energy & Fuels, 19, 1230-1234.
This document summarizes the validation of NeqSim's asphaltene models against published literature and field data. The validation demonstrates that the implemented models correctly capture the physics of asphaltene precipitation and provide reliable predictions for field screening applications.
De Boer, R.B., et al. (1995)
"Screening of Crude Oils for Asphalt Precipitation: Theory, Practice, and the Selection of Inhibitors."
SPE Production & Facilities, 10(1), 55-61. SPE-24987-PA
Akbarzadeh, K., et al. (2007)
"Asphaltenes—Problematic but Rich in Potential."
Oilfield Review, 19(2), 22-43.
Hammami, A., et al. (2000)
"Asphaltene Precipitation from Live Oils: An Experimental Investigation of Onset Conditions and Reversibility."
Energy & Fuels, 14(1), 14-18.
The De Boer correlation was validated against 10 field cases from the original SPE paper:
| Field | Country | P_res [bar] | P_bub [bar] | ρ [kg/m³] | ΔP [bar] |
|---|---|---|---|---|---|
| Hassi Messaoud | Algeria | 414 | 172 | 694 | 242 |
| Mata-Acema | Venezuela | 276 | 138 | 725 | 138 |
| Boscan (Light) | Venezuela | 310 | 103 | 720 | 207 |
| Prinos | Greece | 483 | 207 | 680 | 276 |
| Ula | North Sea | 345 | 145 | 710 | 200 |
| Field | Country | P_res [bar] | P_bub [bar] | ρ [kg/m³] | ΔP [bar] |
|---|---|---|---|---|---|
| Cyrus | North Sea | 207 | 138 | 780 | 69 |
| Ula (Aquifer) | North Sea | 241 | 172 | 810 | 69 |
| Brent | North Sea | 138 | 103 | 850 | 35 |
| Statfjord | North Sea | 172 | 138 | 830 | 34 |
| Forties | North Sea | 207 | 172 | 790 | 35 |
============================================================
VALIDATION SUMMARY: De Boer vs Literature Field Data
============================================================
Confusion Matrix:
Actual
Problem No Problem
Predicted Problem 5 0
Predicted OK 0 5
Performance Metrics:
Accuracy: 100.0% (10/10 correct)
Sensitivity: 100.0% (detects actual problems)
Specificity: 100.0% (avoids false alarms)
Key Findings:
The Hassi Messaoud field in Algeria is a classic example of severe asphaltene problems, documented extensively in the literature:
Field Conditions:
Reservoir Pressure: 414 bar
Bubble Point: 172 bar
Undersaturation: 242 bar
In-situ Density: 694 kg/m³ (~43° API)
NeqSim De Boer Prediction:
Risk Level: SEVERE_PROBLEM
Risk Index: 4.38
Field Experience: SEVERE PROBLEMS (confirmed)
The combination of:
creates conditions highly favorable for asphaltene destabilization.
The Brent, Statfjord, and Forties fields in the North Sea operated for decades without significant asphaltene issues:
| Field | ΔP [bar] | ρ [kg/m³] | Risk Index | Prediction |
|---|---|---|---|---|
| Brent | 35 | 850 | 0.19 | NO_PROBLEM |
| Statfjord | 34 | 830 | 0.21 | NO_PROBLEM |
| Forties | 35 | 790 | 0.27 | NO_PROBLEM |
These fields have:
SARA (Saturates, Aromatics, Resins, Asphaltenes) data from Akbarzadeh et al. (2007):
| Crude Oil | S | A | R | Asp | CII | R/A | Status |
|---|---|---|---|---|---|---|---|
| Alaska North Slope | 0.64 | 0.22 | 0.10 | 0.04 | 2.13 | 2.5 | Stable |
| Arabian Light | 0.63 | 0.25 | 0.09 | 0.03 | 1.94 | 3.0 | Stable |
| Brent Blend | 0.58 | 0.28 | 0.11 | 0.03 | 1.56 | 3.7 | Stable |
| Mars (GoM) | 0.52 | 0.30 | 0.13 | 0.05 | 1.33 | 2.6 | Stable |
| Bonny Light | 0.60 | 0.26 | 0.10 | 0.04 | 1.78 | 2.5 | Stable |
| Maya (Mexico) | 0.42 | 0.28 | 0.18 | 0.12 | 1.17 | 1.5 | Unstable |
| Boscan (Venezuela) | 0.25 | 0.32 | 0.26 | 0.17 | 0.72 | 1.5 | Unstable |
The R/A ratio proved to be a reliable stability indicator:
| Status | R/A Range | Prediction |
|---|---|---|
| Stable | 2.5 - 3.7 | Correctly identified |
| Unstable | 1.5 | Correctly identified |
The R/A ratio thresholds:
The De Boer model correctly captures the physics that risk increases with undersaturation:
ΔP [bar] | Risk Index | Risk Level
---------|------------|------------------
20 | 0.26 | NO_PROBLEM
60 | 0.79 | NO_PROBLEM
100 | 1.32 | SLIGHT_PROBLEM
140 | 1.84 | MODERATE_PROBLEM
180 | 2.37 | MODERATE_PROBLEM
220 | 2.89 | SEVERE_PROBLEM
260 | 3.42 | SEVERE_PROBLEM
300 | 3.95 | SEVERE_PROBLEM
✅ Verified: Risk increases monotonically with undersaturation
Light oils (low density) are more prone to asphaltene problems:
Density [kg/m³] | Risk Index | Risk Level
----------------|------------|------------------
650 | 10.00 | SEVERE_PROBLEM
700 | 3.33 | SEVERE_PROBLEM
750 | 2.00 | MODERATE_PROBLEM
800 | 1.43 | SLIGHT_PROBLEM
850 | 1.11 | SLIGHT_PROBLEM
✅ Verified: Risk decreases monotonically with increasing density
At the bubble point (zero undersaturation), risk should be minimal:
At Bubble Point (ΔP = 0):
Risk Level: NO_PROBLEM
Risk Index: 0.000
Just Above (ΔP = 10 bar):
Risk Level: NO_PROBLEM
Risk Index: 0.100
✅ Verified: Minimal risk at/near bubble point
The CPA model with the asphaltene pseudo-component correctly captures:
Using TBPfraction to create realistic oil densities:
| Case | Target ρ | CPA ρ | Status |
|---|---|---|---|
| Light Oil (Hassi-like) | ~700 kg/m³ | 761 kg/m³ | ✅ Reasonable |
| Heavy Oil (Brent-like) | ~850 kg/m³ | 955 kg/m³ | ✅ Conservative |
The CPA model with TBPfraction produces physically reasonable oil densities that match De Boer field data trends.
The AsphalteneOnsetFitting class successfully fits CPA parameters to match experimental onset data using Levenberg-Marquardt optimization.
| Oil Type | ε/R [K] | κ |
|---|---|---|
| Light oils (>35° API) | 2500-3500 | 0.005-0.015 |
| Medium oils (25-35° API) | 3000-4000 | 0.003-0.008 |
| Heavy oils (<25° API) | 3500-4500 | 0.002-0.005 |
To reproduce these validation results:
# Run all asphaltene validation tests
mvn test -Dtest="*Asphaltene*"
# Run specific De Boer validation
mvn test -Dtest="AsphalteneValidationTest#testDeBoerAgainstPublishedFieldData"
# Run CPA validation tests
mvn test -Dtest="AsphalteneValidationTest#testCPAPhysicalBehavior*"
# Run parameter fitting tests
mvn test -Dtest="AsphalteneOnsetFittingTest"
De Boer Screening: Achieves 100% accuracy on published field data from SPE-24987-PA, correctly identifying all problem and stable fields.
SARA Analysis: R/A ratio achieves 100% accuracy for stability classification on literature crude oil data.
CPA Thermodynamic Model: Correctly captures pressure, temperature, and composition effects on asphaltene phase behavior.
Parameter Fitting: The AsphalteneOnsetFitting class successfully tunes CPA parameters to match experimental onset data.
Physical Behavior: The models correctly capture:
Recommendation: Use De Boer for initial screening. For detailed onset pressure predictions, tune CPA model to experimental AOP data using AsphalteneOnsetFitting.
De Boer, R.B., Leerlooyer, K., Eigner, M.R.P., and van Bergen, A.R.D. (1995). "Screening of Crude Oils for Asphalt Precipitation: Theory, Practice, and the Selection of Inhibitors." SPE Production & Facilities, 10(1), 55-61. SPE-24987-PA
Akbarzadeh, K., Alboudwarej, H., Svrcek, W.Y., and Yarranton, H.W. (2007). "Asphaltenes—Problematic but Rich in Potential." Oilfield Review, 19(2), 22-43.
Leontaritis, K.J., and Mansoori, G.A. (1988). "Asphaltene Deposition: A Survey of Field Experiences and Research Approaches." Journal of Petroleum Science and Engineering, 1(3), 229-239.
Hammami, A., Phelps, C.H., Monger-McClure, T., and Little, T.M. (2000). "Asphaltene Precipitation from Live Oils: An Experimental Investigation of Onset Conditions and Reversibility." Energy & Fuels, 14(1), 14-18.
Li, Z., and Firoozabadi, A. (2010). "Modeling Asphaltene Precipitation by n-Alkanes from Heavy Oils and Bitumens Using Cubic-Plus-Association Equation of State." Energy & Fuels, 24, 1106-1113.
Vargas, F.M., Gonzalez, D.L., Hirasaki, G.J., and Chapman, W.G. (2009). "Modeling Asphaltene Phase Behavior in Crude Oil Systems Using the Perturbed Chain Form of the Statistical Associating Fluid Theory (PC-SAFT) Equation of State." Energy & Fuels, 23, 1140-1146.
The ISO 6976 calorific value and Wobbe index calculations are verified in Standard_ISO6976Test. This page summarizes the tested configurations and equations so you can align custom gas-quality runs with the regression suite.
testCalculate initializes a dry natural gas at 20 °C and 1 bar with a classic mixing rule and executes Standard_ISO6976.calculate() using volume-based reference conditions (0 °C volume base, 15.55 °C energy base).【F:src/test/java/neqsim/standards/gasquality/Standard_ISO6976Test.java†L23-L47】 The test confirms the gross calorific value (GCV) of 39,614.57 kJ/Sm³ and Wobbe index (WI) of 44.61 MJ/Sm³.
The Wobbe index relation checked in the test is
[ WI = \frac{GCV}{\sqrt{\rho_r}}\ , ]
where (\rho_r) is the relative density. Matching the test values indicates both combustion energy and density normalization are consistent with ISO 6976.
testCalculateWithWrongReferenceState shows that if non-standard reference temperatures are provided, the standard falls back to defined bases (15 °C for energy, 0 °C for volume) while still computing GCV and WI.【F:src/test/java/neqsim/standards/gasquality/Standard_ISO6976Test.java†L49-L73】 Use this behavior to guard against user input errors without failing calculations.
testCalculateWithPSeudo adds a TBP pseudo-fraction to the gas and re-runs the calculation to verify heavier fractions contribute to higher heating value (GCV ≈ 42,378 kJ/Sm³).【F:src/test/java/neqsim/standards/gasquality/Standard_ISO6976Test.java†L75-L96】 The setup demonstrates that ISO 6976 evaluation tolerates lumped heavy ends when a classic mixing rule and full flash initialization are applied.
testCalculate2 and testCalculate3 sweep alternative temperatures and reference pairs to assert a complete property set: compression factor, superior/inferior calorific values, Wobbe indices, relative density, and molar mass.【F:src/test/java/neqsim/standards/gasquality/Standard_ISO6976Test.java†L98-L200】 The tests also run a process Stream to ensure downstream WI reporting matches the standard calculation.
When configuring your own gas-quality evaluations:
init(0)) before calling the standard.This page summarises the equations implemented in the HumidAir utility class for psychrometric calculations. The correlations are based on the ASHRAE Handbook Fundamentals (2017), CoolProp and the IAPWS formulation for the saturation pressure of water.
For temperatures $T$ above the triple point the saturation vapour pressure $p_{ws}$ in pascal is given by the IAPWS equation of Wagner and Pruss (2002)
$$ \ln\left(\frac{p_{ws}}{p_c}\right) = \frac{T_c}{T}\left(a_1\theta + a_2\theta^{3/2} + a_3\theta^3 + a_4\theta^{7/2} + a_5\theta^4 + a_6\theta^{15/2}\right) $$
where $\theta = 1 - T/T_c$, $T_c = 647.096\ \text{K}$ and $p_c = 22.064\ \text{MPa}$. Below the triple point a sublimation correlation is used.
The humidity ratio $W$ relates the mass of water vapour to the mass of dry air
$$ W = \varepsilon \frac{p_w}{p - p_w} $$
where $\varepsilon = M_w/M_{da} \approx 0.621945$, $p$ is the total pressure and $p_w$ the partial pressure of water.
For a given relative humidity $\phi$, the partial pressure is $p_w = \phi p_{ws}$.
Given a humidity ratio, the dew point temperature $T_d$ is found by solving $p_{ws}(T_d) = p_w$. The HumidAir implementation uses a simple Newton iteration.
On a dry-air basis the specific enthalpy $h$ in kJ/kg dry air is approximated by
$$ h = 1.006\,t + W (2501 + 1.86\,t) $$
where $t$ is the temperature in degrees Celsius and $W$ is the humidity ratio.
CoolProp provides a correlation for the saturated humid-air specific heat $c_{p,\text{sat}}$ at 1\,atm valid from 250\,K to 300\,K
$$ c_{p,\text{sat}} = 2.146\,27073 \times 10^{3} - 3.289\,17768 \times 10^{1}T + 1.894\,71075 \times 10^{-1}T^2 \
The NeqSim standards package implements international standards for gas and oil quality calculations, enabling compliance verification and sales contract management.
Location: neqsim.standards
The standards package provides implementations of:
Key Applications:
standards/
├── Standard.java # Abstract base class
├── StandardInterface.java # Interface definition
│
├── gasquality/ # Gas quality standards
│ ├── Standard_ISO6976.java # Calorific values & Wobbe index
│ ├── Standard_ISO6976_2016.java # ISO 6976:2016 edition
│ ├── Standard_ISO6974.java # Gas chromatography composition
│ ├── Standard_ISO6578.java # LNG density calculation
│ ├── Standard_ISO15403.java # CNG fuel quality (MON, methane number)
│ ├── Draft_ISO18453.java # Water dew point (GERG-water)
│ ├── Draft_GERG2004.java # GERG-2004 EoS properties
│ ├── BestPracticeHydrocarbonDewPoint.java # HC dew point
│ ├── GasChromotograpyhBase.java # Gas composition base class
│ ├── SulfurSpecificationMethod.java # H2S and sulfur content
│ └── UKspecifications_ICF_SI.java # UK ICF/SI specifications
│
├── oilquality/ # Oil quality standards
│ └── Standard_ASTM_D6377.java # Reid vapor pressure (RVP)
│
└── salescontract/ # Contract management
├── BaseContract.java # Contract implementation
├── ContractInterface.java # Contract interface
└── ContractSpecification.java # Individual specifications
Detailed guides for each major standard:
| Guide | Description |
|---|---|
| ISO 6976 - Calorific Values | GCV, LCV, Wobbe index, density from composition |
| ISO 6578 - LNG Density | LNG density calculation method |
| ISO 15403 - CNG Quality | Methane number and MON for vehicle fuel |
| Dew Point Standards | Water and hydrocarbon dew point methods |
| ASTM D6377 - RVP | Reid vapor pressure for crude and condensate |
| Sales Contracts | Contract specification and compliance checking |
All standards implement StandardInterface:
public interface StandardInterface {
void calculate(); // Run calculation
double getValue(String parameter); // Get result
double getValue(String parameter, String unit); // Get result with unit
String getUnit(String parameter); // Get unit string
boolean isOnSpec(); // Check compliance
ContractInterface getSalesContract(); // Get attached contract
void setSalesContract(ContractInterface contract);
SystemInterface getThermoSystem(); // Get fluid
}
Standards extend Standard:
public abstract class Standard extends NamedBaseClass implements StandardInterface {
protected SystemInterface thermoSystem;
protected ThermodynamicOperations thermoOps;
protected ContractInterface salesContract;
protected String standardDescription;
private String referenceState = "real"; // or "ideal"
private double referencePressure = 70.0;
}
Most gas quality standards support:
standard.setReferenceState("real"); // Default
standard.setReferenceState("ideal"); // Ideal gas assumption
import neqsim.thermo.system.SystemSrkEos;
import neqsim.standards.gasquality.Standard_ISO6976;
// Create gas composition
SystemInterface gas = new SystemSrkEos(273.15 + 15, 1.01325);
gas.addComponent("methane", 0.90);
gas.addComponent("ethane", 0.05);
gas.addComponent("propane", 0.02);
gas.addComponent("nitrogen", 0.02);
gas.addComponent("CO2", 0.01);
gas.setMixingRule("classic");
// Create standard
// Parameters: system, volumeRefT(°C), energyRefT(°C), referenceType
Standard_ISO6976 iso6976 = new Standard_ISO6976(gas, 15, 15, "volume");
iso6976.setReferenceState("real");
// Calculate
iso6976.calculate();
// Get results
double gcv = iso6976.getValue("GCV"); // Gross calorific value [kJ/m³]
double lcv = iso6976.getValue("LCV"); // Net calorific value [kJ/m³]
double wobbe = iso6976.getValue("SuperiorWobbeIndex"); // Wobbe index [kJ/m³]
double relDens = iso6976.getValue("RelativeDensity"); // Relative density [-]
double Z = iso6976.getValue("CompressionFactor"); // Compressibility [-]
double molarMass = iso6976.getValue("MolarMass"); // g/mol
System.out.printf("GCV = %.2f kJ/m³%n", gcv);
System.out.printf("Wobbe Index = %.2f kJ/m³%n", wobbe);
System.out.printf("Relative Density = %.4f%n", relDens);
import neqsim.standards.gasquality.Standard_ISO6578;
// LNG composition
SystemInterface lng = new SystemSrkEos(110, 1.01325); // -163°C
lng.addComponent("methane", 0.92);
lng.addComponent("ethane", 0.05);
lng.addComponent("propane", 0.02);
lng.addComponent("nitrogen", 0.01);
lng.setMixingRule("classic");
// Calculate density
Standard_ISO6578 iso6578 = new Standard_ISO6578(lng);
iso6578.calculate();
double density = iso6578.getValue("density", "kg/m3");
System.out.printf("LNG Density = %.2f kg/m³%n", density);
import neqsim.standards.gasquality.Draft_ISO18453;
// Natural gas with water
SystemInterface wetGas = new SystemSrkCPA(273.15 + 20, 70.0);
wetGas.addComponent("methane", 0.95);
wetGas.addComponent("water", 50e-6); // 50 ppm water
wetGas.setMixingRule("CPA-EoS");
// Calculate water dew point
Draft_ISO18453 waterDewPoint = new Draft_ISO18453(wetGas);
waterDewPoint.calculate();
double wdp = waterDewPoint.getValue("dewPointTemperature");
System.out.printf("Water Dew Point = %.1f °C%n", wdp);
import neqsim.standards.oilquality.Standard_ASTM_D6377;
// Crude oil / condensate
SystemInterface crude = new SystemSrkEos(273.15 + 15, 1.0);
crude.addComponent("methane", 0.02);
crude.addComponent("ethane", 0.03);
crude.addComponent("propane", 0.05);
crude.addComponent("n-butane", 0.08);
crude.addComponent("n-pentane", 0.10);
crude.addTBPfraction("C6", 0.15, 86/1000.0, 0.66);
crude.addTBPfraction("C10", 0.30, 142/1000.0, 0.78);
crude.addTBPfraction("C20", 0.27, 282/1000.0, 0.85);
crude.setMixingRule("classic");
// Calculate RVP
Standard_ASTM_D6377 rvpStandard = new Standard_ASTM_D6377(crude);
rvpStandard.setMethodRVP("VPCR4"); // Options: VPCR4, RVP_ASTM_D6377, RVP_ASTM_D323_82
rvpStandard.calculate();
double rvp = rvpStandard.getValue("RVP", "bara");
double tvp = rvpStandard.getValue("TVP", "bara");
System.out.printf("RVP = %.3f bara%n", rvp);
System.out.printf("TVP = %.3f bara%n", tvp);
import neqsim.standards.salescontract.BaseContract;
import neqsim.standards.salescontract.ContractInterface;
// Create contract from database
ContractInterface contract = new BaseContract(gas, "Kaarstoe", "Norway");
// Run compliance check
contract.runCheck();
// Get results
String[][] results = contract.getResultTable();
int numSpecs = contract.getSpecificationsNumber();
// Display results
contract.display();
Standard_ISO6976 standard = new Standard_ISO6976(gas);
standard.setSalesContract(contract);
standard.calculate();
// Check if on specification
boolean onSpec = standard.isOnSpec();
import neqsim.standards.salescontract.ContractSpecification;
// Create custom specification
ContractSpecification spec = new ContractSpecification(
"Water Dew Point", // Name
"Maximum water dew point", // Description
"Norway", // Country
"Kaarstoe", // Terminal
waterDewPointStandard, // Standard method
-20.0, // Min value
-8.0, // Max value
"°C", // Unit
15.0, // Reference T measurement
15.0, // Reference T combustion
70.0, // Reference pressure
"At 70 bar" // Comments
);
| Parameter | Description | Unit |
|---|---|---|
GCV / SuperiorCalorificValue |
Gross calorific value | kJ/m³ |
LCV / InferiorCalorificValue |
Net calorific value | kJ/m³ |
SuperiorWobbeIndex |
Superior Wobbe index | kJ/m³ |
InferiorWobbeIndex |
Inferior Wobbe index | kJ/m³ |
WI |
Wobbe index (alias) | kJ/m³ |
RelativeDensity |
Relative density (air=1) | - |
CompressionFactor |
Compressibility factor Z | - |
MolarMass |
Average molar mass | g/mol |
DensityIdeal |
Ideal gas density | kg/m³ |
DensityReal |
Real gas density | kg/m³ |
| Parameter | Description | Unit |
|---|---|---|
density |
LNG density | kg/m³ |
| Parameter | Description | Unit |
|---|---|---|
RVP |
Reid vapor pressure | bara |
TVP |
True vapor pressure | bara |
VPCR4 |
Vapor pressure at V/L=4 | bara |
| Standard | Volume Ref T | Energy Ref T | Pressure |
|---|---|---|---|
| ISO 6976 | 0, 15, 20°C | 0, 15, 20, 25°C, 60°F | 1.01325 bar |
| ISO 6578 | -160 to -140°C | - | 1.01325 bar |
| ASTM D6377 | 37.8°C (100°F) | - | - |
// ISO 6976 with specific reference conditions
Standard_ISO6976 standard = new Standard_ISO6976(
gas,
15.0, // Volume reference temperature (°C)
25.0, // Energy reference temperature (°C)
"volume" // Reference type: "volume", "mass", or "molar"
);
// Modify reference conditions after creation
standard.setVolRefT(0.0); // Volume at 0°C
standard.setEnergyRefT(15.0); // Combustion at 15°C
componentsNotDefinedByStandard for warningsgascontractspecificationsISO 6976 provides methods for calculating calorific values, density, relative density, and Wobbe indices from natural gas composition.
Standard: ISO 6976:2016 (and earlier editions)
Purpose: Calculate physical properties of natural gas from composition analysis without direct measurement.
Scope:
Classes:
Standard_ISO6976 - ISO 6976:1995 editionStandard_ISO6976_2016 - ISO 6976:2016 editionGross Calorific Value (GCV) - Also called Higher Heating Value (HHV): $$H_s = \sum_i x_i \cdot H_{s,i}^{id}$$
Net Calorific Value (LCV) - Also called Lower Heating Value (LHV): $$H_i = \sum_i x_i \cdot H_{i,i}^{id}$$
where:
The Wobbe index indicates interchangeability of fuel gases:
$$W_s = \frac{H_s}{\sqrt{d}}$$
where $d$ is the relative density.
Mixture compressibility at reference conditions:
$$Z_{mix} = 1 - \left(\sum_i x_i \sqrt{b_i}\right)^2$$
where $b_i$ is the summation factor for component i.
$$d = \frac{\rho_{gas}}{\rho_{air}} = \frac{M_{gas}}{M_{air}} \cdot \frac{Z_{air}}{Z_{gas}}$$
| Reference T | Description |
|---|---|
| 0°C (273.15 K) | European standard |
| 15°C (288.15 K) | ISO standard / metric |
| 15.55°C (60°F) | US/UK standard |
| 20°C (293.15 K) | Engineering standard |
| 25°C (298.15 K) | Thermochemical standard |
"volume"): kJ/m³ - Standard for gas sales"mass"): kJ/kg - Useful for comparisons"molar"): kJ/mol - Thermodynamic basis// Basic constructor (uses default reference conditions)
Standard_ISO6976 standard = new Standard_ISO6976(thermoSystem);
// Full constructor with reference conditions
Standard_ISO6976 standard = new Standard_ISO6976(
thermoSystem, // SystemInterface
volumeRefT, // Volume reference T [°C]
energyRefT, // Energy reference T [°C]
calculationType // "volume", "mass", or "molar"
);
| Method | Description |
|---|---|
calculate() |
Perform calculations |
getValue(param) |
Get calculated value |
getValue(param, unit) |
Get value with unit conversion |
setReferenceState(state) |
Set "real" or "ideal" |
setReferenceType(type) |
Set "volume", "mass", or "molar" |
setVolRefT(T) |
Set volume reference temperature |
setEnergyRefT(T) |
Set energy reference temperature |
getAverageCarbonNumber() |
Average C number of mixture |
Component properties are loaded from database table ISO6976constants:
import neqsim.thermo.system.SystemSrkEos;
import neqsim.standards.gasquality.Standard_ISO6976;
import neqsim.thermodynamicoperations.ThermodynamicOperations;
// Create natural gas composition
SystemInterface gas = new SystemSrkEos(273.15 + 15, 1.01325);
gas.addComponent("methane", 0.9248);
gas.addComponent("ethane", 0.0350);
gas.addComponent("propane", 0.0098);
gas.addComponent("n-butane", 0.0022);
gas.addComponent("i-butane", 0.0034);
gas.addComponent("n-pentane", 0.0006);
gas.addComponent("i-pentane", 0.0005);
gas.addComponent("nitrogen", 0.0107);
gas.addComponent("CO2", 0.0130);
gas.setMixingRule("classic");
// Flash to ensure single phase
ThermodynamicOperations ops = new ThermodynamicOperations(gas);
ops.TPflash();
// Create ISO 6976 standard
Standard_ISO6976 iso6976 = new Standard_ISO6976(gas, 15, 15, "volume");
iso6976.setReferenceState("real");
// Calculate
iso6976.calculate();
// Get results
System.out.println("=== ISO 6976 Results (15°C/15°C, real gas) ===");
System.out.printf("GCV = %.2f kJ/m³%n", iso6976.getValue("GCV"));
System.out.printf("LCV = %.2f kJ/m³%n", iso6976.getValue("LCV"));
System.out.printf("Wobbe Index = %.2f kJ/m³%n", iso6976.getValue("SuperiorWobbeIndex"));
System.out.printf("Relative Density = %.5f%n", iso6976.getValue("RelativeDensity"));
System.out.printf("Z (compressibility) = %.6f%n", iso6976.getValue("CompressionFactor"));
System.out.printf("Molar Mass = %.4f g/mol%n", iso6976.getValue("MolarMass"));
System.out.printf("Density (real) = %.4f kg/m³%n", iso6976.getValue("DensityReal"));
// Standard conditions for US market (60°F)
Standard_ISO6976 us_standard = new Standard_ISO6976(gas, 15.55, 15.55, "volume");
us_standard.setReferenceState("real");
us_standard.calculate();
System.out.printf("GCV (60°F) = %.2f kJ/m³%n", us_standard.getValue("GCV"));
// European conditions (0°C metering, 25°C combustion)
Standard_ISO6976 eu_standard = new Standard_ISO6976(gas, 0, 25, "volume");
eu_standard.setReferenceState("real");
eu_standard.calculate();
System.out.printf("GCV (0°C/25°C) = %.2f kJ/m³%n", eu_standard.getValue("GCV"));
// Mass-based calorific value
Standard_ISO6976 mass_standard = new Standard_ISO6976(gas, 15, 25, "mass");
mass_standard.calculate();
System.out.printf("GCV (mass) = %.2f kJ/kg%n", mass_standard.getValue("GCV"));
// Get Wobbe index in kWh/m³
double wobbeKWh = iso6976.getValue("SuperiorWobbeIndex", "kWh");
System.out.printf("Wobbe Index = %.4f kWh/m³%n", wobbeKWh);
// Gas with heavy ends characterized as TBP fraction
SystemInterface richGas = new SystemSrkEos(273.15 + 15, 1.01325);
richGas.addComponent("methane", 0.85);
richGas.addComponent("ethane", 0.05);
richGas.addComponent("propane", 0.03);
richGas.addTBPfraction("C7+", 0.02, 100.0/1000.0, 0.75); // MW=100, SG=0.75
richGas.addComponent("CO2", 0.03);
richGas.addComponent("nitrogen", 0.02);
richGas.setMixingRule("classic");
Standard_ISO6976 standard = new Standard_ISO6976(richGas, 15, 15, "volume");
standard.calculate();
// Note: TBP fractions are approximated using n-heptane properties
double gcv = standard.getValue("GCV");
// Create formatted table
String[][] table = iso6976.createTable("ISO 6976 Results");
// Or display in GUI
iso6976.display();
| Parameter | Aliases | Description |
|---|---|---|
SuperiorCalorificValue |
GCV |
Gross calorific value |
InferiorCalorificValue |
LCV |
Net calorific value |
SuperiorWobbeIndex |
WI |
Superior Wobbe index |
InferiorWobbeIndex |
- | Inferior Wobbe index |
RelativeDensity |
- | Relative density (air=1) |
CompressionFactor |
- | Compressibility factor Z |
MolarMass |
- | Average molar mass [g/mol] |
DensityIdeal |
- | Ideal gas density [kg/m³] |
DensityReal |
- | Real gas density [kg/m³] |
For calorific values and Wobbe index:
"kWh": Converts kJ to kWh (divides by 3600)The standard includes data for:
Components not in the ISO 6976 database are approximated:
| Component Type | Approximated As |
|---|---|
| HC, TBP, plus | n-heptane |
| alcohol, glycol | methanol |
| Other | inert (N₂) |
Check for approximations:
ArrayList<String> unknowns = standard.getComponentsNotDefinedByStandard();
if (!unknowns.isEmpty()) {
System.out.println("Warning: Components approximated: " + unknowns);
}
Original implementation based on ISO 6976:1995.
Updated implementation with:
iso6976constants2016import neqsim.standards.gasquality.Standard_ISO6976_2016;
Standard_ISO6976_2016 iso2016 = new Standard_ISO6976_2016(gas, 15, 25, "volume");
iso2016.calculate();
double gcv = iso2016.getValue("GCV");
| Temperature | Z_air (1995) | Z_air (2016) |
|---|---|---|
| 0°C | 0.99941 | 0.999419 |
| 15°C | 0.99958 | 0.999595 |
| 20°C | 0.99963 | 0.999645 |
| 60°F | - | 0.999601 |
For a typical North Sea gas (mostly methane):
ISO 6578 provides methods for calculating the density of liquefied natural gas (LNG) from composition and temperature.
Standard: ISO 6578:2017 - Refrigerated hydrocarbon liquids — Static measurement — Calculation procedure
Purpose: Calculate LNG density for custody transfer and inventory management.
Scope:
Class: Standard_ISO6578
LNG density is calculated from:
$$\rho = \frac{M_{mix}}{V_{mix}}$$
where: $$V_{mix} = \sum_i x_i V_i + \Delta V_{correction}$$
$$V_{mix} = \sum_i x_i V_i - k_1 x_{N_2} - k_2 x_{CH_4}$$
where:
import neqsim.standards.gasquality.Standard_ISO6578;
// Create standard
Standard_ISO6578 iso6578 = new Standard_ISO6578(thermoSystem);
| Method | Description |
|---|---|
calculate() |
Perform density calculation |
getValue("density", "kg/m3") |
Get density in kg/m³ |
useISO6578VolumeCorrectionFacotrs(boolean) |
Toggle ISO 6578 vs alternative factors |
The class includes tabulated data for:
import neqsim.thermo.system.SystemSrkEos;
import neqsim.standards.gasquality.Standard_ISO6578;
// Create LNG composition at storage temperature
SystemInterface lng = new SystemSrkEos(273.15 - 162, 1.01325); // -162°C
lng.addComponent("methane", 0.9200);
lng.addComponent("ethane", 0.0500);
lng.addComponent("propane", 0.0180);
lng.addComponent("n-butane", 0.0040);
lng.addComponent("i-butane", 0.0030);
lng.addComponent("nitrogen", 0.0050);
lng.setMixingRule("classic");
// Calculate density
Standard_ISO6578 iso6578 = new Standard_ISO6578(lng);
iso6578.calculate();
double density = iso6578.getValue("density", "kg/m3");
System.out.printf("LNG Density at %.1f°C = %.2f kg/m³%n",
lng.getTemperature() - 273.15, density);
// Temperature sweep
double[] temperatures = {-165, -162, -160, -155, -150}; // °C
for (double T : temperatures) {
lng.setTemperature(T + 273.15);
iso6578.calculate();
double rho = iso6578.getValue("density", "kg/m3");
System.out.printf("T = %.0f°C: ρ = %.2f kg/m³%n", T, rho);
}
// Rich LNG composition
SystemInterface richLNG = new SystemSrkEos(273.15 - 162, 1.01325);
richLNG.addComponent("methane", 0.8500);
richLNG.addComponent("ethane", 0.0900);
richLNG.addComponent("propane", 0.0400);
richLNG.addComponent("n-butane", 0.0100);
richLNG.addComponent("nitrogen", 0.0100);
richLNG.setMixingRule("classic");
Standard_ISO6578 standard = new Standard_ISO6578(richLNG);
standard.calculate();
double density = standard.getValue("density", "kg/m3");
System.out.printf("Rich LNG Density = %.2f kg/m³%n", density);
// Rich LNG has higher density due to heavier components
// Using ISO 6578 correction factors (default)
Standard_ISO6578 iso_factors = new Standard_ISO6578(lng);
iso_factors.useISO6578VolumeCorrectionFacotrs(true);
iso_factors.calculate();
double rho_iso = iso_factors.getValue("density", "kg/m3");
// Using alternative correction factors
Standard_ISO6578 alt_factors = new Standard_ISO6578(lng);
alt_factors.useISO6578VolumeCorrectionFacotrs(false);
alt_factors.calculate();
double rho_alt = alt_factors.getValue("density", "kg/m3");
System.out.printf("ISO 6578 factors: %.2f kg/m³%n", rho_iso);
System.out.printf("Alternative factors: %.2f kg/m³%n", rho_alt);
The implementation includes two sets of correction factors:
Temperature range: 93.15 K to 133.15 K (-180°C to -140°C) Molar mass range: 16 to 30 g/mol
Temperatures: {93.15, 98.15, 103.15, 108.15, 113.15, 118.15, 123.15, 128.15, 133.15} K
Molar Masses: {16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30} g/mol
Temperature range: 105 K to 135 K Molar mass range: 16 to 25 g/mol
Bicubic interpolation is used for:
Linear interpolation for pure component molar volumes.
| Component | Formula | Molar Volume Data |
|---|---|---|
| Methane | CH₄ | Yes |
| Ethane | C₂H₆ | Yes |
| Propane | C₃H₈ | Yes |
| n-Butane | C₄H₁₀ | Yes |
| i-Butane | C₄H₁₀ | Yes |
| n-Pentane | C₅H₁₂ | Yes |
| i-Pentane | C₅H₁₂ | Yes |
| n-Hexane | C₆H₁₄ | Yes |
| Nitrogen | N₂ | Yes |
Pure component molar volumes at reference temperatures:
Temperatures: {93.15, 98.15, 103.15, 108.15, 113.15, 118.15, 123.15, 128.15, 133.15} K
Example - Methane (dm³/mol):
{0.035771, 0.036315, 0.036891, 0.037500, 0.038149, 0.038839, 0.039580, 0.040375, 0.041237}
| Composition | Value |
|---|---|
| Methane | 95% |
| Ethane | 3% |
| Others | 2% |
| Density at -162°C | 425-435 kg/m³ |
| Composition | Value |
|---|---|
| Methane | 90-92% |
| Ethane | 5-6% |
| Propane | 2% |
| Others | 1-2% |
| Density at -162°C | 440-460 kg/m³ |
| Composition | Value |
|---|---|
| Methane | 85% |
| Ethane | 9% |
| Propane | 4% |
| Others | 2% |
| Density at -162°C | 470-490 kg/m³ |
LNG density is strongly temperature dependent:
Typical uncertainty: ±0.1% for well-characterized LNG
ISO 15403 specifies requirements for natural gas used as compressed fuel for vehicles (CNG).
Standard: ISO 15403-1:2006 - Natural gas — Natural gas for use as a compressed fuel for vehicles
Purpose: Assess natural gas quality for use in vehicle engines by calculating:
Class: Standard_ISO15403
MON indicates knock resistance. Calculated from gas composition:
$$MON = 137.78 \cdot x_{CH_4} + 29.948 \cdot x_{C_2H_6} - 18.193 \cdot x_{C_3H_8} - 167.062 \cdot (x_{nC_4} + x_{iC_4}) + 181.233 \cdot x_{CO_2} + 26.944 \cdot x_{N_2}$$
where $x_i$ is the mole fraction of component i.
MN is derived from MON:
$$MN = 1.445 \cdot MON - 103.42$$
Interpretation:
import neqsim.standards.gasquality.Standard_ISO15403;
// Create from gas composition
Standard_ISO15403 iso15403 = new Standard_ISO15403(thermoSystem);
| Method | Description |
|---|---|
calculate() |
Calculate MON and MN |
getValue("MON") |
Get Motor Octane Number |
getValue("MN") |
Get Methane Number |
import neqsim.thermo.system.SystemSrkEos;
import neqsim.standards.gasquality.Standard_ISO15403;
// CNG composition
SystemInterface cng = new SystemSrkEos(273.15 + 15, 200.0);
cng.addComponent("methane", 0.92);
cng.addComponent("ethane", 0.04);
cng.addComponent("propane", 0.01);
cng.addComponent("n-butane", 0.002);
cng.addComponent("i-butane", 0.003);
cng.addComponent("CO2", 0.015);
cng.addComponent("nitrogen", 0.01);
cng.setMixingRule("classic");
// Calculate methane number
Standard_ISO15403 iso15403 = new Standard_ISO15403(cng);
iso15403.calculate();
double mon = iso15403.getValue("MON");
double mn = iso15403.getValue("MN");
System.out.printf("Motor Octane Number (MON) = %.1f%n", mon);
System.out.printf("Methane Number (MN) = %.1f%n", mn);
// Analyze MN sensitivity to C2+ content
double[] ethaneContents = {0.01, 0.03, 0.05, 0.08, 0.10};
System.out.println("Ethane (mol%) | MON | MN");
System.out.println("--------------|-------|------");
for (double c2 : ethaneContents) {
SystemInterface gas = new SystemSrkEos(273.15 + 15, 200.0);
gas.addComponent("methane", 0.97 - c2);
gas.addComponent("ethane", c2);
gas.addComponent("CO2", 0.02);
gas.addComponent("nitrogen", 0.01);
gas.setMixingRule("classic");
Standard_ISO15403 std = new Standard_ISO15403(gas);
std.calculate();
System.out.printf("%13.0f | %.1f | %.1f%n",
c2 * 100,
std.getValue("MON"),
std.getValue("MN"));
}
// CO2 and N2 improve methane number
SystemInterface leanGas = new SystemSrkEos(273.15 + 15, 200.0);
leanGas.addComponent("methane", 0.85);
leanGas.addComponent("ethane", 0.05);
leanGas.addComponent("propane", 0.02);
// Base case - no inerts
leanGas.addComponent("CO2", 0.0);
leanGas.addComponent("nitrogen", 0.0);
leanGas.setMixingRule("classic");
Standard_ISO15403 baseStd = new Standard_ISO15403(leanGas);
baseStd.calculate();
System.out.printf("Base case MN: %.1f%n", baseStd.getValue("MN"));
// With 5% CO2
SystemInterface withCO2 = leanGas.clone();
withCO2.addComponent("CO2", 0.05);
Standard_ISO15403 co2Std = new Standard_ISO15403(withCO2);
co2Std.calculate();
System.out.printf("With 5%% CO2 MN: %.1f%n", co2Std.getValue("MN"));
// With 5% N2
SystemInterface withN2 = leanGas.clone();
withN2.addComponent("nitrogen", 0.05);
Standard_ISO15403 n2Std = new Standard_ISO15403(withN2);
n2Std.calculate();
System.out.printf("With 5%% N2 MN: %.1f%n", n2Std.getValue("MN"));
| Component | Effect on MN |
|---|---|
| Methane | Increases MN |
| Ethane | Slight decrease |
| Propane | Moderate decrease |
| Butanes | Strong decrease |
| CO₂ | Increases MN |
| N₂ | Increases MN |
| H₂ | Decreases MN |
| Gas Type | Typical MN |
|---|---|
| Pure methane | 100 |
| Lean natural gas | 85-95 |
| Associated gas | 70-85 |
| Biogas | 130-140 |
| LNG regasified | 75-90 |
| Region | Minimum MN |
|---|---|
| Europe (typical) | 65-70 |
| Germany (DIN 51624) | 70 |
| California | 80 |
The correlation is valid for:
For more complex gases, consider:
Standards for calculating water and hydrocarbon dew points of natural gas.
Dew point specifications are critical for:
Available Implementations:
Draft_ISO18453 - Water dew point using GERG-water equationBestPracticeHydrocarbonDewPoint - Hydrocarbon dew point using SRK-EoSISO 18453:2004 - Natural gas — Correlation between water content and water dew point
Calculate the temperature at which water vapor in natural gas begins to condense at a given pressure.
Class: Draft_ISO18453
Uses the GERG-water equation of state which is specifically designed for water in natural gas systems.
import neqsim.standards.gasquality.Draft_ISO18453;
// Create standard from any fluid
Draft_ISO18453 waterDewPoint = new Draft_ISO18453(thermoSystem);
| Method | Description |
|---|---|
calculate() |
Perform dew point calculation |
getValue("dewPointTemperature") |
Get dew point in °C |
getValue("pressure") |
Get reference pressure in bar |
setReferencePressure(P) |
Set reference pressure |
isOnSpec() |
Check against sales contract specification |
import neqsim.thermo.system.SystemSrkCPAstatoil;
import neqsim.standards.gasquality.Draft_ISO18453;
// Natural gas with water
SystemInterface wetGas = new SystemSrkCPAstatoil(273.15 + 20, 50.0);
wetGas.addComponent("methane", 0.90);
wetGas.addComponent("ethane", 0.05);
wetGas.addComponent("propane", 0.02);
wetGas.addComponent("CO2", 0.02);
wetGas.addComponent("water", 100e-6); // 100 ppm water
wetGas.setMixingRule("CPA_Statoil");
// Calculate water dew point
Draft_ISO18453 iso18453 = new Draft_ISO18453(wetGas);
iso18453.setReferencePressure(70.0); // 70 bar reference
iso18453.calculate();
double wdp = iso18453.getValue("dewPointTemperature");
System.out.printf("Water Dew Point at 70 bar = %.1f °C%n", wdp);
// Check against contract specification
iso18453.getSalesContract().setWaterDewPointTemperature(-8.0); // Max -8°C
if (iso18453.isOnSpec()) {
System.out.println("Gas meets water dew point specification");
} else {
System.out.println("Gas FAILS water dew point specification");
}
Calculate the temperature at which hydrocarbon liquids begin to condense from natural gas (cricondentherm).
Class: BestPracticeHydrocarbonDewPoint
Uses SRK equation of state with Peneloux volume correction (mixing rule 2) for hydrocarbon phase behavior.
import neqsim.standards.gasquality.BestPracticeHydrocarbonDewPoint;
// Create from any fluid (water is automatically removed)
BestPracticeHydrocarbonDewPoint hcDewPoint =
new BestPracticeHydrocarbonDewPoint(thermoSystem);
| Method | Description |
|---|---|
calculate() |
Perform dew point calculation |
getValue("hydrocarbondewpointTemperature") |
Get dew point in °C |
getValue("pressure") |
Get reference pressure in bar |
isOnSpec() |
Check against specification |
import neqsim.thermo.system.SystemSrkEos;
import neqsim.standards.gasquality.BestPracticeHydrocarbonDewPoint;
// Rich natural gas
SystemInterface richGas = new SystemSrkEos(273.15 + 20, 50.0);
richGas.addComponent("methane", 0.85);
richGas.addComponent("ethane", 0.06);
richGas.addComponent("propane", 0.03);
richGas.addComponent("n-butane", 0.02);
richGas.addComponent("n-pentane", 0.01);
richGas.addComponent("n-hexane", 0.005);
richGas.addComponent("n-heptane", 0.003);
richGas.addComponent("n-octane", 0.002);
richGas.addComponent("nitrogen", 0.02);
richGas.setMixingRule("classic");
// Calculate hydrocarbon dew point
BestPracticeHydrocarbonDewPoint hcDP = new BestPracticeHydrocarbonDewPoint(richGas);
hcDP.calculate();
double hcdp = hcDP.getValue("hydrocarbondewpointTemperature");
System.out.printf("Hydrocarbon Dew Point at 50 bar = %.1f °C%n", hcdp);
// Calculate HCDP curve at multiple pressures
double[] pressures = {20, 30, 40, 50, 60, 70, 80};
System.out.println("Pressure (bar) | HC Dew Point (°C)");
System.out.println("---------------|-----------------");
for (double P : pressures) {
richGas.setPressure(P);
BestPracticeHydrocarbonDewPoint hcDP = new BestPracticeHydrocarbonDewPoint(richGas);
hcDP.calculate();
double hcdp = hcDP.getValue("hydrocarbondewpointTemperature");
System.out.printf("%14.0f | %16.1f%n", P, hcdp);
}
import neqsim.thermo.system.SystemSrkCPAstatoil;
import neqsim.standards.gasquality.Draft_ISO18453;
import neqsim.standards.gasquality.BestPracticeHydrocarbonDewPoint;
// Create wet gas with heavy ends
SystemInterface gas = new SystemSrkCPAstatoil(273.15 + 20, 70.0);
gas.addComponent("methane", 0.88);
gas.addComponent("ethane", 0.05);
gas.addComponent("propane", 0.02);
gas.addComponent("n-butane", 0.01);
gas.addComponent("n-pentane", 0.005);
gas.addComponent("n-hexane", 0.002);
gas.addComponent("CO2", 0.02);
gas.addComponent("water", 50e-6);
gas.setMixingRule("CPA_Statoil");
// Water dew point
Draft_ISO18453 waterDP = new Draft_ISO18453(gas);
waterDP.setReferencePressure(70.0);
waterDP.calculate();
double wdp = waterDP.getValue("dewPointTemperature");
// Hydrocarbon dew point
BestPracticeHydrocarbonDewPoint hcDP = new BestPracticeHydrocarbonDewPoint(gas);
hcDP.calculate();
double hcdp = hcDP.getValue("hydrocarbondewpointTemperature");
System.out.println("=== Dew Point Analysis ===");
System.out.printf("Water Dew Point (at 70 bar): %.1f °C%n", wdp);
System.out.printf("Hydrocarbon Dew Point (at 50 bar): %.1f °C%n", hcdp);
// Analyze water dew point vs water content
double[] waterContents = {10, 20, 50, 100, 200, 500}; // ppm
System.out.println("Water Content (ppm) | Water Dew Point (°C)");
System.out.println("--------------------|--------------------");
for (double ppm : waterContents) {
SystemInterface gas = new SystemSrkCPAstatoil(273.15 + 20, 70.0);
gas.addComponent("methane", 0.95);
gas.addComponent("ethane", 0.03);
gas.addComponent("CO2", 0.01);
gas.addComponent("water", ppm * 1e-6);
gas.setMixingRule("CPA_Statoil");
Draft_ISO18453 waterDP = new Draft_ISO18453(gas);
waterDP.setReferencePressure(70.0);
waterDP.calculate();
double wdp = waterDP.getValue("dewPointTemperature");
System.out.printf("%19.0f | %19.1f%n", ppm, wdp);
}
| Aspect | ISO 18453 (GERG-water) | CPA-EoS |
|---|---|---|
| Model | GERG-water specific | General CPA |
| Accuracy | Optimized for NG | Good general accuracy |
| Speed | Fast | Moderate |
| Water association | Empirical | Explicit |
| Aspect | Best Practice (SRK) | PR-EoS | GERG-2004 |
|---|---|---|---|
| Heavy ends | Good | Good | Limited |
| Accuracy | Typical ±2-3°C | Typical ±2-3°C | Best for lean gas |
| Speed | Fast | Fast | Moderate |
| Parameter | Typical Limit |
|---|---|
| Water dew point | < -8°C at 70 bar |
| HC dew point | < -2°C at 1-70 bar |
| Parameter | Typical Limit |
|---|---|
| Water dew point | < -7°C (20°F) at max operating P |
| HC dew point | < -4°C (25°F) at cricondenbar |
ASTM D6377 provides methods for determining vapor pressure of crude oil and petroleum products.
Standard: ASTM D6377 - Standard Test Method for Determination of Vapor Pressure of Crude Oil: VPCRx (Expansion Method)
Purpose: Determine the vapor pressure of crude oil and condensates for:
Class: Standard_ASTM_D6377
The equilibrium pressure of vapor above a liquid at a specified temperature when vapor/liquid ratio approaches zero.
$$TVP = P_{bubble}(T)$$
The vapor pressure measured at 100°F (37.8°C) in a standardized apparatus with vapor/liquid volume ratio of 4:1.
The pressure at which 80% by volume is vapor at 37.8°C (100°F).
Different VPCR ratios are used in various standards:
import neqsim.standards.oilquality.Standard_ASTM_D6377;
// Create standard from fluid
Standard_ASTM_D6377 rvpStandard = new Standard_ASTM_D6377(thermoSystem);
| Method Name | Description |
|---|---|
VPCR4 |
Vapor pressure at V/L = 4 (default) |
VPCR4_no_water |
VPCR4 excluding water |
RVP_ASTM_D6377 |
RVP correlation from D6377 |
RVP_ASTM_D323_73_79 |
RVP per D323 (1973/1979) |
RVP_ASTM_D323_82 |
RVP per D323 (1982) |
| Method | Description |
|---|---|
calculate() |
Perform vapor pressure calculations |
getValue("RVP", "bara") |
Get Reid vapor pressure |
getValue("TVP", "bara") |
Get true vapor pressure |
getValue("VPCR4", "bara") |
Get VPCR4 |
setMethodRVP(method) |
Select RVP calculation method |
getMethodRVP() |
Get current method |
import neqsim.thermo.system.SystemSrkEos;
import neqsim.standards.oilquality.Standard_ASTM_D6377;
// Create condensate/crude composition
SystemInterface crude = new SystemSrkEos(273.15 + 15, 1.01325);
crude.addComponent("methane", 0.01);
crude.addComponent("ethane", 0.02);
crude.addComponent("propane", 0.04);
crude.addComponent("n-butane", 0.06);
crude.addComponent("i-butane", 0.03);
crude.addComponent("n-pentane", 0.08);
crude.addComponent("i-pentane", 0.05);
crude.addComponent("n-hexane", 0.10);
crude.addTBPfraction("C7", 0.15, 100.0/1000.0, 0.72);
crude.addTBPfraction("C10", 0.20, 142.0/1000.0, 0.78);
crude.addTBPfraction("C20", 0.26, 282.0/1000.0, 0.85);
crude.setMixingRule("classic");
// Calculate RVP
Standard_ASTM_D6377 rvpStandard = new Standard_ASTM_D6377(crude);
rvpStandard.setMethodRVP("VPCR4");
rvpStandard.calculate();
// Get results
double tvp = rvpStandard.getValue("TVP", "bara");
double rvp = rvpStandard.getValue("RVP", "bara");
double vpcr4 = rvpStandard.getValue("VPCR4", "bara");
System.out.println("=== Vapor Pressure Results ===");
System.out.printf("True Vapor Pressure (TVP) = %.4f bara%n", tvp);
System.out.printf("Reid Vapor Pressure (RVP) = %.4f bara%n", rvp);
System.out.printf("VPCR4 = %.4f bara%n", vpcr4);
// Calculate using all available methods
String[] methods = {"VPCR4", "RVP_ASTM_D6377", "RVP_ASTM_D323_73_79", "RVP_ASTM_D323_82"};
System.out.println("Method | RVP (bara)");
System.out.println("----------------------|----------");
for (String method : methods) {
Standard_ASTM_D6377 std = new Standard_ASTM_D6377(crude);
std.setMethodRVP(method);
std.calculate();
double rvp = std.getValue("RVP", "bara");
System.out.printf("%-21s | %.4f%n", method, rvp);
}
// Analyze RVP sensitivity to light ends
double[] methaneContent = {0.0, 0.005, 0.01, 0.02, 0.05};
System.out.println("Methane (mol%) | RVP (bara)");
System.out.println("---------------|----------");
for (double ch4 : methaneContent) {
SystemInterface fluid = new SystemSrkEos(273.15 + 15, 1.0);
fluid.addComponent("methane", ch4);
fluid.addComponent("ethane", 0.02);
fluid.addComponent("propane", 0.04);
fluid.addComponent("n-butane", 0.08);
fluid.addComponent("n-pentane", 0.10);
fluid.addTBPfraction("C7", 0.20, 100/1000.0, 0.72);
fluid.addTBPfraction("C15", 0.56 - ch4, 200/1000.0, 0.80);
fluid.setMixingRule("classic");
Standard_ASTM_D6377 std = new Standard_ASTM_D6377(fluid);
std.calculate();
double rvp = std.getValue("RVP", "bara");
System.out.printf("%14.1f | %.4f%n", ch4 * 100, rvp);
}
// Calculate with and without water
SystemInterface wetCrude = crude.clone();
wetCrude.addComponent("water", 0.01); // 1% water
Standard_ASTM_D6377 wetStd = new Standard_ASTM_D6377(wetCrude);
wetStd.calculate();
double vpcr4Wet = wetStd.getValue("VPCR4", "bara");
double vpcr4Dry = wetStd.getValue("VPCR4_no_water", "bara");
System.out.printf("VPCR4 (with water) = %.4f bara%n", vpcr4Wet);
System.out.printf("VPCR4 (dry basis) = %.4f bara%n", vpcr4Dry);
Best for:
Correlation from ASTM D6377: $$RVP = 0.834 \times VPCR4$$
Correlation from ASTM D323 (1982 edition): $$RVP = \frac{0.752 \times (100 \times VPCR4) + 6.07}{100}$$
For comparison with historical data using D323 (1973/1979 editions). Uses VPCR4 without water contribution.
Approximate relationship: $$RVP \approx 0.75 \times TVP + constant$$
The constant depends on crude composition.
For estimation at temperatures other than 37.8°C:
$$\log_{10}(P_{vap}) = A - \frac{B}{T + C}$$
Antoine-type equation where A, B, C are crude-specific.
| Product | Typical RVP Limit |
|---|---|
| Crude oil (export) | < 0.7 bara (10 psia) |
| Stabilized condensate | < 0.5 bara (7 psia) |
| Gasoline (summer) | < 0.62 bara (9 psi) |
| Gasoline (winter) | < 0.90 bara (13 psi) |
| Parameter | Value |
|---|---|
| Temperature | 37.8°C (100°F) |
| V/L ratio | 4:1 (80% vapor by volume) |
| Pressure | Equilibrium |
Uses SRK-EoS for phase equilibrium calculations.
| Method | Uncertainty |
|---|---|
| VPCR4 calculation | ±0.02 bara |
| RVP correlation | ±0.03-0.05 bara |
The sales contract system enables specification verification and compliance checking for natural gas quality.
Location: neqsim.standards.salescontract
Purpose:
Classes:
BaseContract - Main contract implementationContractInterface - Contract interfaceContractSpecification - Individual specificationContractInterface
│
└── BaseContract
│
├── ArrayList<ContractSpecification>
│ │
│ └── StandardInterface (method)
│
└── Database connection (gascontractspecifications)
1. Create Contract (from database or manually)
↓
2. Add Specifications (linked to standards)
↓
3. Run Compliance Check
↓
4. Generate Results Table
↓
5. Display/Export Results
import neqsim.standards.salescontract.BaseContract;
import neqsim.standards.salescontract.ContractInterface;
// Load contract from database by terminal and country
ContractInterface contract = new BaseContract(
thermoSystem, // Gas composition
"Kaarstoe", // Terminal name
"Norway" // Country
);
// Create empty contract
ContractInterface contract = new BaseContract();
// Or with basic water dew point spec
ContractInterface contract = new BaseContract(thermoSystem);
Each specification contains:
| Field | Description |
|---|---|
name |
Specification name |
specification |
Description |
country |
Country code |
terminal |
Terminal/delivery point |
standard |
StandardInterface method |
minValue |
Minimum acceptable value |
maxValue |
Maximum acceptable value |
unit |
Unit of measurement |
referenceTemperatureMeasurement |
Reference T for measurement |
referenceTemperatureCombustion |
Reference T for combustion |
referencePressure |
Reference pressure |
comments |
Additional notes |
import neqsim.standards.salescontract.ContractSpecification;
import neqsim.standards.gasquality.Draft_ISO18453;
// Create water dew point specification
StandardInterface waterDPMethod = new Draft_ISO18453(thermoSystem);
ContractSpecification waterSpec = new ContractSpecification(
"Water Dew Point", // Name
"Maximum water dew point", // Specification description
"Norway", // Country
"Kaarstoe", // Terminal
waterDPMethod, // Calculation method
-20.0, // Minimum value
-8.0, // Maximum value
"°C", // Unit
15.0, // Reference T measurement
15.0, // Reference T combustion
70.0, // Reference pressure (bar)
"At 70 bar" // Comments
);
| Method Name | Class | Purpose |
|---|---|---|
ISO18453 |
Draft_ISO18453 |
Water dew point |
ISO6974 |
Standard_ISO6974 |
Gas composition |
ISO6976 |
Standard_ISO6976 |
Calorific values, Wobbe |
BestPracticeHydrocarbonDewPoint |
BestPracticeHydrocarbonDewPoint |
HC dew point |
SulfurSpecificationMethod |
SulfurSpecificationMethod |
H2S and sulfur |
UKspecifications |
UKspecifications_ICF_SI |
UK ICF/SI specs |
import neqsim.thermo.system.SystemSrkCPAstatoil;
import neqsim.standards.salescontract.BaseContract;
// Create gas composition
SystemInterface gas = new SystemSrkCPAstatoil(273.15 + 15, 70.0);
gas.addComponent("methane", 0.90);
gas.addComponent("ethane", 0.05);
gas.addComponent("propane", 0.02);
gas.addComponent("CO2", 0.02);
gas.addComponent("nitrogen", 0.005);
gas.addComponent("water", 30e-6); // 30 ppm water
gas.setMixingRule("CPA_Statoil");
// Load contract from database
BaseContract contract = new BaseContract(gas, "Kaarstoe", "Norway");
// Run compliance check
contract.runCheck();
// Get results
String[][] results = contract.getResultTable();
int numSpecs = contract.getSpecificationsNumber();
System.out.printf("Checked %d specifications%n", numSpecs);
// Display results
contract.display();
import neqsim.standards.salescontract.*;
import neqsim.standards.gasquality.*;
// Create empty contract
BaseContract contract = new BaseContract();
// Add water dew point specification
Draft_ISO18453 waterMethod = new Draft_ISO18453(gas);
waterMethod.setReferencePressure(70.0);
ContractSpecification waterSpec = new ContractSpecification(
"Water DP", "Water dew point max", "Export", "Platform",
waterMethod, -100, -8, "°C", 15, 15, 70, ""
);
contract.addSpecification(waterSpec);
// Add Wobbe index specification
Standard_ISO6976 wobbeMethod = new Standard_ISO6976(gas, 15, 25, "volume");
ContractSpecification wobbeSpec = new ContractSpecification(
"Wobbe Index", "Wobbe index range", "Export", "Platform",
wobbeMethod, 47300, 51500, "kJ/m³", 15, 25, 1.01325, ""
);
contract.addSpecification(wobbeSpec);
// Add GCV specification
ContractSpecification gcvSpec = new ContractSpecification(
"GCV", "Gross calorific value", "Export", "Platform",
wobbeMethod, 37500, 43000, "kJ/m³", 15, 25, 1.01325, ""
);
contract.addSpecification(gcvSpec);
// Run check
contract.runCheck();
import neqsim.standards.gasquality.Draft_ISO18453;
// Create standard with contract
Draft_ISO18453 waterDP = new Draft_ISO18453(gas);
waterDP.setSalesContract(contract);
// Calculate
waterDP.calculate();
// Check specification compliance
if (waterDP.isOnSpec()) {
System.out.println("PASS: Water dew point within specification");
} else {
System.out.println("FAIL: Water dew point out of specification");
}
// After running check
String[][] results = contract.getResultTable();
// Results table structure:
// [row][0] = Specification name
// [row][1] = Measured value
// [row][2] = Min specification
// [row][3] = Max specification
// [row][4] = Unit
// [row][5] = Pass/Fail status
System.out.println("Specification | Value | Min | Max | Unit | Status");
System.out.println("--------------|-------|-----|-----|------|-------");
for (int i = 0; i < contract.getSpecificationsNumber(); i++) {
System.out.printf("%13s | %5s | %3s | %3s | %4s | %6s%n",
results[i][0], results[i][1], results[i][2],
results[i][3], results[i][4], results[i][5]);
}
Table: gascontractspecifications
| Column | Description |
|---|---|
NAME |
Specification name |
SPECIFICATION |
Description |
COUNTRY |
Country code |
TERMINAL |
Delivery point |
METHOD |
Calculation method name |
MINVALUE |
Minimum value |
MAXVALUE |
Maximum value |
UNIT |
Unit string |
ReferenceTdegC |
Reference temperature |
ReferencePbar |
Reference pressure |
Comments |
Additional notes |
| Database Method | Class |
|---|---|
ISO18453 |
Draft_ISO18453 |
ISO6974 |
Standard_ISO6974 |
ISO6976 |
Standard_ISO6976 |
BestPracticeHydrocarbonDewPoint |
BestPracticeHydrocarbonDewPoint |
SulfurSpecificationMethod |
SulfurSpecificationMethod |
UKspecifications |
UKspecifications_ICF_SI |
// Contracts are loaded by terminal and country
BaseContract norwegianContract = new BaseContract(gas, "Kaarstoe", "Norway");
BaseContract ukContract = new BaseContract(gas, "StFergus", "UK");
BaseContract belgianContract = new BaseContract(gas, "Zeebrugge", "Belgium");
| Parameter | Min | Max | Unit | Reference |
|---|---|---|---|---|
| Water dew point | - | -8 | °C | 70 bar |
| HC dew point | - | -2 | °C | cricondentherm |
| GCV | 36.5 | 44.0 | MJ/Sm³ | 15°C/15°C |
| Wobbe index | 47.0 | 52.0 | MJ/Sm³ | 15°C/15°C |
| CO₂ | - | 2.5 | mol% | - |
| H₂S | - | 5 | mg/Sm³ | - |
| Parameter | Min | Max | Unit | Reference |
|---|---|---|---|---|
| GCV | 36.9 | 42.3 | MJ/m³ | 15°C/15°C |
| Wobbe index | 47.2 | 51.41 | MJ/m³ | 15°C/15°C |
| ICF | - | 0.48 | - | - |
| SI | - | 0.60 | - | - |
This document provides an overview of the foundational infrastructure added to NeqSim to support the future of process simulation.
The future of process simulation involves:
neqsim/process/
├── processmodel/
│ └── lifecycle/ # Living Digital Twins
│ ├── ProcessSystemState.java
│ └── ModelMetadata.java
├── advisory/ # Real-Time Advisory
│ └── PredictionResult.java
├── ml/
│ └── surrogate/ # AI + Physics Integration
│ ├── SurrogateModelRegistry.java
│ └── PhysicsConstraintValidator.java
├── safety/
│ └── scenario/ # Safety Analysis
│ └── AutomaticScenarioGenerator.java
├── sustainability/ # Emissions Tracking
│ └── EmissionsTracker.java
└── util/
└── optimization/ # Rapid Screening
└── BatchStudy.java
// Export model state for version control
ProcessSystemState state = process.exportState();
state.setVersion("1.0.0");
state.saveToFile("model_v1.0.0.json");
// Track model lifecycle
ModelMetadata metadata = state.getMetadata();
metadata.setLifecyclePhase(LifecyclePhase.OPERATION);
metadata.recordValidation("Matched field data", "TEST-001");
// Calculate emissions (includes Expander power generation)
EmissionsReport report = process.getEmissions();
System.out.println(report.getSummary());
// With location-specific grid factor
EmissionsReport norwayReport = process.getEmissions(0.05);
// Export to JSON for external tools
report.exportToJSON("emissions.json");
String json = report.toJson();
// Generate what-if scenarios
AutomaticScenarioGenerator generator = new AutomaticScenarioGenerator(process);
generator.addFailureModes(FailureMode.COOLING_LOSS, FailureMode.COMPRESSOR_TRIP);
// Run all scenarios automatically
List<ScenarioRunResult> results = generator.runAllSingleFailures();
// Get execution summary
String summary = generator.summarizeResults(results);
System.out.println(summary);
// Or run manually
List<ProcessSafetyScenario> scenarios = generator.generateSingleFailures();
for (ProcessSafetyScenario scenario : scenarios) {
ProcessSystem copy = process.copy();
scenario.applyTo(copy);
copy.run();
// Analyze...
}
// Run parameter study with extended parameter paths
BatchStudy study = BatchStudy.builder(process)
.vary("compressor.outletPressure", 30.0, 80.0, 6)
.vary("heater.outletTemperature", 50.0, 150.0, 5)
.addObjective("power", Objective.MINIMIZE, p -> p.getTotalPower())
.addObjective("emissions", Objective.MINIMIZE, p -> p.getTotalCO2Emissions())
.parallelism(8)
.build();
BatchStudyResult result = study.run();
// Export results
result.exportToCSV("results.csv");
result.exportToJSON("results.json");
// Pareto front analysis
List<CaseResult> paretoFront = result.getParetoFront("power", "emissions");
// Register surrogate model
SurrogateModelRegistry.getInstance()
.register("flash-calc", myNeuralNetwork);
// Validate AI actions
PhysicsConstraintValidator validator = new PhysicsConstraintValidator(process);
ValidationResult check = validator.validate(aiProposedAction);
if (!check.isValid()) {
System.out.println("Rejected: " + check.getRejectionReason());
}
// Create prediction result
PredictionResult prediction = new PredictionResult(Duration.ofHours(2));
prediction.addPredictedValue("pressure", new PredictedValue(50.0, 2.5, "bara"));
if (prediction.hasViolations()) {
System.out.println(prediction.getAdvisoryRecommendation());
}
Detailed documentation for each module:
| Module | Documentation |
|---|---|
| Lifecycle Management | lifecycle/README.md |
| Sustainability | sustainability/README.md |
| Advisory Systems | advisory/README.md |
| ML Integration | ml/README.md |
| Safety Scenarios | safety/README.md |
| Batch Studies | optimization/README.md |
All new features are additive. Existing code continues to work unchanged.
Start with simple APIs, access advanced features when needed.
ML models include automatic fallback to physics calculations.
All AI actions are validated against physical constraints.
Emissions tracking is first-class, not an afterthought.
When extending these modules:
Quick reference for the future infrastructure APIs added to NeqSim.
New methods added directly to ProcessSystem for easy access:
// Export current state
ProcessSystemState state = process.exportState();
// Save state to file
process.exportStateToFile("checkpoint.json");
// Load and apply state from file
process.loadStateFromFile("checkpoint.json");
// Calculate emissions with default grid factor (0.4 kg CO2/kWh)
EmissionsReport report = process.getEmissions();
// Calculate emissions with custom grid factor
EmissionsReport norwayReport = process.getEmissions(0.05);
// Get total CO2 emissions directly (kg/hr)
double totalCO2 = process.getTotalCO2Emissions();
// Generate single-failure scenarios
List<ProcessSafetyScenario> scenarios = process.generateSafetyScenarios();
// Generate combination scenarios (up to n simultaneous failures)
List<ProcessSafetyScenario> combinations = process.generateCombinationScenarios(2);
// Create batch study builder
BatchStudy.Builder builder = process.createBatchStudy();
State snapshot for checkpointing and version control.
ProcessSystemState state = ProcessSystemState.fromProcessSystem(process);
state.setVersion("1.2.3");
state.setDescription("Post-tuning checkpoint");
state.setCreatedBy("engineer@company.com");
// Save to file
state.saveToFile("model_v1.2.3.json");
// Load from file
ProcessSystemState loaded = ProcessSystemState.loadFromFile("model_v1.2.3.json");
// Validate integrity
boolean valid = loaded.validateIntegrity();
String version = state.getVersion();
String name = state.getProcessName();
Instant timestamp = state.getTimestamp();
String json = state.toJson();
// Create new ProcessSystem from state
ProcessSystem restored = state.toProcessSystem();
// Apply state to existing ProcessSystem
state.applyTo(existingProcess);
Lifecycle and calibration tracking.
public enum LifecyclePhase {
CONCEPT, // Early screening
DESIGN, // Detailed engineering
COMMISSIONING, // Construction/startup
OPERATION, // Live digital twin
LATE_LIFE, // Decommissioning
ARCHIVED // No longer active
}
public enum CalibrationStatus {
UNCALIBRATED,
CALIBRATED,
IN_PROGRESS,
FRESHLY_CALIBRATED,
NEEDS_RECALIBRATION
}
ModelMetadata metadata = new ModelMetadata();
metadata.setAssetId("PLATFORM-A");
metadata.setAssetName("Gas Processing Platform A");
metadata.setLifecyclePhase(LifecyclePhase.OPERATION);
metadata.setResponsibleEngineer("jane.doe@company.com");
// Record validation
metadata.recordValidation("Matched well test", "TEST-001");
// Record modification
metadata.recordModification("Updated compressor curves");
// Update calibration
metadata.updateCalibration(CalibrationStatus.FRESHLY_CALIBRATED, 0.02);
// Check revalidation need
boolean needsRevalidation = metadata.needsRevalidation(90); // days
CO2 equivalent emissions tracking.
EmissionsTracker tracker = new EmissionsTracker(process);
tracker.setGridEmissionFactor(0.05); // kg CO2/kWh (Norway)
EmissionsReport report = tracker.calculateEmissions();
| Category | Description |
|---|---|
COMPRESSION |
Power consumed by compressors |
EXPANSION |
Power generated by expanders (negative) |
PUMPING |
Power consumed by pumps |
HEATING |
Power consumed by electric heaters |
COOLING |
Power consumed by coolers |
FLARING |
Direct CO2 from flaring |
VENTING |
Direct methane/CO2 emissions |
// Total emissions
double kgPerHr = report.getTotalCO2e("kg/hr");
double tonPerYr = report.getTotalCO2e("ton/yr");
// Power consumption
double kW = report.getTotalPower("kW");
double MW = report.getTotalPower("MW");
// Export
report.exportToCSV("emissions.csv");
report.exportToJSON("emissions.json");
String json = report.toJson(); // Get as JSON string
String summary = report.getSummary();
Look-ahead prediction output.
PredictionResult result = new PredictionResult(
Duration.ofHours(2), // horizon
"Base Case" // scenario name
);
result.addPredictedValue(
"separator.pressure",
new PredictedValue(52.5, 2.1, "bara") // mean, stddev, unit
);
// With standard deviation
PredictedValue value = new PredictedValue(50.0, 2.5, "bara");
// With explicit bounds
PredictedValue value = new PredictedValue(50.0, 45.0, 55.0, "bara", 0.95);
// Deterministic
PredictedValue value = PredictedValue.deterministic(50.0, "bara");
// Add violation
result.addViolation(new ConstraintViolation(...));
// Check for violations
if (result.hasViolations()) {
String summary = result.getViolationSummary();
String advice = result.getAdvisoryRecommendation();
}
public enum PredictionStatus {
SUCCESS,
WARNING,
FAILED,
DATA_QUALITY_ISSUE
}
result.setStatus(PredictionStatus.SUCCESS);
ML model management.
SurrogateModelRegistry registry = SurrogateModelRegistry.getInstance();
registry.register("flash-model", surrogateModel);
registry.register("flash-model", surrogateModel, metadata);
// Direct prediction
double[] result = registry.get("flash-model").orElseThrow().predict(input);
// With automatic fallback
double[] result = registry.predictWithFallback(
"flash-model",
input,
this::physicsCalculation
);
registry.saveModel("flash-model", "models/flash.ser");
registry.loadModel("flash-model", "models/flash.ser");
Optional<SurrogateMetadata> meta = registry.getMetadata("flash-model");
AI action validation.
PhysicsConstraintValidator validator = new PhysicsConstraintValidator(process);
validator.addPressureLimit("separator", 10.0, 80.0, "bara");
validator.addTemperatureLimit("heater-outlet", 0.0, 300.0, "C");
validator.addFlowLimit("feed", 0.0, 1000.0, "kg/hr");
validator.setMassBalanceTolerance(0.01); // 1%
validator.setEnergyBalanceTolerance(0.05); // 5%
validator.setEnforceMassBalance(true);
validator.setEnforceEnergyBalance(true);
Map<String, Double> proposedAction = new HashMap<>();
proposedAction.put("heater.duty", 5000000.0);
ValidationResult result = validator.validate(proposedAction);
if (result.isValid()) {
// Safe to apply
} else {
String reason = result.getRejectionReason();
List<ConstraintViolation> violations = result.getViolations();
}
// Validate current state
ValidationResult currentState = validator.validateCurrentState();
Safety scenario generation and execution.
AutomaticScenarioGenerator generator = new AutomaticScenarioGenerator(process);
// Add specific modes
generator.addFailureModes(
FailureMode.COOLING_LOSS,
FailureMode.VALVE_STUCK_CLOSED
);
// Or enable all
generator.enableAllFailureModes();
// Single failures
List<ProcessSafetyScenario> single = generator.generateSingleFailures();
// Combinations
List<ProcessSafetyScenario> combos = generator.generateCombinations(2);
// Run all single-failure scenarios
List<ScenarioRunResult> results = generator.runAllSingleFailures();
// Run specific scenarios
List<ScenarioRunResult> results = generator.runScenarios(scenarios);
// Get execution summary
String summary = generator.summarizeResults(results);
ScenarioRunResult result = results.get(0);
boolean success = result.isSuccessful();
String error = result.getErrorMessage();
Map<String, Double> values = result.getResultValues();
long timeMs = result.getExecutionTimeMs();
List<EquipmentFailure> failures = generator.getIdentifiedFailures();
String summary = generator.getFailureModeSummary();
Parallel parameter studies.
BatchStudy study = BatchStudy.builder(baseCase)
.name("ParameterStudy")
.vary("pressure", 20.0, 80.0, 7)
.vary("temperature", 50.0, 100.0, 5)
.addObjective("power", Objective.MINIMIZE, p -> getPower(p))
.addObjective("emissions", Objective.MINIMIZE, p -> getEmissions(p))
.parallelism(8)
.stopOnFailure(false)
.build();
| Property | Equipment Types |
|---|---|
duty |
Heater, Cooler |
outletPressure |
Valve, Compressor, Pump |
outletTemperature |
Heater, Cooler |
percentValveOpening, cv |
Valve |
polytropicEfficiency, isentropicEfficiency |
Compressor |
temperature, flowRate |
Stream |
internalDiameter |
Separator |
BatchStudyResult result = study.run();
int total = result.getTotalCases();
int completed = result.getCompletedCases();
int failed = result.getFailedCases();
Duration runtime = result.getTotalRuntime();
CaseResult best = result.getBestCase("power");
List<CaseResult> successful = result.getSuccessfulCases();
// Export results
result.exportToCSV("results.csv");
result.exportToJSON("results.json");
String json = result.toJson();
// Pareto front analysis
List<CaseResult> pareto = result.getParetoFront("power", "emissions");
| Package | Classes | Purpose |
|---|---|---|
lifecycle |
ProcessSystemState, ModelMetadata | State management, versioning |
sustainability |
EmissionsTracker, EmissionsReport | CO2e tracking |
advisory |
PredictionResult, PredictedValue | Look-ahead predictions |
ml.surrogate |
SurrogateModelRegistry, PhysicsConstraintValidator | ML integration |
safety.scenario |
AutomaticScenarioGenerator | Safety scenario generation |
util.optimization |
BatchStudy, BatchStudyResult | Parallel studies |
Documentation for integrating NeqSim with external systems and platforms.
This folder contains guides for integrating NeqSim with machine learning platforms, model predictive control systems, real-time data systems, and P&ID tools.
| Document | Description |
|---|---|
| ai_validation_framework.md | AI-friendly validation framework with structured error handling |
| ai_platform_integration.md | AI platform integration guide |
| ml_integration.md | Machine learning integration |
| Document | Description |
|---|---|
| mpc_integration.md | Model Predictive Control integration |
| neqsim_industrial_mpc_integration.md | Industrial MPC integration |
| Document | Description |
|---|---|
| REAL_TIME_INTEGRATION_GUIDE.md | Real-time systems integration |
| QRA_INTEGRATION_GUIDE.md | Quantitative Risk Assessment integration |
| Document | Description |
|---|---|
| dexpi-reader.md | DEXPI P&ID import/export and diagram generation |
This document describes the NeqSim extensions designed for integration with AI-based production optimization platforms and real-time digital twin systems.
Modern AI-based production optimization platforms typically require:
NeqSim provides dedicated packages to support these requirements.
Package: neqsim.process.streaming
The streaming package enables real-time data publishing from NeqSim simulations to external platforms.
Represents a value with timestamp, unit, and quality indicator.
import neqsim.process.streaming.TimestampedValue;
// Create a timestamped value
TimestampedValue value = new TimestampedValue(
100.5, // value
"bara", // unit
Instant.now(), // timestamp
TimestampedValue.Quality.GOOD // quality
);
// Access properties
double val = value.getValue();
String unit = value.getUnit();
Instant ts = value.getTimestamp();
TimestampedValue.Quality quality = value.getQuality();
Quality Levels:
GOOD - Normal measurementUNCERTAIN - Measurement with degraded confidenceBAD - Invalid or failed measurementSIMULATED - Value from simulationESTIMATED - Interpolated or estimated valuePublishes process data from a ProcessSystem with subscription support.
import neqsim.process.streaming.ProcessDataPublisher;
import neqsim.process.processmodel.ProcessSystem;
// Create publisher linked to process system
ProcessSystem process = new ProcessSystem();
// ... add equipment ...
ProcessDataPublisher publisher = new ProcessDataPublisher(process);
// Subscribe to updates
publisher.subscribeToUpdates("Inlet.pressure", value -> {
System.out.println("Pressure: " + value.getValue() + " " + value.getUnit());
});
// Publish current state
publisher.publishFromProcessSystem();
// Get state vector for ML models
double[] stateVector = publisher.getStateVector();
Interface for custom streaming implementations:
public interface StreamingDataInterface {
void subscribeToUpdates(String tagId, Consumer<TimestampedValue> callback);
void publishBatch(Map<String, TimestampedValue> values);
double[] getStateVector();
List<TimestampedValue> getHistory(String tagId, Duration period);
}
Package: neqsim.process.measurementdevice.vfm
Virtual Flow Meters calculate multiphase flow rates using thermodynamic models when physical meters are unavailable or unreliable.
import neqsim.process.measurementdevice.vfm.VirtualFlowMeter;
import neqsim.process.measurementdevice.vfm.VFMResult;
// Create VFM from a stream
StreamInterface wellStream = new Stream("Well-A", fluid);
wellStream.run();
VirtualFlowMeter vfm = new VirtualFlowMeter("VFM-Well-A", wellStream);
// Calculate flow rates with uncertainty
VFMResult result = vfm.calculate();
// Access results
double gasRate = result.getGasFlowRate(); // Sm3/day
double oilRate = result.getOilFlowRate(); // Sm3/day
double waterRate = result.getWaterFlowRate(); // Sm3/day
double gor = result.getGOR(); // Sm3/Sm3
double waterCut = result.getWaterCut(); // fraction
// Get uncertainties
UncertaintyBounds gasUncertainty = result.getGasFlowRateUncertainty();
double lower95 = gasUncertainty.getLower95();
double upper95 = gasUncertainty.getUpper95();
VFMs can be calibrated using well test data:
// Create calibration from well test
VFMCalibration calibration = new VFMCalibration();
calibration.setWellTestGasRate(50000); // Sm3/day
calibration.setWellTestOilRate(500); // Sm3/day
calibration.setWellTestWaterRate(100); // Sm3/day
calibration.setWellTestDate(Instant.now());
vfm.setCalibration(calibration);
VFMResult result = VFMResult.builder()
.gasFlowRate(45000)
.oilFlowRate(450)
.waterFlowRate(95)
.gasFlowRateUncertainty(new UncertaintyBounds(42000, 48000, 2000))
.timestamp(Instant.now())
.quality(VFMResult.Quality.GOOD)
.build();
Package: neqsim.process.measurementdevice.vfm
Soft sensors estimate unmeasured properties from available measurements using thermodynamic models.
import neqsim.process.measurementdevice.vfm.SoftSensor;
// Create soft sensor for GOR estimation
SoftSensor gorSensor = new SoftSensor("GOR-Sensor", stream, SoftSensor.PropertyType.GOR);
// Get estimated value
double gor = gorSensor.getMeasuredValue();
String unit = gorSensor.getUnit();
// Get sensitivity to input changes
double sensitivity = gorSensor.getSensitivity("pressure");
Available Property Types:
GOR - Gas-Oil RatioWATER_CUT - Water CutDENSITY - Fluid DensityVISCOSITY - Dynamic ViscosityMOLECULAR_WEIGHT - Molecular WeightZ_FACTOR - Compressibility FactorENTHALPY - Specific EnthalpyENTROPY - Specific EntropyHEAT_CAPACITY - Heat CapacityPackage: neqsim.process.util.uncertainty
Propagates measurement uncertainties through thermodynamic calculations.
import neqsim.process.util.uncertainty.UncertaintyAnalyzer;
import neqsim.process.util.uncertainty.UncertaintyResult;
// Create analyzer for a process system
ProcessSystem process = new ProcessSystem();
// ... configure process ...
UncertaintyAnalyzer analyzer = new UncertaintyAnalyzer(process);
// Define input uncertainties (relative)
analyzer.setInputUncertainty("inlet_pressure", 0.01); // 1%
analyzer.setInputUncertainty("inlet_temperature", 0.005); // 0.5%
analyzer.setInputUncertainty("inlet_flowrate", 0.02); // 2%
// Perform analytical (linear) uncertainty propagation
UncertaintyResult result = analyzer.analyzeAnalytical();
// Get output uncertainties
Map<String, Double> outputUncertainties = result.getOutputUncertainties();
// Get sensitivity matrix
SensitivityMatrix sensMatrix = result.getSensitivityMatrix();
double dP_dT = sensMatrix.getSensitivity("outlet_pressure", "inlet_temperature");
For nonlinear systems, use Monte Carlo:
// Configure Monte Carlo
analyzer.setMonteCarloSamples(10000);
// Run Monte Carlo uncertainty propagation
UncertaintyResult mcResult = analyzer.analyzeMonteCarlo(1000);
// Get percentiles
double p05 = mcResult.getPercentile("outlet_pressure", 0.05);
double p95 = mcResult.getPercentile("outlet_pressure", 0.95);
import neqsim.process.util.uncertainty.SensitivityMatrix;
// Get Jacobian matrix
SensitivityMatrix matrix = new SensitivityMatrix(inputNames, outputNames);
// Calculate sensitivities via finite differences
for (String input : inputNames) {
for (String output : outputNames) {
double sensitivity = calculateNumericalDerivative(input, output);
matrix.setSensitivity(output, input, sensitivity);
}
}
// Propagate input variances to output variances
Map<String, Double> inputVariances = Map.of("pressure", 0.01, "temperature", 0.0025);
Map<String, Double> outputVariances = matrix.propagateUncertainty(inputVariances);
Package: neqsim.process.integration.ml
Interfaces for combining physics models with machine learning corrections.
Wraps a NeqSim model with ML corrections:
import neqsim.process.integration.ml.HybridModelAdapter;
import neqsim.process.integration.ml.MLCorrectionInterface;
// Create hybrid adapter
ProcessSystem physicsModel = new ProcessSystem();
// ... configure model ...
HybridModelAdapter hybrid = new HybridModelAdapter(physicsModel);
// Add ML correction (implement MLCorrectionInterface)
MLCorrectionInterface mlCorrection = new MyNeuralNetworkCorrection();
hybrid.setCorrection(mlCorrection);
// Set combination strategy
hybrid.setCombinationStrategy(HybridModelAdapter.CombinationStrategy.ADDITIVE);
// Run hybrid model
hybrid.run();
// Get corrected outputs
double correctedPressure = hybrid.getCorrectedOutput("outlet_pressure");
Combination Strategies:
ADDITIVE - Output = Physics + ML_CorrectionMULTIPLICATIVE - Output = Physics × ML_FactorREPLACEMENT - Output = ML (physics as feature)WEIGHTED_AVERAGE - Output = w × Physics + (1-w) × MLImplement this interface to connect external ML models:
public interface MLCorrectionInterface {
// Get correction for a specific output
double getCorrection(String outputName, Map<String, Double> inputs);
// Update model with new training data
void update(Map<String, Double> inputs, Map<String, Double> targets);
// Get model confidence (0-1)
double getConfidence();
}
Extract features from streams for ML models:
import neqsim.process.integration.ml.FeatureExtractor;
// Create feature extractor
FeatureExtractor extractor = new FeatureExtractor();
// Extract features from a stream
StreamInterface stream = process.getStream("inlet");
double[] features = extractor.extractFeatures(stream);
// Get feature names
String[] featureNames = extractor.getFeatureNames();
// Normalize features
double[] normalized = extractor.normalize(features);
Package: neqsim.process.calibration
Continuously calibrates models using real-time data.
import neqsim.process.calibration.OnlineCalibrator;
import neqsim.process.calibration.CalibrationResult;
import neqsim.process.calibration.CalibrationQuality;
// Create calibrator for a process system
ProcessSystem process = new ProcessSystem();
OnlineCalibrator calibrator = new OnlineCalibrator(process);
// Configure tunable parameters
calibrator.setTunableParameters(Arrays.asList(
"separator_efficiency",
"heat_exchanger_UA",
"compressor_polytropic_efficiency"
));
// Set deviation threshold for triggering recalibration
calibrator.setDeviationThreshold(0.1); // 10%
// Record measurements and predictions
Map<String, Double> measurements = Map.of(
"outlet_pressure", 45.2,
"outlet_temperature", 35.5
);
Map<String, Double> predictions = Map.of(
"outlet_pressure", 44.8,
"outlet_temperature", 36.1
);
// Check if recalibration is needed
boolean needsRecalibration = calibrator.recordDataPoint(measurements, predictions);
// Perform incremental update (fast, for real-time)
CalibrationResult incrementalResult = calibrator.incrementalUpdate(measurements, predictions);
// Or perform full recalibration (thorough, periodic)
CalibrationResult fullResult = calibrator.fullRecalibration();
// Check calibration quality
CalibrationQuality quality = calibrator.getQualityMetrics();
System.out.println("Quality Score: " + quality.getOverallScore());
System.out.println("Rating: " + quality.getRating());
System.out.println("Needs Recalibration: " + quality.needsRecalibration());
CalibrationResult result = calibrator.fullRecalibration();
if (result.isSuccessful()) {
Map<String, Double> params = result.getCalibratedParameters();
double improvement = result.getImprovementPercent();
System.out.println("Improved by " + improvement + "%");
}
CalibrationQuality quality = calibrator.getQualityMetrics();
// Metrics
double rmse = quality.getRootMeanSquareError();
double r2 = quality.getR2Score();
int samples = quality.getSampleCount();
double coverage = quality.getCoveragePercent();
// Overall assessment
double score = quality.getOverallScore(); // 0-100
CalibrationQuality.Rating rating = quality.getRating(); // EXCELLENT, GOOD, FAIR, POOR
// Check calibration age
Duration age = quality.getCalibrationAge();
Package: neqsim.process.equipment.well.allocation
Allocates commingled production back to individual wells.
import neqsim.process.equipment.well.allocation.WellProductionAllocator;
import neqsim.process.equipment.well.allocation.AllocationResult;
// Create allocator
WellProductionAllocator allocator = new WellProductionAllocator("Field-A-Allocation");
// Add wells with test data
WellProductionAllocator.WellData wellA = allocator.addWell("Well-A");
wellA.setTestRates(500, 50000, 100); // oil, gas, water (Sm3/day)
wellA.setVFMRates(480, 48000, 95);
wellA.setChokePosition(0.75);
wellA.setProductivityIndex(10.0);
wellA.setReservoirPressure(250);
WellProductionAllocator.WellData wellB = allocator.addWell("Well-B");
wellB.setTestRates(300, 30000, 200);
wellB.setVFMRates(290, 29000, 195);
wellB.setChokePosition(0.60);
wellB.setProductivityIndex(8.0);
wellB.setReservoirPressure(245);
// Set allocation method
allocator.setAllocationMethod(WellProductionAllocator.AllocationMethod.VFM_BASED);
// Allocate total production
AllocationResult result = allocator.allocate(
780, // total oil (Sm3/day)
78000, // total gas (Sm3/day)
290 // total water (Sm3/day)
);
// Get allocated rates per well
double wellAOil = result.getOilRate("Well-A");
double wellAGas = result.getGasRate("Well-A");
double wellAGOR = result.getGOR("Well-A");
double wellAWC = result.getWaterCut("Well-A");
double uncertainty = result.getUncertainty("Well-A");
// Check allocation balance
boolean balanced = result.isBalanced();
double error = result.getAllocationError();
Allocation Methods:
WELL_TEST - Based on periodic well test dataVFM_BASED - Based on virtual flow meter estimatesCHOKE_MODEL - Based on choke performance curvesCOMBINED - Weighted combination of above methodsPackage: neqsim.process.util.event
Publish-subscribe system for process events.
import neqsim.process.util.event.ProcessEventBus;
import neqsim.process.util.event.ProcessEvent;
import neqsim.process.util.event.ProcessEventListener;
// Get event bus instance
ProcessEventBus eventBus = ProcessEventBus.getInstance();
// Subscribe to all events
eventBus.subscribe(event -> {
System.out.println("Event: " + event.getDescription());
});
// Subscribe to specific event types
eventBus.subscribe(ProcessEvent.EventType.ALARM, event -> {
// Handle alarm
sendAlarmNotification(event);
});
// Publish events
eventBus.publish(ProcessEvent.info("Compressor-1", "Startup complete"));
eventBus.publish(ProcessEvent.warning("Separator-1", "Level approaching high limit"));
eventBus.publish(ProcessEvent.alarm("Valve-V101", "Emergency shutdown activated"));
// Publish threshold crossing
eventBus.publish(ProcessEvent.thresholdCrossed(
"Pressure-PT101", "pressure", 52.5, 50.0, true // value, threshold, above
));
// Publish model deviation
eventBus.publish(ProcessEvent.modelDeviation(
"VFM-Well-A", "gas_rate", 48500, 50000 // measured, predicted
));
ProcessEvent event = ProcessEvent.alarm("Source", "Description");
// Set custom properties
event.setProperty("priority", 1);
event.setProperty("acknowledged", false);
event.setProperty("operator", "John");
// Get properties
int priority = event.getProperty("priority", Integer.class);
// Standard properties
String eventId = event.getEventId();
ProcessEvent.EventType type = event.getType();
String source = event.getSource();
Instant timestamp = event.getTimestamp();
ProcessEvent.Severity severity = event.getSeverity();
// Get recent events
List<ProcessEvent> recent = eventBus.getRecentEvents(100);
// Get events by type
List<ProcessEvent> alarms = eventBus.getEventsByType(ProcessEvent.EventType.ALARM, 50);
// Get events by severity
List<ProcessEvent> critical = eventBus.getEventsBySeverity(ProcessEvent.Severity.ERROR, 20);
Package: neqsim.process.util.export
Export simulation data for external analysis and ML training.
import neqsim.process.util.export.TimeSeriesExporter;
// Create exporter
ProcessSystem process = new ProcessSystem();
TimeSeriesExporter exporter = new TimeSeriesExporter(process);
// Collect snapshots during simulation
for (int step = 0; step < 1000; step++) {
process.run();
exporter.collectSnapshot();
Thread.sleep(1000); // 1 second intervals
}
// Export to JSON (AI platform format)
String json = exporter.exportToJson();
Files.writeString(Path.of("timeseries.json"), json);
// Export to CSV for ML training
String csv = exporter.exportToCsv();
Files.writeString(Path.of("training_data.csv"), csv);
// Export as feature matrix for ML
double[][] features = exporter.exportAsMatrix();
import neqsim.process.util.export.ProcessSnapshot;
// Create snapshot
ProcessSnapshot snapshot = new ProcessSnapshot("snap-001");
// Add measurements
snapshot.setMeasurement("inlet_pressure", 50.0, "bara");
snapshot.setMeasurement("inlet_temperature", 25.0, "C");
snapshot.setMeasurement("outlet_flowrate", 1000.0, "kg/hr");
// Serialize
String json = snapshot.toJson();
// Restore
ProcessSnapshot restored = ProcessSnapshot.fromJson(json);
Efficiently sync state changes:
import neqsim.process.util.export.ProcessDelta;
// Create delta between snapshots
ProcessSnapshot before = exporter.createSnapshot("before");
process.run();
ProcessSnapshot after = exporter.createSnapshot("after");
ProcessDelta delta = ProcessDelta.between(before, after);
// Get changes
Map<String, Double> changes = delta.getChangedValues();
double pressureChange = delta.getChange("outlet_pressure");
// Apply delta to another snapshot
ProcessSnapshot updated = delta.applyTo(before);
// Create process system
SystemInterface fluid = new SystemSrkEos(298.15, 50.0);
fluid.addComponent("methane", 0.85);
fluid.addComponent("ethane", 0.10);
fluid.addComponent("propane", 0.05);
fluid.setMixingRule("classic");
Stream inlet = new Stream("Inlet", fluid);
inlet.setFlowRate(10000, "kg/hr");
inlet.run();
ProcessSystem process = new ProcessSystem();
process.add(inlet);
// Setup streaming
ProcessDataPublisher publisher = new ProcessDataPublisher(process);
// Setup VFM
VirtualFlowMeter vfm = new VirtualFlowMeter("VFM-1", inlet);
// Setup online calibration
OnlineCalibrator calibrator = new OnlineCalibrator(process);
calibrator.setTunableParameters(Arrays.asList("efficiency"));
calibrator.setDeviationThreshold(0.05);
// Setup event bus
ProcessEventBus eventBus = ProcessEventBus.getInstance();
eventBus.subscribe(ProcessEvent.EventType.MODEL_DEVIATION, event -> {
// Trigger recalibration on significant deviation
if (calibrator.getQualityMetrics().needsRecalibration()) {
calibrator.fullRecalibration();
}
});
// Setup data export
TimeSeriesExporter exporter = new TimeSeriesExporter(process);
// Real-time loop
while (running) {
// Run simulation step
process.run();
// Publish streaming data
publisher.publishFromProcessSystem();
// Get VFM estimate
VFMResult vfmResult = vfm.calculate();
// Check for deviations
if (Math.abs(vfmResult.getGasFlowRate() - measuredGasRate) / measuredGasRate > 0.1) {
eventBus.publish(ProcessEvent.modelDeviation(
"VFM-1", "gas_rate", measuredGasRate, vfmResult.getGasFlowRate()
));
}
// Record for calibration
calibrator.recordDataPoint(measurements, predictions);
// Collect for export
exporter.collectSnapshot();
Thread.sleep(1000);
}
// Export training data
String trainingData = exporter.exportToCsv();
setMaxHistorySize()ProcessDataPublisher uses ConcurrentHashMap and CopyOnWriteArrayListProcessEventBus supports async event deliveryOnlineCalibrator history is synchronizedSerializable for persistenceFor integration with AI-based production optimization platforms:
ProcessDataPublisher to stream real-time dataTimeSeriesExporter in JSON formatMLCorrectionInterface to connect external ML modelsHybridModelAdapter to combine physics with ML correctionsProcessEventBus for real-time alerts and triggersThis document describes the AI-friendly validation and integration framework added to NeqSim. The framework provides structured validation, error remediation hints, and API discovery for AI/ML agents working with NeqSim thermodynamic simulations.
neqsim.util.validation/
├── ValidationResult.java # Structured validation container
├── SimulationValidator.java # Static validation facade
├── AIIntegrationHelper.java # Unified AI entry point
└── contracts/
├── ModuleContract.java # Base contract interface
├── ThermodynamicSystemContract.java
├── StreamContract.java
├── SeparatorContract.java
└── ProcessSystemContract.java
neqsim.util.annotation/
├── AIExposable.java # Method discovery annotation
├── AIParameter.java # Parameter documentation annotation
└── AISchemaDiscovery.java # Reflection-based API discovery
A structured container for validation issues with severity levels:
ValidationResult result = SimulationValidator.validate(fluid);
if (!result.isValid()) {
System.out.println(result.getReport()); // Human-readable
for (ValidationIssue issue : result.getIssues()) {
String fix = issue.getRemediation(); // AI-parseable hint
}
}
Severity Levels:
CRITICAL - Blocks executionMAJOR - Likely to cause errorsMINOR - May affect resultsINFO - Informational onlyStatic facade for validating any NeqSim object:
// Validate any object
ValidationResult result = SimulationValidator.validate(object);
// Validate outputs after execution
ValidationResult postRun = SimulationValidator.validateOutput(process);
// Combined validate-and-run
ValidationResult combined = SimulationValidator.validateAndRun(stream);
Pre/post-condition checking for specific NeqSim types:
ThermodynamicSystemContract contract = ThermodynamicSystemContract.getInstance();
ValidationResult pre = contract.checkPreconditions(system);
ValidationResult post = contract.checkPostconditions(system);
Available Contracts:
ThermodynamicSystemContract - Validates SystemInterfaceStreamContract - Validates StreamInterfaceSeparatorContract - Validates Separator equipmentProcessSystemContract - Validates ProcessSystemAll process equipment classes implement validateSetup() to check equipment-specific configuration:
// Validate individual equipment
Separator separator = new Separator("V-100");
ValidationResult result = separator.validateSetup();
if (!result.isValid()) {
System.out.println("Configuration issues:");
result.getErrors().forEach(System.out::println);
}
Equipment Validation Checks:
| Equipment | Validations |
|---|---|
| Stream | Fluid set, temperature > 0 K |
| Separator | Inlet stream connected |
| Mixer | At least one inlet stream added |
| Splitter | Inlet stream connected, split fractions sum to 1.0 |
| Tank | Has fluid or input stream connected |
| DistillationColumn | Feed streams connected, condenser/reboiler configured |
| Recycle | Inlet and outlet streams set, tolerance > 0 |
| Adjuster | Target and adjustment variables set, tolerance > 0 |
| TwoPortEquipment | Inlet stream connected |
ProcessSystem provides aggregate validation across all equipment:
ProcessSystem process = new ProcessSystem();
process.add(feed);
process.add(separator);
process.add(compressor);
// Quick check before running
if (process.isReadyToRun()) {
process.run();
} else {
// Get combined validation result
ValidationResult result = process.validateSetup();
System.out.println(result.getReport());
}
// Get per-equipment validation
Map<String, ValidationResult> allResults = process.validateAll();
for (Map.Entry<String, ValidationResult> entry : allResults.entrySet()) {
if (!entry.getValue().isValid()) {
System.out.println(entry.getKey() + ": " + entry.getValue().getErrors());
}
}
ProcessSystem Validation Methods:
| Method | Returns | Description |
|---|---|---|
validateSetup() |
ValidationResult |
Combined result for all equipment |
validateAll() |
Map<String, ValidationResult> |
Per-equipment results |
isReadyToRun() |
boolean |
True if no CRITICAL errors |
ProcessModel provides aggregate validation across all contained ProcessSystems:
ProcessModel model = new ProcessModel();
model.add("GasProcessing", gasProcess);
model.add("OilProcessing", oilProcess);
// Quick check before running
if (model.isReadyToRun()) {
model.run();
} else {
// Get formatted validation report
System.out.println(model.getValidationReport());
}
// Get per-process validation
Map<String, ValidationResult> allResults = model.validateAll();
for (Map.Entry<String, ValidationResult> entry : allResults.entrySet()) {
if (!entry.getValue().isValid()) {
System.out.println(entry.getKey() + ": " + entry.getValue().getErrors());
}
}
ProcessModel Validation Methods:
| Method | Returns | Description |
|---|---|---|
validateSetup() |
ValidationResult |
Combined result for all processes |
validateAll() |
Map<String, ValidationResult> |
Per-process results |
isReadyToRun() |
boolean |
True if no CRITICAL errors |
getValidationReport() |
String |
Human-readable formatted report |
Unified entry point connecting validation with RL/ML infrastructure:
AIIntegrationHelper helper = AIIntegrationHelper.forProcess(process);
// Check readiness
if (helper.isReady()) {
ExecutionResult result = helper.safeRun();
System.out.println(result.toAIReport());
}
// Get API documentation for agent
String docs = helper.getAPIDocumentation();
// Create RL environment
RLEnvironment env = helper.createRLEnvironment();
Annotations for exposing methods to AI agents:
@AIExposable(
description = "Add a chemical component to the system",
category = "composition",
example = "addComponent(\"methane\", 0.9)",
priority = 100,
safe = false
)
public void addComponent(
@AIParameter(name = "name", description = "Component name") String name,
@AIParameter(name = "moles", minValue = 0.0, maxValue = 1.0) double moles
) { ... }
Discovers annotated methods via reflection:
AISchemaDiscovery discovery = new AISchemaDiscovery();
// Discover methods in a class
List<MethodSchema> methods = discovery.discoverMethods(SystemSrkEos.class);
// Generate prompt for AI
String prompt = discovery.generateMethodPrompt(methods);
// Get quick-start documentation
String quickStart = discovery.getQuickStartPrompt();
The framework integrates with NeqSim's existing ML capabilities:
| Component | Package | Integration |
|---|---|---|
| RLEnvironment | neqsim.process.ml |
Create from AIIntegrationHelper |
| GymEnvironment | neqsim.process.ml |
Compatible state/action vectors |
| ProcessLinkedMPC | neqsim.process.mpc |
Validate MPC process systems |
| ProductionOptimizer | neqsim.process.util.optimization |
Validate optimizer inputs |
| SurrogateModelRegistry | neqsim.process.ml.surrogate |
Physics constraint checking |
Enhanced exceptions with remediation hints:
try {
process.run();
} catch (InvalidInputException e) {
String fix = e.getRemediation(); // "Provide valid values: ..."
} catch (TooManyIterationsException e) {
String fix = e.getRemediation(); // "Increase max iterations or adjust initial estimate"
int tried = e.getMaxIterations();
}
Enhanced Exceptions:
InvalidInputException - Lists valid optionsTooManyIterationsException - Suggests convergence fixesIsNaNException - Identifies the problematic parameterInvalidOutputException - Describes expected output typeNotInitializedException - Lists required initialization steps// Create fluid
SystemInterface fluid = new SystemSrkEos(298.15, 10.0);
fluid.addComponent("methane", 0.9);
fluid.addComponent("ethane", 0.1);
fluid.setMixingRule("classic");
// Validate before flash
ValidationResult result = SimulationValidator.validate(fluid);
if (!result.isValid()) {
System.out.println("Issues found:");
System.out.println(result.getReport());
}
ProcessSystem process = new ProcessSystem();
process.add(feed);
process.add(separator);
process.add(compressor);
AIIntegrationHelper helper = AIIntegrationHelper.forProcess(process);
if (helper.isReady()) {
ExecutionResult result = helper.safeRun();
if (result.isSuccess()) {
// Process ran successfully
}
} else {
// Get structured issues for AI to fix
String[] issues = helper.getIssuesAsText();
for (String issue : issues) {
System.out.println(issue);
}
}
ProcessSystem process = buildProcess();
AIIntegrationHelper helper = AIIntegrationHelper.forProcess(process);
// Validate before creating RL environment
if (helper.isReady()) {
RLEnvironment env = helper.createRLEnvironment();
// RL training loop
StateVector obs = env.reset();
while (!done) {
ActionVector action = agent.selectAction(obs);
RLEnvironment.StepResult result = env.step(action);
obs = result.observation;
done = result.done;
}
}
All components have comprehensive unit tests:
| Test Class | Tests | Coverage |
|---|---|---|
| ValidationResultTest | 13 | Core validation logic |
| SimulationValidatorTest | 16 | Static facade methods |
| ModuleContractTest | 14 | Contract implementations |
| AISchemaDiscoveryTest | 13 | Annotation discovery |
| AIIntegrationHelperTest | 15 | Integration helper |
| EquipmentValidationTest | 41 | Equipment and ProcessModel validateSetup() methods |
Run tests:
./mvnw test -Dtest="ValidationResultTest,SimulationValidatorTest,ModuleContractTest,AISchemaDiscoveryTest,AIIntegrationHelperTest,EquipmentValidationTest"
The chemicalreactions package provides tools for chemical equilibrium calculations and reaction kinetics.
Location: neqsim.chemicalreactions
Purpose:
chemicalreactions/
├── ChemicalReactionOperations.java # Main operations class
│
├── chemicalequilibrium/ # Equilibrium calculations
│ ├── ChemicalEquilibrium.java # Base class
│ ├── ChemEq.java # Equilibrium solver
│ ├── LinearProgrammingChemicalEquilibrium.java
│ └── ReferencePotComparator.java
│
├── chemicalreaction/ # Reaction definitions
│ ├── ChemicalReaction.java # Single reaction
│ └── ChemicalReactionList.java # Reaction set
│
└── kinetics/ # Kinetics models
└── Kinetics.java # Kinetic rate calculations
Chemical equilibrium is achieved when the Gibbs energy is minimized:
$$\min G = \sum_i n_i \mu_i$$
Subject to element balance constraints:
$$\sum_i a_{ji} n_i = b_j \quad \text{for each element } j$$
Where:
import neqsim.thermo.system.SystemSrkEos;
import neqsim.thermodynamicoperations.ThermodynamicOperations;
// Create reactive system
SystemInterface reactive = new SystemSrkEos(700.0, 10.0);
reactive.addComponent("methane", 1.0);
reactive.addComponent("water", 2.0);
reactive.addComponent("CO2", 0.0);
reactive.addComponent("CO", 0.0);
reactive.addComponent("hydrogen", 0.0);
reactive.setMixingRule("classic");
// Enable chemical reactions
reactive.setChemicalReactions(true);
// Perform equilibrium calculation
ThermodynamicOperations ops = new ThermodynamicOperations(reactive);
ops.calcChemicalEquilibrium();
// Display results
for (int i = 0; i < reactive.getNumberOfComponents(); i++) {
System.out.println(reactive.getComponent(i).getName() +
": " + reactive.getComponent(i).getNumberOfmable() + " mol");
}
CH₄ + H₂O ⇌ CO + 3H₂
CO + H₂O ⇌ CO₂ + H₂
CH₄ + 2O₂ → CO₂ + 2H₂O
C₂H₆ + 3.5O₂ → 2CO₂ + 3H₂O
CO₂ + H₂O ⇌ H₂CO₃
H₂S + H₂O ⇌ HS⁻ + H₃O⁺
NH₃ + H₂O ⇌ NH₄⁺ + OH⁻
CO₂ + 2RNH₂ ⇌ RNHCOO⁻ + RNH₃⁺
CO₂ + RNH₂ + H₂O ⇌ RNH₃⁺ + HCO₃⁻
import neqsim.chemicalreactions.ChemicalReactionOperations;
ChemicalReactionOperations reactionOps = new ChemicalReactionOperations(fluid);
// Add reactions
reactionOps.addReaction("methane_reforming");
reactionOps.addReaction("water_gas_shift");
// Calculate equilibrium
reactionOps.calcChemicalEquilibrium();
// Get equilibrium constants
double Keq = reactionOps.getEquilibriumConstant("methane_reforming");
For rate-limited reactions, use kinetic models.
General rate expression:
$$r = k \cdot \prod_i C_i^{n_i}$$
Where:
$$k = A \cdot \exp\left(-\frac{E_a}{RT}\right)$$
Where:
import neqsim.chemicalreactions.kinetics.Kinetics;
Kinetics kinetics = new Kinetics(fluid);
// Set reaction parameters
kinetics.setPreExponentialFactor(1.0e10); // 1/s
kinetics.setActivationEnergy(80000.0); // J/mol
// Calculate rate at current conditions
double rate = kinetics.getReactionRate();
Combine phase equilibrium with chemical equilibrium.
// Set up reactive system
SystemInterface fluid = new SystemSrkEos(500.0, 20.0);
fluid.addComponent("methane", 1.0);
fluid.addComponent("oxygen", 0.5);
fluid.addComponent("CO2", 0.0);
fluid.addComponent("water", 0.0);
fluid.setMixingRule("classic");
fluid.setChemicalReactions(true);
// Reactive TP flash
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
// Results include both phase and chemical equilibrium
System.out.println("Number of phases: " + fluid.getNumberOfPhases());
for (int i = 0; i < fluid.getNumberOfComponents(); i++) {
System.out.println(fluid.getComponent(i).getName() +
": " + fluid.getComponent(i).getz() + " mol/mol");
}
For complex systems, use linear programming approach.
import neqsim.chemicalreactions.chemicalequilibrium.LinearProgrammingChemicalEquilibrium;
LinearProgrammingChemicalEquilibrium lpEquil =
new LinearProgrammingChemicalEquilibrium(fluid);
lpEquil.solve();
// Get equilibrium composition
double[] composition = lpEquil.getEquilibriumComposition();
Chemical reactions and their parameters are stored in the database.
| Field | Description |
|---|---|
| name | Reaction identifier |
| reactants | Reactant species |
| products | Product species |
| stoichiometry | Stoichiometric coefficients |
| deltaH | Enthalpy of reaction |
| deltaG | Gibbs energy of reaction |
| Keq_A, Keq_B, Keq_C | Equilibrium constant correlation |
// Load reactions from database
fluid.createChemicalReactions(true);
// Or specify specific reactions
fluid.addChemicalReaction("steam_reforming");
fluid.addChemicalReaction("water_gas_shift");
// Ammonia synthesis: N₂ + 3H₂ ⇌ 2NH₃
SystemInterface syngas = new SystemSrkEos(673.15, 200.0); // 400°C, 200 bar
syngas.addComponent("nitrogen", 1.0);
syngas.addComponent("hydrogen", 3.0);
syngas.addComponent("ammonia", 0.0);
syngas.setMixingRule("classic");
syngas.setChemicalReactions(true);
ThermodynamicOperations ops = new ThermodynamicOperations(syngas);
ops.calcChemicalEquilibrium();
double NH3fraction = syngas.getComponent("ammonia").getz();
double conversion = 2 * NH3fraction /
(syngas.getComponent("nitrogen").getz() + NH3fraction);
System.out.println("NH₃ mole fraction: " + NH3fraction);
System.out.println("N₂ conversion: " + (conversion * 100) + "%");
// CO₂ absorption in MEA solution
SystemInterface solution = new SystemElectrolyteCPA(313.15, 1.01325);
solution.addComponent("CO2", 0.05);
solution.addComponent("water", 0.75);
solution.addComponent("MEA", 0.20);
solution.setMixingRule("CPA_Statoil");
solution.setChemicalReactions(true);
// Flash with reactions
ThermodynamicOperations ops = new ThermodynamicOperations(solution);
ops.TPflash();
// Get CO₂ loading
double CO2loading = solution.getComponent("CO2").getx() /
solution.getComponent("MEA").getx();
System.out.println("CO₂ loading: " + CO2loading + " mol CO₂/mol MEA");
The NeqSim statistics package provides tools for parameter fitting, uncertainty quantification, and data analysis for thermodynamic model development and validation.
The statistics package supports:
Location: neqsim.statistics
Key Applications:
statistics/
├── parameterfitting/ # Core parameter fitting framework
│ ├── StatisticsBaseClass.java # Abstract base for all fitting
│ ├── StatisticsInterface.java # Interface definition
│ ├── SampleSet.java # Collection of experimental points
│ ├── SampleValue.java # Single experimental data point
│ ├── BaseFunction.java # Abstract objective function
│ ├── FunctionInterface.java # Function interface
│ ├── NumericalDerivative.java # Numerical differentiation
│ └── nonlinearparameterfitting/ # Nonlinear optimization
│ ├── LevenbergMarquardt.java # L-M optimizer (least squares)
│ ├── LevenbergMarquardtAbsDev.java # Absolute deviation
│ ├── LevenbergMarquardtBiasDev.java # Bias deviation
│ └── LevenbergMarquardtFunction.java # Example function
│
├── montecarlosimulation/ # Uncertainty quantification
│ └── MonteCarloSimulation.java # MC simulation runner
│
├── dataanalysis/ # Data processing
│ └── datasmoothing/
│ └── DataSmoother.java # Savitzky-Golay smoothing
│
├── experimentalsamplecreation/ # Sample generation
│ ├── readdatafromfile/ # File I/O for experimental data
│ └── samplecreator/
│ ├── SampleCreator.java # Base sample creator
│ └── wettedwallcolumnsamplecreator/ # Specialized creator
│
└── experimentalequipmentdata/ # Equipment modeling
├── ExperimentalEquipmentData.java
└── wettedwallcolumndata/ # Wetted wall column
Detailed guides for each major subsystem:
| Guide | Description |
|---|---|
| Parameter Fitting | Levenberg-Marquardt optimization, creating objective functions, bounds |
| Monte Carlo Simulation | Uncertainty propagation, confidence intervals, distribution sampling |
| Data Analysis | Data smoothing, filtering, statistical measures |
A SampleValue represents one experimental data point:
// Experimental value with uncertainty
double experimentalValue = 0.5; // Measured value (e.g., pressure)
double standardDeviation = 0.05; // Experimental uncertainty
double[] independentVariables = {300.0, 0.1}; // e.g., temperature, composition
SampleValue sample = new SampleValue(
experimentalValue,
standardDeviation,
independentVariables
);
A SampleSet is a collection of experimental points:
SampleSet sampleSet = new SampleSet();
sampleSet.add(sample1);
sampleSet.add(sample2);
sampleSet.add(sample3);
// Or from array
SampleValue[] samples = {sample1, sample2, sample3};
SampleSet sampleSet = new SampleSet(samples);
Functions extend BaseFunction or LevenbergMarquardtFunction:
public class MyObjectiveFunction extends LevenbergMarquardtFunction {
@Override
public double calcValue(double[] dependentValues) {
// params[0], params[1], ... are the fitting parameters
// dependentValues are the independent variables (T, P, x, ...)
double T = dependentValues[0];
double x = dependentValues[1];
// Calculate model prediction
double predicted = params[0] * Math.exp(-params[1] / T) * x;
return predicted;
}
@Override
public void setFittingParams(int i, double value) {
params[i] = value;
}
}
import neqsim.statistics.parameterfitting.*;
import neqsim.statistics.parameterfitting.nonlinearparameterfitting.*;
import java.util.ArrayList;
// 1. Create objective function
MyObjectiveFunction function = new MyObjectiveFunction();
// 2. Set initial parameter guess
double[] initialGuess = {1.0, 500.0}; // Two parameters to fit
function.setInitialGuess(initialGuess);
// 3. Create experimental samples
ArrayList<SampleValue> samples = new ArrayList<>();
double[] x1 = {300.0, 0.1}; // T=300K, x=0.1
SampleValue s1 = new SampleValue(0.05, 0.005, x1); // exp=0.05 ± 0.005
s1.setFunction(function);
samples.add(s1);
double[] x2 = {350.0, 0.2};
SampleValue s2 = new SampleValue(0.12, 0.01, x2);
s2.setFunction(function);
samples.add(s2);
// Add more samples...
// 4. Create sample set and optimizer
SampleSet sampleSet = new SampleSet(samples);
LevenbergMarquardt optimizer = new LevenbergMarquardt();
optimizer.setSampleSet(sampleSet);
// 5. Solve
optimizer.solve();
// 6. Get results
double[] fittedParams = sampleSet.getSample(0).getFunction().getFittingParams();
System.out.println("Fitted parameters: " + Arrays.toString(fittedParams));
// 7. Display results
optimizer.displayCurveFit();
optimizer.displayResult();
// After solving...
optimizer.runMonteCarloSimulation(100); // 100 Monte Carlo runs
// This generates samples with normally distributed perturbations
// around experimental values and re-fits, providing parameter distributions
public class KijFittingFunction extends LevenbergMarquardtFunction {
@Override
public double calcValue(double[] dependentValues) {
double temperature = dependentValues[0];
double pressure = dependentValues[1];
double x_exp = dependentValues[2]; // Experimental composition
// Set up thermodynamic system
system.setTemperature(temperature);
system.setPressure(pressure);
// Set kij from fitting parameters
((PhaseEos) system.getPhase(0)).getMixingRule()
.setBinaryInteractionParameter(0, 1, params[0]);
((PhaseEos) system.getPhase(1)).getMixingRule()
.setBinaryInteractionParameter(0, 1, params[0]);
// Flash calculation
thermoOps.TPflash();
// Return calculated liquid composition
return system.getPhase(1).getComponent(0).getx();
}
@Override
public void setFittingParams(int i, double value) {
params[i] = value;
}
}
public class CPAFittingFunction extends LevenbergMarquardtFunction {
@Override
public double calcValue(double[] dependentValues) {
double T = dependentValues[0];
double P = dependentValues[1];
// params[0] = epsilon (association energy)
// params[1] = beta (association volume)
system.getComponent("water").setAssociationEnergy(params[0]);
system.getComponent("water").setAssociationVolume(params[1]);
thermoOps.TPflash();
return system.getPhase(1).getDensity("kg/m3");
}
@Override
public void setFittingParams(int i, double value) {
params[i] = value;
}
}
After fitting, several statistics are available:
// Solve first
optimizer.solve();
// Chi-square statistic
double chiSquare = optimizer.calcChiSquare();
// Absolute deviation statistics
optimizer.calcAbsDev();
// Covariance matrix
optimizer.calcCoVarianceMatrix();
// Parameter standard deviations
optimizer.calcParameterStandardDeviation();
// Parameter correlation matrix
optimizer.calcCorrelationMatrix();
// 95% confidence intervals
optimizer.calcParameterUncertainty();
Good initial guesses are critical for convergence:
Proper uncertainty specification affects:
Set physical bounds to prevent unphysical solutions:
double[][] bounds = {
{0.0, 1.0}, // Parameter 0: between 0 and 1
{100.0, 1000.0} // Parameter 1: between 100 and 1000
};
function.setBounds(bounds);
optimizer.setMaxNumberOfIterations(100); // Increase if needed
The util package provides common utilities for database access, unit conversion, serialization, exceptions, and threading.
Location: neqsim.util
Purpose:
util/
├── NamedBaseClass.java # Base class with name property
├── NamedInterface.java # Named interface
├── NeqSimLogging.java # Logging utilities
├── NeqSimThreadPool.java # Thread pool management
├── ExcludeFromJacocoGeneratedReport.java
│
├── database/ # Database access
│ ├── NeqSimDataBase.java # Main database class
│ ├── NeqSimContractDataBase.java
│ ├── NeqSimExperimentDatabase.java
│ └── NeqSimFluidDataBase.java
│
├── exception/ # Custom exceptions
│ ├── InvalidInputException.java
│ ├── ThermoException.java
│ └── NotImplementedException.java
│
├── generator/ # Code generation
│ └── PropertyGenerator.java
│
├── manifest/ # Manifest handling
│ └── ManifestHandler.java
│
├── python/ # Python integration
│ └── PythonIntegration.java
│
├── serialization/ # Serialization utilities
│ └── SerializationManager.java
│
├── unit/ # Unit conversion
│ ├── Units.java
│ └── UnitConverter.java
│
└── util/ # General utilities
└── Utilities.java
Main class for database connectivity.
import neqsim.util.database.NeqSimDataBase;
// Get database connection
try (NeqSimDataBase db = new NeqSimDataBase()) {
// Execute query
ResultSet rs = db.getResultSet(
"SELECT * FROM comp WHERE compname = 'methane'"
);
while (rs.next()) {
double Tc = rs.getDouble("TC");
double Pc = rs.getDouble("PC");
double omega = rs.getDouble("ACF");
}
}
// Set database path (for embedded Derby)
NeqSimDataBase.setDataBaseType("Derby");
NeqSimDataBase.setConnectionString("jdbc:derby:NeqSimDatabase");
// Or use PostgreSQL
NeqSimDataBase.setDataBaseType("PostgreSQL");
NeqSimDataBase.setConnectionString("jdbc:postgresql://localhost:5432/neqsim");
NeqSimDataBase.setUsername("user");
NeqSimDataBase.setPassword("password");
// Component data
NeqSimFluidDataBase.getComponentData("methane");
// Binary interaction parameters
NeqSimFluidDataBase.getInteractionParameters("methane", "ethane", "SRK");
// Experiment data
NeqSimExperimentDatabase.getExperimentData("VLE_CH4_CO2");
For comprehensive unit conversion documentation, see Unit Conversion Guide.
import neqsim.util.unit.Units;
import neqsim.util.unit.PressureUnit;
import neqsim.util.unit.TemperatureUnit;
// Direct unit conversion
PressureUnit pu = new PressureUnit(50.0, "bara");
double p_psia = pu.getValue("psia");
TemperatureUnit tu = new TemperatureUnit(25.0, "C");
double t_K = tu.getValue("K");
// In fluid properties
double T_C = fluid.getTemperature("C");
fluid.setTemperature(25.0, "C");
double P_bara = fluid.getPressure("bara");
fluid.setPressure(50.0, "bara");
double flow = stream.getFlowRate("kg/hr");
stream.setFlowRate(1000.0, "kg/hr");
| Property | Units |
|---|---|
| Temperature | K, C, F, R |
| Pressure | Pa, bara, barg, psia, psig, atm, mmHg, kPa, MPa |
| Flow rate | kg/s, kg/hr, lb/hr, Sm3/hr, MSm3/day, mol/s, kmol/hr |
| Volume | m3, L, ft3, bbl, gal |
| Density | kg/m3, g/cm3, lb/ft3 |
| Viscosity | Pa.s, cP, mPa.s |
| Energy | J, kJ, MJ, cal, BTU |
| Power | W, kW, MW, hp |
// Enthalpy double H_kJ = fluid.getEnthalpy("kJ/kg");
---
## Serialization
NeqSim provides multiple serialization options for saving and loading simulations.
### Process System Serialization
```java
// Save process system to compressed .neqsim file
ProcessSystem process = new ProcessSystem("My Process");
// ... add equipment ...
process.saveToNeqsim("myprocess.neqsim");
// Load from file (auto-runs after loading)
ProcessSystem loaded = ProcessSystem.loadFromNeqsim("myprocess.neqsim");
// Auto-detect format by extension
process.saveAuto("myprocess.neqsim"); // Compressed
process.saveAuto("myprocess.json"); // JSON state
// Save ProcessModel containing multiple ProcessSystems
ProcessModel model = new ProcessModel();
model.add("upstream", upstreamProcess);
model.add("downstream", downstreamProcess);
model.saveToNeqsim("field_model.neqsim");
// Load (auto-runs after loading)
ProcessModel loaded = ProcessModel.loadFromNeqsim("field_model.neqsim");
// Export to Git-friendly JSON format
ProcessSystemState state = ProcessSystemState.fromProcessSystem(process);
state.setVersion("1.0.0");
state.saveToFile("process_v1.0.0.json");
// Load and validate
ProcessSystemState loaded = ProcessSystemState.loadFromFile("process_v1.0.0.json");
if (loaded.validate().isValid()) {
ProcessSystem restored = loaded.toProcessSystem();
}
// Clone using serialization (deep copy)
SystemInterface clone = fluid.clone();
// Or for process equipment
ProcessEquipmentInterface copy = equipment.copy();
For full documentation: See Process Serialization Guide
import neqsim.util.exception.*;
// Invalid input
if (temperature < 0) {
throw new InvalidInputException("Temperature",
"Temperature must be positive");
}
// Thermodynamic calculation failure
try {
ops.TPflash();
} catch (ThermoException e) {
System.err.println("Flash calculation failed: " + e.getMessage());
}
// Not implemented feature
throw new NotImplementedException("This feature",
"Will be available in next release");
try {
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
} catch (InvalidInputException e) {
// Handle invalid inputs
logger.error("Invalid input: " + e.getMessage());
} catch (ThermoException e) {
// Handle calculation failures
logger.error("Calculation failed: " + e.getMessage());
} catch (Exception e) {
// Handle unexpected errors
logger.error("Unexpected error", e);
}
Manage parallel calculations.
import neqsim.util.NeqSimThreadPool;
// Configure thread pool
NeqSimThreadPool.setNumberOfThreads(8);
// Submit tasks
Future<Double> result1 = NeqSimThreadPool.submit(() -> {
// Parallel calculation
return calculateProperty1();
});
Future<Double> result2 = NeqSimThreadPool.submit(() -> {
return calculateProperty2();
});
// Get results
double prop1 = result1.get();
double prop2 = result2.get();
// Run multiple flashes in parallel
List<SystemInterface> fluids = prepareFluids();
List<Future<SystemInterface>> futures = fluids.stream()
.map(f -> NeqSimThreadPool.submit(() -> {
ThermodynamicOperations ops = new ThermodynamicOperations(f);
ops.TPflash();
return f;
}))
.collect(Collectors.toList());
// Collect results
for (Future<SystemInterface> future : futures) {
SystemInterface result = future.get();
// Process result
}
Configure logging.
import neqsim.util.NeqSimLogging;
// Set log level
NeqSimLogging.setLogLevel(Level.DEBUG);
// Log messages
NeqSimLogging.info("Process started");
NeqSimLogging.debug("Temperature: " + T);
NeqSimLogging.error("Calculation failed", exception);
NeqSim uses Log4j2. Configure via log4j2.xml:
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
</Root>
<Logger name="neqsim" level="debug"/>
</Loggers>
</Configuration>
import jpype
import jpype.imports
from jpype.types import *
# Start JVM
jpype.startJVM(classpath=['neqsim.jar'])
from neqsim.thermo.system import SystemSrkEos
from neqsim.thermodynamicoperations import ThermodynamicOperations
# Create fluid
fluid = SystemSrkEos(300.0, 50.0)
fluid.addComponent("methane", 0.9)
fluid.addComponent("ethane", 0.1)
fluid.setMixingRule("classic")
# Flash
ops = ThermodynamicOperations(fluid)
ops.TPflash()
print(f"Density: {fluid.getDensity('kg/m3'):.2f} kg/m³")
Base class for named objects.
public class MyEquipment extends NamedBaseClass {
public MyEquipment(String name) {
super(name);
}
}
// Usage
MyEquipment eq = new MyEquipment("E-100");
String name = eq.getName();
eq.setName("E-101");
The mathlib package provides mathematical utilities, nonlinear solvers, and numerical methods.
Location: neqsim.mathlib
Purpose:
mathlib/
├── generalmath/ # General mathematical utilities
│ ├── GeneralMath.java # Common math functions
│ ├── TDMAsolve.java # Tridiagonal matrix solver
│ └── SplineInterpolation.java # Spline interpolation
│
└── nonlinearsolver/ # Nonlinear equation solvers
├── NonLinearSolver.java # Base solver
├── NewtonRaphson.java # Newton-Raphson method
├── Brent.java # Brent's method
├── Bisection.java # Bisection method
└── NumericalDerivative.java # Numerical derivatives
Iterative method for finding roots of functions.
$$x_{n+1} = x_n - \frac{f(x_n)}{f'(x_n)}$$
import neqsim.mathlib.nonlinearsolver.NewtonRaphson;
// Define function to solve: f(x) = x² - 2 (find √2)
Function<Double, Double> f = x -> x * x - 2.0;
Function<Double, Double> df = x -> 2.0 * x;
NewtonRaphson solver = new NewtonRaphson();
solver.setFunction(f);
solver.setDerivative(df);
solver.setInitialGuess(1.0);
solver.setTolerance(1e-10);
solver.setMaxIterations(100);
double root = solver.solve();
System.out.println("√2 = " + root); // 1.4142135623...
Robust root-finding combining bisection, secant, and inverse quadratic interpolation.
import neqsim.mathlib.nonlinearsolver.Brent;
Function<Double, Double> f = x -> x * x * x - x - 2.0;
Brent solver = new Brent();
solver.setFunction(f);
solver.setBracket(1.0, 2.0); // Root is in [1, 2]
solver.setTolerance(1e-10);
double root = solver.solve();
System.out.println("Root: " + root);
Simple but robust root-finding.
import neqsim.mathlib.nonlinearsolver.Bisection;
Function<Double, Double> f = x -> Math.sin(x) - 0.5;
Bisection solver = new Bisection();
solver.setFunction(f);
solver.setBracket(0.0, Math.PI);
solver.setTolerance(1e-8);
double root = solver.solve();
System.out.println("arcsin(0.5) = " + root); // π/6 ≈ 0.5236
$$f'(x) \approx \frac{f(x+h) - f(x)}{h}$$
$$f'(x) \approx \frac{f(x+h) - f(x-h)}{2h}$$
import neqsim.mathlib.nonlinearsolver.NumericalDerivative;
Function<Double, Double> f = x -> Math.exp(x);
NumericalDerivative deriv = new NumericalDerivative();
deriv.setFunction(f);
deriv.setStepSize(1e-6);
double df = deriv.centralDifference(1.0);
System.out.println("d/dx(e^x) at x=1: " + df); // ≈ e ≈ 2.718
Efficient solver for tridiagonal systems.
$$\begin{bmatrix} b_1 & c_1 \ a_2 & b_2 & c_2 \ & \ddots & \ddots & \ddots \ & & a_{n-1} & b_{n-1} & c_{n-1} \ & & & a_n & b_n \end{bmatrix} \begin{bmatrix} x_1 \ x_2 \ \vdots \ x_{n-1} \ x_n \end{bmatrix} = \begin{bmatrix} d_1 \ d_2 \ \vdots \ d_{n-1} \ d_n \end{bmatrix}$$
import neqsim.mathlib.generalmath.TDMAsolve;
// Coefficients
double[] a = {0, 1, 1, 1}; // Lower diagonal
double[] b = {4, 4, 4, 4}; // Main diagonal
double[] c = {1, 1, 1, 0}; // Upper diagonal
double[] d = {5, 5, 5, 5}; // Right-hand side
double[] x = TDMAsolve.solve(a, b, c, d);
Cubic spline interpolation for smooth curves.
import neqsim.mathlib.generalmath.SplineInterpolation;
double[] xData = {0, 1, 2, 3, 4, 5};
double[] yData = {0, 1, 4, 9, 16, 25}; // y = x²
SplineInterpolation spline = new SplineInterpolation(xData, yData);
// Interpolate at any point
double y = spline.interpolate(2.5); // ≈ 6.25
import neqsim.mathlib.generalmath.GeneralMath;
// Safe logarithm (handles near-zero)
double logVal = GeneralMath.safeLog(x);
// Polynomial evaluation
double[] coeffs = {1, 2, 3}; // 1 + 2x + 3x²
double polyVal = GeneralMath.polynomial(x, coeffs);
// Linear interpolation
double y = GeneralMath.linearInterpolate(x, x1, y1, x2, y2);
For matrix operations, NeqSim uses external libraries:
import org.ejml.simple.SimpleMatrix;
// Matrix multiplication
SimpleMatrix A = new SimpleMatrix(new double[][] {
{1, 2}, {3, 4}
});
SimpleMatrix B = new SimpleMatrix(new double[][] {
{5, 6}, {7, 8}
});
SimpleMatrix C = A.mult(B);
// Solve linear system Ax = b
double[][] bData = { {1}, {2} };
SimpleMatrix b = new SimpleMatrix(bData);
SimpleMatrix x = A.solve(b);
// Eigenvalue decomposition
SimpleEVD evd = A.eig();
Newton-Raphson used in flash convergence:
// Simplified flash iteration
while (error > tolerance) {
// Calculate fugacities
double[] fugL = calculateLiquidFugacity();
double[] fugV = calculateVaporFugacity();
// Newton-Raphson update for K-values
for (int i = 0; i < nc; i++) {
K[i] = K[i] * fugL[i] / fugV[i];
}
// Rachford-Rice equation
beta = solveRachfordRice(K, z);
error = calculateError();
}
Continuation methods for phase boundary tracking:
// Predictor-corrector method
while (pressure < maxPressure) {
// Predict next point
double[] predicted = predictNextPoint(direction, stepSize);
// Correct using Newton-Raphson
double[] corrected = correctPoint(predicted);
// Update direction for next step
direction = updateDirection(corrected);
}
// Golden section search for minimum
Function<Double, Double> f = x -> (x - 2) * (x - 2) + 1;
double a = 0, b = 5;
double tolerance = 1e-6;
double phi = (1 + Math.sqrt(5)) / 2;
while ((b - a) > tolerance) {
double x1 = b - (b - a) / phi;
double x2 = a + (b - a) / phi;
if (f.apply(x1) < f.apply(x2)) {
b = x2;
} else {
a = x1;
}
}
double minimum = (a + b) / 2; // ≈ 2.0
For parameter fitting, NeqSim uses:
$$|x_{n+1} - x_n| < \epsilon$$
$$\frac{|x_{n+1} - x_n|}{|x_n|} < \epsilon$$
$$|f(x_n)| < \epsilon$$