# Neqsim Unisim Reader

> Reads Honeywell UniSim Design / Aspen HYSYS .usc files via COM automation and converts them to NeqSim ProcessSystem / ProcessModule structures. USE WHEN: a user has UniSim/HYSYS simulation files and wants to recreate or compare the model in NeqSim. Covers COM API navigation, column AttachedFeeds/AttachedProducts connectivity, component mapping, E300 fluid transfer, operation-handler registry strategy, topology reconstruction, sub-flowsheet handling, batch regression against the UniSim sample library, and result verification.

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

---


# UniSim Design / HYSYS → NeqSim Conversion Skill

Convert Honeywell UniSim Design (.usc) files into NeqSim ProcessSystem or
ProcessModule structures using Windows COM automation.

## Prerequisites

- **Windows only** — UniSim Design must be installed (COM server)
- **Python packages**: `pywin32` (`pip install pywin32`)
- **UniSim Design R510+** (R460+ should also work)
- COM ProgID: `UnisimDesign.Application`

## Core Module

The `devtools/unisim_reader.py` module provides three main classes:

| Class | Purpose |
|-------|---------|
| `UniSimReader` | Opens .usc files via COM, extracts all data |
| `UniSimToNeqSim` | Converts extracted model to NeqSim JSON builder format or standalone Python code |
| `UniSimComparator` | Compares UniSim vs NeqSim results for verification |

Supporting tools:

| Tool | Purpose |
|------|---------|
| `devtools/unisim_batch_check.py` | Convert a whole corpus, one subprocess per case; timings, unmapped types, warnings, compile + undefined-name checks |
| `devtools/unisim_run_generated.py` | Execute every generated model in its own subprocess and report how far it gets |
| `devtools/unisim_probe_ops.py` | Dump the readable COM properties of an operation type |
| `devtools/unisim_probe_column.py` | Dump a column's attached streams and `ColumnFlowsheet` internals |
| `devtools/test_unisim_outputs.py` | Pure-Python regression tests (no COM needed) |

---

## 1. UniSim COM Object Model

UniSim's COM automation exposes the following hierarchy:

```
Application
├── SimulationCases
│   └── Case
│       ├── Solver (CanSolve, Converge)
│       ├── BasisManager
│       │   └── FluidPackages[]
│       │       ├── name, PropertyPackageName
│       │       └── Components[]
│       │           └── name
│       └── Flowsheet (main)
│           ├── MaterialStreams[]
│           │   ├── name
│           │   ├── Temperature.GetValue("C")
│           │   ├── Pressure.GetValue("bar")
│           │   ├── MassFlow.GetValue("kg/h")
│           │   ├── MolarFlow.GetValue("kgmole/h")
│           │   ├── MassDensity.GetValue("kg/m3")
│           │   ├── MolecularWeight.GetValue()
│           │   ├── VapourFraction.GetValue()
│           │   ├── MassEnthalpy.GetValue("kJ/kg")
│           │   ├── ComponentMolarFraction.GetValues()
│           │   └── ComponentMolarFraction.SetValues([...])
│           ├── EnergyStreams[]
│           │   ├── name
│           │   └── HeatFlow.GetValue("kW")
│           ├── Operations[]
│           │   ├── name, TypeName
│           │   ├── Feeds[] (multi-feed ops: mixers, separators)
│           │   ├── Products[] (multi-product ops: tee/splitter)
│           │   ├── FeedStream / ProductStream (single-stream ops)
│           │   ├── Product (singular, for mixers)
│           │   ├── VapourProduct, LiquidProduct, WaterProduct (separators)
│           │   ├── EnergyFeeds[], EnergyProducts[]
│           │   └── Type-specific: DutyValue, AdiabaticEfficiency,
│           │       PolytropicEfficiency, PressureDrop, Length, Diameter
│           └── Flowsheets[] (sub-flowsheets, recursive)
```

### CRITICAL: Operation Connectivity Patterns

UniSim COM uses **different** property names for different operation types.
You MUST check multiple patterns to extract feed/product connections:

| Operation Type | Feed Source | Product Source |
|---|---|---|
| **compressor** | `FeedStream` (single) | `ProductStream` (single) |
| **coolerop / heaterop** | `FeedStream` (single) | `ProductStream` (single) |
| **valveop** | `FeedStream` (single) | `ProductStream` (single) |
| **recycle** | `FeedStream` (single) | `ProductStream` (single) |
| **pumpop / expandop** | `FeedStream` (single) | `ProductStream` (single) |
| **mixerop** | `Feeds[]` (array) | `Product` (singular!) |
| **teeop** | `FeedStream` (single) | `Products[]` (array) |
| **flashtank** | `Feeds[]` (array) | `VapourProduct`, `LiquidProduct` |
| **sep3op** | `Feeds[]` (array) | `VapourProduct`, `LiquidProduct`, `WaterProduct` |
| **heatexop** | Has shell-side / tube-side sub-objects | |
| **columns** (see below) | `AttachedFeeds[]` | `AttachedProducts[]` |

**WARNING**: `op.Products` does NOT exist on mixers and separators — it throws
`AttributeError`. You must use `op.Product` (singular) for mixers and
`op.VapourProduct` / `op.LiquidProduct` for separators.

### CRITICAL: Columns use AttachedFeeds / AttachedProducts

A UniSim column (`distillation`, `columnop`, `absorber`, `reboiledabsorber`,
`refluxedabsorber`, `ratedistillation`) exposes **none** of `Feeds`,
`FeedStream`, `Products`, `Product` or `ProductStream` — every one raises
`AttributeError`. Its external connections live on `AttachedFeeds` /
`AttachedProducts`, and those collections **mix material and energy streams**:

