# Neqsim API Patterns

> NeqSim API patterns and code recipes. USE WHEN: writing Java or Python code that uses NeqSim for thermodynamic calculations, process simulation, or property retrieval. Covers EOS selection, fluid creation, flash calculations, property access, equipment patterns, and unit conventions.

- Skill: `equinor/neqsim-api-patterns` (Agent Skill)
- Install (CLI): `npx skillmds@latest add equinor/neqsim-api-patterns`
- Raw SKILL.md: https://api.skillmd.com/api/skills/equinor/neqsim-api-patterns/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: equinor (https://skillmd.com/u/equinor)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/equinor/neqsim-api-patterns

---


# NeqSim API Patterns

Copy-paste reference for common NeqSim operations. All Java code must be Java 8 compatible.

## MCP Runtime Capability Routing

When a calculation is not exposed by a curated MCP domain tool, use the generic runtime index
before proposing a new tool:

1. Search with `runCapability({"action":"search","query":"<domain method>"})`.
2. Inspect the selected class with `inspectApi` to pin the deployed signature.
3. Follow the returned route:
     - `static-json`: invoke through `runCapability` with exact `className`, `methodName`,
         `parameterTypes`, and ordered JSON `arguments`.
     - `process-json`: build the stateful equipment through `runProcess`.
     - `inspect-only`: use a curated tool or add an explicit, reviewed adapter.

Discovery is broader than execution by design. `runCapability` only invokes public static methods
in approved domain packages with scalar, enum, or bounded-array JSON types; MCP runners, raw generic
containers, oversized payloads, arbitrary objects, and instance methods are excluded. Its timeout
uses cooperative Java interruption, so route long-running calculations through a curated runner or
`runProcess`. Treat runtime presence as capability evidence, then check tests, benchmark trust, and
standards before using the result for engineering decisions.

## EOS Selection Guide

| Fluid Type | Java Class | Mixing Rule |
|-----------|-----------|-------------|
| Dry/lean gas, simple HC | `SystemSrkEos` | `"classic"` |
| General hydrocarbons, oil | `SystemPrEos` | `"classic"` |
| **Matched to commercial simulator PR-LK** | **`SystemPrLeeKeslerEos`** | `"classic"` |
| Water, MEG, methanol, polar | `SystemSrkCPAstatoil` | `10` (numeric) |
| Custody transfer, fiscal metering | `SystemGERG2008Eos` | (none needed) |
| Electrolyte systems, **hydrate with salt brine** | `SystemElectrolyteCPAstatoil` | `10` |
| Volume-corrected SRK | `SystemSrkEosvolcor` | `"classic"` |

**PR-LK vs PR78**: `SystemPrLeeKeslerEos` uses PR76 alpha for ALL ω:
`m = 0.37464 + 1.54226ω − 0.26992ω²`. Standard `SystemPrEos1978` uses a modified
cubic for ω > 0.49. Use PR-LK when matching commercial simulator models that use
this EOS label.

## Fluid Creation (Required Sequence)

```java
// 1. Create: temperature in KELVIN, pressure in bara
SystemInterface fluid = new SystemSrkEos(273.15 + 25.0, 60.0);

// 2. Add components (name, mole fraction)
fluid.addComponent("methane", 0.85);
fluid.addComponent("ethane", 0.10);
fluid.addComponent("propane", 0.05);

// 3. MANDATORY: set mixing rule — NEVER skip
fluid.setMixingRule("classic");

// 4. Optional: multi-phase check for water/heavy systems
fluid.setMultiPhaseCheck(true);
```

## Oil Characterization (C7+ Fractions)

```java
fluid.addTBPfraction("C7", 0.05, 92.0 / 1000, 0.727);   // name, moleFrac, MW_kg/mol, density
fluid.addTBPfraction("C8", 0.04, 104.0 / 1000, 0.749);
fluid.addPlusFraction("C20+", 0.02, 350.0 / 1000, 0.88);
fluid.getCharacterization().getLumpingModel().setNumberOfLumpedComponents(6);
fluid.getCharacterization().characterisePlusFraction();
```

## Loading Fluids from E300 Files

NeqSim can read Eclipse E300-format fluid files with full component properties
and binary interaction parameters:

```java
// Load fluid from E300 file (returns SystemInterface with PR-EOS)
SystemInterface fluid = EclipseFluidReadWrite.read("path/to/fluid.e300");
// Returns a PR-EOS fluid with all components, properties, and BIPs set
```

**Required E300 sections**: `CNAMES`, `TCRIT`, `PCRIT`, `ACF`, `MW`, `TBOIL`,
`VCRIT`, `PARACHOR`, `SSHIFT`, `BIC`, `ZI`.

**Optional E300 sections** (NeqSim parses and applies these):
- `OMEGAA` / `OMEGAB` — per-component OmegaA/B overrides (applied after `init(0)`)
- `BICS` — surface-condition BICs (parsed, same lower-triangular format as `BIC`)
- `SSHIFTS` — surface-condition volume shift
- `PEDERSEN` — activates Pedersen viscosity model

**EOS keyword determines fluid class:**
- `EOS\nSRK /` → `SystemSrkEos`
- `EOS\nPR /\nPRCORR` → `SystemPrEos1978`
- `EOS\nPR /\nPRLKCORR` → `SystemPrLeeKeslerEos` ← use for PR-LK matching
- `EOS\nPR /` → `SystemPrEos`

