neqsim-unisim-reader · diff
git:20260904.b9a920c to git:20260907.9ca9ea2
236 added, 8 removed. Audit A to A.
---
name: neqsim-unisim-reader
- description: "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, component mapping, E300 fluid transfer, operation-handler registry strategy, topology reconstruction, sub-flowsheet handling, and result verification."
- last_verified: "2026-07-04"
+ description: "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."
+ last_verified: "2026-09-07"
---
# 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
- - Use `time.sleep()` between COM calls if stability issues arise
- 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 calls (efficiency, outlet pressure, etc.)
6. Stream wiring through outlet stream references (e.g., `separator.getGasOutStream()`)
7. Sub-flowsheet operations (included by default in `full_mode=True`)
8. `process.run()` call at the end
The generated code uses **direct NeqSim Java API calls** — no JSON intermediate.
Equipment that cannot be mapped (reactors, controllers, spreadsheets) is commented
out with a skip reason. This output is ideal for:
- **Code review** — every connection is visible and auditable
- **Manual editing** — users can modify equipment parameters, add controllers
- **Learning** — shows the exact NeqSim API mapping for each UniSim operation
**Example for a large platform model:** `to_python()` generates ~850 lines
covering ~180 operations including Splitters, ThreePhaseSeparators, Compressors,
Coolers, Mixers, ThrottlingValves, and sub-flowsheet equipment.
### Generate Jupyter Notebook (Interactive Version)
The notebook uses the **exact same shared code generators** as `to_python()` —
so the resulting process logic is always identical — but wraps equipment in
separate cells with markdown explanations (descriptions, feed tables, model
overview):
```python
converter = UniSimToNeqSim(model)
converter.save_notebook("process.ipynb") # full_mode=True by default
```
Or get the raw dict (nbformat v4):
```python
nb_dict = converter.to_notebook() # full_mode=True by default
```
The notebook contains:
1. **Title & overview table** — components, operations, feed count, EOS
2. **Setup cell** — `from neqsim import jneqsim` + class aliases
3. **Fluid markdown + code** — composition table and fluid creation
4. **Feed streams markdown + code** — T/P/flow table and stream creation
5. **Equipment cells** — one markdown description + one code cell per unit
6. **Run cell** — `process.run()`
7. **Results cell** — prints T, P for every unit operation
8. **Warnings cell** — any conversion assumptions or skipped items
### Generate EOT / ProcessPilot Simulator
Generate a `BaseSimulator` subclass for the ProcessPilot-NeqSimInterface
framework (reinforcement learning / optimization):
```python
converter = UniSimToNeqSim(model)
converter.save_eot_simulator("my_simulator.py", class_name="MySimulator")
```
The generated code:
- Subclasses `eot.simulators.base_simulator.BaseSimulator`
- Uses `eot.components` factory functions (`get_stream`, `get_compressor`, etc.)
- Implements `build_process()` with all equipment
- Reports `name` property from the UniSim file name
- Falls back to raw `jneqsim` calls for equipment not covered by EOT factories
**EOT demo notebook** — shows how to instantiate, run, and step the simulator:
```python
nb = converter.to_eot_notebook(class_name="MySimulator")
import json
with open("eot_demo.ipynb", "w") as f:
json.dump(nb, f, indent=1)
```
### Code Sharing Architecture
The `to_python()`, `to_notebook()`, and `to_eot_simulator()` methods all share
the same internal code generators:
| Shared Method | Purpose |
|--------------|---------|
| `_prepare_topology()` | Topological sort, variable naming, stream resolution |
| `_gen_fluid_lines()` | Fluid creation code (EOS, components, mixing rule) |
| `_gen_feed_lines()` | Feed stream setup (T, P, flow, `process.add()`) |
| `_gen_equipment_lines()` | Per-equipment code (properties, wiring, `process.add()`) |
| `_to_pyvar()` | Convert arbitrary name to valid Python identifier |
| `_unique_var()` | Assign unique variable name avoiding collisions |
| `_outlet_ref()` | Resolve dot-notation outlet references |
This ensures that **Python scripts, notebooks, and EOT simulators always
produce the same process logic** — only the wrapping differs.
### For Complex Models (Sub-Flowsheets → ProcessModule)
Large UniSim models with sub-flowsheets should be decomposed:
```python
# The converter produces sub_flowsheets as separate process sections
neqsim_json = converter.to_json(include_subflowsheets=True)
# Main process
main_process = neqsim_json['process']
# Sub-flowsheets (each is a separate ProcessSystem)
for sf_name, sf_process in neqsim_json.get('sub_flowsheets', {}).items():
print(f"Sub-flowsheet '{sf_name}': {len(sf_process)} operations")
```
---
## 6. Result Verification
Always compare UniSim and NeqSim results after conversion:
```python
# After building NeqSim process
comparator = UniSimComparator(model, neqsim_process)
comparisons = comparator.compare_streams()
comparator.print_report(comparisons)
```
+ ### The R510 sample library is the converter's regression bench
+
+ `C:\Program Files (x86)\Honeywell\UniSim Design R510\Samples` ships **66 .usc
+ cases** (0.11–6.96 MB) spanning tutorials, refinery (FCC, isomerization),
+ gasifiers, electrolyzers, amine, rate-based columns, OLGA link, CCC controls and
+ EO. Converting the whole library exercises far more of the converter than any
+ single plant model, and it is how the column defect above was found.
+
+ ```bash
+ # Convert every case, one subprocess each (a COM failure cannot abort the batch)
+ python devtools/unisim_batch_check.py \
+ --samples-dir "C:\Program Files (x86)\Honeywell\UniSim Design R510\Samples" \
+ --out report.json --work-dir %TEMP%\unisim_batch\run1
+
+ # Then RUN every generated model — static checks are not enough
+ python devtools/unisim_run_generated.py --dir %TEMP%\unisim_batch\run1 \
+ --out runs.json --timeout 300
+
+ # Discover COM attribute names of an operation type the reader mishandles
+ python devtools/unisim_probe_ops.py --usc <case.usc> --types columnop,absorber
+ python devtools/unisim_probe_column.py <case.usc>
+ ```
+
+ `unisim_batch_check.py` records read/convert timings, the operation-type
+ histogram, unmapped types, converter warnings, and an AST *undefined-name* check
+ plus a compile check on the generated module. Track these four numbers; all
+ should be zero on a healthy converter:
+
+ - unmapped operation types
+ - "Skipped column" / "Skipped absorber" warnings
+ - cases whose generated module does not compile
+ - cases with undefined names in the generated module
+
+ **Do not edit `unisim_reader.py` while a batch is running** — every subprocess
+ re-imports it, so a mid-run edit silently mixes old and new behaviour (and a
+ half-written file makes the remaining cases fail with "no result file").
+
+ **Reference scores on the R510 library (2026-09-07)** — use these as the bar a
+ converter change must not fall below:
+
+ | Metric | Value |
+ |---|---|
+ | Cases converted | 62 / 66 (4 need an uninstalled UniSim module) |
+ | Unmapped operation types | 0 |
+ | Skipped columns / absorbers / unsupported ops | 0 / 0 / 0 |
+ | Generated modules that compile | 62 / 62 |
+ | Generated modules with undefined names | 0 / 62 |
+ | **Generated models that run to completion** | **60 / 62** |
+ | Read time, 66 cases | 530 s total, 7.3 s median |
+
+ ### RUNTIME failures that static checks miss
+
+ A model can convert, compile and contain no undefined names, and still abort on
+ `run()` — and because `ProcessSystem.run()` stops at the first throwing unit,
+ one bad unit leaves the whole flowsheet at its seed values. Executing the whole
+ corpus surfaced five distinct classes:
+
+ | Symptom | Cause | Rule |
+ |---|---|---|
+ | `'DistillationColumn' object has no attribute 'getOutletStream'` | tear-closing used `getOutletStream()` on a multi-outlet unit | use the per-type accessor map |
+ | `NullPointerException ... "inStream" is null` | `Type("name", None)` emitted for a unit whose feed COM did not expose | synthesise a boundary feed from the unit's product |
+ | `IllegalArgumentException: Feed tray index must be between 0 and N-1` | absorber fed at tray `n` with gas/liquid swapped | gas -> 0, solvent -> n-1 |
+ | TIMEOUT | converted column iterates without converging | `setMaxNumberOfIterations(30, True)` (hard-cap overload) |
+ | `'ComponentSplitter' object has no attribute 'getOutletStream'` | same as row 1 | see accessor table below |
+
+ **Outlet accessor by type** (audited against the released jar — do not assume
+ `getOutletStream()` exists):
+
+ | Type | Outlet accessor |
+ |---|---|
+ | `Splitter`, `ComponentSplitter` | `getSplitStream(int(i))` |
+ | `Separator`, `GasScrubber` | `getGasOutStream()` / `getLiquidOutStream()` |
+ | `ThreePhaseSeparator` | `getGasOutStream()` / `getOilOutStream()` / `getWaterOutStream()` |
+ | `HeatExchanger` | `getOutStream(int(i))` |
+ | `DistillationColumn` | `getGasOutStream()` / `getLiquidOutStream()` |
+ | `Electrolyzer` | `getHydrogenOutStream()` / `getOxygenOutStream()` |
+ | everything else converted | `getOutletStream()` |
+
+ Keep this as data (`TEAR_OUTLET_ACCESSORS`, `TEAR_SKIP_TYPES`,
+ `_placeholder_ports`), not as duplicated `if` chains: the port list for
+ forward-reference placeholders was duplicated between registration and emission,
+ and the two drifted apart, producing `NameError: name '_fwd_X_liquidOut' is not
+ defined`.
+
### Expected Deviations
| Property | Typical Deviation | Acceptable | Notes |
|----------|------------------|------------|-------|
| Temperature | < 1 °C | < 3 °C | Flash calculation differences |
| Pressure | 0% | 0% | Should match exactly (input) |
| Mass flow | < 0.1% | < 1% | Mass balance differences |
| Density | < 2% | < 5% | EOS differences (PR vs SRK) |
| Vapour fraction | < 0.02 | < 0.05 | Phase split sensitivity |
| Compressor power | < 5% | < 10% | Efficiency model differences |
| Heat duty | < 5% | < 10% | Enthalpy model differences |
### Factors Causing Deviations
1. **EOS differences**: UniSim PR-LK uses PR76 alpha (`m = 0.37464 + 1.54226ω − 0.26992ω²`)
for ALL components, including those with ω > 0.49 (where PR78 uses a different cubic).
**Fix**: Use `SystemPrLeeKeslerEos` in NeqSim — add `PRLKCORR` line after `EOS PR` in E300 file.
This typically reduces vapour-fraction bias from −1.6% to < 0.3%.
2. **BIP (binary interaction parameters)**: UniSim PR-LK correlation BIPs are non-zero even for
pure-prediction (never user-tuned). Extract via `pp.Kij.Values` (Section 1.1) and include in
E300 BIC section. Without correct BIPs, vapour fraction can differ by 5%+ and oil MW by 100%+.
3. **Water BIPs**: PR-LK BIPs for H2O–N2 (≈−2.24), H2O–CO2 (≈−0.557), H2O–H2S (≈−0.390)
are **negative** (increase cross-attraction → keep water in liquid). If a file has both
water BIPs AND OMEGAA overrides, they must be used as a matched set. Using standard BIPs
with modified OmegaA causes catastrophic phase split errors.
4. **Water distribution (multi-stage)**: In UniSim with recycles, water may split across
multiple separators. NeqSim forward-flow models capture water mainly at the first stage.
+70% water flow error at secondary stages is structurally expected if recycles are missing.
5. **Hypothetical components**: Pseudo-component property estimation differs between simulators.
Critical properties and acentric factor estimation methods vary.
**First check the acentric factor was transferred, not estimated** — compare the exported
`ACF` block against `comp.AcentricityValue`. Estimated (or `0.0`) values biased a
23-component SRK-Peneloux oil's bubble point / TVP by 12–15 %; reading `Acentricity`
brought it to < 0.5 % (Section 1.2).
6. **Compressor efficiency**: UniSim COM sometimes returns `None` for `AdiabaticEfficiency`.
Always check extracted efficiency values; default to 75% isentropic with a warning.
7. **Mixing rules**: UniSim may use advanced mixing rules not available in NeqSim.
8. **Transport properties**: Different viscosity/thermal conductivity correlations.
9. **Convergence**: Different solver algorithms, tolerance settings, and recycle initialization.
---
## 7. Stream Topology Reconstruction
UniSim operations reference streams by name via `.Feeds[]` and `.Products[]`.
The converter reconstructs the process topology:
1. **Identify external feeds**: Streams not produced by any operation
2. **Build producer map**: `stream_name → (operation_name, port)`
3. **Topological sort**: Kahn's algorithm to order operations by dependency
4. **Handle cycles**: Recycle blocks break dependency loops
### Dot-notation for NeqSim wiring
| Producer Type | Stream | NeqSim Reference |
|--------------|--------|------------------|
| Separator (gas) | 1st product | `"Sep.gasOut"` |
| Separator (liquid) | 2nd product | `"Sep.liquidOut"` |
| 3-Phase Sep (gas) | 1st product | `"Sep.gasOut"` |
| 3-Phase Sep (oil) | 2nd product | `"Sep.oilOut"` |
| 3-Phase Sep (water) | 3rd product | `"Sep.waterOut"` |
| Compressor | output | `"Comp.outlet"` |
| Cooler/Heater | output | `"Cooler.outlet"` |
| Valve | output | `"Valve.outlet"` |
| Mixer | output | `"Mixer.outlet"` |
### Forward Reference Handling (Recycle Loops)
When the topological sort detects cycles (equipment B referenced before it is
defined because B depends on equipment A which depends on B), the converter
creates **forward reference placeholders** — temporary Stream objects that
stand in for the not-yet-created equipment outlets.
#### How It Works
1. **Cycle detection** (`_prepare_topology`): Operations in a cycle are
identified. The back-edge producers become forward references stored in
`topo['fwd_ref_placeholders']`.
2. **Placeholder registration** (`_register_fwd_placeholders`): For each
forward-referenced producer, a `_fwd_XXX` variable is created. For
multi-outlet equipment (Separator, ThreePhaseSeparator), **port-specific
placeholders** are also created:
| Equipment | Placeholders Created |
|-----------|---------------------|
| `V-100` (Separator) | `_fwd_V_100` (generic), `_fwd_V_100_gasOut`, `_fwd_V_100_liquidOut` |
| `S-200` (ThreePhaseSeparator) | `_fwd_S_200` (generic), `_fwd_S_200_gasOut`, `_fwd_S_200_oilOut`, `_fwd_S_200_waterOut` |
| `K-100` (Compressor) | `_fwd_K_100` (generic only) |
3. **Placeholder stream creation** (in `to_python` / `to_notebook` /
`to_eot_simulator`): Each placeholder is created as a `Stream` with
temperature, pressure, and flow rate from the UniSim stream data for
that product outlet. Port-specific placeholders use the stream data for
each individual outlet.
4. **Outlet resolution** (`_outlet_ref`): When downstream equipment
references a forward-referenced separator's liquid outlet (e.g.
`"V-100.liquidOut"`), the resolver first checks for a port-specific key
(`V-100.liquidOut`) in `fwd_ref_vars` before falling back to the
generic placeholder.
5. **Auto-Recycle wiring** (`_gen_equipment_lines`): After the actual
separator is created and added to the process, Recycle objects are
automatically generated to wire each actual outlet back to its
forward reference placeholder:
```python
# Auto-recycle: wire V-100 gasOut back to forward ref placeholder
_rcy_V_100_gasOut = Recycle("V-100_gasOut_loop")
_rcy_V_100_gasOut.addStream(V_100.getGasOutStream())
_rcy_V_100_gasOut.setOutletStream(_fwd_V_100_gasOut)
process.add(_rcy_V_100_gasOut)
```
#### Why Port-Specific Placeholders Matter
Without port-specific placeholders, a valve downstream of a separator's
liquid outlet would incorrectly receive the combined feed placeholder
(with gas + liquid flow). This caused 500%+ flow deviations in early
versions. Port-specific placeholders ensure each downstream unit gets
the correct phase with approximately correct T, P, and flow.
#### HeatExchanger Outlet Port Resolution
HeatExchanger has two feed/product sides indexed 0 (Shell) and 1 (Tube).
The converter maps downstream references to `getOutStream(int(0))` or
`getOutStream(int(1))` based on the product's position in the products
list (set by `_extract_heatexchanger`: ShellSideProduct = index 0,
TubeSideProduct = index 1).
Port naming convention: `hx0` and `hx1` (analogous to `gasOut`/`liquidOut`
for separators). Forward-reference placeholders are created per-side when
a HeatExchanger is in a recycle loop.
**Generated code example:**
```python
E_100 = HeatExchanger("E-100")
E_100.setFeedStream(0, shell_feed) # Shell side
E_100.setFeedStream(1, tube_feed) # Tube side
process.add(E_100)
# Downstream: Cooler on shell product (index 0)
C_100 = Cooler("C-100", E_100.getOutStream(int(0)))
# Downstream: Valve on tube product (index 1)
VLV_100 = ThrottlingValve("VLV-100", E_100.getOutStream(int(1)))
```
**Important:** Do NOT use `getOutletStream()` for HeatExchanger — it only
returns one side. Always use `getOutStream(int(index))` for explicit
side selection.
#### Compressor Efficiency Defaults
When compressor efficiency is not available from the UniSim COM extraction
(returns `None`), the code applies these rules:
1. If the extracted value is > 1.0, it's treated as a percentage and
converted to a fraction (e.g., 75 → 0.75)
2. If adiabatic efficiency is available (0 < eff ≤ 1), it's set via
`setIsentropicEfficiency()`
3. If only polytropic efficiency is available, it's set via
`setPolytropicEfficiency()` + `setUsePolytropicCalc(True)`
4. If **neither** efficiency is available, a 75% isentropic default is
applied with a warning comment in the generated code
This matters because NeqSim defaults to 100% isentropic efficiency,
which produces unrealistically low outlet temperatures.
#### Separator Phase Detection and Entrainment Extraction
The reader now detects 2-phase vs 3-phase separators by two mechanisms:
1. **UniSim TypeName**: `flashtank` → `Separator` (2-phase),
`sep3op` → `ThreePhaseSeparator` (3-phase)
2. **WaterProduct heuristic**: If a `flashtank` has a `WaterProduct`
connected, it is automatically promoted to `ThreePhaseSeparator`
#### Orientation Detection (Vertical → GasScrubber)
The reader extracts separator orientation from UniSim COM attributes
(`Orientation`, `VesselOrientation`, `SeparatorOrientation`). When a
`flashtank` is detected as **vertical**, the NeqSim type is mapped to
`GasScrubber` instead of `Separator`.
| UniSim Type | Orientation | NeqSim Type |
|---|---|---|
| `flashtank` | horizontal (default) | `Separator` |
| `flashtank` | vertical | `GasScrubber` |
| `flashtank` + WaterProduct | any | `ThreePhaseSeparator` |
| `sep3op` | any | `ThreePhaseSeparator` |
`GasScrubber` extends `Separator` in NeqSim — it is a vertical vessel
optimised for removing liquid droplets from a gas stream, with K-value
sizing constraints and a default 10% liquid level.
**Entrainment** is extracted from UniSim COM and mapped to NeqSim
`setEntrainment()` calls. The reader tries multiple COM attribute names
for each entrainment direction:
| Entrainment Direction | UniSim COM Attributes (tried in order) | NeqSim `setEntrainment` Args |
|---|---|---|
| Liquid in gas (oil carryover) | `LiqCarryOverMolFrac`, `LiqCarryOverFrac`, `LiquidInVapourFraction`, `LiqInVap`, `LiquidCarryover` | `(val, "volume", "product", "oil", "gas")` |
| Gas in liquid (gas carry-under) | `VapCarryUnderMolFrac`, `VapCarryUnderFrac`, `VapourInLiquidFraction`, `VapInLiq`, `VapourCarryunder` | `(val, "volume", "product", "gas", "liquid")` |
| Water in oil (3-phase) | `WaterInOilFraction`, `WaterInOil`, `AqInOil`, `AqueousInOilFraction` | `(val, "volume", "product", "aqueous", "oil")` |
| Oil in water (3-phase) | `OilInWaterFraction`, `OilInWater`, `OilInAq`, `OilInAqueousFraction` | `(val, "volume", "product", "oil", "aqueous")` |
Generated Python code example:
```python
# Three-phase separator with entrainment from UniSim
mp_sep = ThreePhaseSeparator("20VA102", heater_mp.getOutletStream())
mp_sep.setEntrainment(0.084, "volume", "product", "aqueous", "oil") # water in oil
mp_sep.setEntrainment(0.002, "volume", "product", "oil", "aqueous") # oil in water
process.add(mp_sep)
```
**Note:** If the UniSim COM does not expose entrainment attributes (some
model versions or configurations may not), the extraction silently skips
them — the separator will use NeqSim defaults (zero entrainment).
---
## 8. Handling Sub-Flowsheets
UniSim uses sub-flowsheets (template operations) for modular process sections.
In NeqSim, these map to either:
1. **Separate ProcessSystem objects** composed in a `ProcessModel`
2. **Flattened into the main ProcessSystem** (simpler but may not handle inter-area recycles)
### Auto-Classification (full_mode=True, Default)
When `full_mode=True` (the default), the converter automatically classifies
sub-flowsheets into **process** (shares material streams with the main
flowsheet) and **utility** (isolated, e.g., heating/cooling medium loops).
Only process sub-flowsheets are included in the generated code.
Classification is done by `classify_subflowsheets()`:
```python
classification = converter.classify_subflowsheets()
# Returns: {'TPL1': 'process', 'TPL3': 'utility', 'COL1': 'process', ...}
process_sfs = converter.get_process_subflowsheets()
# Returns: ['TPL1', 'TPL2', 'TPL5', 'TPL7', 'COL1']
```
A sub-flowsheet is classified as **process** if any of its operations produce
or consume a stream that also appears in the main flowsheet or another process
sub-flowsheet. Otherwise it is **utility** (typically heating/cooling medium,
flare, utility water systems).
### Architecture Decision
| Model Complexity | Strategy |
|-----------------|----------|
| Main + 1-2 small sub-flowsheets | Flatten into single ProcessSystem |
| Main + 3+ sub-flowsheets | ProcessModel with separate ProcessSystems |
| Sub-flowsheet has own fluid package | Must be separate ProcessSystem |
### Sub-Flowsheet to ProcessModel Mapping
In `full_mode=True`, each process sub-flowsheet becomes its own
`ProcessSystem`, all composed inside a `ProcessModel`:
```python
from neqsim import jneqsim
ProcessModel = jneqsim.process.processmodel.ProcessModel
plant = ProcessModel("Platform")
plant.add("Main", main_process)
plant.add("TPL1", tpl1_process)
plant.add("TPL2", tpl2_process)
plant.add("COL1", col1_process)
# Utility sub-flowsheets (TPL3, TPL4, TPL6) are excluded
plant.run()
```
---
## 9. Command-Line Interface
```bash
# Print model summary
python devtools/unisim_reader.py path/to/file.usc --summary
# Output NeqSim JSON
python devtools/unisim_reader.py path/to/file.usc --json
# Generate standalone Python script
python devtools/unisim_reader.py path/to/file.usc --python process.py
# Generate Jupyter notebook
python devtools/unisim_reader.py path/to/file.usc --notebook process.ipynb
# Generate EOT / ProcessPilot simulator
python devtools/unisim_reader.py path/to/file.usc --eot my_sim.py --eot-class MySimulator
# Generate EOT demo notebook
python devtools/unisim_reader.py path/to/file.usc --eot-notebook eot_demo.ipynb
# Save full extracted model
python devtools/unisim_reader.py path/to/file.usc --save extracted.json
# Fast extraction (topology only, no stream properties)
python devtools/unisim_reader.py path/to/file.usc --no-streams --summary
# Combined: Python + notebook + EOT in one command
python devtools/unisim_reader.py path/to/file.usc --python p.py --notebook n.ipynb --eot s.py
# Show UniSim GUI window during extraction (useful for debugging)
python devtools/unisim_reader.py path/to/file.usc --visible --summary
```
---
## 10. Known Limitations
1. **Windows only** — COM automation requires Windows + UniSim installed
2. **Hypothetical components** — pseudo-components need manual C7+ characterization
3. **Tuned BIPs** — Extractable via `pp.Kij.Values` (see Section 1.1). The
`unisim_reader.py` does not yet automate this, but manual extraction is
straightforward. The returned matrix uses -32767.0 as a diagonal sentinel.
- 4. **Column internals** — distillation column tray/packing details not fully mapped
+ 4. **Column internals** — tray count, condenser/reboiler presence, feed stage,
+ reflux ratio and top/bottom pressure ARE transferred (see the column section
+ in Part 1). Tray hydraulics, packing details, side draws, pumparounds and
+ multi-spec column control are not.
5. **Dynamic models** — only steady-state data extracted
6. **Control logic** — PID controllers produce TODO comments, not functional controllers
7. **Custom correlations** — UniSim's user-defined correlations not transferred
8. **Performance curves** — compressor/pump performance maps not extracted
9. **Multiple fluid packages** — the reader exports one E300 file per fluid
package, but most generated ProcessSystem builds still use the default
process fluid unless per-stream or per-sub-flowsheet package assignment is
explicitly wired. Treat multi-package models as verified only after the
generated JSON/Python shows the expected `e300FilePath` for each area and
the build route loads E300 through `EclipseFluidReadWrite.read(...)`.
- 10. **Absorber columns** — single-feed only; multi-feed absorbers show a TODO.
- Glycol/TEG contactors (name contains "glyc", "teg", or "dehydrat") are
- modeled as `ComponentSplitter` for water removal instead of `DistillationColumn`
+ 10. **Absorber columns** — two-feed absorbers are wired (vapour-rich feed to the
+ bottom tray, lean solvent to the top); a single-feed absorber still emits a
+ TODO for the missing solvent. Glycol/TEG contactors (name contains "glyc",
+ "teg", or "dehydrat") are modeled as `ComponentSplitter` for water removal
+ instead of `DistillationColumn`.
11. **SetPoint / Adjuster wiring** — generates skeleton code but wiring is often incomplete
12. **Recycle convergence** — heavily circular models (5+ forward references) may
not converge with placeholder initial values; multiple `process.run()` calls
or manual tuning may be needed
13. **Compressor efficiency** — COM extraction sometimes returns `None` even when
the UniSim model has efficiency data; defaults to 75% isentropic
14. **Spreadsheet operations** — represented as `SpreadsheetBlock`, but formula
fidelity depends on extracting import/export cells and formulas from COM.
15. **Logical operations** — produce comments/controller metadata; no executable
process logic is transferred.
16. **Balance, virtual-stream, and template placeholders** — represented through
`UnisimCalculator`/`SubFlowsheet` adapters to preserve topology. This solves
structural build gaps but not detailed specification or formula semantics.
17. **DistillationColumn solver divergence** — NeqSim's sequential-substitution and
inside-out column solvers diverge for C3/C4-rich (NGL-range) feeds at low
pressure. Feeds with < 30% methane and significant C3+ fractions will not
converge. Lighter feeds (e.g. deethanizer with 51% CH4) converge reliably.
- Build the column **outside** the `ProcessSystem` to avoid re-run divergence.
+ Every generated column therefore carries a hard iteration cap
+ (`setMaxNumberOfIterations(30, True)`) so a non-converging column cannot
+ hang the whole `process.run()`; the trade-off is a column that stops short
+ of its own convergence criterion. Build the column **outside** the
+ `ProcessSystem` when you need a converged column.
See the TUTOR1 notebook for a worked example.
18. **HeatExchanger pressure drops** — NeqSim `HeatExchanger` does not model
pressure drops; outlet pressures equal inlet pressures. UniSim models
typically include 0.5–1.0 bar pressure drop per side. This causes small
temperature deviations in downstream equipment.
19. **HeatExchanger UA tuning** — The `HeatExchanger.setUAvalue()` parameter
must be tuned to match UniSim's heat duty. Counter-current heat balance
differences between UniSim and NeqSim typically produce 1–2°C deviation
on outlet temperatures.
20. **Full ProcessModel timeout** — Large models with 5+ sub-flowsheets, 10+
recycles, and Adjusters may time out during `plant.run()` even with relaxed
recycle tolerances. Root cause is typically the combination of Adjuster
iteration, absorber column convergence, and multi-area coordination.
**Workaround**: Test individual ProcessSystem areas or connected sub-paths
first, then build up incrementally. The connected main-path approach (no
recycles, manual feed data) runs in < 1 second for even large models.
+ Measured on the R510 sample library (60 of 62 converted models run to
+ completion): the only two that still exceed 240 s are the cases with
+ **several columns inside recycle loops** — Soybean-Biofuel (3 columns,
+ 4 recycles) and the eNRTL amine plant (absorber + column). Each outer
+ recycle pass re-solves every column from scratch, so cost scales as
+ `n_columns x column_iterations x outer_passes`. The column iteration cap
+ bounds a single column, not this product.
21. **Separator liquid MW deviation** — When UniSim separators have
`has_water_product=False` (2-product), the NeqSim `Separator` includes
water in the liquid phase. This causes liquid MW to be lower than UniSim's
value (water dilutes the MW). Using `ThreePhaseSeparator` overcorrects by
removing ALL water. Expect 20-40% liquid MW deviation at low/medium
pressures. Gas MW and temperatures are much more accurate.
22. **Utility sub-flowsheets excluded** — In `full_mode=True`, sub-flowsheets
classified as "utility" (no shared streams with process flowsheet) are
excluded. This is correct for heating/cooling medium loops but may
miss utility systems that interact with process streams through
non-standard connections.
23. **E300 fluid parity is necessary but not sufficient** — A full-fluid E300
route fixes component-property transfer, but it does not reconcile UniSim
virtual streams, spreadsheet/balance calculations, template units, or
sub-flowsheet interface wiring. Keep the verification report split into
separate statuses for fluid export/use, structural build, and numerical
stream matching.
+ 24. **Synthesised boundary feeds** — when UniSim COM exposes no inlet for a
+ block, the converter seeds a boundary feed from that block's own product
+ stream so the unit (and everything downstream) survives. The unit then runs
+ at roughly the right operating point but is no longer connected to its real
+ upstream source. Every such feed is reported as a converter warning
+ ("Synthesised a boundary feed for ...") — review them before trusting a
+ mass balance.
+ 25. **Cases that need a UniSim module you do not have** cannot be opened at all
+ (`E_ACCESSDENIED` from `SimulationCases.Open`). In the R510 library this is
+ the 3 `CCC Series 5` controls cases and the EO electrical case: 62 of 66
+ convert, 4 are environment-limited, not converter-limited.
---
## 11. Data Class Reference
The extracted UniSim model uses these Python dataclasses:
### UniSimComponent
```python
@dataclass
class UniSimComponent:
name: str # e.g. "Methane", "CO2", "C7 GRAND*"
index: int # Position in fluid package component list
is_hypothetical: bool # True if name ends with '*'
```
### UniSimFluidPackage
```python
@dataclass
class UniSimFluidPackage:
name: str # e.g. "Basis-1"
property_package: str # e.g. "Peng-Robinson", "SRK", "CPA"
components: List[UniSimComponent]
@property
def component_names(self) -> List[str]: ...
```
### UniSimStreamData
```python
@dataclass
class UniSimStreamData:
name: str
temperature_C: Optional[float] # Celsius
pressure_bara: Optional[float] # bara
mass_flow_kgh: Optional[float] # kg/h
molar_flow_kgmolh: Optional[float] # kgmol/h
vapour_fraction: Optional[float] # 0-1
mass_density_kgm3: Optional[float] # kg/m3
molecular_weight: Optional[float] # g/mol
enthalpy_kJkg: Optional[float] # kJ/kg
composition: Optional[Dict[str, float]] # component_name -> mole fraction
n_phases: Optional[int]
viscosity_cP: Optional[float]
thermal_conductivity: Optional[float]
specific_heat_kJkgC: Optional[float]
```
### UniSimEnergyStream
```python
@dataclass
class UniSimEnergyStream:
name: str
heat_flow_kW: Optional[float] # kW
```
### UniSimOperation
```python
@dataclass
class UniSimOperation:
name: str # Equipment name e.g. "K-100"
type_name: str # UniSim internal type e.g. "compressor"
feeds: List[str] # Feed stream names
products: List[str] # Product stream names
energy_feeds: List[str] # Energy feed stream names
energy_products: List[str] # Energy product stream names
properties: Dict[str, Any] # Type-specific extracted properties
```
Common `properties` keys by operation type:
| Operation | Property Keys |
|-----------|--------------|
| Compressor | `outlet_pressure_bara`, `adiabatic_efficiency`, `polytropic_efficiency` |
| Valve | `outlet_pressure_bara` |
| Cooler / Heater | `outlet_temperature_C`, `duty_kW` |
| HeatExchanger | `UA`, `duty_kW` |
| Pipe | `length_m`, `diameter_m`, `roughness_m` |
| Pump | `outlet_pressure_bara`, `efficiency` |
### UniSimFlowsheet
```python
@dataclass
class UniSimFlowsheet:
name: str
material_streams: List[UniSimStreamData]
energy_streams: List[UniSimEnergyStream]
operations: List[UniSimOperation]
sub_flowsheets: List['UniSimFlowsheet'] # Recursive
```
### UniSimModel
```python
@dataclass
class UniSimModel:
file_path: str
file_name: str
fluid_packages: List[UniSimFluidPackage]
flowsheet: Optional[UniSimFlowsheet]
def summary(self) -> str: ... # Human-readable overview
def all_operations(self) -> List[UniSimOperation]: ... # Flat list incl. sub-FS
def all_streams(self) -> List[UniSimStreamData]: ... # Flat list incl. sub-FS
def to_dict(self) -> Dict: ... # JSON-serializable
```
---
## 12. Driving the native UniSim case instead of converting it
Conversion is not always the right answer. If the converted NeqSim flowsheet
does not close its recycles, any capacity or tie-in conclusion drawn from it is
unsafe. For **capacity studies, tie-back evaluations and bottleneck ranking on a
large as-built case**, drive the original `.usc` through COM and read the
results back. This keeps the fluid package, every adjust and every recycle
exactly as built. Verified on a 151-component, 366-stream, 8-recycle,
42-adjust platform case.
### Operation TypeName values — a wrong filter fails silently
`compressor` (**not** `compressop`), `expandop`, `pumpop`, `coolerop`,
`heaterop`, `mixerop`, `teeop`, `valveop`, `flashtank`, `sep3op`, `fractop`,
`recycle`, `adjust`, `spreadsheetop`, `templateop`.
Filtering rotating equipment on `compressop` returns **zero** compressors and
the duty table silently comes back exchangers-only. Always enumerate first:
```python
from collections import Counter
Counter(str(sheet.Operations.Item(i).TypeName)
for i in range(sheet.Operations.Count))
```
### Component properties: use the `*Value` float accessors
| Use | Not |
|-----|-----|
| `comp.MolecularWeightValue` | `comp.MolecularWeight` (returns `RealVariable`) |
| `comp.StdLiquidDensityValue` | `IdealLiqDensityValue`, `MassDensityValue` (both `None` for pseudo components) |
| `comp.AcentricityValue`, `comp.NormalBoilingPointValue` (°C) | — |
`float(comp.MolecularWeight)` raises, and a `try/except → None` around it
silently drops the entire pseudo-component slate. If a composition mapping
reports the C7+ fractions as unmapped, this is the cause.
### Topology can be modified, not just specifications
On a licensed case retrieved from a document system these all succeed:
```python
sheet.MaterialStreams.Add("NewFeed") # create a stream
feed.Temperature.SetValue(35.1, "C")
feed.Pressure.SetValue(70.3, "bar")
feed.MassFlow.SetValue(58080.0, "kg/h")
feed.ComponentMolarFraction.SetValues(fractions, "") # ordered by FluidPackage.Components
mixer.Feeds.Add(feed) # connect it
solver.CanSolve = True # re-solve, typically < 1 s
```
`E_ACCESSDENIED` on a write means the variable is **calculated**, not that the
licence blocks writing. Probe `variable.CanModify` before concluding otherwise.
### Process lifetime and reproducibility
When the Python process exits, the COM server closes the case; a follow-up
script doing `SimulationCases.Item(i)` gets `IndexError`. Every script must
`app.SimulationCases.Open(path)` itself. Start each run by closing any open work
case and `shutil.copy2`-ing a fresh copy from the source, otherwise repeated
runs stack duplicate feeds onto the same mixer.
### Interpreting a tie-in result
- **Rank every stream by relative change**, not just the ones you expected to
move. A gas-rich tie-back loads the gas path far harder than the bulk mass
suggests — one verified case added 6.6% to plant feed but 80.9% to
first-stage flash gas.
- **Compute actual volumetric rate** (`mass / MassDensity`) for gas outlets.
Separator and gas-train capacity scales with actual m³/h, not kg/h.
- **Fixed pressure ratios are not a capacity check.** The model specifies
compressor pressure ratios and solves for power, with no performance maps and
no driver ratings. The output is the *duty required*, not whether the machine
can deliver it. Say so explicitly in any capacity report.
- Check the product mass balance closes against the added feed increment before
trusting the deltas.
---
## 13. Troubleshooting
| Issue | Cause | Fix |
|-------|-------|-----|
| `pywintypes.com_error` | UniSim not installed or wrong version | Verify UniSim installation |
| Empty stream values | Stream not solved in UniSim | Open file in UniSim GUI, run solver first |
| `-32767` values | UniSim empty marker | Filter with `val > -30000` check |
| COM timeout | Large model loading | Increase `time.sleep()` after `.Open()` |
| Wrong component count | Multiple fluid packages | Check which FP the stream uses |
| Missing operations | Sub-flowsheet not recursed | Use `model.all_operations()` |
| Composition doesn't sum to 1 | Hypothetical components excluded | Re-normalize after filtering |
| Compressor outlet T too low | Efficiency = 100% (ideal) | Check for missing efficiency; defaults to 75% |
| Valve flow deviation 500%+ | Wrong forward ref placeholder | Port-specific placeholders now fix this |
| Separator downstream wrong phase | Generic fwd ref placeholder used | Ensure `_register_fwd_placeholders` ran |
| Recycle not converging | Too many forward references | Try multiple `process.run()` calls or tune placeholders |
| `AttributeError` on COM property | Operation type doesn't have that property | Wrap in try/except or check `TypeName` |
| Column diverges / mass runaway | C3/C4-rich feed at low P | Known solver limitation; build column outside ProcessSystem; use flash separation as fallback |
| HX outlet T differs 1–2°C | UA mismatch or no ΔP modeled | Tune UA value; add note that NeqSim HX has no pressure drop |
| SalesGas T off by 5°C | Propagated HX deviation | CoolGas deviation amplified through counter-current HX; adjust UA |
---
## 14. Verified Reference Cases
| Case | File | Components | Operations | Converged | Notes |
|------|------|-----------|------------|-----------|-------|
| TUTOR1 | `TUTOR1.usc` | 7 (N₂, CO₂, C₁–nC₄) | 13 | 11/13 streams | DePropanizer column diverges; upstream matches within 1°C. Notebook: `examples/notebooks/tutor1_gas_processing.ipynb` |
| R510 SG Cond | R510 model | 31 (lumped pseudo-C7+ through C30P*) | 185 main + 8 sub-flowsheets (~250 total) | 78% isolated, 71% connected | PR-LK EOS with E300 BIPs. Full mode: 5 process + 3 utility sub-flowsheets |
### TUTOR1 Lessons Learned
The TUTOR1 gas processing tutorial was the first end-to-end verified conversion.
Key findings that apply to any UniSim conversion:
1. **Recycle loops converge well**: The Gas/Gas HX ↔ LTS recycle loop converged
in 3 iterations using placeholder streams seeded from UniSim data.
2. **HeatExchanger UA tuning**: Setting `UA = 35000 W/K` produced CoolGas ≈ 5.5°C
vs UniSim's 6.6°C. The deviation propagates to SalesGas (15°C vs 10°C). The
UA value needs case-by-case tuning against UniSim's heat duty.
3. **Column solver limitation**: The DePropanizer (7 components, 24% CH4,
27% C₂, 24% C₃, 24% C₄ at 14 bara) diverged with all three solver types
(DIRECT_SUBSTITUTION, INSIDE_OUT, DAMPED_SUBSTITUTION) and multiple
configurations (1–5 trays). This is a known NeqSim limitation for NGL-range
feeds. **Workaround**: Build the column outside the ProcessSystem to avoid
re-run divergence affecting upstream convergence.
4. **Pressure drops not modeled**: UniSim's Gas/Gas HX has ~0.7 bar ΔP per side;
NeqSim passes pressure through unchanged. This is cosmetic for most
comparisons but compounds through multi-stage processes.
5. **DewPoint and Balance operations**: UniSim's DewPoint (balance op) and
ADJ-1 (adjuster) are not needed for mass balance; safe to skip.
6. **Heating Value spreadsheet**: Property-only calculations; safe to skip.
### R510 SG Condensation Model — Lessons Learned
The R510 SG Condensation model is a large-scale offshore processing model with
31 components (including lumped pseudo-components C10-C11* through C30P*, aromatics,
and glycols), 8 sub-flowsheets, 13+ recycle loops, and ~250 total operations.
This is the first full-mode verified conversion with sub-flowsheet classification.
**Model characteristics:**
- **Fluid package**: PR-LK (Peng-Robinson Lee-Kesler) with 31 components
- **Feed streams**: 3 feeds — "Reservoir oil" (MW=58.5, 1,950,684 kg/h),
"Formation water" (MW=18.0, 398,728 kg/h), "Res gas" (MW=19.6, 30,271 kg/h)
- **Sub-flowsheets**: 5 process (TPL1, TPL2, TPL5, TPL7, COL1) +
3 utility (TPL3, TPL4, TPL6)
- **Generated code**: ~2000 lines of Python with ProcessModel architecture
#### Comparison Results
**Isolated unit-by-unit** (each unit fed with correct UniSim inlet data):
- 97 GOOD (< 5% deviation), 9 WARN (5-15%), 30 BAD (> 15%), 66 SKIPPED
- **78% match rate** across 136 comparable stream properties
**Connected main-path model** (17 units, no recycles, error propagation):
- 11 OK, 1 WARN, 5 BAD — **71% match rate** in 0.5 seconds
- Temperature accuracy: excellent (< 0.3°C for most streams)
- Gas MW matching: good (< 5%)
- Liquid MW: moderate deviation due to water handling (see below)
#### Key Findings
1. **E300 fluid loading is essential for pseudo-components.** The `read(export_e300=True)`
option extracts Tc, Pc, ω, MW, and BIPs for all 31 components including lumped
pseudo-components (C10-C11*, C12-C13*, etc.). Without E300 BIPs, phase split
deviations exceed 100% for heavy components.
2. **Component name mapping for lumped pseudo-components.** UniSim uses names like
`C10-C11*`, `C12-C13*`, `C14-C15*`, `C16-C18*`, `C19-C20*`, `C21-C23*`,
`C24-C29*`, `C30P*`. These map 1:1 to the same names in the E300 file.
Do NOT assume individual components (C10, C11, C12, ...) — the model uses
lumped groups.
3. **Separator water handling (2-phase vs 3-phase).**
When UniSim's `sep3op` has `has_water_product: False` (all separators in R510),
use NeqSim `Separator` (2-phase), NOT `ThreePhaseSeparator`. The 2-phase Separator
gives combined oil+water liquid matching UniSim's 2-product behavior.
`ThreePhaseSeparator` removes ALL water, overcorrecting liquid MW
(e.g., 201.9 vs target 98.2, while 2-phase gives 66.2 — closer overall).
4. **Compressor efficiency extraction fails for some models.** All 6 compressors
in R510 returned `None` for `AdiabaticEfficiency` from COM. The 75% isentropic
default causes 10-32°C outlet temperature deviations. To improve accuracy,
back-calculate efficiency from UniSim inlet/outlet data.
5. **Recycle convergence.** Even with `setTolerance(1e6)` on all 13 recycles,
the full ProcessModel with 8 areas still times out. Root cause is likely the
combination of Adjusters, absorber columns, and multi-area iteration.
**Workaround**: Test individual process areas or connected sub-paths first
(the 17-unit connected model runs in 0.5s). Build up to full model incrementally.
6. **JSON key format.** The extracted JSON uses `pressure_bara` (not `pressure_kPa`)
and `mass_flow_kgh` (not `mass_flow_kg_h`). When writing comparison scripts,
use these exact keys.
7. **Sub-flowsheet mapping uses positional order.** When the JSON has sub-flowsheet
operations that cannot be matched by name or stream overlap, the reader falls
back to positional matching based on the original JSON order. This handles
cases where sub-flowsheet operation names are generic (e.g., `MIX-100` appears
in both main and sub-flowsheet).
8. **Temperature matching is the strongest comparison metric.** Temperatures
showed < 0.3°C deviation across the connected path. Gas-phase MW was within
5% for most equipment. Use temperature as the primary validation metric;
MW and flow deviations are often caused by liquid-phase water handling
rather than thermodynamic model errors.
### Operation Handler Registry Lessons Learned
For large UniSim conversions, do **not** implement every UniSim operation as a
separate UniSim-named NeqSim equipment class. Keep the core physical API native
to NeqSim and extend the converter registry instead:
1. Add or update the `UniSimOperationHandler` entry with the correct
`strategy` and `stream_role`.
2. Use native NeqSim equipment for real process physics.
3. Use `UnisimCalculator` for stream-carrying balance, virtual-stream, and
template-interface placeholders when equations are not yet extracted.
4. Use `SpreadsheetBlock` for spreadsheet formulas once import/export cells are
known.
5. Promote an adapter to a real NeqSim equipment class only when equations,
ports, properties, and regression tests are clear.
Validate registry changes with `python devtools/test_unisim_outputs.py`. The
suite includes pure-Python checks for output modes, E300 transfer, operation
handler strategy, and JSON `_unisim_operation_mapping` summaries.
## Reverse direction: NeqSim JSON → UniSim (`devtools/unisim_writer.py`)
`UniSimWriter` builds a wired `.usc` case from the same JSON that
`ProcessSystem.fromJsonAndRun()` consumes, so one JSON file can drive both
tools and the results can be compared directly.
```python
from devtools.unisim_writer import UniSimWriter
writer = UniSimWriter(
visible=True,
template_path="licensed_case.usc", # copy first — it is modified
clear_template=True, # empty flowsheet + fluid package
)
writer.build_from_json(json_str, save_path="gas_compression.usc")
writer.close()
```
### Verified COM rules (UniSim Design R510)
1. **The template must come from a licensed UniSim session.** A `.usc` saved
from a case created by `SimulationCases.Add()` inherits restricted write
access: reads and `MaterialStreams.Add` succeed, but every
`Operations.Add` and `variable.SetValue` fails with E_ACCESSDENIED
(`-2147024891`). Symptom: "Operations created: 0" with 10+ access-denied
warnings. Fix: point `template_path` at a real saved case.
2. **Fluid-package edits require a basis change.** Wrap component add/remove in
`BasisManager.StartBasisChange()` … `EndBasisChange()` (or the
`…Invisibly` variants). Outside a basis change, `Components.Add` returns
E_ACCESSDENIED on a live case.
3. **Collections use `Remove(i)` / `RemoveAll()`, not `Item(i).Delete()`.**
This applies to `Flowsheet.Operations`, `MaterialStreams`, `EnergyStreams`
and `FluidPackage.Components`.
4. **Compressor efficiency members are `CompPolytropicEff` and
`CompAdiabaticEff`** (percent), not `PolytropicEfficiency`. Related useful
members: `ProductPressure`, `ProductTemperature`, `FeedPressure`, `Energy`,
`PolytropicHead`, `EfficiencyType`, `UsingCurves`, `Curves`.
5. **Duty-consuming operations need an attached energy stream.** Without
`flowsheet.EnergyStreams.Add(name)` + `op.EnergyStream = …`, a compressor,
pump, expander, cooler or heater never closes its energy balance: discharge
temperature and power read back as the empty sentinel **`-32767`**. Seeing
`-32767` in results is the signature of a missing energy stream, not a
convergence failure.
6. `Pressure.Calculate()` can be denied even when `Pressure.SetValue(v, 'bar')`
works — always try `SetValue` first, `Calculate` as fallback.
Discover member names for an unfamiliar operation with early binding
(`win32com.client.gencache.EnsureDispatch`) in a throwaway process, then keep
the writer itself on `win32com.client.dynamic.Dispatch`.
### Round-trip accuracy
A three-stage export compression train (25 → 200 bara, SRK, polytropic
efficiency 0.78, interstage cooling to 35 °C, knock-out scrubbers) built from
one JSON gave:
| Quantity | NeqSim | UniSim | Deviation |
|---|---|---|---|
| Stage 1 shaft power | 4216.4 kW | 4206.4 kW | −0.24 % |
| Stage 2 shaft power | 3571.7 kW | 3555.1 kW | −0.46 % |
| Stage 3 shaft power | 2824.0 kW | 2802.7 kW | −0.75 % |
| Total shaft power | 10 612.1 kW | 10 564.2 kW | −0.45 % |
| Stage 1 discharge T | 97.56 °C | 97.24 °C | −0.32 °C |
| Export gas flow | 117 593 kg/h | 117 623 kg/h | +0.03 % |
Sub-1 % agreement on power is the expected level for identical SRK setups; the
residual comes from small differences in the polytropic path integration.