```text
DePropanizer (TUTOR1, type=distillation)
  AttachedFeeds     ['TowerFeed', 'RebDuty']            <- RebDuty is ENERGY
  AttachedProducts  ['LiquidProd', 'Ovhd', 'CondDuty']  <- CondDuty is ENERGY
```

Because the generic feed extractor found nothing, **every column in every case
was silently dropped from the converted flowsheet** (an unfed NeqSim column
throws on `run()`, so the converter skipped it). Always extract columns through
their own path.

The column's `ColumnFlowsheet` supplies everything else:

```python
cfs = column_op.ColumnFlowsheet
cfs.EnergyStreams      # ['CondDuty', 'RebDuty'] -> classify material vs energy
cfs.MaterialStreams    # incl. the external feed/products; read VapourFraction,
                       # Pressure off these items
cfs.RefluxRatio        # 0.9999 — direct scalar, no Specifications parsing
cfs.Specifications     # ['Reflux Ratio', 'Propane Fraction', 'Ovhd Vap Rate', ...]
cfs.Operations         # ['Main TS', 'Condenser', 'Reboiler']
#   traysection        -> NumberOfTrays=10, FeedStages=['5__Main TS']
#   partialcondenser / totalcondenser / condenser3op -> hasCondenser
#   bpreboiler                                       -> hasReboiler
```

**Product order is not physical.** `AttachedProducts` lists bottoms before
overhead in TUTOR1, but the converter maps product index 0 to the column gas
outlet. Classify instead:

- distillate = condenser internal's `AttachedProducts` ∩ column products
- bottoms = reboiler internal's `AttachedProducts` ∩ column products
- anything left over (and absorber feeds) is ordered by `VapourFraction`,
  vapour-rich first — an absorber's gas feed must be index 0

`CondenserPressure` / `ReboilerPressure` do **not** exist on the operation; read
top/bottom pressure off the distillate/bottoms product streams instead.

**Tray-index translation.** UniSim numbers tray-section stages from the TOP
(stage 1 = top tray). NeqSim numbers trays from the BOTTOM, with index 0 the
reboiler when present and the condenser last, and the constructor argument
excludes reboiler/condenser:

```text
neqsim_index = (1 if hasReboiler else 0) + (n_trays - unisim_stage)
TUTOR1: n_trays=10, stage 5  ->  tray 6 of DistillationColumn(name, 10, True, True)
```

An absorber has neither, so gas enters tray `0` and lean solvent tray `n-1`.
Feeding at `n` throws `IllegalArgumentException: Feed tray index must be
between 0 and n-1`.

The recommended extraction order (as implemented in `unisim_reader.py`):
1. Try `Feeds[]` array first (multi-feed ops)
2. Fall back to `FeedStream` (single-stream ops)
3. Try `Products[]` array first (multi-product ops)
4. Try `VapourProduct` / `LiquidProduct` / `WaterProduct` (separators)
5. Try `Product` singular (mixers)
6. Fall back to `ProductStream` (single-stream ops)
```

### Key COM Patterns

```python
import win32com.client
import time

# Start UniSim
app = win32com.client.dynamic.Dispatch('UnisimDesign.Application')
app.Visible = True  # or False for headless

# Open a case
case = app.SimulationCases.Open(r'C:\path\to\file.usc')
time.sleep(3)  # Wait for loading

# Pause solver during extraction
solver = case.Solver
solver.CanSolve = False

# Access data
fs = case.Flowsheet
stream = fs.MaterialStreams.Item(0)
temp_C = stream.Temperature.GetValue('C')
pres_bar = stream.Pressure.GetValue('bar')
flow_kgh = stream.MassFlow.GetValue('kg/h')
comp_fracs = stream.ComponentMolarFraction.GetValues()

# Unit operations
op = fs.Operations.Item(0)
op_type = op.TypeName  # e.g. "compressor", "valveop", "sep3op"
op_name = op.name

# Feed/product streams — varies by operation type!
# For single-stream ops (compressor, valve, cooler, pump, heater):
feed_name = op.FeedStream.name
prod_name = op.ProductStream.name

# For mixers: Feeds[] array + Product singular
for i in range(op.Feeds.Count):
    feed_name = op.Feeds.Item(i).name
prod_name = op.Product.name  # singular!

# For separators: Feeds[] + VapourProduct / LiquidProduct
for i in range(op.Feeds.Count):
    feed_name = op.Feeds.Item(i).name
vap_name = op.VapourProduct.name
liq_name = op.LiquidProduct.name

# For tee/splitter: FeedStream + Products[]
feed_name = op.FeedStream.name
for i in range(op.Products.Count):
    prod_name = op.Products.Item(i).name