**CRITICAL**: The `BIC` section must ALWAYS be present. If omitted, NeqSim
defaults to zero BIPs (no crash, but results may differ significantly from the
source simulator). The `PARACHOR` section is also required — estimate unknown
values with `4.0 * MW^0.77`.

**Component name mapping**: `C1`→methane, `C2`→ethane, `C3`→propane,
`iC4`→i-butane, `C4`→n-butane, `iC5`→i-pentane, `C5`→n-pentane,
`C6`→n-hexane, `N2`→nitrogen, `CO2`→CO2, `H2O`→water. All other names are
treated as TBP pseudo-fractions via `addTBPfraction()` — including aromatics
(Benzene, Toluene, etc.).

```python
# Python usage — auto-detects EOS from file
from neqsim import jneqsim
EclipseFluidReadWrite = jneqsim.thermo.util.readwrite.EclipseFluidReadWrite
fluid = EclipseFluidReadWrite.read("path/to/fluid.e300")

# Force a specific EOS regardless of what's in the file
SystemPrLeeKeslerEos = jneqsim.thermo.system.SystemPrLeeKeslerEos
target_fluid = SystemPrLeeKeslerEos(288.15, 1.01325)
fluid = EclipseFluidReadWrite.read("path/to/fluid.e300", target_fluid)
```

**JSON process builder also supports PR-LK** via `"model": "PR_LK"`:
```json
{ "fluid": { "model": "PR_LK", "temperature": 288.15, "pressure": 50.0, ... } }
```

## Flash Calculations and Property Retrieval

```java
ThermodynamicOperations ops = new ThermodynamicOperations(fluid);
ops.TPflash();

// CRITICAL: call initProperties() AFTER flash, BEFORE reading properties
// init(3) alone does NOT initialize transport properties — they return ZERO
fluid.initProperties();

// Bulk properties
double density = fluid.getDensity("kg/m3");
double molarMass = fluid.getMolarMass("kg/mol");
double Z = fluid.getZ();

// Phase properties
double gasDensity = fluid.getPhase("gas").getDensity("kg/m3");
double gasViscosity = fluid.getPhase("gas").getViscosity("kg/msec");
double gasThermalCond = fluid.getPhase("gas").getThermalConductivity("W/mK");
double gasCp = fluid.getPhase("gas").getCp("J/kgK");

// Phase checks
int numPhases = fluid.getNumberOfPhases();
boolean hasGas = fluid.hasPhaseType("gas");
```

### Other Flash Types

```java
ops.PHflash(enthalpy);                  // Pressure-Enthalpy
ops.PSflash(entropy);                   // Pressure-Entropy
ops.dewPointTemperatureFlash();          // Dew point temperature
ops.bubblePointPressureFlash();          // Bubble point pressure
ops.hydrateFormationTemperature();       // Hydrate T at given P
ops.calcPTphaseEnvelope();              // Phase envelope
```

> **Saturation flashes can fail without throwing.** A failed continuation in
> `dewPointPressureFlash()` / `bubblePointPressureFlash()` can leave a
> non-physical pressure on the system and return normally. In a single run someone
> notices; inside a Monte Carlo loop it silently poisons a percentile. Validate the
> result on physical grounds instead of trusting the absence of an exception:
> ```python
> ops.dewPointPressureFlash()
> p_dew = fluid.getPressure()
> if not (10.0 < p_dew < 3.0 * p_reservoir):
>     p_dew = float("nan")     # reject, do not propagate
> ```

### Re-flashing a characterised fluid many times (CRITICAL for loops)

To take a phase's composition and flash it somewhere else — produced gas at each
depletion step, a recycle stream, a Monte Carlo realization — **clone the already
characterised fluid and overwrite its composition**. Do NOT rebuild it with
`addTBPfraction`: that re-runs the TBP characterisation on every call.

```python
probe = fluid.clone()
probe.setTemperature(T_res + 273.15)
probe.setPressure(p)
ns.ThermodynamicOperations(probe).TPflash()
probe.initProperties()

gas_phase = probe.getPhase("gas")
produced = fluid.clone()                        # keeps the characterisation
produced.setMolarComposition(
    [gas_phase.getComponent(i).getx()
     for i in range(probe.getNumberOfComponents())])
produced.setTemperature(288.15)
produced.setPressure(1.01325)
ns.ThermodynamicOperations(produced).TPflash()  # -> CGR, gas gravity, etc.
```

Measured on a 26-component P/A gas condensate with nine pressure nodes: the
`addTBPfraction` rebuild cost **4.96 s** per realization against **0.97 s** for
the clone-and-overwrite route, for bit-identical results. Over a 4000-member
ensemble that is the difference between three hours and four minutes — i.e.
between propagating uncertainty through the model and deciding not to.


## Unit Conventions

| Quantity | Constructor default | Setter pattern |
|----------|-------------------|----------------|
| Temperature | **Kelvin** | `setTemperature(25.0, "C")` |
| Pressure | **bara** | `setPressure(50.0, "bara")` |
| Flow rate | — | `setFlowRate(50000.0, "kg/hr")` |
| Getting temp | Returns **Kelvin** | `getTemperature() - 273.15` for °C |

### Non-obvious return units

| Call | Returns | Trap |
|------|---------|------|
| `Standard_ISO6976(sys, 15, 15, "volume").getValue("GCV")` | **kJ/Sm³** (~40 000) | Dividing by 1e6 gives a nonsense 0.04 MJ/Sm³ — divide by **1e3** |
| `Standard_ISO6976(...).getValue("WI")` | **kJ/Sm³** | Same |
| `SURFCostEstimator.setContingencyPct(x)` | — | Takes a **fraction** (0.35), not a percent, despite the name. Same for `WellCostEstimator` |
| `Cooler.getDuty()` | **W** | Divide by 1e3 for kW |

