| name | neqsim-flow-assurance |
| description | Flow assurance analysis patterns for NeqSim. USE WHEN: predicting hydrate formation, wax appearance, asphaltene stability, CO2/H2S corrosion (NORSOK M-506, de Waard-Milliams, FeCO3 film), mineral scale (saturation index, scale kinetics, brine mixing / seawater incompatibility), scale/solids valve plugging & Cv/opening drift (ValveScaleDrift), scale/deposit remediation & dissolver/solvent/wash selection for cleaning fouled equipment (ScaleRemediationAdvisor), elemental sulfur (S8) deposition from oxygen ingress / H2S oxidation at pressure or temperature letdown (compressor inlets, valves, dry-gas seals, letdown stations), per-segment pipeline corrosion+scale profiles, pipeline hydraulics, water/liquid hammer screening, slug flow, thermal analysis, or chemical inhibitor dosing. Covers all flow assurance threats with NeqSim code patterns and industry standards. |
| last_verified | 2026-07-11 |
Flow Assurance Analysis with NeqSim
Consolidated guide for all flow assurance threats — hydrate, wax, asphaltene, corrosion,
hydraulics, water/liquid hammer screening, slugging, and thermal management — with
NeqSim code patterns.
When to Use This Skill
- Hydrate formation temperature/pressure prediction
- Hydrate inhibitor dosing (MEG, methanol, ethanol)
- Wax appearance temperature (WAT) and wax deposition risk
- Asphaltene stability screening (de Boer, CII)
- CO2 and H2S corrosion rate estimation
- Elemental sulfur (S8) deposition risk at pressure/temperature letdown (compressor inlets, control/letdown valves, dry-gas seals, filters)
- Pipeline pressure drop and temperature profile
- Water hammer/liquid hammer screening for fast valve closure, pump trip, or check-valve slam
- Multiphase flow pattern prediction (slug, annular, stratified)
- Thermal insulation sizing for subsea pipelines
- Arrival temperature and cooldown calculations
Applicable Standards
| Domain | Standards | Key Requirements |
|---|
| Pipeline design | DNV-ST-F101, NORSOK L-001, ASME B31.4/B31.8 | Wall thickness, design factors, corrosion allowance |
| Corrosion | NORSOK M-001, DNV-RP-F112, ISO 21457 | Material selection, CO2/H2S corrosion rates |
| Subsea pipelines | DNV-RP-F109, NORSOK U-001 | On-bottom stability, span assessment |
| Hydrate management | DNV-RP-F116 | Hydrate prevention and remediation |
| GRP piping | ISO 14692 | Non-metallic pipe design |
| Pipeline integrity | DNV-RP-F116, API 1160 | Integrity management |
For fast acoustic transients, also load neqsim-water-hammer. Use
WaterHammerStudy or MCP runWaterHammer with STID route geometry, tagreader
event windows, and valve/pump event schedules; use this flow-assurance skill for
the broader operating-envelope and mitigation context.
1. Hydrate Analysis
EOS Selection for Hydrate Calculations
| Aqueous phase | NeqSim Class | Mixing Rule |
|---|
| Pure water / fresh water | SystemSrkCPAstatoil | 10 |
| Water + MEG / methanol / ethanol | SystemSrkCPAstatoil | 10 |
| Salt brine / formation water (NaCl, CaCl2, ...) | SystemElectrolyteCPAstatoil | 10 |
CPA is required for water–hydrocarbon hydrate modeling. When dissolved salts /
electrolytes are present, use SystemElectrolyteCPAstatoil so the salt
thermodynamic (hydrate-suppression) effect is captured — plain
SystemSrkCPAstatoil ignores ion activity and underestimates subcooling margin.
Hydrate Formation Temperature
SystemInterface fluid = new SystemSrkCPAstatoil(273.15 + 10, 100.0);
fluid.addComponent("methane", 0.80);
fluid.addComponent("ethane", 0.05);
fluid.addComponent("propane", 0.03);
fluid.addComponent("CO2", 0.02);
fluid.addComponent("water", 0.10);
fluid.setMixingRule(10);
fluid.setMultiPhaseCheck(true);
fluid.setHydrateCheck(true);
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.hydrateFormationTemperature();
double hydrateT_C = fluid.getTemperature() - 273.15;
Hydrate Equilibrium Curve (Multiple Pressures)
double[] pressures = {20, 40, 60, 80, 100, 120, 150, 200};
double[] hydrateTemps = new double[pressures.length];
for (int i = 0; i < pressures.length; i++) {
SystemInterface testFluid = fluid.clone();
testFluid.setPressure(pressures[i]);
ThermodynamicOperations testOps = new ThermodynamicOperations(testFluid);
testOps.hydrateFormationTemperature();
hydrateTemps[i] = testFluid.getTemperature() - 273.15;
}
Hydrate Inhibitor Dosing (MEG)
SystemInterface inhibitedFluid = new SystemSrkCPAstatoil(273.15 + 4, 100.0);
inhibitedFluid.addComponent("methane", 0.80);
inhibitedFluid.addComponent("water", 0.15);
inhibitedFluid.addComponent("MEG", 0.05);
inhibitedFluid.setMixingRule(10);
inhibitedFluid.setMultiPhaseCheck(true);
inhibitedFluid.setHydrateCheck(true);
ThermodynamicOperations ops = new ThermodynamicOperations(inhibitedFluid);
ops.hydrateFormationTemperature();
double inhibitedHydrateT = inhibitedFluid.getTemperature() - 273.15;
Salt-Inhibited Hydrate (Formation Water / Brine)
SystemInterface brineGas = new SystemElectrolyteCPAstatoil(273.15 + 4, 100.0);
brineGas.addComponent("methane", 0.80);
brineGas.addComponent("water", 0.18);
brineGas.addComponent("Na+", 0.01);
brineGas.addComponent("Cl-", 0.01);
brineGas.setMixingRule(10);
brineGas.setMultiPhaseCheck(true);
brineGas.setHydrateCheck(true);
ThermodynamicOperations ops = new ThermodynamicOperations(brineGas);
ops.hydrateFormationTemperature();
double brineHydrateT = brineGas.getTemperature() - 273.15;
MEG Concentration Sweep
double[] megWtPct = {0, 10, 20, 30, 40, 50};
for (double wt : megWtPct) {
double waterFrac = 0.20 * (1.0 - wt / 100.0);
double megFrac = 0.20 * (wt / 100.0);
SystemInterface testFluid = new SystemSrkCPAstatoil(273.15, 100.0);
testFluid.addComponent("methane", 0.80);
testFluid.addComponent("water", waterFrac);
testFluid.addComponent("MEG", megFrac);
testFluid.setMixingRule(10);
testFluid.setHydrateCheck(true);
ThermodynamicOperations testOps = new ThermodynamicOperations(testFluid);
testOps.hydrateFormationTemperature();
}
2. Wax Analysis
Wax Appearance Temperature (WAT)
SystemInterface oil = new SystemSrkEos(273.15 + 60, 50.0);
oil.addComponent("methane", 0.30);
oil.addComponent("ethane", 0.10);
oil.addTBPfraction("C7", 0.10, 92.0 / 1000, 0.727);
oil.addTBPfraction("C10", 0.15, 134.0 / 1000, 0.78);
oil.addTBPfraction("C15", 0.15, 206.0 / 1000, 0.83);
oil.addPlusFraction("C20", 0.20, 350.0 / 1000, 0.88);
oil.getCharacterization().setWaxModel(true);
oil.getCharacterization().characterisePlusFraction();
oil.setMixingRule("classic");
ThermodynamicOperations ops = new ThermodynamicOperations(oil);
ops.calcWAT();
double wat_C = oil.getTemperature() - 273.15;
Wax Fraction vs Temperature (PVT Simulation)
import neqsim.pvtsimulation.simulation.WaxFractionSim;
WaxFractionSim waxSim = new WaxFractionSim(oil);
waxSim.setTemperatures(new double[]{333.15, 313.15, 293.15, 273.15});
waxSim.run();
double[] waxFractions = waxSim.getWaxFraction();
3. Asphaltene Stability
de Boer Screening
SystemInterface aspFluid = new SystemSrkCPAstatoil(273.15 + 90, 300.0);
aspFluid.setMixingRule(10);
ThermodynamicOperations ops = new ThermodynamicOperations(aspFluid);
ops.TPflash();
aspFluid.initProperties();
4. Pipeline Hydraulics
Simple Adiabatic Pipe
AdiabaticPipe pipe = new AdiabaticPipe("Export Pipeline", feedStream);
pipe.setLength(50000.0);
pipe.setDiameter(0.508);
pipe.setInletElevation(0.0);
pipe.setOutletElevation(-350.0);
pipe.run();
double outletP = pipe.getOutletStream().getPressure();
double outletT = pipe.getOutletStream().getTemperature() - 273.15;
double dP = feedStream.getPressure() - outletP;
Beggs and Brill Multiphase Correlation
PipeBeggsAndBrills pipe = new PipeBeggsAndBrills("Subsea Flowline", feedStream);
pipe.setPipeWallRoughness(5e-5);
pipe.setLength(50000.0);
pipe.setAngle(0.0);
pipe.setDiameter(0.254);
pipe.setOuterTemperature(277.15);
pipe.run();
double outP = pipe.getOutletStream().getPressure();
double outT = pipe.getOutletStream().getTemperature() - 273.15;
Liquid Holdup, Flow Regime & Liquid-Loading (gravity-dominated screening)
Verified reader methods on PipeBeggsAndBrills (after run()):
String regime = pipe.getFlowRegime();
double dP = pipe.getPressureDrop();
double vmix = pipe.getMixtureVelocity();
double[] holdupProfile = pipe.getLiquidHoldupProfile();
Double hSeg = pipe.getSegmentLiquidHoldup(i);
Double vsgSeg = pipe.getSegmentGasSuperficialVelocity(i);
Double vslSeg = pipe.getSegmentLiquidSuperficialVelocity(i);
Double elevSeg = pipe.getSegmentElevation(i);
Liquid-loading / gravity-dominated screening (PEPR-style "is the line filling
with liquid?"): sweep gas rate (and water cut) and read average holdup +
regime. As gas rate falls, holdup rises and the regime moves
INTERMITTENT -> TRANSITION -> SEGREGATED (stratified) = gravity-dominated /
liquid loading. Higher water cut lifts holdup at every rate and pushes toward
INTERMITTENT (slugging). Define liquid-loading onset as the gas rate where
average holdup crosses a threshold (e.g. 25%).
for (double qgMSm3d : gasRates) {
SystemInterface feed = fluidTemplate.clone();
Stream s = new Stream("feed", feed);
s.setFlowRate(qgMSm3d, "MSm3/day");
s.setTemperature(50.0, "C"); s.setPressure(150.0, "bara");
s.run();
PipeBeggsAndBrills p = new PipeBeggsAndBrills("line", s);
p.setLength(21000.0); p.setDiameter(0.254); p.setAngle(0.0);
p.setPipeWallRoughness(5e-5); p.setNumberOfIncrements(40);
try {
p.run();
double[] h = p.getLiquidHoldupProfile();
} catch (RuntimeException e) {
}
}
GOTCHA — deliverability limit vs bug. PipeBeggsAndBrills uses a fixed
inlet pressure. If frictional ΔP over a long/small line exceeds the inlet
pressure, run() throws InvalidOutputException: ... Outlet pressure is negative. That is a genuine deliverability limit (the line cannot pass
that rate at that inlet P), not a solver failure — catch it and report the
max deliverable rate. To model to a fixed arrival (outlet) pressure
instead, raise the inlet pressure until the delivered rate matches, or iterate
inlet P per rate.
GOTCHA — getFlowRegime() naming. Beggs & Brill regimes are returned as
SEGREGATED (stratified/annular — gravity-dominated), TRANSITION,
INTERMITTENT (plug/slug), DISTRIBUTED (bubble/mist). "Gravity-dominated /
liquid loading" = SEGREGATED (+ low-velocity TRANSITION).
Gray (1974) Correlation — Gas / Gas-Condensate Vertical Wells
PipeGray implements the Gray (1974) correlation, the industry standard for
gas-dominated vertical wells producing condensate and/or water (API 14B
program). Prefer it over Beggs & Brill for vertical/near-vertical gas-condensate
tubing where the superficial gas velocity is high (> ~4.6 m/s), the tubing is
small (< ~3.5 in), and condensate loading is low (< ~50 bbl/MMscf). It predicts
in-situ liquid holdup and a condensate-film effective roughness.
PipeGray well = new PipeGray("Gray well", inletStream);
well.setDiameter(0.0889);
well.setLength(3000.0);
well.setElevation(3000.0);
well.setNumberOfIncrements(10);
well.setHoldupMethod(PipeGray.HoldupMethod.WOLDESEMAYAT_GHAJAR);
well.run();
double dP = well.getTotalPressureDrop();
double holdup = well.getLiquidHoldup();
double vsg = well.getSuperficialGasVelocity();
double ke = well.getEffectiveRoughness();
Single-phase gas and single-phase liquid segments fall back to a Haaland
friction-factor Darcy-Weisbach drop, so the same model spans wet-gas wells that
drop out condensate along the tubing. The void-fraction closures are exposed
directly via VoidFractionCorrelations.woldesemayatGhajar(...).
Pipeline with Formation Temperature Gradient (Wells / Risers)
PipeBeggsAndBrills wellbore = new PipeBeggsAndBrills("Production Well", feedStream);
wellbore.setLength(3000.0);
wellbore.setElevation(-3000.0);
wellbore.setDiameter(0.1571);
wellbore.setPipeWallRoughness(5e-5);
wellbore.setFormationTemperatureGradient(4.0, -0.03, "C");
wellbore.run();
Pipeline Sizing (Iterative)
double[] diameters = {0.1524, 0.2032, 0.254, 0.3048, 0.3556, 0.4064, 0.508};
for (double d : diameters) {
Stream testFeed = new Stream("feed", feedFluid.clone());
testFeed.setFlowRate(flowRate, "kg/hr");
testFeed.run();
PipeBeggsAndBrills testPipe = new PipeBeggsAndBrills("test", testFeed);
testPipe.setLength(pipeLength);
testPipe.setDiameter(d);
testPipe.setPipeWallRoughness(5e-5);
testPipe.run();
double dP = testFeed.getPressure() - testPipe.getOutletStream().getPressure();
double velocity = testPipe.getSuperficialVelocity();
}
5. CO2 / H2S Corrosion Assessment
CO2 Partial Pressure Based Screening
fluid.initProperties();
double pCO2 = fluid.getPhase("gas").getComponent("CO2").getx()
* fluid.getPressure();
Temperature and pH Effects
Network-Level Corrosion (LoopedPipeNetwork)
For production gathering networks, LoopedPipeNetwork has inline corrosion
models that compute rates per element during network solution:
net.setCorrosiveGas("trunk", 0.035, 0.002);
net.setCorrosionModel("trunk", "NORSOK");
net.setMinAllowableWallLife(20.0);
net.run();
Map<String, double[]> corr = net.calculateCorrosion();
List<String> violations = net.getCorrosionViolations();
Models: de Waard-Milliams (log10(Vcorr) = 5.8 - 1710/T + 0.67*log10(pCO2))
and NORSOK M-506 (Vcorr = Kt * fCO2^0.62 * (S/19)^0.146).
Rigorous NORSOK M-506 from a brine (electrolyte pH + FeCO3 film)
NorsokM506CorrosionRate (neqsim.process.corrosion) is the standalone NORSOK
M-506 model (fugacity, in-situ pH, FeCO3 scaling temperature, wall shear, glycol,
inhibitor). By default it estimates pH from a CO2-water correlation. To drive it
from rigorous electrolyte thermodynamics instead, use NorsokM506ElectrolyteBridge
— the M-506 analogue of CO2CorrosionAnalyzer:
NorsokM506ElectrolyteBridge bridge = new NorsokM506ElectrolyteBridge(fluid);
bridge.setFlowVelocityMs(3.0);
bridge.setPipeDiameterM(0.254);
bridge.run();
double rate = bridge.getModel().getCorrectedCorrosionRate();
double pH = bridge.getInSituPH();
double sr = bridge.getFeCO3SaturationRatio();
FeCO3 film feedback (closes the corrosion↔scaling loop): when the aqueous phase
is supersaturated in siderite (SR>1), a protective film suppresses corrosion even
below the scaling temperature. Supply the ratio directly on the model, or let the
bridge compute it from aqueous Fe++/CO3-- molalities (Sun & Nesic 2009 Ksp):
model.setFeCO3SaturationRatio(sr);
double film = model.calculateFeCO3FilmFactor();
Gotchas (verified): SystemInterface.clone() drops the chemical-reaction setup
— re-run chemicalReactionInit() on the clone before flashing or the CO2-brine pH
comes out unphysically basic (~10). setActualPH() is read back via getEffectivePH(),
NOT getCalculatedPH(). Proven converging brine: CO2 0.10 / water 0.88 / Na+ 0.01 /
Cl- 0.01, setMixingRule(10).
Robust in-situ pH for investigations
getpH() on the aqueous phase (or SystemInterface.getpH()) returns the in-situ
pH. It now has a built-in acid-gas dissociation fallback: when the aqueous
phase has water + dissolved CO2/H2S but no explicit H3O+ species (i.e.
chemicalReactionInit() was not run) or the electrolyte solver is unstable and
returns NaN at low pressure, getpH() estimates pH from the carbonic/sulfide
acid first-dissociation equilibria — [H+]=sqrt(K1_CO2·C_CO2 + K1_H2S·C_H2S + Kw)
— instead of the old silent, unphysical 7.0. CO2-saturated water → pH ≈ 3.9.
Force it explicitly with getpH("acidgas"). This is a screening estimate
(ignores bicarbonate buffering and salt-ion alkalinity); for a rigorous speciated
pH in a buffered brine still run chemicalReactionInit().
For corrosion/scale investigations that must always return a finite, bounded,
source-tagged value with an explicit pCO2 basis, use
RobustAqueousPH.estimate(fluid, pCO2Bar): it takes the rigorous getpH() when
valid, else a CO2-water correlation, and records which source was used
(getSource(), isFellBack()).
Regression test for the fallback: neqsim.chemicalreactions.AcidGasPHTest
(CO2-saturated water pH ≈ 3.9, neutral water ≈ 7, CO2+H2S acidic — all without
chemicalReactionInit()).
Per-segment corrosion + scale along a line (PipeSegmentIntegrity)
PipeSegmentIntegrity (neqsim.process.corrosion) walks a temperature/pressure/
velocity profile and reports, per segment, the NORSOK M-506 corrosion rate AND the
CaCO3 scale saturation, then ranks the worst corrosion and worst scale segments —
so mitigation can be located along the line. Feed it a run PipeBeggsAndBrills:
PipeSegmentIntegrity integrity = new PipeSegmentIntegrity();
integrity.fromPipe(pipe)
.setPipeAndGas(0.2, 0.05)
.setBrineChemistry(1500, 400, 0, 0, 12000, 35000);
integrity.evaluate();
int worstCorr = integrity.getWorstCorrosionIndex();
int worstScale = integrity.getWorstScaleIndex();
Network-Level Sand Erosion (DNV RP O501)
net.setSandRate("W1", 3.0);
net.setMaxAllowableSandRate(10.0);
net.setMaxAllowableErosionRate(5.0);
net.run();
Map<String, double[]> sand = net.calculateSandTransport();
List<String> violations = net.getSandViolations();
Erosion per DNV RP O501: E = K * Csand * v^2.6 * dp^0.2.
Deposition flagged when v < 1 m/s.
Mineral Scale (thermodynamics + kinetics + brine mixing)
NeqSim offers two routes to mineral scale. Pick by how much you know about
the brine and whether you need a rigorous precipitated amount.
(A) Screening SI from an ion analysis (fast, no flash). Activity-corrected
saturation index (Davies + Ksp(T)) for the common scales directly from a
produced-water ion table in mg/L:
ElectrolyteScaleCalculator scale = new ElectrolyteScaleCalculator();
scale.setTemperatureCelsius(60).setPressureBara(100).setPH(6.0);
scale.setCations(caMgL, baMgL, srMgL, mgMgL, naMgL, kMgL, feMgL);
scale.setAnions(clMgL, so4MgL, hco3MgL, co3MgL);
scale.calculate();
double siCaCO3 = scale.getCaCO3SaturationIndex();
(B) Rigorous scale potential from a speciated electrolyte fluid — REQUIRES
chemical reactions + speciation. The rigorous saturation ratio needs the in
situ ion molalities (Ca²⁺, HCO₃⁻, CO₃²⁻, Ba²⁺, SO₄²⁻, …), which only exist
after the aqueous speciation equilibria have been solved. So you MUST call
chemicalReactionInit() before createDatabase(true) / setMixingRule(10), and
you should get the in-situ pH from the same speciated flash (see the in-situ pH
section above). Then checkScalePotential(phaseNumber) returns, per salt, the
saturation ratio SR = IAP/Ksp ("relative solubility"): SR > 1 means
supersaturated and at scale risk.
SystemInterface brine = new SystemElectrolyteCPAstatoil(273.15 + 60.0, 100.0);
brine.addComponent("CO2", 0.02);
brine.addComponent("water", 55.5);
brine.addComponent("Na+", 1.0);
brine.addComponent("Cl-", 1.0);
brine.addComponent("Ca++", 0.02);
brine.addComponent("HCO3-", 0.04);
brine.chemicalReactionInit();
brine.createDatabase(true);
brine.setMixingRule(10);
brine.setMultiPhaseCheck(true);
ThermodynamicOperations ops = new ThermodynamicOperations(brine);
ops.TPflash();
brine.initProperties();
int aq = brine.getPhaseNumberOfPhase("aqueous");
ops.checkScalePotential(aq);
String[][] sr = ops.getResultTable();
double pH = brine.getpH();
Convenience equipment wrapper: ScalePotentialCheckStream runs the same SR
check on a process Stream.
Precipitation amount (how much solid forms). Two options:
-
Rigorous (solid phase flash): with setMultiPhaseCheck(true), a
supersaturated brine drops a solid phase during TPflash(). Read the
precipitated mineral directly:
if (brine.hasPhaseType("solid")) {
double solidMoles = brine.getPhase("solid").getNumberOfMolesInPhase();
double solidMassKg = solidMoles * brine.getPhase("solid").getMolarMass();
}
-
Screening (from SI): ScaleMassCalculator estimates the mg/L precipitated to
reach equilibrium from ion concentrations and SI (per mineral):
ScaleMassCalculator mass = new ScaleMassCalculator(predictionCalc);
mass.setWaterVolume(1.0);
double caco3MgL = mass.calcCaCO3Mass(cCaMolL, cCO3MolL, siCaCO3);
⚠️ ScaleMassCalculator is decoupled — each mineral is treated
independently, so it overpredicts competing minerals (e.g. it reports
celestite scaling even when barite has already consumed the shared sulphate).
-
Coupled screening (recommended for shared-ion brines):
MultiMineralScaleEquilibrium drops all supersaturated minerals
simultaneously and re-equilibrates the shared ion pools (sulphate: BaSO4 /
SrSO4 / CaSO4; carbonate: CaCO3 / FeCO3; calcium shared by anhydrite and
calcite), giving OLI/ScaleChem-style precipitated amounts plus the residual
brine. It reuses the same Ksp/ion-pairing/activity chemistry as
ScalePredictionCalculator, and can upgrade the activity model from Davies
(I ≤ 0.5 m) to an extended B-dot (Helgeson) model for high-salinity brines:
MultiMineralScaleEquilibrium eq = new MultiMineralScaleEquilibrium(predictionCalc)
.setActivityModel(MultiMineralScaleEquilibrium.ActivityModel.BDOT)
.setWaterVolume(1.0);
eq.solve();
double baso4MgL = eq.getPrecipitatedMassMgPerL("BaSO4");
double totalMgL = eq.getTotalScaleMassMgPerL();
double srResid = eq.getResidualFreeIonMolPerL("SO4--");
For high-pressure brines, enable the second-order (compressibility) Ksp term on
the predictor first: predictionCalc.setSecondOrderPressureCorrection(true).
-
In a process flowsheet (scaling mass RATE): StreamScaleAnalyzer extracts the
aqueous ion chemistry + produced-water throughput from a run Stream and turns
the coupled equilibrium into a kg/day scaling rate — the number that matters
for operations and deposition:
StreamScaleAnalyzer a = StreamScaleAnalyzer.fromStream(producedWaterStream);
a.analyze();
double bariteKgPerDay = a.getScaleRateKgPerDay("BaSO4");
double totalKgPerDay = a.getTotalScaleRateKgPerDay();
String dominant = a.getDominantScale();
(Needs an electrolyte-CPA fluid with an aqueous ion phase; a non-electrolyte
stream carries no ions and reports no scale.)
-
Root cause analysis: RootCauseAnalyser.setWaterChemistryFromStream(stream)
(or the ion setters) makes a scale-deposit symptom self-quantifying — the
MINERAL_SCALE candidate then carries the coupled dominant mineral and its
predicted mg/L, and is down-weighted when the brine is thermodynamically
undersaturated (deposit is more likely wax/asphaltene/corrosion product).
-
Agentic (MCP): the runChemistry tool exposes analysis:"multiMineralScale"
(ions in mg/L + T/P + optional waterFlow_LPerDay, activityModel:"BDOT",
secondOrderPressure) returning per-mineral amounts and a kg/day rate.
Model validation: the Ksp correlations are pinned to published log10(Ksp)
at 25 °C in ScaleKspLiteratureBenchmarkTest — calcite −8.48, barite −9.97,
celestite −6.63, anhydrite −4.36, siderite −10.89 (Greenberg & Tomson 1992).
Keep the siderite form -59.3498 - 0.041377*T - 2.1963/T + 24.5724*log10(T)
consistent across ScalePredictionCalculator, CheckScalePotential and
CalcSaltSatauration (no T^2 term).
Kinetics — SI says if, not how fast. ScaleKinetics adds induction time
and growth regime on top of the SI:
ScaleKinetics k = new ScaleKinetics().setSaturationIndex(si).evaluate();
double tInd = k.getInductionTimeHours();
String regime = k.getLimitingRegime();
double growth = k.getEffectiveGrowthRateMmYr();
Brine mixing (seawater + formation water incompatibility) — sulphate scale
often peaks at an intermediate mixing ratio, not either end member:
BrineMixingScaleEvaluator mix = new BrineMixingScaleEvaluator(formationWater, seawater);
mix.setConditions(60, 100).setPH(6.0).setSteps(21).evaluate();
double worstFrac = mix.getWorstFractionA();
String worst = mix.getWorstMineral();
Deposition along a line: ScaleDepositionAccumulator (walks a PipeBeggsAndBrills),
or the coupled corrosion+scale PipeSegmentIntegrity above.
Valve scale drift (plugging → Cv loss → opening drift → RCA)
Scale/solids on a control-valve trim do not fail the valve suddenly: the deposit
shrinks the open flow area, the effective Kv/Cv drops, and the level/pressure
controller opens the valve further to hold setpoint — an observable upward
drift of the opening until it pins at 100% and control is lost. This is the
classic level-valve-plugging signature. ThrottlingValve now carries a fouling
term and ValveScaleDrift turns a deposit growth rate into that drift:
valve.setFoulingFraction(0.5);
double effCv = valve.getEffectiveCv();
ValveScaleDrift drift = new ValveScaleDrift(valve)
.setPortDiameter(0.05)
.setKinetics(scaleKinetics);
for (int day = 0; day < 60; day++) {
drift.advance(1.0);
process.run();
}
double daysToPin = drift.predictTimeToPinDays(cleanOpeningPercent);
double tPlug = drift.getTimeToPlugDays();
Uniform radial deposit model: foulingFraction = 1 - ((d0 - 2t)/d0)^2; the
opening required to hold flow rises as cleanOpening / (1 - fouling). Use it in
an RCA to reproduce the "both LVs → 100% open, level still rising, no inflow
surge" trend and to bound time-to-plug.
Scale / precipitation remediation (dissolvers & washing for RCA solutions)
Once a deposit is identified, ScaleRemediationAdvisor recommends the dissolver /
solvent / wash to clean the already-fouled equipment (valves, trim, bridles,
lines, exchangers) — the proposed-solution counterpart to inhibitor prevention.
It is backed by a knowledge-base CSV (/data/scale_remediation.csv) and maps
common mineral names to canonical formulae:
ScaleRemediationAdvisor advisor = new ScaleRemediationAdvisor();
List<ScaleRemediationAdvisor.RemediationOption> opts = advisor.recommendFor("calcite");
ScaleRemediationAdvisor.RemediationOption best = opts.get(0);
best.getDissolver();
best.getMethod();
best.getCautions();
String json = advisor.toJson("BaSO4");
Coverage & the key gotcha it encodes: acid dissolves carbonate/sulfide scale
(CaCO3, FeCO3, FeS) but NOT sulfate scale — barite/celestite (BaSO4, SrSO4)
need a high-pH chelant (DTPA/EDTA); dithiazine (H2S-scavenger solids) needs a
proprietary dissolver plus restoring water pH control to prevent recurrence;
asphaltene/wax/naphthenate use aromatic solvent / hot oil. Unknown deposits
return a "sample-and-identify first (XRD/SEM-EDX)" guard.
RootCauseAnalyser uses this automatically: every deposit candidate
(MINERAL_SCALE, WAX_DEPOSITION, ASPHALTENE, FES_DEPOSITION) now appends a
concrete "to clean fouled equipment: …" hint to its recommendation,
targeting the coupled dominant mineral when ion chemistry is available.
Deposits on compressors (fouling → performance loss → washing)
When deposit-forming species (elemental sulfur S8, salt from entrained produced
water, mineral scale, wax) reach a compressor, they foul the impeller and reduce
head/efficiency. The neqsim.process.equipment.compressor deposit model bridges
these flow-assurance calculations to compressor performance and washing:
CompressorDeposit dep = CompressorDeposit.fromCompressor(comp);
dep.accumulate(new SolidFlashDepositSource(feed, "S8", DepositMechanism.SULFUR_S8, 0.3), 500.0);
dep.accumulate(new EntrainedSaltDepositSource(10.0, 0.05), 500.0);
comp.setDepositModel(dep);
CompressorChart chart500 = comp.buildDegradedChart();
WashFluid wash = CompressorDepositWash.recommend(dep);
comp.washOnline(wash, 300.0, 3.0);
Per-impeller deposit location: CompressorDepositProfile.compute(...) /
computeFromPropertyProfile(...). Full API in the neqsim-api-patterns skill and the
compressor_deposit_degradation doc. Use this to plan sulfur/salt washing of fouled
compressors and to recommend a wash fluid.
6. Thermal Analysis
Cooldown Calculation
Arrival Temperature Check
7. Flow Assurance Decision Matrix
| Threat | Detection | NeqSim Method | Mitigation |
|---|
| Hydrate | hydrateFormationTemperature() | CPA EOS + hydrate check | MEG, methanol, insulation, DEH |
| Wax | calcWAT(), WaxFractionSim | Wax characterization | Pigging, inhibitor, insulation |
| Asphaltene | de Boer screening | CPA flash at multiple P | Inhibitor, avoid P drop |
| Corrosion (CO2) | CO2 partial pressure | Standard flash | CRA, inhibitor, pH stabilization |
| Slugging | Beggs & Brill flow regime | PipeBeggsAndBrills | Slug catcher, topside choking |
| Scale | Ion activity product | Electrolyte-CPA | Scale inhibitor, pH control |
| Elemental sulfur (S8) | S8 solid drop-out at P/T letdown | setSolidPhaseCheck("S8") + TPSolidflash() | Remove O2 source, heat gas before letdown, reduce dP, SulfurFilter |
8. CO2 Injection Well Analysis
For CCS/injection well safety analysis, use the dedicated analyzer:
CO2InjectionWellAnalyzer analyzer = new CO2InjectionWellAnalyzer("InjWell-1");
analyzer.setFluid(co2Fluid);
analyzer.setWellGeometry(measuredDepth, innerDiameter, roughness);
analyzer.setOperatingConditions(inletP_bara, inletT_C, massFlowRate_kg_h);
analyzer.setFormationTemperature(surfaceT_C, bottomholeT_C);
analyzer.addTrackedComponent("hydrogen", maxMolFracLimit);
analyzer.runFullAnalysis();
boolean safe = analyzer.isSafeToOperate();
Impurity Monitoring
ImpurityMonitor monitor = new ImpurityMonitor("H2-Mon", stream);
monitor.addTrackedComponent("hydrogen", 0.10);
monitor.addTrackedComponent("H2S", 0.001);
double enrichment = monitor.getEnrichmentFactor("hydrogen");
boolean exceeds = monitor.exceedsLimit("hydrogen");
9. Elemental Sulfur (S8) Deposition
Elemental sulfur (cyclo-octasulfur, S8) drops out of natural gas as a yellow/grey
solid at points of pressure and/or temperature letdown — compressor inlets, control
and letdown valves, dry-gas seals, pressure regulators, and filters. The gas dissolves
a small amount of S8 (solubility rises with pressure and with H2S/CO2 content, and is
high in condensate, MEG, methanol and TEG); when pressure or temperature drops, the
gas becomes supersaturated and S8 deposits. The root cause is almost always an
oxygen source oxidising H2S: 8 H2S + 4 O2 -> S8 + 8 H2O (O2 typically enters via
preservation fluids, platform-nitrogen purge/seal gas, or injected chemicals).
Screening workflow: assume the gas is S8-saturated at a baseline (e.g. scrubber /
separator conditions such as 100 bara, 45 C), then check whether S8 drops to a solid
as the stream follows the compressor / valve pressure-temperature path. S8 is a
database component (fluid.addComponent("S8", ...)), and deposition is detected with a
solid flash.
S8 solid-drop-out (solubility) check
SystemInterface fluid = new SystemSrkEos(273.15 + 45.0, 100.0);
fluid.addComponent("methane", 0.90);
fluid.addComponent("CO2", 0.02);
fluid.addComponent("H2S", 0.001);
fluid.addComponent("S8", 1.0e-6);
fluid.setMixingRule("classic");
fluid.setMultiPhaseCheck(true);
fluid.setSolidPhaseCheck("S8");
fluid.setPressure(60.0, "bara");
fluid.setTemperature(30.0, "C");
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPSolidflash();
fluid.initProperties();
boolean s8Deposits = fluid.hasPhaseType(PhaseType.SOLID);
if (s8Deposits) {
double solidMass = fluid.getPhaseOfType("solid").getMass("kg");
}
Rule of thumb: deposition risk exists wherever a saturated (or nearly saturated)
stream sees a pressure or temperature drop. Heating the gas before letdown or
reducing the dP moves the point back into the single-phase (dissolved) region.
SulfurFilter equipment (solid S8 removal + change interval)
SulfurFilter runs a TPSolidflash internally, removes the solid S8, and tracks the
kg/hr loading for element sizing and change-out interval:
SulfurFilter filter = new SulfurFilter("S8 Filter", valveOutletStream);
filter.setRemovalEfficiency(0.99);
filter.setFilterElementCapacity(50.0);
filter.setDeltaP(0.5);
filter.run();
boolean s8Present = filter.isSolidS8Detected();
double s8Rate = filter.getSolidSulfurRemovalRate();
double interval = filter.getChangeIntervalHours();
Particle nucleation (optional, for particle-size / filter rating)
ClassicalNucleationTheory cnt = ClassicalNucleationTheory.sulfurS8();
Mitigations (in order of effectiveness): eliminate the O2 source (nitrogen/
preservation-fluid quality and routing); heat the gas upstream of pressure reduction;
reduce the dP / minimum landing pressure; install a SulfurFilter (watch the dP);
keep dry-gas-seal gas warm and well above its S8 saturation point.
Related: SourServiceAssessment (setElementalSulfurPresent(true)) for materials
impact; neqsim-electrolyte-systems for the aqueous-phase corrosivity of wet sulfur.
10. Common Pitfalls
| Pitfall | Solution |
|---|
| Hydrate T too low (no water in fluid) | Add water component to fluid |
| Using SRK instead of CPA for water systems | Use SystemSrkCPAstatoil with mixing rule 10 |
| Pipeline output T = input T (adiabatic) | Set outerTemperature for heat loss |
| Zero viscosity from pipeline calculation | Call fluid.initProperties() after flash |
| Wax prediction fails (no heavy fractions) | Add C7+ TBP fractions with wax model enabled |
| MEG not reducing hydrate T | Check MEG is partitioning to aqueous phase |
| No solid S8 phase forms | Use TPSolidflash() (not TPflash()) and call setSolidPhaseCheck("S8") first |
| S8 deposition risk missed | Saturate the gas at a realistic baseline (scrubber/separator P,T) before checking the letdown point |