# Close
case.Close()
app.Quit()
```

### Important Notes

- Always call `solver.CanSolve = False` before reading to prevent recalculation
- UniSim uses -32767 for empty/unset values — filter these out
- Property values accessed via `.GetValue(unit_string)`
- Composition accessed via `.GetValues()` returning a sequence

### GOTCHA: the UniSim COM server is a SINGLETON

`Dispatch('UnisimDesign.Application')` attaches to the **one** running UniSim
instance, it does not start a private one. Consequences:

- Running a second COM script while a batch runs gives
  `com_error: The RPC server is unavailable`.
- `reader.close()` calls `app.Quit()`, which closes UniSim for **every** other
  script using it.
- **Never run two UniSim COM scripts concurrently.** Subprocess isolation makes
  a crash survivable, but it does not make concurrency safe.

### Prefer readiness polling over fixed sleeps

UniSim needs an unpredictable time to publish its automation object model. A
fixed `time.sleep(3)` is both slower than needed on small cases and unsafe on
large ones. `UniSimReader._wait_ready(probe, timeout, description)` polls a
cheap COM property instead (`app.SimulationCases.Count`,
`case.Flowsheet.MaterialStreams.Count`). Removing the two blind sleeps cut the
TUTOR1 read from 18.3 s to 6.8 s and the 66-case corpus from 816 s to 656 s.

### A refused Open is usually a missing module, not a bad file

`SimulationCases.Open` raises a bare `com_error ... E_ACCESSDENIED
(-2147024891)` for cases needing a UniSim extension that is not installed (the
R510 `CCC Series 5\*` controls cases and the EO electrical case). The files are
not read-only — verified. `UniSimReader._open_case()` retries once on a fresh
session (which does fix a genuinely wedged shared session) and then raises a
`RuntimeError` naming the likely cause.

### 1.1 Extracting Binary Interaction Parameters (BIPs / kij)

UniSim stores the full BIP (kij) matrix on the PropertyPackage object. The
correct COM access pattern is:

```python
fp = case.Flowsheet.FluidPackage
pp = fp.PropertyPackage

kij_obj = pp.Kij          # Returns CDispatch (RealFlexVariable)
raw = kij_obj.Values      # Returns tuple-of-tuples (n×n matrix)

# IMPORTANT: The .Values property returns the full symmetric matrix.
# Diagonal values are -32767.0 (sentinel for "self-interaction").
# Replace with 0.0 when parsing.
n = fp.Components.Count
comp_names = [fp.Components.Item(i).Name for i in range(n)]

bic = []
for i in range(n):
    row = []
    for j in range(n):
        val = float(raw[i][j])
        if abs(val + 32767.0) < 1.0:  # diagonal sentinel
            val = 0.0
        row.append(val)
    bic.append(row)
```

**Key discoveries:**
- `pp.Kij` returns a `CDispatch` (UniSim RealFlexVariable), NOT a Python-iterable
- `kij_obj.Values` is the correct access pattern — returns tuple-of-tuples
- `kij_obj.GetValues()` fails with "Invalid number of parameters"
- `kij_obj.__call__(i,j)` fails with "Does not support a collection"
- `pp.GetInteractionParameter(i,j)` returns 0.0 for PR-LK (correlation-based
  BIPs are stored internally, not as user-defined parameters)
- The matrix is symmetric: `kij[i][j] == kij[j][i]`
- For PR-LK, BIPs are generated from the Lee-Kesler correlation — they are
  non-zero even if never explicitly tuned by the user

**Common BIP patterns (PR-LK, hydrocarbon system):**
- H2O–HC: +0.48 to +0.50 (strong positive interaction)
- H2O–N2: -2.24 (strong negative)
- H2O–CO2: -0.56
- CO2–C1: +0.105
- N2–C1: +0.025
- HC–HC (light–heavy): small values, typically < 0.01

### 1.2 Extracting Component Thermodynamic Properties

For pseudo-components and library components, extract critical properties
and other thermodynamic data:

```python
fp = case.Flowsheet.FluidPackage
pp = fp.PropertyPackage

n = fp.Components.Count
for i in range(n):
    comp = fp.Components.Item(i)
    name = comp.Name
    mw = comp.MolecularWeight.GetValue()
    # Prefer native UniSim units and convert explicitly.
    # Some COM surfaces return misleading values when requesting alternate units.
    tc = comp.CriticalTemperature.GetValue("C") + 273.15   # K
    pc = comp.CriticalPressure.GetValue("kPa") * 0.01      # bara
    nbp = comp.NormalBoilingPt.GetValue("C") + 273.15      # K
    vc = comp.CriticalVolume.GetValue("m3/kgmole") # m3/kmol
    # Acentric factor: the UniSim COM attribute is `Acentricity` (NOT
    # `AcentricFactor`). Only fall back to Edmister if it is absent.
    omega = comp.AcentricityValue