### Dense-phase CO₂ needs GERG-2008, not a cubic

Benchmarked against CoolProp (Span-Wagner) — density deviation at 40 °C / 100 bara and
100 °C / 200 bara:

| System class | Deviation |
|---|---|
| `SystemSrkEos` | −14.1 % / −7.3 % |
| `SystemPrEos`, `SystemPrEos1978`, `SystemUMRPRUMCEos` | −12.4 % / −6.1 % |
| `SystemSrkCPAstatoil` | −17.5 % / −10.3 % |
| **`SystemGERG2008Eos`** | **+0.01 % / +0.02 %** |

Use `SystemGERG2008Eos` for any CO₂ compression, injection or transport duty. Cubics are
acceptable for the gas-phase part of the train but not near or above the critical density.

## Process Equipment Patterns

### Standard outlet-stream contract

Equipment that produces phase-separated gas and liquid products must expose the
conventional `getGasOutStream()` and `getLiquidOutStream()` methods. A
three-phase unit should also expose its conventional water outlet. Domain names
such as `getOverheadGasStream()`, `getLeanLiquidStream()`, or
`getBottomsStream()` are useful aliases, but they supplement rather than replace
the conventional accessors and must return the same stream objects.

Every equipment class must also report all connected streams through
`getInletStreams()` and `getOutletStreams()`. These topology lists must contain
the live public stream objects, not clones or solver-internal tray streams. Once
an outlet has been handed to downstream equipment, preserve its object identity
across `run(...)` calls by updating its thermodynamic system in place or using
the established identity-preserving adoption helper.

For phase-separated equipment, add a focused contract test after a successful
solve that verifies:

- `assertSame` between conventional accessors, domain aliases, and the matching
    entries in `getOutletStreams()`;
- the gas product contains a `gas` phase and the liquid product contains an
    `oil`, `liquid`, or `aqueous` phase;
- expected product flows are positive and total/per-component balances close;
- outlet identity remains unchanged after a warm rerun or changed feed.

Do not add a new equipment-wide interface solely for gas/liquid naming. The
generic topology contract belongs to `ProcessEquipmentInterface`; conventional
phase-product methods belong on the phase-separating equipment abstraction.

### Stream

```java
Stream feed = new Stream("feed", fluid);
feed.setFlowRate(100.0, "kg/hr");
feed.setPressure(50.0, "bara");
feed.setTemperature(30.0, "C");
```

### Separator

```java
Separator sep = new Separator("HP Sep", feedStream);
Stream gasOut = sep.getGasOutStream();
Stream liqOut = sep.getLiquidOutStream();
```

**Separator class ↔ orientation (affects gas-capacity results):**

| Class | Default orientation | Use for |
|-------|--------------------|---------|
| `Separator`, `ThreePhaseSeparator` | **horizontal** | horizontal separators (VA-tag) |
| `GasScrubber`, `GasScrubberSimple`, `NeqGasScrubber` (2-phase) | **vertical** | vertical scrubbers (VG-tag) |
| `ThreePhaseGasScrubber` (3-phase) | **vertical** | vertical 3-phase scrubbers |

A horizontal vessel derates the gas area by the design liquid level (default 80% →
gas area `(1−0.8)=0.2×`), so using a horizontal `Separator`/`ThreePhaseSeparator`
for a physically **vertical** scrubber over-reads `getGasLoadFactor()` /
`getGasSuperficialVelocity()` by ~5×. Prefer the `*GasScrubber` classes for vertical
scrubbers, or override with `sep.setOrientation("vertical")`. `setInternalDiameter()`
propagates correctly through `run()` — the trap is orientation, not diameter.

### Separator Mechanical Design (Physical Configuration)

Physical dimensions, internals, and design parameters are configured through
`SeparatorMechanicalDesign` — NOT directly on `Separator`. The `Separator`
class handles process simulation (flash, entrainment); `SeparatorMechanicalDesign`
owns the physical vessel design.

```java
// After process.run():
sep.initMechanicalDesign();
SeparatorMechanicalDesign design =
    (SeparatorMechanicalDesign) sep.getMechanicalDesign();

// Design envelope
design.setMaxOperationPressure(85.0);           // bara
design.setMaxOperationTemperature(273.15 + 80); // K

// Vessel sizing parameters (configured via MechanicalDesign)
design.setGasLoadFactor(0.107);       // K-factor [m/s]
design.setRetentionTime(120.0);       // Liquid retention [s]
design.setFg(0.5);                    // Gas area fraction

// Nozzle diameters (set via MechanicalDesign, NOT on Separator)
design.setInletNozzleID(0.254);       // 10-inch inlet nozzle [m]
design.setGasOutletNozzleID(0.20);    // Gas outlet [m]
design.setOilOutletNozzleID(0.15);    // Oil outlet [m]

// Demister/mist eliminator parameters
design.setDemisterType("wire_mesh");  // "wire_mesh", "vane_pack", "cyclone"
design.setDemisterPressureDrop(1.5);  // [mbar]
design.setDemisterThickness(150.0);   // [mm]
design.setFoamAllowanceFactor(1.0);   // 1.0 = no foam

// Bridge methods — entrainment internals (delegate to Separator)
design.setInletPipeDiameter(0.254);   // Inlet pipe ID for DSD generation [m]
design.setInletDeviceType(InletDeviceModel.InletDeviceType.INLET_VANE);
design.setGasLiquidSurfaceTension(0.020); // Interfacial tension [N/m]
design.addSeparatorSection("Demister", "meshpad");

// Bridge methods — dynamic internals (delegate to Separator)
design.setWeirHeightAbsolute(0.30);   // Weir height [m] (syncs weirFraction)
design.setWeirLength(1.5);            // Weir crest length [m]
design.setBootVolume(2.0);            // Boot/sump volume [m3]
design.setMistEliminatorDpCoeff(150.0);  // Euler number for dP calc
design.setMistEliminatorThickness(0.15); // Demister pad thickness [m]

// Run design calculation
design.readDesignSpecifications();
design.calcDesign();
String report = design.toJson();

// Results: design.getInnerDiameter(), design.getTantanLength(),
//          design.getWallThickness(), design.getInletNozzleID(), etc.
```

