Fluid Characterization in NeqSim
Real 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
1. Adding Heavy Fractions
You can add heavy fractions to a system using two primary methods: addTBPfraction and addPlusFraction.
1.1 TBP Fractions
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);
1.2 Plus Fractions
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);
2. Characterization Process
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).
2.1 Setting the Model
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");
Available TBP Models
NeqSim recognizes the following TBP model names for estimating critical properties (Tc, Pc, ω) from molecular weight and density:
| Model | Best Application |
|---|---|
PedersenSRK |
General SRK EOS (default), MW < 1120 g/mol |
PedersenPR |
General Peng-Robinson EOS, MW < 1120 g/mol |
PedersenSRKHeavyOil |
Property estimation unavailable; throws UnsupportedOperationException |
PedersenPRHeavyOil |
Property estimation unavailable; throws UnsupportedOperationException |
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 |
Pedersen property estimation rejects molecular weights at or above 1120 g/mol, including
through the RiaziDaubert fallback. For heavy cuts with independently validated critical
properties, use the seven-argument addTBPfraction overload. See
TBP Fraction Models for units, restrictions and model selection guidance.
Available Plus Fraction Models
Pedersen Model
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.
Whitson Gamma Model
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:
- $M$ is the molar mass.
- $\eta$ is the minimum molar mass (default 90 g/mol).
- $\alpha$ is the shape factor (default 1.0).
- $\beta$ is the scale parameter, calculated as $\beta = \frac{M_{plus} - \eta}{\alpha}$.
- $\Gamma(\alpha)$ is the Gamma function.
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).
2.2 Running Characterization
Once models are set, execute the characterization.
system.getCharacterization().characterisePlusFraction();
This process will:
- Extrapolate the molar distribution to C80+.
- Calculate properties for each carbon number.
- Group them into pseudo-components (if lumping is enabled).
3. Lumping Models
To reduce simulation time, it is often necessary to group the many characterized components into a smaller number of “lumped” pseudo-components. NeqSim provides three lumping models with different behaviors.
3.1 Available Lumping Models
| Lumping Model | Behavior | Use Case |
|---|---|---|
"PVTlumpingModel" |
Keeps TBP fractions (C6-C9) as separate pseudo-components, only lumps the plus fraction (C10+) | When you want to preserve individual TBP fractions (default) |
"standard" |
Lumps all TBP fractions and plus fractions together into N pseudo-components | When you want fewer total heavy components starting from C6 |
"no lumping" |
Keeps all individual carbon numbers (C6, C7…C80) | Maximum detail (slower simulation) |
3.2 Understanding numberOfPseudoComponents vs numberOfLumpedComponents
The lumping model has two configuration parameters:
| Method | What it Controls |
|---|---|
setNumberOfPseudoComponents(n) |
Total number of pseudo-components (TBP + lumped) |
setNumberOfLumpedComponents(n) |
Number of groups created from the plus fraction only |
Recommended Method by Model
| Model | Recommended Method | Reason |
|---|---|---|
"standard" |
setNumberOfPseudoComponents(n) |
Directly controls total pseudo-components |
"PVTlumpingModel" |
setNumberOfLumpedComponents(n) |
Directly controls C10+ grouping without side effects |
3.3 Fluent Configuration API (Recommended)
NeqSim provides a fluent builder API that makes lumping configuration clearer and less error-prone:
// PVTlumpingModel: keep C6-C9 separate, lump C10+ into 5 groups
fluid.getCharacterization().configureLumping()
.model("PVTlumpingModel")
.plusFractionGroups(5)
.build();
// Standard model: create exactly 6 total pseudo-components from C6+
fluid.getCharacterization().configureLumping()
.model("standard")
.totalPseudoComponents(6)
.build();
// No lumping: keep all individual SCN components
fluid.getCharacterization().configureLumping()
.noLumping()
.build();
Custom Carbon Number Boundaries
Match specific PVT lab report groupings by specifying carbon number boundaries:
// Creates groups: C6, C7-C9, C10-C14, C15-C19, C20+
fluid.getCharacterization().configureLumping()
.customBoundaries(6, 7, 10, 15, 20)
.build();
| Boundary Array | Resulting Groups |
|---|---|
[6, 10, 20] |
C6-C9, C10-C19, C20+ |
[6, 7, 10, 15, 20] |
C6, C7-C9, C10-C14, C15-C19, C20+ |
Behavior in "standard" Lumping Model
In the standard model, use setNumberOfPseudoComponents(n) to specify the total number of pseudo-components created from all heavy fractions (C6 through C80). All TBP fractions and plus fractions are combined and redistributed into equal-weight groups.
fluid.getCharacterization().setLumpingModel("standard");
fluid.getCharacterization().getLumpingModel().setNumberOfPseudoComponents(5);
// Result: 5 pseudo-components covering C6 through C80 (PC1, PC2, PC3, PC4, PC5)
Behavior in "PVTlumpingModel" (default)
In the PVTlumpingModel, the two parameters interact:
setNumberOfLumpedComponents(n): Number of groups created from the plus fraction only (e.g., C10+). TBP fractions (C6-C9) remain as separate pseudo-components.setNumberOfPseudoComponents(n): Total pseudo-components = TBP fractions + lumped components
The relationship:
numberOfLumpedComponents = numberOfPseudoComponents - numberOfDefinedTBPComponents
Example with 4 TBP fractions (C6, C7, C8, C9):
| You Set | Calculation | Final Result |
|---|---|---|
setNumberOfPseudoComponents(12) |
12 - 4 = 8 lumped | 4 TBP + 8 lumped = 12 total |
setNumberOfLumpedComponents(8) |
4 + 8 = 12 total | 4 TBP + 8 lumped = 12 total |
Both give the same result in this case.
⚠️ Gotcha: Minimum Lumped Components Override
If you set setNumberOfPseudoComponents() too low, the model may override your setting!
Example: With 4 TBP fractions and setNumberOfPseudoComponents(5):
- Calculated lumped = 5 - 4 = 1
- But default
numberOfLumpedComponentsis 7 - Since 1 < 7, the model overrides: 4 + 7 = 11 total (not 5!)
Solution: Use setNumberOfLumpedComponents() directly for PVTlumpingModel:
// RECOMMENDED for PVTlumpingModel - directly control C10+ grouping
fluid.getCharacterization().setLumpingModel("PVTlumpingModel");
fluid.getCharacterization().getLumpingModel().setNumberOfLumpedComponents(5);
// Result: C6_PC, C7_PC, C8_PC, C9_PC + 5 lumped groups from C10+ = 9 total
3.3 Quick Reference: Which Method to Use
| I want to… | Model | Method |
|---|---|---|
| Get exactly N total pseudo-components (lumping from C6) | "standard" |
setNumberOfPseudoComponents(N) |
| Keep C6-C9 separate, lump C10+ into N groups | "PVTlumpingModel" |
setNumberOfLumpedComponents(N) |
| Keep all SCN components (C6-C80) | "no lumping" |
N/A |
3.4 Choosing the Right Model
| Scenario | Recommended Model | Configuration |
|---|---|---|
| Standard PVT simulation | "PVTlumpingModel" |
configureLumping().plusFractionGroups(6-8) |
| Minimal components for speed | "standard" |
configureLumping().totalPseudoComponents(3-5) |
| Detailed compositional study | "no lumping" |
configureLumping().noLumping() |
| Match specific software output | Use custom boundaries | configureLumping().customBoundaries(...) |
3.5 Full Examples
Example 1: PVTlumpingModel with Fluent API (Recommended)
import neqsim.thermo.system.SystemSrkEos;
import neqsim.thermodynamicoperations.ThermodynamicOperations;
public class FluentLumpingExample {
public static void main(String[] args) {
SystemSrkEos fluid = new SystemSrkEos(298.15, 50.0);
fluid.addComponent("methane", 60.0);
fluid.addComponent("ethane", 5.0);
fluid.addComponent("propane", 3.0);
fluid.getCharacterization().setTBPModel("PedersenSRK");
// Add TBP fractions (these will be preserved as individual pseudo-components)
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);
fluid.getCharacterization().setPlusFractionModel("Pedersen");
// Fluent API: Lump C10+ into 5 groups (C6-C9 remain separate)
fluid.getCharacterization().configureLumping()
.model("PVTlumpingModel")
.plusFractionGroups(5)
.build();
fluid.getCharacterization().characterisePlusFraction();
// Result: C6_PC, C7_PC, C8_PC, C9_PC + 5 lumped groups = 9 pseudo-components
fluid.setMixingRule("classic");
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
fluid.prettyPrint();
}
}
Example 2: Standard model with Fluent API
import neqsim.thermo.system.SystemPrEos;
import neqsim.thermodynamicoperations.ThermodynamicOperations;
public class StandardLumpingExample {
public static void main(String[] args) {
SystemPrEos fluid = new SystemPrEos(298.15, 50.0);
fluid.addComponent("methane", 60.0);
fluid.addComponent("ethane", 5.0);
fluid.addComponent("propane", 3.0);
fluid.getCharacterization().setTBPModel("PedersenPR");
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);
fluid.getCharacterization().setPlusFractionModel("Pedersen");
// Fluent API: Lump ALL heavy fractions (C6 through C80) into 5 pseudo-components
fluid.getCharacterization().configureLumping()
.model("standard")
.totalPseudoComponents(5)
.build();
fluid.getCharacterization().characterisePlusFraction();
// Result: 5 pseudo-components covering C6-C80 (PC1, PC2, PC3, PC4, PC5)
fluid.setMixingRule("classic");
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
fluid.prettyPrint();
}
}
Example 3: Custom Boundaries (Match PVT Report Groupings)
import neqsim.thermo.system.SystemSrkEos;
import neqsim.thermodynamicoperations.ThermodynamicOperations;
public class CustomBoundariesExample {
public static void main(String[] args) {
SystemSrkEos fluid = new SystemSrkEos(298.15, 50.0);
fluid.addComponent("methane", 60.0);
fluid.addComponent("ethane", 5.0);
fluid.addComponent("propane", 3.0);
fluid.getCharacterization().setTBPModel("PedersenSRK");
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);
fluid.getCharacterization().setPlusFractionModel("Pedersen");
// Custom boundaries to match PVT lab report: C6, C7-C9, C10-C14, C15-C19, C20+
fluid.getCharacterization().configureLumping()
.customBoundaries(6, 7, 10, 15, 20)
.build();
fluid.getCharacterization().characterisePlusFraction();
fluid.setMixingRule("classic");
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();
fluid.prettyPrint();
}
}
Example 4: Python (neqsim-python)
from neqsim.thermo.thermoTools import TPflash, printFrame, fluid
# Create fluid
fluid1 = fluid('pr')
fluid1.addComponent("methane", 65.0)
fluid1.addComponent("ethane", 3.0)
fluid1.addComponent("propane", 2.0)
fluid1.getCharacterization().setTBPModel("PedersenPR")
fluid1.addTBPfraction("C6", 1.0, 90.0 / 1000.0, 0.7)
fluid1.addTBPfraction("C7", 1.0, 110.0 / 1000.0, 0.73)
fluid1.addTBPfraction("C8", 1.0, 120.0 / 1000.0, 0.76)
fluid1.addTBPfraction("C9", 1.0, 140.0 / 1000.0, 0.79)
fluid1.addPlusFraction("C10", 11.0, 290.0 / 1000.0, 0.82)
fluid1.getCharacterization().setPlusFractionModel("Pedersen")
# Option A: Fluent API (Recommended) - PVTlumpingModel with 6 plus fraction groups
fluid1.getCharacterization().configureLumping() \
.model("PVTlumpingModel") \
.plusFractionGroups(6) \
.build()
# Option B: Legacy API - PVTlumpingModel keeps C6-C9 separate
# fluid1.getCharacterization().setLumpingModel("PVTlumpingModel")
# fluid1.getCharacterization().getLumpingModel().setNumberOfLumpedComponents(6)
# Option C: Fluent API - standard model lumps everything from C6
# fluid1.getCharacterization().configureLumping() \
# .model("standard") \
# .totalPseudoComponents(5) \
# .build()
# Option D: Custom boundaries to match lab report groupings
# fluid1.getCharacterization().configureLumping() \
# .customBoundaries(6, 10, 15, 20) \ # C6-C9, C10-C14, C15-C19, C20+
# .build()
fluid1.getCharacterization().characterisePlusFraction()
fluid1.setMixingRule('classic')
fluid1.setTemperature(80.0, 'C')
fluid1.setPressure(30.0, 'bara')
TPflash(fluid1)
fluid1.initProperties()
printFrame(fluid1)
4. Advanced Options
- Heavy Oil: For very heavy oils, use
setPlusFractionModel("Pedersen Heavy Oil"). - Whitson Gamma: Use
setPlusFractionModel("Whitson Gamma")if you have specific gamma distribution parameters. - No Lumping: To keep all individual carbon number components (C6, C7… C80), use
configureLumping().noLumping().build()orsetLumpingModel("no lumping"). Note that this will result in a system with many components, which is slower to simulate. - Custom Boundaries: Match PVT lab report groupings with
configureLumping().customBoundaries(6, 10, 20).build().
5. Common Issues and Solutions
Issue: More pseudo-components than expected
Problem: Setting setNumberOfPseudoComponents(5) with PVTlumpingModel results in more than 5 components.
Cause: With PVTlumpingModel, the TBP fractions (C6-C9) are preserved separately. If you have 4 TBP fractions and request 5 total, that leaves only 1 lumped component for C10+. However, the default numberOfLumpedComponents is 7, so the model overrides your setting. A warning is now logged when this occurs.
Solution: Use the fluent API or setNumberOfLumpedComponents():
// Fluent API (recommended)
fluid.getCharacterization().configureLumping()
.model("PVTlumpingModel")
.plusFractionGroups(5) // Directly controls C10+ grouping
.build();
// Or legacy API
fluid.getCharacterization().getLumpingModel().setNumberOfLumpedComponents(5);
Issue: Want to start lumping from C6 or C7
Problem: You want all heavy fractions lumped together, not just C10+.
Solution: Use the "standard" lumping model:
// Fluent API (recommended)
fluid.getCharacterization().configureLumping()
.model("standard")
.totalPseudoComponents(6)
.build();
// Or legacy API
fluid.getCharacterization().setLumpingModel("standard");
fluid.getCharacterization().getLumpingModel().setNumberOfPseudoComponents(6);
Issue: Need to match specific PVT lab report groupings
Problem: Your PVT report uses groupings like C6, C7-C9, C10-C14, C15-C19, C20+ and you need to match exactly.
Solution: Use custom boundaries:
fluid.getCharacterization().configureLumping()
.customBoundaries(6, 7, 10, 15, 20)
.build();
6. API Deprecation Notice
⚠️ Deprecation: For
PVTlumpingModel, the methodsetNumberOfPseudoComponents()is deprecated because it can lead to unexpected override behavior. Use one of these alternatives:
- Fluent API:
configureLumping().plusFractionGroups(n).build()- Legacy API:
getLumpingModel().setNumberOfLumpedComponents(n)