```

**Notes:**
- UniSim component collections can be 0-based or 1-based depending on the COM
   collection surface. If `Components.Item(i)` fails, retry `Components.Item(i+1)`
   before dropping the component.
- For component critical properties, request `CriticalTemperature` and
   `NormalBoilingPoint` in `C` first and convert to K; request `CriticalPressure`
   in `kPa` first and convert to bara. Sanity-check known components after export:
   methane Tc ≈ 190.7 K and Pc ≈ 46.4 bara; water Tc ≈ 647.3 K and Pc ≈ 221 bara.
- **Acentric factor: read `comp.Acentricity` / `comp.AcentricityValue`.** This is
   the attribute UniSim actually exposes; `AcentricFactor` / `Omega` do NOT exist on
   the component COM surface. Reading the Edmister estimate instead silently changes
   the EOS alpha function and shifts every bubble point / TVP of the converted fluid
   (measured: 12–15 % low on a 23-component SRK-Peneloux oil, and `0.0` for the
   heaviest pseudos where the Edmister value exceeded the `omega > 2` sanity guard).
   With `Acentricity` read correctly, a NeqSim E300 round-trip reproduces UniSim
   bubble points to < 0.5 %. Keep the property-package vectors
   (`Acentricity`, `AcentricFactor`, `Omega`, `ACF`) and Edmister only as fallbacks.
- Parachor can sometimes be read via `pp.Parachor.Values` (same pattern as Kij).
- Volume shift: `pp.VolumShift.Values` (note the UniSim spelling: "VolumShift",
  not "VolumeShift").

### 1.2.1 Reading TVP / RVP (Cold Properties) off a stream

Vapour-pressure results live on the **material stream**, in **kPa** (temperatures
in **°C**). Missing values return the sentinel `-32767`.

```python
s = case.Flowsheet.MaterialStreams.Item(0)
s.TrueVPValue                 # TVP, evaluated at 37.8 C
s.RVP_37_8_DegCValue          # Reid VP at 37.8 C
s.RVPASTM_D323_73_79Value     # ASTM / API RVP correlations
s.RVPAPI_5B_1_1Value, s.RVPAPI_5B_1_2Value
cp = s.ColdProperty           # Cold Properties utility
cp.TrueVapourPressureValue, cp.ReidVapourPressureValue
cp.FlashPointValue, cp.PourPointValue, cp.D86CurveValue
```

For TVP at **any other temperature** (e.g. a 30 °C export spec), flash a duplicate —
this never touches the case:

```python
f = s.DuplicateFluid()
f.TVFlash(30.0, 0.0)          # (T [C], vapour fraction) -> bubble point
tvp_bara = f.PressureValue * 0.01
```

Water-free basis: assign a renormalised `f.MolarFractionsValue` with H2O zeroed
*before* the flash (the setter works); `f.MassFractionsValue` gives water wt%
directly. Free water only adds its own saturation pressure (~0.04 bara at 30 °C).

> **NeqSim comparison gotcha:** `bubblePointPressureFlash` is not three-phase aware.
> With free water in the feed it returns a grossly inflated TVP (measured 5–6.5 bara
> instead of 2.2) because water is treated as dissolved in the oil, and
> `getPhase("oil")` then returns the aqueous phase. Strip water first, or use
> `Standard_ASTM_D6377`'s `VPCR4_no_water` / `RVP_ASTM_D323_73_79` variants.
>
> A TVP/RVP ratio far above ~1.3 is usually **physical**, not an error: a bubble
> point is hypersensitive to trace dissolved light ends (N2/C1/CO2) while the
> V/L = 4, 80 vol %-vaporised RVP test is not. It signals incompletely stabilised oil.

### 1.3 Generating E300 Fluid Files from UniSim Data

To create an Eclipse E300-format fluid file from UniSim-extracted properties
for loading into NeqSim via `EclipseFluidReadWrite.read()`:

**Required E300 sections** (NeqSim reader will crash without these):
- `CNAMES` — component names
- `TCRIT` — critical temperatures (K)
- `PCRIT` — critical pressures (bar/bara as used by NeqSim's E300 writer)
- `ACF` — acentric factors
- `MW` — molecular weights (g/mol)
- `TBOIL` — normal boiling points (K)
- `VCRIT` — critical volumes (m3/kg-mol)
- `PARACHOR` — parachor values (if unknown: `4.0 * MW^0.77`)
- `SSHIFT` — volume shift parameters (can be all zeros)
- `BIC` — binary interaction coefficients (lower triangular)
- `ZI` — mole fractions

**Optional E300 sections** (NeqSim supports these):
- `BICS` — volume-corrected BICs at surface conditions (parsed but same format as BIC)
- `OMEGAA` — per-component OmegaA override values (one value per line + `/` terminator)
- `OMEGAB` — per-component OmegaB override values (same format)
- `SSHIFTS` — volume shift at surface conditions (same format as SSHIFT)
- `PEDERSEN` — keyword (no values) → activates Pedersen viscosity correlation

**EOS Selection via E300:**
- `EOS\nSRK /` → `SystemSrkEos`
- `EOS\nPR /\nPRCORR` → `SystemPrEos1978` (PR1978 correction)
- `EOS\nPR /\nPRLKCORR` → **`SystemPrLeeKeslerEos`** (PR-LK, PR76 alpha for all ω)
- `EOS\nPR /` → `SystemPrEos` (base PR)

**CRITICAL**: If the BIC section is omitted, NeqSim's `EclipseFluidReadWrite`
will crash with a NullPointerException. Always include BIC, even if all zeros.

**Loading with a forced EOS (ignores EOS keyword in file):**
```python
from neqsim import jneqsim
EclipseFluidReadWrite = jneqsim.thermo.util.readwrite.EclipseFluidReadWrite
SystemPrLeeKeslerEos = jneqsim.thermo.system.SystemPrLeeKeslerEos

# Force PR-LK regardless of EOS in file
fluid = SystemPrLeeKeslerEos(288.15, 1.01325)
fluid = EclipseFluidReadWrite.read(e300_path, fluid)
```

**⚠️ CRITICAL WARNING — Water BIPs and OmegaA:**

When water is present, the water–hydrocarbon BIPs (kij) interact dangerously
with OmegaA. **NEVER mix BIP conventions with OmegaA modifications:**

- Standard E300 BIPs for water–HC are typically **+0.48 to +0.50**
- PR-LK correlation BIPs for H2O–N2 are typically **−2.24** (negative!)
- H2O–CO2 is typically **−0.557**, H2O–H2S is typically **−0.390**

These negative BIPs **intentionally** increase cross-attraction and keep water
in the liquid phase. If you simultaneously set `OMEGAA` for water to a
non-standard value (e.g., 0.42748), the phase behavior will be wrong —
HP Separator vapour fraction can jump from 0.41 to 0.81 (catastrophic).

**Rule**: Only use `OMEGAA` for water together with its matched BIP set.
Default (no OMEGAA section) is safer unless you have a PVTsim-generated file
that was specifically fitted with both OMEGAA and BICs together.

**NeqSim E300 component name mapping:**
| E300 Name | NeqSim Maps To |
|-----------|---------------|
| `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 others | TBP pseudo-fraction (via `addTBPfraction()`) |