### Separator Entrainment (Carry-Over)

Imperfect separation is modelled with `setEntrainment()` on the `Separator` /
`ThreePhaseSeparator` itself (not the mechanical design). It transfers a fraction
of one phase into another outlet stream.

```java
// setEntrainment(double val, String specType, String specifiedStream,
//                String phaseFrom, String phaseTo)
//   specType        : "mole" | "mass" | "volume"
//   specifiedStream : "feed" (fraction of feed) | "product" (fraction of receiving outlet)
//   phaseFrom/To    : "gas" | "oil" | "aqueous"  (base Separator also accepts "liquid")

ThreePhaseSeparator sep = new ThreePhaseSeparator("1st Stage", feed);

// Liquid carry-over into gas (feed basis)
sep.setEntrainment(0.001, "mole", "feed", "oil", "gas");      // oil-in-gas
sep.setEntrainment(0.001, "mole", "feed", "aqueous", "gas");  // water-in-gas

// Cross-contamination expressed on the receiving product stream
sep.setEntrainment(0.005, "mass",   "product", "aqueous", "oil"); // 0.5 mass% BS&W in oil
sep.setEntrainment(500e-6, "mass",   "product", "oil", "aqueous"); // 500 ppm oil-in-water
sep.run();
```

- **Base `Separator`** supports 3 paths: `oil→gas`, `aqueous→gas`, `gas→liquid`.
- **`ThreePhaseSeparator`** supports all 6 paths: `oil→gas`, `aqueous→gas`,
  `gas→oil`, `gas→aqueous`, `oil→aqueous`, `aqueous→oil`.
- With `specifiedStream="product"`, `val` is clamped: `≤0` transfers nothing,
  `≥1` transfers the entire source phase.

**Typical screening values** (indicative only — always defer to the project
separation spec / datasheet; for rigorous physics use the enhanced entrainment
model and `SeparatorMechanicalDesign`):

| Carry-over path | Typical range | Basis | Notes |
| --------------- | ------------- | ----- | ----- |
| Liquid-in-gas (oil or water → gas) | 0.01 – 0.5 % | mole/mass, feed | Well-designed mist extractor; tighter (<0.01%) with high-efficiency internals |
| Gas carry-under (gas → liquid) | 0.1 – 2 % | mole, feed | Higher with foaming / short retention |
| Water-in-oil (BS&W, aqueous → oil) | 0.5 – 5 vol% | volume, product | Export crude spec often ≤ 0.5 vol%; inter-stage higher |
| Oil-in-water (oil → aqueous) | 100 – 1000 ppm | mass, product | Produced-water inlet; overboard discharge typically ≤ 30 ppm (OSPAR) |

### Separation Efficiency Report (K-Factor Operating Windows)

`SeparatorMechanicalDesign.calculateSeparationEfficiency()` returns a
`SeparatorEfficiencyReport` that combines the physics-based entrainment /
carry-under fractions with a per-internal Souders-Brown **K-factor operating
window** check (from the internals database `MinKFactor`/`MaxKFactor`). It answers
"is this mist mat / vane pack / cyclone inside its good performance band, below
turndown, or into flooding?" and works for two-phase AND three-phase separators
and gas scrubbers (`GasScrubberMechanicalDesign` inherits it).

It is **read-only** — it does not change what `run()` does. Whether the physics
entrainment model is *applied* at run time is a separate opt-in toggle
(`setEfficiencyModelEnabled`). Default behaviour (no entrainment, or manual
`setEntrainment(...)`) is unchanged.

```java
sep.run();                                    // flash
SeparatorMechanicalDesign design =
    (SeparatorMechanicalDesign) sep.getMechanicalDesign();
design.calcDesign();
design.setDesign();                           // push sized diameter to the separator

// Optional: pick a specific database sub-type for the mist mat
design.setDemisterType("wire_mesh");          // "wire_mesh" | "vane_pack" | "cyclone"
design.setDemisterSubType("High Efficiency"); // sub-type from SeparatorInternals.csv

// Read-only assessment (2-phase or 3-phase, auto-detected)
SeparatorEfficiencyReport report = design.calculateSeparationEfficiency();
double opK      = report.getOperatingKFactor();           // m/s
double effGL    = report.getOverallGasLiquidEfficiency(); // 0-1
String verdict  = report.getVerdict();  // GOOD_PERFORMANCE | BELOW_TURNDOWN | FLOODING_RISK | MARGINAL_EFFICIENCY
for (InternalOperatingWindow w : report.getWindows()) {
  // w.getStatus(): BELOW_MIN_TURNDOWN | IN_RANGE | ABOVE_MAX_FLOODING
  // w.getMinKFactor(), w.getMaxKFactor(), w.getUtilization(), w.getTurndownRatio()
}
String json = report.toJson();  // full report incl. per-internal windows

// Apply the physics entrainment/carry-under model during run() (opt-in):
design.setEfficiencyModelEnabled(true);   // delegates to setDetailedEntrainmentCalculation(true)
sep.run();                                // gas/liquid outlets now reflect computed carry-over
design.setEfficiencyModelEnabled(false);  // back to no-entrainment / manual setEntrainment
```