**Note:** Aromatics (Benzene, Toluene, E-Benzene, m-Xylene, etc.) are NOT in
NeqSim's E300 recognized name map — they will be treated as TBP pseudo-fractions
with estimated density.

### E300 Fluid Export (DEFAULT — Recommended Route)

**The E300 export route is the default and preferred method for transferring
fluid definitions from UniSim to NeqSim.** It preserves all critical properties
(Tc, Pc, acentric factor, MW, BIPs, volume shifts, parachors) for both standard
and hypothetical/pseudo components — including C7+ fractions that cannot be
accurately recreated by component name mapping alone.

When `UniSimReader.read()` is called with `export_e300=True` (the default),
it extracts critical properties from each component in each fluid package via COM,
then writes an E300 file per fluid package to the output directory.

**COM properties extracted per component:**
- `component.CriticalTemperature` → Tc (request C first, convert to K)
- `component.CriticalPressure` → Pc (request kPa first, convert to bara)
- `component.AcentricFactor` / package vector / Edmister fallback → omega
- `component.MolecularWeight` → MW (g/mol)
- `component.NormalBoilingPoint` → Tboil (request C first, convert to K)
- `component.CriticalVolume` → Vcrit (m³/kgmol)

**BIPs extracted via:** `FluidPackage.PropertyPackage.GetBIP(i, j)` or
`PropertyPackage.BinaryInteractionParameters` (matrix fallback).

**E300 file format keywords:**
`METRIC`, `NCOMPS`, `EOS`, `PRCORR`, `RTEMP`, `STCOND`, `CNAMES`, `TCRIT`,
`PCRIT`, `ACF`, `MW`, `TBOIL`, `VCRIT`, `SSHIFT`, `PARACHOR`, `ZI`, `BIC`

**NeqSim loading:** Use `EclipseFluidReadWrite.read(e300Path)` in Java, or
via Python: `jneqsim.thermo.util.readwrite.EclipseFluidReadWrite.read(path)`.

**Automatic integration:** When `build_and_run()` detects an E300 file in the
fluid section, it loads the fluid via `EclipseFluidReadWrite.read()` and passes
it to `ProcessSystem.fromJsonAndRun(json, fluid)`, bypassing component name
mapping entirely.

```python
# Example: Full E300 workflow
reader = UniSimReader()
model = reader.read(r'C:\path\to\model.usc')  # auto-exports E300 files

# E300 files now available:
for fp in model.fluid_packages:
    print(f"  {fp.name}: {fp.e300_file_path}")

# Convert to NeqSim and run — E300 fluid used automatically
converter = UniSimToNeqSim(model)
result = converter.build_and_run()
```

---

## 2. Operation Type Mapping

UniSim internal operation type names (from `op.TypeName`) mapped to NeqSim types:

### Mapping Architecture

`devtools/unisim_reader.py` uses a typed `UniSimOperationHandler` registry. Do
not add scattered local skip lists or one physical NeqSim class per UniSim name.
The registry records:

| Field | Meaning |
|-------|---------|
| `neqsim_type` | Target NeqSim type or converter pseudo-type |
| `strategy` | `native`, `adapter`, `reference`, `control`, `column_internal`, or `skip` |
| `stream_role` | `material`, `reference`, or `none` for topology reconstruction |
| `note` | Human-readable rationale written to JSON mapping summaries |

**Policy:** Native physical UniSim operations map to native NeqSim equipment.
UniSim-specific topology placeholders use `UnisimCalculator`; spreadsheets and
set/adjust logic are reference objects; controllers and logical operations do
not create material topology edges; column internals configure the column rather
than becoming standalone equipment. Generated JSON includes
`_unisim_operation_mapping` so imported cases can audit the strategy used for
each UniSim operation type present in the model.

### Core Process Equipment

| UniSim TypeName | NeqSim Type | Description |
|-----------------|-------------|-------------|
| `valveop` | `ThrottlingValve` | Pressure letdown valve, choke |
| `sep3op` | `ThreePhaseSeparator` | Three-phase separator |
| `flashtank` | `Separator` / `GasScrubber` | Two-phase separator. Auto-promoted to `ThreePhaseSeparator` if `WaterProduct` connected. Vertical orientation → `GasScrubber`. |
| `mixerop` | `Mixer` | Stream mixer/junction |
| `teeop` | `Splitter` | Stream splitter/tee |
| `compressor` | `Compressor` | Gas compressor |
| `coolerop` | `Cooler` | Cooler/aftercooler |
| `heaterop` | `Heater` | Heater/pre-heater |
| `pumpop` | `Pump` | Liquid pump |
| `expandop` | `Expander` | Turboexpander |
| `heatexop` | `HeatExchanger` | Shell-and-tube / plate HX |
| `firedheaterop` | `FiredHeater` | Fired heater / process furnace |
| `pipeseg` | `AdiabaticPipe` | Pipe segment |
| `olgapipe` | `AdiabaticPipe` | OLGA-link pipe; upgraded to `PipeBeggsAndBrills` when segment geometry is extracted |
| `sep1op` / `sep2op` | `Separator` | Separator variants |
| `pemelectrolyzer` | `Electrolyzer` | PEM electrolyzer (`setTechnology(PEM)`) |
| `alkalineelectrolyzer` | `Electrolyzer` | Alkaline electrolyzer (`ALKALINE`) |
| `soecelectrolyzer` | `Electrolyzer` | Solid-oxide electrolyzer (`SOEC`) |
| `recycle` | `Recycle` | Recycle convergence block |
| `adjust` | `Adjuster` | Process variable adjuster |
| `setop` | `SetPoint` | Set variable/propagation |
| `saturateop` | `StreamSaturatorUtil` | Stream saturator |
| `spreadsheetop` | `SpreadsheetBlock` | Spreadsheet calculator/reference block; formulas need explicit import/export cell extraction |
| `templateop` | `SubFlowsheet` / `UnisimCalculator` | Sub-flowsheet template or interface placeholder; JSON factory aliases placeholder builds to `UnisimCalculator` |
| `fluidizedcatalyticcrackertemplate` | `SubFlowsheet` | FCC template |
| `isomerizationtemplate` | `SubFlowsheet` | Isomerization template |
| `virtualstreamop` | `UnisimCalculator` | Virtual stream/topology adapter with pass-through outlet |
| `streamcutterop` | `UnisimCalculator` | Assay stream cutter; pass-through adapter |
| `fluidizedcatalyticcrackerop` | `UnisimCalculator` | FCC — no NeqSim equivalent; pass-through keeps downstream topology |
| `isomerizationreactorop` | `UnisimCalculator` | Isomerization reactor — pass-through adapter |

### Columns & Absorbers

| UniSim TypeName | NeqSim Type | Description |
|-----------------|-------------|-------------|
| `fractop` | `DistillationColumn` | Fractionation column |
| `distillation` | `DistillationColumn` | Distillation column |
| `columnop` | `DistillationColumn` | Generic column |
| `reboiledabsorber` | `DistillationColumn` | Reboiled absorber |
| `refluxedabsorber` | `DistillationColumn` | Refluxed absorber |
| `ratedistillation` | `DistillationColumn` | Rate-based column (equilibrium-stage approximation) |
| `threephasedistillation` | `DistillationColumn` | Three-phase column |
| `absorberop` | `Absorber` | Absorption column (see glycol note below) |
| `absorber` | `Absorber` | Absorber (see glycol note below) |

> **Glycol/TEG Contactor Rule**: When an Absorber operation has a name
> containing "glyc", "teg", or "dehydrat" (case-insensitive), the code
> generator produces a `ComponentSplitter` instead of a `DistillationColumn`.
> This removes water from the gas stream using the standard pattern:
> `setSplitFactors([1.0] * (N-1) + [0.0])` where water is the last component.
> Stream 0 = dry gas, stream 1 = removed water. Port resolution uses
> `split0`/`split1` instead of `gasOut`/`liquidOut`. Non-glycol absorbers
> still use `DistillationColumn`.

### Column Internals (Sub-parts, Not Standalone)

| UniSim TypeName | NeqSim Type | Description |
|-----------------|-------------|-------------|
| `partialcondenser` | `ColumnInternals` | Partial condenser (skipped) |
| `totalcondenser` | `ColumnInternals` | Total condenser (skipped) |
| `condenser3op` | `ColumnInternals` | Three-outlet condenser (skipped) |
| `traysection` | `ColumnInternals` | Tray section (skipped) |
| `bpreboiler` | `ColumnInternals` | Reboiler (skipped) |

### Reactors

| UniSim TypeName | NeqSim Type | Description |
|-----------------|-------------|-------------|
| `reactorop` | `GibbsReactor` | Generic reactor → Gibbs |
| `gibbsreactorop` | `GibbsReactor` | Gibbs reactor |
| `eqreactorop` | `GibbsReactor` | Equilibrium reactor → Gibbs |
| `equilibriumreactorop` | `GibbsReactor` | Equilibrium reactor (alternate) |
| `convreactorop` | `GibbsReactor` | Conversion reactor → Gibbs |
| `conversionreactorop` | `GibbsReactor` | Conversion reactor (alternate) |
| `pfreactorop` | `PlugFlowReactor` | Plug flow reactor |
| `kineticreactorop` | `PlugFlowReactor` | Kinetic reactor → PFR |
| `cstrop` | `StirredTankReactor` | CSTR |
| `gasifierop` / `gasifieroppy` / `gsfrxsecop` | `GibbsReactor` | Gasifier blocks; the solid (coal/biomass) feed cannot be flashed, so the fluid feed is equilibrated |

### Controllers & Logic

| UniSim TypeName | NeqSim Type | Description |
|-----------------|-------------|-------------|
| `pidfbcontrolop` | `PIDController` | PID feedback controller |
| `surgecontroller` | `SurgeController` | Surge controller (skipped) |
| `selectionop` / `fanoutop` | `LogicalOp` | Signal selector / fan-out; no material topology |
| `genesimop` / `machinelearningtoolop` | skipped | Non-physical utilities |
| `balanceop` | `UnisimCalculator` | Balance/topology adapter with pass-through outlet and source-operation metadata |
| `logicalop` | `LogicalOp` | Logic operation (skipped) |
| `selectop` | `LogicalOp` | Selector (skipped) |

### Handler Strategies and Skipped Operations

The handler registry determines whether material topology is created. The
following types are recognized but do not become standalone material equipment:

- `SurgeController` — Compressor surge control logic
- `ColumnInternals` — Sub-parts of column operations (condenser, reboiler, tray sections)
- `LogicalOp` — logical/select operations produce comments or controller metadata
- `BlowdownGeneSim` — non-physical UniSim utility operation

Use `UniSimReader.is_material_stream_operation(type_name)` when modifying
topology reconstruction. Unknown operation types are treated as material stream
operations so skipped stream-carrying blocks remain visible as warnings.

---

## 3. Component Name Mapping

UniSim component names to NeqSim database names:

| UniSim Name | NeqSim Name |
|-------------|-------------|
| `Nitrogen` | `nitrogen` |
| `CO2` | `CO2` |
| `Methane` | `methane` |
| `Ethane` | `ethane` |
| `Propane` | `propane` |
| `i-Butane` | `i-butane` |
| `n-Butane` | `n-butane` |
| `i-Pentane` | `i-pentane` |
| `n-Pentane` | `n-pentane` |
| `n-Hexane` | `n-hexane` |
| `n-Heptane` | `n-heptane` |
| `n-Octane` | `n-octane` |
| `n-Nonane` | `n-nonane` |
| `n-Decane` | `nC10` |
| `H2O` | `water` |
| `EGlycol` | `MEG` |
| `TEGlycol` | `TEG` |
| `DEGlycol` | `DEG` |
| `MeOH` | `methanol` |
| `Hydrogen` | `hydrogen` |
| `H2S` | `H2S` |
| `Oxygen` | `oxygen` |
| `Argon` | `argon` |
| `Helium` | `helium` |
| `nC11`–`nC24` | `nC11`–`nC24` |
| `Benzene` | `benzene` |
| `Toluene` | `toluene` |
| `E-Benzene` | `ethylbenzene` |
| `m-Xylene` | `m-Xylene` |
| `o-Xylene` | `o-Xylene` |
| `p-Xylene` | `p-Xylene` |
| `COS` | `COS` |
| `SO2` | `SO2` |
| `NH3` / `Ammonia` | `ammonia` |
| `Ethylene` / `Ethene` | `ethylene` |
| `Propylene` / `Propene` | `propene` |
| `1-Butene` | `1-butene` |
| `cis-2-Butene` | `c2-butene` |
| `trans-2-Butene` | `t2-butene` |
| `Isobutene` | `isobutene` |
| `Cyclohexane` | `cyclohexane` |
| `CO` / `CarbonMonoxide` | `CO` |
| `DEAmine` | `DEA` |
| `MEAmine` | `MEA` |
| `MDEAmine` | `MDEA` |
| `AceticAcid` | `acetic acid` |
| `Ethanol` | `ethanol` |
| `c-Hexane` | `c-hexane` |

**Alternate aliases**: The map also includes short-form aliases like `C1`→methane, `C2`→ethane, `N2`→nitrogen, `H2`→hydrogen, `O2`→oxygen, `Ar`→argon, `He`→helium, `iC4`→i-butane, `nC4`→n-butane, `iC5`→i-pentane, `nC5`→n-pentane, `nC6`→n-hexane, etc.

**Unmapped components**: `12C3Oxide` (propylene oxide) maps to `None` — it is not in the NeqSim database and will be skipped with a warning.

### Hypothetical Components

UniSim components ending with `*` are hypothetical (pseudo-components), e.g.:
- `C6 GRAND*`, `C7 GRAND*`, ..., `C55-C80 GRAND*`
- `251116-01*`, `251115-01*` (numbered hypos)

These require C7+ characterization in NeqSim. Strategies:
1. **Skip**: Remove hypos from composition, re-normalize known components
2. **Approximate**: Map to nearest real component by molecular weight
3. **Characterize**: Use NeqSim's `characterisePlusFraction()` with MW and density data

---

## 4. Property Package Mapping

The code variable is `PROPERTY_PACKAGE_MAP` (not EOS_MAP). It maps UniSim
property package names (including common spelling variants) to NeqSim EOS
model strings:

### Primary Mappings

| UniSim Property Package | NeqSim EOS Model | Mixing Rule | Notes |
|-------------------------|------------------|-------------|-------|
| `Peng-Robinson` / `PengRobinson` / `Peng Robinson` | `PR` | `classic` | |
| `Peng-Robinson - LK` / `Peng Robinson - LK` | `PR` | `classic` | |
| `SRK` / `Soave-Redlich-Kwong` | `SRK` | `classic` | |
| `CPA` / `CPA-SRK` | `CPA` | `10` | For polar systems (water, glycols, amines) |
| `Glycol Package` | `CPA` | `10` | Maps to CPA for MEG/TEG |
| `GERG 2008` | `GERG2008` | (built-in) | Natural gas |
| `Sour PR` / `SourPR` | `PR` | `classic` | H2S/CO2 systems |
| `Sour SRK` | `SRK` | `classic` | H2S/CO2 systems |

### Fallback Mappings (Approximated as SRK)

These UniSim packages have no direct NeqSim equivalent and fall back to `SRK`:

| UniSim Property Package | NeqSim Fallback | Notes |
|-------------------------|-----------------|-------|
| `ASME Steam` | `SRK` | Water only |
| `MBWR` | `SRK` | NeqSim has BWRS but limited components |
| `Lee-Kesler-Plocker` | `SRK` | No LKP model |
| `NRTL` / `UNIQUAC` / `UNIQUAC - Ideal` / `Wilson` | `SRK` | Activity models |
| `Zudkevitch Joffee` / `Kabadi Danner` | `SRK` | Specialized EOS |
| `Antoine` / `Chao Seader` / `Grayson Streed` | `SRK` | Legacy correlations |
| `DBR Amine Package` | `SRK` | DBR proprietary |
| `OLI` | `SRK` | Electrolyte package |
| `COMPropertyPkg` | `SRK` | COM extension package |

A warning is logged when a fallback mapping is used.

---

## 5. Workflow: From .usc File to Running NeqSim Model

### Full Mode (Default — Recommended)

All four output methods (`to_json()`, `build_and_run()`, `to_python()`,
`to_notebook()`) default to **`full_mode=True`**. This means:

1. **Sub-flowsheet auto-classification**: Sub-flowsheets are classified as
   either "process" (shares streams with the main flowsheet) or "utility"
   (isolated). Only process sub-flowsheets are included.