**K-factor window meaning** (limits from `SeparatorInternals.csv`):
`K < Kmin` → below turndown (poor coalescence, droplets slip through);
`Kmin ≤ K ≤ Kmax` → good performance band; `K > Kmax` → flooding / re-entrainment.

### Compressor

```java
Compressor comp = new Compressor("Comp", gasStream);
comp.setOutletPressure(120.0);
// comp.setIsentropicEfficiency(0.75);
Stream out = comp.getOutletStream();
// After run: comp.getPower("kW")
```

### Compressor chart library (multiple named/selectable charts)

A `Compressor` can hold several performance maps at once via a
`CompressorChartLibrary` and switch the active chart by name — the professional
way to keep vendor-expected, as-tested and field-fitted curves for the same
machine side by side (revamp studies, digital twins, design-vs-tested checks).
See the [Compressor Chart Library](../../docs/process/equipment/compressor_curves.md#compressor-chart-library-multiple-named-charts) doc.

```java
comp.addChart("BCL405B-design", expectedChart);
comp.addChart("BCL405B-tested", asTestedChart,
    new CompressorChartMetadata("BCL 405/B", "gas export", "27-KA01",
        "8300199-CA-001", CompressorChartMetadata.CurveType.AS_TESTED));
comp.selectChart("BCL405B-tested");   // sets + enables the chart, turns on polytropic calc
comp.run();

List<String> charts = comp.getAvailableCharts();      // ["BCL405B-design", "BCL405B-tested"]
String active = comp.getSelectedChartName();          // "BCL405B-tested"

// Persist / reload a shared vendor-curve database (all curves + metadata):
comp.getChartLibrary().saveToFile("BCL405B_charts.json");
comp.setChartLibrary(CompressorChartLibrary.loadFromFile("BCL405B_charts.json"));
```

### Compressor deposit / fouling degradation and washing

Model deposit (fouling) mass from process thermodynamics, its effect on
performance, where it lands per impeller, the degraded chart after N hours, and
online washing. Package `neqsim.process.equipment.compressor`. See the
[Compressor Deposit and Performance Degradation](../../docs/process/compressor_deposit_degradation.md)
doc.

```java
// 1) Deposit mass -> performance effect (combine several mechanisms)
CompressorDeposit dep = CompressorDeposit.fromCompressor(comp); // sizes foulable geometry
dep.addDeposit(DepositMechanism.SULFUR_S8, 1.2);   // kg (S8 study)
dep.addDeposit(DepositMechanism.SALT_NACL, 0.4);   // kg (salt study)
comp.setDepositModel(dep);                          // run() now degrades efficiency/power
comp.run();
double effLoss = 1.0 - dep.getEfficiencyMultiplier();

// 2) Deposit mass FROM the process (precipitation bridge)
SolidFlashDepositSource s8 =
    new SolidFlashDepositSource(feed, "S8", DepositMechanism.SULFUR_S8, 0.3); // TPSolidflash
EntrainedSaltDepositSource salt =
    new EntrainedSaltDepositSource(10.0, 0.05);     // 10 kg/hr entrained water, 5 wt% salt
dep.accumulate(s8, 500.0);                          // deposit after 500 operating hours
dep.accumulate(salt, 500.0);

// 3) Degraded performance chart after N hours (chart-based machines)
CompressorChart chart500 = comp.buildDegradedChart();

// 4) Where deposits form (per impeller). Rigorous = real per-step flashed states:
comp.setPolytropicMethod("detailed");
comp.getPropertyProfile().setActive(true);
comp.run();
List<CompressorDepositProfile.StageDeposit> profile =
    CompressorDepositProfile.computeFromPropertyProfile(comp, 5, "S8");
int worst = CompressorDepositProfile.worstStage(profile); // 1 = cold first impeller

// 5) Online washing: recommend fluid, plan rate, simulate removal
WashFluid fluid = CompressorDepositWash.recommend(dep);   // salt->WATER, S8->XYLENE
CompressorDepositWash washer = new CompressorDepositWash();
washer.setContactEfficiency(0.7);
double rateKgHr = washer.requiredFluidRateKgHr(dep, fluid, 2.0, 3.0); // remove 2 kg in 3 h
CompressorDepositWash.WashResult r = comp.washOnline(fluid, rateKgHr, 3.0);
comp.run();                                               // performance recovers
```

Wash-fluid → deposit matching (screening solubilities): water dissolves salt/scale;
xylene/toluene dissolve S8 and wax; condensate dissolves wax; methanol moderate salt.
`recommend()` returns the fluid that removes the most mass — for mixed salt+S8 fouling,
wash in sequence (water, then xylene).

### Cooler / Heater

```java
Cooler cooler = new Cooler("Cooler", hotStream);
cooler.setOutTemperature(273.15 + 30.0);
Stream out = cooler.getOutletStream();
// After run: cooler.getDuty() — Watts
```

### HeatExchanger (Two-Sided)

`HeatExchanger` has two feed/outlet sides indexed 0 and 1. Use
`setFeedStream(int, StreamInterface)` to connect both sides and
`getOutStream(int)` to retrieve the outlet for each side.

**IMPORTANT:** Do NOT use `getOutletStream()` when you need a specific
side — it only returns side 0. Always use `getOutStream(int)`.

```java
HeatExchanger hx = new HeatExchanger("E-100");
hx.setFeedStream(0, shellSideFeed);   // side 0 = shell
hx.setFeedStream(1, tubeSideFeed);    // side 1 = tube
// Optional: hx.setUAvalue(35000.0);  // W/K

// After run: retrieve each side's outlet
Stream shellOut = (Stream) hx.getOutStream(0);
Stream tubeOut  = (Stream) hx.getOutStream(1);
double duty = hx.getDuty();  // Watts
```

```python
# Python
hx = HeatExchanger("E-100")
hx.setFeedStream(0, shell_feed)
hx.setFeedStream(1, tube_feed)
# Downstream connections:
cooler = Cooler("C-100", hx.getOutStream(int(0)))   # shell side out
valve  = ThrottlingValve("VLV-100", hx.getOutStream(int(1)))  # tube side out
```

### Valve (JT / Isenthalpic Expansion)

```java
ThrottlingValve valve = new ThrottlingValve("JT Valve", stream);
valve.setOutletPressure(20.0);
Stream out = valve.getOutletStream();
```

**CRITICAL:** Always use `ThrottlingValve` inside a `ProcessSystem` for Joule-Thomson
cooling calculations. Manual `PHflash()` on a cloned fluid gives wrong JT temperatures
(tested: 14.9°C error vs 1.7°C with ThrottlingValve). The valve handles the isenthalpic
enthalpy bookkeeping internally.

```python
# Python — Correct JT expansion pattern
proc = ProcessSystem()
feed = Stream('SG', fluid.clone())
feed.setFlowRate(flow, 'kg/hr')
feed.setTemperature(T_in, 'C')
feed.setPressure(P_in, 'bara')
proc.add(feed)
valve = ThrottlingValve('JT', feed)
valve.setOutletPressure(P_out)
proc.add(valve)
proc.run()
T_jt = float(valve.getOutletStream().getTemperature('C'))
```

### Mixer

```java
Mixer mixer = new Mixer("Mix");
mixer.addStream(stream1);
mixer.addStream(stream2);
Stream out = mixer.getOutletStream();
```

### ComponentSplitter (TEG / Glycol Contactor — Water Removal)

Used to model TEG dehydration contactors as simple water-removal units.
Splits a stream per-component: `splitFactor[k] = 1.0` keeps the component in
stream 0 (dry gas), `0.0` removes it to stream 1 (water).

**TEG dehydration pattern**: water is always the last component added,
so use `[1.0] * (N-1) + [0.0]` to remove only water.

```java
// Java
ComponentSplitter dehydrator = new ComponentSplitter("TEG contactor", wetGasStream);
int nComp = wetGasStream.getFluid().getNumberOfComponents();
double[] sf = new double[nComp];
Arrays.fill(sf, 1.0);
sf[nComp - 1] = 0.0;  // last component = water
dehydrator.setSplitFactors(sf);
// After run:
Stream dryGas = dehydrator.getSplitStream(0);   // all components except water
Stream water  = dehydrator.getSplitStream(1);   // removed water
```

```python
# Python
water_dehydration = neqsim.process.equipment.splitter.ComponentSplitter(
    "dehyd", wet_gas_stream)
complen = wet_gas_stream.getFluid().getNumberOfComponents()
water_dehydration.setSplitFactors([1.0] * (complen - 1) + [0.0])
water_dehydration.run()
dry_gas = water_dehydration.getSplitStream(0)
```

> **When to use**: Any absorber with a glycol-related name ("glyc", "teg",
> "dehydrat") should be modeled as a ComponentSplitter rather than a
> DistillationColumn. This avoids solver convergence issues and is the
> standard pattern for production platform models.

### Pump

```java
Pump pump = new Pump("P-100", liquidStream);
pump.setOutletPressure(20.0);           // bara
pump.setIsentropicEfficiency(0.75);     // 0-1
Stream out = pump.getOutletStream();
// After run: pump.getPower("kW")
```

**Three operating modes:**
1. **Isentropic (default):** PS flash → isentropic enthalpy → divide by efficiency → PH flash
2. **Fixed outlet temperature:** `pump.setOutletTemperature(40.0, "C")` → back-calculates power
3. **Pump chart:** `pump.getPumpChart()` → head, efficiency, NPSH curves

### Pipeline

```java
AdiabaticPipe pipe = new AdiabaticPipe("Pipeline", stream);
pipe.setLength(50000.0);   // meters
pipe.setDiameter(0.508);   // meters (20 inch)
Stream out = pipe.getOutletStream();
```

### Route-Level Piping From STID/E3D Line Lists

For route pressure-drop tasks based on STID P&IDs, E3D exports, stress
isometrics, or line-list tables, prefer `PipingRouteBuilder` over manually
creating many pipe units. It creates a serial `ProcessSystem` with one
`PipeBeggsAndBrills` unit per segment and stores explicit material connection
metadata.

```java
PipingRouteBuilder route = new PipingRouteBuilder()
    .setDefaultPipeWallRoughness(45.0, "micrometer")
    .setMinorLossFrictionFactor(0.02)
    .addSegment("S1", "Manifold", "Valve Station", 100.0, "m", 0.2, "m")
    .setSegmentWallThickness("S1", 8.0, "mm")
    .addMinorLoss("S1", "manual valve", 1.0)
    .addSegment("S2", "Valve Station", "Compressor Scrubber", 25.0, "m", 8.0, "inch")
    .addMinorLoss("Valve Station->Compressor Scrubber", "long-radius bend", 0.3);

ProcessSystem routeProcess = route.build(feedStream);
routeProcess.run();
String routeJson = route.toJson();
```

To embed the extracted route in a larger flowsheet, add it to the existing
`ProcessSystem` and use the returned outlet stream as the inlet to downstream
equipment:

```java
ProcessSystem process = new ProcessSystem("Full plant process");
process.add(feedStream);
StreamInterface routeOutlet = route.addToProcessSystem(process, feedStream);
Cooler downstreamCooler = new Cooler("Downstream cooler", routeOutlet);
process.add(downstreamCooler);
process.run();
```

If the route starts from an upstream equipment outlet, use the overload with
source-equipment metadata: `route.addToProcessSystem(process, sep.getGasOutStream(),
"HP Sep", "gasOut")`.

Always preserve source document/page/row references in the task notes and export
`route.toJson()` in the task results so later STID work can reuse the route.

### Recycle (Detailed)

Recycles enable iterative convergence of process loops. The `ProcessSystem`
automatically detects and iterates recycles up to 100 times.

```java
// 1. Create placeholder stream with estimated conditions
Stream placeholder = new Stream("recycle estimate", fluidGuess.clone());
placeholder.setFlowRate(estimatedFlow, "kg/hr");
placeholder.setTemperature(estimatedT, "C");
placeholder.setPressure(estimatedP, "bara");
process.add(placeholder);

// 2. Build downstream equipment using the placeholder as input
Mixer mixer = new Mixer("recycle mixer");
mixer.addStream(mainFeed);
mixer.addStream(placeholder);       // ← placeholder used here
process.add(mixer);
// ... more equipment in the loop ...

// 3. Create Recycle that connects actual outlet back to placeholder
Recycle recycle = new Recycle("RCY-1");
recycle.addStream(actualOutletStream);    // downstream end of loop
recycle.setOutletStream(placeholder);      // connects back to start
recycle.setTolerance(1e-3);               // tighter than default 1e-2
process.add(recycle);
```

**Convergence tuning:**
```java
recycle.setFlowTolerance(1e-3);          // flow convergence (default 1e-2)
recycle.setTemperatureTolerance(1e-3);   // temperature convergence
recycle.setCompositionTolerance(1e-3);   // composition convergence
recycle.setPriority(50);                 // lower = solved first (default 100)
recycle.setAccelerationMethod("Wegstein"); // or "Direct Substitution", "Broyden"
```

**Priority-based nesting:** Set lower priority numbers on inner recycle loops.
The `RecycleController` solves lower-priority recycles first, then higher.
ProcessSystem hard cap: 100 iterations (not user-configurable).

### Adjuster

```java
Adjuster adjuster = new Adjuster("Adj");
adjuster.setAdjustedVariable(equipment, "methodName");
adjuster.setTargetVariable(stream, "methodName", targetValue);
```

## ProcessSystem Assembly

```java
ProcessSystem process = new ProcessSystem();
process.add(feed);
process.add(separator);
process.add(compressor);
process.add(cooler);
process.run();  // Run ONCE after adding all equipment
```

For multi-area plants, use `ProcessModel` to combine multiple `ProcessSystem` instances (see below).

## ProcessModel — Combining Multiple Process Areas (MANDATORY for Large Plants)

For large process plants (platforms, refineries, gas plants), split the model into
separate `ProcessSystem` objects per process area, then combine them into a single
`ProcessModel`. **NEVER try to add a ProcessModule or ProcessSystem to another
ProcessSystem** — use `ProcessModel` as the top-level container.

### Architecture Pattern (from reference platform models)

```
ProcessModel ("Gas Platform")                ← TOP-LEVEL CONTAINER
  ├── ProcessSystem ("well process")          ← Well feed & manifold
  ├── ProcessSystem ("separation train A")    ← HP/LP separation
  ├── ProcessSystem ("separation train B")    ← HP/LP separation
  ├── ProcessSystem ("TEX process A")         ← Turbo-expander
  ├── ProcessSystem ("TEX process B")         ← Turbo-expander
  ├── ProcessSystem ("export compressor A")   ← Gas compression
  ├── ProcessSystem ("export gas")            ← Gas export pipeline
  └── ProcessSystem ("export oil")            ← Oil export
```

### Java Example

```java
// Each area is its own ProcessSystem
ProcessSystem wellProcess = new ProcessSystem();
wellProcess.add(wellFeed);
wellProcess.add(manifold);
wellProcess.add(splitter);

ProcessSystem separationA = new ProcessSystem();
separationA.add(new Heater("HP heater", splitter.getSplitStream(0)));
separationA.add(new ThreePhaseSeparator("1st stage", ...));
// ... more equipment

ProcessSystem compressionA = new ProcessSystem();
compressionA.add(new Compressor("export comp",
    separationA.getUnit("gas mixer").getOutletStream()));  // cross-ref

// Combine into ProcessModel
ProcessModel plant = new ProcessModel();
plant.add("well process", wellProcess);
plant.add("separation train A", separationA);
plant.add("export compressor A", compressionA);
plant.run();  // Iterates until all converge

// Access equipment by process area
plant.get("separation train A").getUnit("1st stage separator");

// Convergence info
System.out.println(plant.getConvergenceSummary());
System.out.println(plant.getMassBalanceReport());
```

### Python Example (Recommended Pattern)

The reference model uses **functions** that return ProcessSystem objects:

```python
def create_well_feed_model(inp):
    well_process = neqsim.process.processmodel.ProcessSystem()
    feed = Stream("feed", fluid)
    feed.setFlowRate(inp.flow_rate, "kg/hr")
    well_process.add(feed)
    splitter = Splitter("manifold", feed)
    splitter.setSplitFactors([0.5, 0.5])
    well_process.add(splitter)
    return well_process

def create_separation_process(inp, feed_stream):
    sep_process = neqsim.process.processmodel.ProcessSystem()
    separator = ThreePhaseSeparator("1st stage", feed_stream)  # cross-ref!
    sep_process.add(separator)
    # ... more equipment
    return sep_process

# Build and run each area
well_model = create_well_feed_model(params)
well_model.run()

sep_train_A = create_separation_process(params,
    well_model.getUnit("manifold").getSplitStream(0))  # cross-system stream
sep_train_A.run()

# Combine into ProcessModel
ProcessModel = jneqsim.process.processmodel.ProcessModel
plant = ProcessModel()
plant.add("well process", well_model)
plant.add("separation train A", sep_train_A)
plant.run()  # Iterates until convergence

print(plant.getConvergenceSummary())
print(plant.getMassBalanceReport())
```

### ProcessModel Key Features

| Feature | Method |
|---------|--------|
| Add named sub-process | `add("name", processSystem)` |
| Get sub-process | `get("name")` |
| Remove sub-process | `remove("name")` |
| Run all (iterates to convergence) | `run()` |
| Run single step | `runStep()` |
| Run in background thread | `runAsTask()` returns `Future` |
| Check convergence | `isModelConverged()`, `getConvergenceSummary()` |
| Mass balance report | `getMassBalanceReport()`, `getFailedMassBalanceReport()` |
| Validation | `validateSetup()`, `validateAll()`, `getValidationReport()` |
| Execution analysis | `getExecutionPartitionInfo()` |
| Set convergence tolerance | `setTolerance(1e-4)` or individual `setFlowTolerance()` etc. |
| Save/load model | `saveToNeqsim("file.neqsim")`, `loadFromNeqsim("file.neqsim")` |
| JSON report | `getReport_json()` |
| Automation facade | `getAutomation()` returns `ProcessAutomation` (string-addressable variables) |
| Lifecycle state | `ProcessModelState.fromProcessModel(plant)`, `.saveToFile()`, `.compare(v1, v2)` |

### Cross-System Stream Sharing

Streams cross sub-system boundaries by **direct object reference**:
- Equipment in System B takes an outlet stream from System A as a constructor argument
- `ProcessModel.run()` executes systems in insertion order
- System A populates its outlet streams BEFORE System B reads from them
- **Order of `add()` calls matters** — add upstream systems first

### ProcessModel vs ProcessModule vs ProcessSystem

| Class | Purpose | Use When |
|-------|---------|----------|
| `ProcessSystem` | Single process area with equipment | Always — the basic building block |
| `ProcessModel` | **Named** collection of ProcessSystems with convergence tracking | Multi-area plants (platforms, gas plants) |
| `ProcessModule` | Legacy container for ProcessSystems | Backward compatibility only — prefer ProcessModel |

**NEVER** add a `ProcessModule` or `ProcessModel` to a `ProcessSystem` — it will throw `TypeError`.

## Key Rules

- **Clone fluids** before branching: `fluid.clone()` to avoid shared-state bugs
- Equipment constructors take `(String name, StreamInterface inlet)`
- Connect equipment via outlet streams — don't create separate streams
- Add equipment to `ProcessSystem` in topological order
- Call `process.run()` only ONCE after building the entire flowsheet
- **For multi-area plants**: use `ProcessModel` to combine `ProcessSystem` objects — never nest them

## Automation API (String-Addressable Variables)

Use `ProcessAutomation` for agent-friendly variable access — no Java class navigation needed.

### Setup and Discovery

```java
ProcessAutomation auto = process.getAutomation();   // or plant.getAutomation()
List<String> units = auto.getUnitList();             // ["Feed Gas", "HP Sep", ...]
List<SimulationVariable> vars = auto.getVariableList("HP Sep");
// Each variable: address, name, type (INPUT/OUTPUT), defaultUnit, description
String eqType = auto.getEquipmentType("HP Sep");     // "Separator"
```

### Read / Write Variables

```java
// Read with unit conversion (dot-notation addressing)
double temp = auto.getVariableValue("HP Sep.gasOutStream.temperature", "C");
double flow = auto.getVariableValue("HP Sep.gasOutStream.flowRate", "kg/hr");

// Write INPUT variables, then re-run
auto.setVariableValue("Compressor.outletPressure", 150.0, "bara");
process.run();
```

### Multi-Area Addressing

```java
ProcessAutomation plantAuto = plant.getAutomation();
List<String> areas = plantAuto.getAreaList();
// Area-qualified: "Area::Unit.property"
double t = plantAuto.getVariableValue("Separation::HP Sep.gasOutStream.temperature", "C");
```

## Lifecycle State (Save / Restore / Compare)

JSON snapshots for reproducibility and version tracking.

```java
// Save
ProcessSystemState state = ProcessSystemState.fromProcessSystem(process);
state.setName("Gas Processing"); state.setVersion("1.0.0");
state.saveToFile("model_v1.json");

// Load and validate
ProcessSystemState loaded = ProcessSystemState.loadFromFile("model_v1.json");
assert loaded.validate().isValid();

// Multi-area
ProcessM

…(truncated)