2. **ProcessModel architecture**: The main flowsheet and each process
   sub-flowsheet become separate `ProcessSystem` objects composed inside a
   `ProcessModel` (multi-area plant model).
3. **E300 fluid loading**: When the `UniSimReader.read(export_e300=True)`
   option was used (default), the converter uses `EclipseFluidReadWrite.read()`
   to load the fluid with exact Tc, Pc, ω, MW, and BIPs from UniSim.
4. **Recycle convergence**: Auto-generated `Recycle` objects (in `to_python()` /
   `to_notebook()`) get a **real tolerance (`recycle_tolerance`, default `1e-2`)
   plus Wegstein acceleration (`recycle_acceleration`, default `"WEGSTEIN"`)**,
   and the generated run tail **iterates** (`setRunStep(True)` loop, then a final
   run) so the tear streams actually converge. A very large tolerance (the old
   `1e6`) accepted the seeded tear on the first pass, so recycle-fed streams
   never updated and deviated strongly from UniSim. Tune via:

   ```python
   converter = UniSimToNeqSim(model)
   converter.recycle_tolerance = 1e-3        # tighter tear
   converter.recycle_acceleration = "WEGSTEIN"  # or None to disable
   python_code = converter.to_python()
   ```
5. **Unfed columns/absorbers are skipped**: A `DistillationColumn`/`Absorber`
   whose feed stream could not be extracted from UniSim (UniSim `fractop`/
   `absorber` COM connectivity is not always resolvable) is **omitted** rather
   than emitted as a bare, feed-less column. A feed-less column **throws on
   `run()` and aborts the whole `process.run()`**, so every downstream unit
   stops executing and stays at its seed value — this was the single biggest
   cause of a "runs but nothing matches" result. The skipped column is reported
   in `converter.warnings`; reconnect its feed manually to include it.

> **Verification vehicle — `to_python()` is still the most faithful path, but
> the JSON/MCP path now iterates recycles.** The JSON path (`build_and_run()` /
> `ProcessSystem.fromJsonAndRun` / MCP `runProcess`) emits recycles with only an
> `inlet` and relies on `JsonProcessBuilder`'s Pass-2 iterative wiring to close
> forward-referenced tears. Since the multi-pass auto-run fix, when the built
> process `hasRecycles()` the builder loops `process.run()` up to
> `MAX_AUTORUN_PASSES` (15), **guarding each pass** (a unit throwing on an early
> pass no longer aborts the whole auto-run) and stopping early on
> `process.solved()` — so nested/forward-referenced recycle loops seeded at ~0
> flow now get the outer passes they need to converge on the JSON/MCP path too.
> The **generated Python** (`to_python()`) additionally seeds forward-reference
> placeholders + auto-`Recycle`, so it remains the most robust vehicle for a
> recycle-heavy plant; compare with `UniSimComparator(model, process)` after
> `exec()`-ing the generated script.
>
> **Residual JSON-path failures are model-specific topology gaps, not recycle
> convergence.** If the full plant still stops (e.g. a pipe `Failed to run … —
> Total mass cannot be zero`), trace the dead branch: the cause is usually a
> genuinely external UniSim stream with no producer (a `Valve leak`-type feed),
> or a zero-feed pipe/riser fed by a scrubber whose own inlet mixer is only
> partially wired (`Mixer … wired with N of M inlets`). These need the missing
> inlet wired manually — no number of recycle passes fixes a stream that no unit
> produces. `ProcessSystem.run()` still aborts a pass at the first throwing
> unit, so a single unfeedable unit blocks everything downstream in that pass.

To disable full mode and get only the main flowsheet operations:

```python
converter = UniSimToNeqSim(model)
python_code = converter.to_python(full_mode=False)
```

### Quick Usage (Python)

```python
from devtools.unisim_reader import UniSimReader, UniSimToNeqSim, UniSimComparator

# Step 1: Read the UniSim file (export_e300=True is default)
with UniSimReader(visible=False) as reader:
    model = reader.read(r"path\to\file.usc")

# Step 2: Inspect what was extracted
print(model.summary())

# Step 3: Convert to NeqSim JSON (full_mode=True by default)
converter = UniSimToNeqSim(model)
neqsim_json = converter.to_json()

# Step 4: View warnings and assumptions
for w in converter.warnings:
    print(f"WARNING: {w}")

# Step 5: Build and run in NeqSim
import json
from neqsim import jneqsim
ProcessSystem = jneqsim.process.processmodel.ProcessSystem
result = ProcessSystem.fromJsonAndRun(json.dumps(neqsim_json))

# Check for partial success (tolerant error handling)
if result.hasWarnings():
    print(f"Warnings: {len(list(result.getWarnings()))}")
    for w in result.getWarnings():
        print(f"  [{w.getCode()}] {w.getMessage()}")

if not result.isError():
    process = result.getProcessSystem()
    print(f"Units built: {process.size()}")
```

### Generate Python Code (Human-Readable Alternative)

Instead of JSON, generate a standalone Python script with explicit `jneqsim` API calls:

```python
converter = UniSimToNeqSim(model)
python_code = converter.to_python()  # full_mode=True by default

# Save to file
with open("process.py", "w") as f:
    f.write(python_code)
print(f"Generated {len(python_code.splitlines())} lines of Python")
```

The generated script is a **complete, runnable Python file** that includes:
1. All `jneqsim` imports (thermo systems, equipment classes)
2. Fluid/EOS definition with mapped composition and mixing rule
3. Feed streams created with temperature, pressure, and flow rate from UniSim
4. All equipment in **topological order** (upstream before downstream)
5. Equipment properties set via `jneqsim` API 

…(truncated)
