Skip to main content

neqsim-platform-modeling

Production platform topside modeling patterns for NeqSim (FPSO, fixed, semi-sub). USE WHEN: building full topside models from design documents, P&IDs or operating data - TBP-fraction fluids, multi-stage separation with recycles, recompression with compressor curves and anti-surge, export/injection compression, oil stabilization, scrubber liquid recovery, iteration strategies and result extraction. Derived from 15+ NCS platform models.

Source facts

Repository
equinor/neqsim
Last source activity
October 15, 2026 at 13:45
Detected SKILL.md language
English
Stars
156
Forks
73

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
neqsim-platform-modeling
description
Production platform topside modeling patterns for NeqSim (FPSO, fixed, semi-sub). USE WHEN: building full topside models from design documents, P&IDs or operating data - TBP-fraction fluids, multi-stage separation with recycles, recompression with compressor curves and anti-surge, export/injection compression, oil stabilization, scrubber liquid recovery, iteration strategies and result extraction. Derived from 15+ NCS platform models.
last_verified
2026-07-04
# NeqSim Production Platform Modeling Comprehensive patterns for building complete topside process simulations from platform design documents. Derived from production-grade models of 15+ NCS platforms (Åsgard A/B, Troll A/B, Grane, Castberg, Martin Linge, Gudrun, etc.) in the NeqSim-dev-environment. --- ## 1. Architecture Overview ### 1.1 Standard Model Structure Every production platform model follows the same architecture: ``` ProcessInput (pydantic) → simulate() function → ProcessSystem → calc_result() → ProcessOutput (pydantic) ``` | Component | Purpose | Pattern | |-----------|---------|---------| | `ProcessInput` | All operating conditions as typed fields | Pydantic BaseModel with 50-100+ fields | | `simulate()` | Builds and runs the ProcessSystem | Single function, 500-1500 lines | | `calc_result()` | Extracts structured results from solved system | Calls `simulate()`, builds ProcessOutput | | `ProcessOutput` | All results in typed, serializable format | Pydantic BaseModel with response objects | | `neqsim_responses.py` | Helper functions to extract equipment data | `get_separator_response()`, `get_compressor_response()`, etc. | | `pydantic_classes.py` | Shared response types | `CompressorResponse`, `SeparatorResponse`, `HeaterResponse`, etc. | ### 1.2 Two Execution Strategies **Strategy A — Manual Iteration (ASGA pattern):** ```python NUMBER_OF_ITERATIONS = 25 operations = ProcessSystem() # ... build entire flowsheet ... for _ in range(NUMBER_OF_ITERATIONS): operations.run_step() ``` Best for: models with many recycles that need controlled convergence. **Strategy B — Single Blocking Run (Martin Linge pattern):** ```python operations = ProcessSystem() # ... build entire flowsheet ... thread = operations.runAsThread() thread.join(config.SYNC_REQUEST_TIMEOUT_MS) # e.g., 120000 ms if thread.isAlive(): thread.interrupt() raise CalculationTimeout("Timed out") ``` Best for: models with few recycles where NeqSim's internal solver handles convergence. **Strategy C — `ProcessModel.runUntilConverged` with flow filters (recommended for multi-area plants with stagnant legs):** ```python plant = ProcessModel() plant.add("sep train A", sep_a) # ... add all areas ... # Stagnant sections (e.g. an HT injection train at zero rate) are not solved at # all. Manifold, ThrottlingValve, PipeBeggsAndBrills and MultiStreamHeatExchanger # all honour this and still publish their outlet pressure at zero flow, so a # downstream mixer keeps a valid pressure boundary. if inp.injection_gas_rate_ht <= 0.0: for area in ("HT injection process A", "HT injection process B"): plant.get(area).setSectionLowFlowThreshold(50.0, "kg/hr") # The plant gate is a MAX over RELATIVE boundary-stream errors, which a dead leg # dominates (0.007 kg/hr on 0.1 kg/hr = 6.6e-02, vs a real 443 kg/hr on # 138 t/hr = 3.2e-03). Filter it at the source: plant.setBoundaryFlowFloor(1.0) # drop sub-1 kg/hr streams converged = plant.runUntilConverged(15, 1e-3, 1.0) # rel 1e-3 OR abs 1 kg/hr print(plant.getConvergenceSummary()) # prints absolute Δflow too for e in plant.getNonConvergedBoundaryStreamErrors(): print(e.getStreamName(), e.getFlowError(), e.getAbsoluteFlowChange()) ``` Best for: full-platform `ProcessModel`s where some trains are seasonally or scenario-wise inactive. > **Gotcha:** `setSectionLowFlowThreshold()` deactivates units for the rest of > the solve pass. Never apply it to a section that is only dry on the first > recycle iteration (e.g. a JT valve on a separator liquid outlet). --- ## 2. Fluid Creation ### 2.1 Composition with TBP Fractions (Recommended) Production fluids almost always include heavy ends characterized as TBP (True Boiling Point) fractions. The `fluid_creator()` pattern accepts a composition dictionary with optional molar mass and density for TBP fractions: ```python from neqsim import jneqsim def fluid_creator(composition: dict) -> "SystemInterface": """Create a NeqSim fluid from a composition dictionary. Args: composition: dict with keys: - "component_name": list of component names - "molar_composition[-]": list of mole fractions - "molar_mass[kg/mol]": list (None for defined components) - "relative_density[-]": list (None for defined components) """ fluid = jneqsim.thermo.system.SystemSrkEos(273.15 + 15.0, 1.01325) names = composition["component_name"] molfracs = composition["molar_composition[-]"] molar_masses = composition["molar_mass[kg/mol]"] rel_densities = composition["relative_density[-]"] for i, name in enumerate(names): if molar_masses[i] is not None and rel_densities[i] is not None: # TBP fraction — heavy hydrocarbon pseudo-component fluid.addTBPfraction( name, molfracs[i], molar_masses[i], rel_densities[i] ) else: # Defined component (methane, ethane, CO2, water, etc.) fluid.addComponent(name, molfracs[i]) fluid.setMixingRule("classic") fluid.setMultiPhaseCheck(True) fluid.useVolumeCorrection(True) return fluid ``` ### 2.2 Typical NCS Gas Condensate Composition ```python composition = { "component_name": [ "water", "nitrogen", "CO2", "methane", "ethane", "propane", "i-butane", "n-butane", "i-pentane", "n-pentane", "2-methylpentane", # proxy for nC6 fraction # TBP fractions for C10+ (named by carbon range) "nC10-nC12", "nC13-nC15", "nC16-nC18", "nC19-nC22", "nC23-nC26", "nC27-nC30", "nC31-nC34", "nC35-nC38", "nC39+" ], "molar_composition[-]": [ 0.058, 0.005, 0.037, 0.664, 0.086, 0.050, 0.008, 0.016, 0.005, 0.006, 0.006, 0.010, 0.009, 0.006, 0.011, 0.005, 0.004, 0.004, 0.003, 0.002 ], "molar_mass[kg/mol]": [ None, None, None, None, None, None, None, None, None, None, None, 0.091, 0.103, 0.117, 0.146, 0.181, 0.212, 0.248, 0.289, 0.330 ], "relative_density[-]": [ None, None, None, None, None, None, None, None, None, None, None, 0.741, 0.769, 0.789, 0.804, 0.825, 0.838, 0.849, 0.863, 0.875 ], } ``` ### 2.3 Multi-Well Composition (Martin Linge Pattern) When a platform receives fluid from multiple wells/fields, define a base fluid template with ALL possible TBP fractions, then clone and adjust per well: ```python # Base template with all TBP fraction definitions base_fluid = jneqsim.thermo.system.SystemPrEos(273.15 + 30.0, 65.0) base_fluid.addComponent("nitrogen", 0.08) base_fluid.addComponent("CO2", 3.38) base_fluid.addComponent("methane", 69.83) # ... light HCs ... base_fluid.addTBPfraction("C6_FieldA", 0.24, 84.99/1000, 695/1000) base_fluid.addTBPfraction("C6_FieldB", 0.0, 84.0/1000, 684/1000) # ... more TBP fractions for each field source ... base_fluid.setMixingRule("classic") # Per-well fluids via clone + setMolarComposition well_fluid_A = base_fluid.clone() well_fluid_A.setMolarComposition([0.108, 3.379, 69.5, ...]) # FieldA composition well_fluid_B = base_fluid.clone() well_fluid_B.setMolarComposition([0.095, 2.100, 72.1, ...]) # FieldB composition ``` **Key rule**: All wells must share the same component list (same TBP fraction definitions). Use `setMolarComposition()` to set zero fractions for components not present in a particular well. **Prefer a shared E300 characterization for multi-reservoir mixing.** When two or more reservoirs (e.g. an oil and a gas-condensate field) feed a common topside, build BOTH fluids from a SINGLE PVTsim/E300 characterization file and apply different compositions via `setMolarComposition()`, rather than hand-building two separate `addTBPfraction` templates: ```python e300 = jneqsim.thermo.util.readwrite.EclipseFluidReadWrite oil = e300.read("shared_characterization.e300") oil.setMolarComposition([...oil-heavy fractions...]) # same component order as the E300 CNAMES oil.setMixingRule(2); oil.setMultiPhaseCheck(True) gas = e300.read("shared_characterization.e300") gas.setMolarComposition([...condensate-light fractions...]) gas.setMixingRule(2); gas.setMultiPhaseCheck(True) ``` Why this matters: well-defined pseudo-components from a real characterization carry full ideal-gas Cp coefficients, so they keep **finite enthalpy** after the `clone()` + re-flash that happens inside `WellFlow`/`PipeBeggsAndBrills` and isenthalpic chokes. Hand-built `addTBPfraction` pseudo-components can have degenerate Cp coefficients that lose enthalpy on re-flash and produce **NaN** downstream (e.g. NaN compressor power). A shared file also makes topside mixing consistent because every stream uses the same component basis. Set a nonzero water mole fraction directly in the composition (H2O is a CNAMES entry) instead of calling `addWater()`. --- ## 3. Process Building Patterns ### 3.1 Pre-create Mixers Before Adding Streams A **critical pattern** in production models: create `StaticMixer` objects at the start, then add streams to them as equipment is built. This solves the forward-reference problem (downstream mixer needs to exist before upstream equipment creates its outlet stream). ```python operations = ProcessSystem() # Pre-create all mixers/manifolds FIRST (no streams yet) inlet_oil_mixer = jneqsim.process.equipment.mixer.StaticMixer("Inlet oil mixer") recomp_gas_mixer = jneqsim.process.equipment.mixer.StaticMixer("Recomp gas mixer") # A production/gathering/export MANIFOLD is modelled with the Manifold class, # NOT a Mixer/StaticMixer: it carries header/branch diameters and ALWAYS routes # downstream through a split stream. For a single destination give it one split # (setSplitFactors([1.0])) and route getSplitStream(0); for several destinations # set setSplitFactors([...]) and read getSplitStream(i). getMixedStream() is the # internal commingled stream (before the split) - for inspection only. export_manifold = jneqsim.process.equipment.manifold.Manifold("Export manifold") export_manifold.setHeaderInnerDiameter(0.9) export_manifold.setSplitFactors([1.0]) recycle_oil_mixer = jneqsim.process.equipment.mixer.StaticMixer("Recycle from export") # Build upstream equipment... # Then add their outlets to the pre-created mixers: inlet_oil_mixer.addStream(oil_heater_test.getOutStream()) inlet_oil_mixer.addStream(oil_heater_prod.getOutStream()) # ... later, when you're ready to use the mixer: operations.add(inlet_oil_mixer) ``` **Important**: Add the mixer to ProcessSystem AFTER all its inlet streams are connected. All `addStream()` calls must happen before `operations.add(mixer)`. ### 3.2 Multi-Stage Separation Train Standard NCS platform separation: HP → MP → LP with oil heating/letdown between stages: ```python # === First Stage (HP) Separator === well_stream = Stream("Well Stream", well_fluid) well_stream.setFlowRate(process_input.flow_rate, "kg/hr") well_stream.setTemperature(process_input.inlet_temp, "C") well_stream.setPressure(process_input.hp_pressure, "bara") operations.add(well_stream) hp_separator = ThreePhaseSeparator("HP Separator", well_stream) operations.add(hp_separator) # === Oil letdown to Second Stage === oil_heater_to_mp = Heater("Oil heater to MP", hp_separator.getLiquidOutStream()) oil_heater_to_mp.setOutTemperature(process_input.mp_temperature, "C") oil_heater_to_mp.setOutPressure(process_input.mp_pressure, "bara") operations.add(oil_heater_to_mp) # Collect oil from multiple sources (see pre-created mixer pattern §3.1) inlet_oil_mixer.addStream(oil_heater_to_mp.getOutStream()) # === Second Stage (MP) Separator === mp_separator = ThreePhaseSeparator("MP Separator", inlet_oil_mixer.getOutletStream()) operations.add(mp_separator) # === Oil letdown to Third Stage === oil_to_lp = Heater("Oil to LP", mp_separator.getLiquidOutStream()) oil_to_lp.setOutTemperature(process_input.lp_temperature, "C") oil_to_lp.setOutPressure(process_input.lp_pressure, "bara") operations.add(oil_to_lp) # === Third Stage (LP) Separator === lp_separator = Separator("LP Separator", oil_to_lp.getOutletStream()) operations.add(lp_separator) # Oil export pump oil_pump = Pump("Oil Export Pump", lp_separator.getLiquidOutStream()) oil_pump.setOutletPressure(process_input.oil_export_pressure + 1.01325) # barg to bara operations.add(oil_pump) ``` ### 3.2.1 Separator Physical Configuration via MechanicalDesign Physical dimensions (vessel ID, nozzle sizes), internals (demister type, inlet device), and design parameters (K-factor, retention time) are set via `SeparatorMechanicalDesign` — NOT directly on the `Separator`. This follows the same pattern used for wells, pipelines, and compressors in NeqSim. Call `initMechanicalDesign()` **after** the process has been `run()` so the design calculation has access to process conditions. ```python # After operations.run(): hp_separator.initMechanicalDesign() hp_design = hp_separator.getMechanicalDesign() hp_design.setMaxOperationPressure(85.0) hp_design.setMaxOperationTemperature(273.15 + 80.0) hp_design.setGasLoadFactor(0.107) # K-factor [m/s] hp_design.setRetentionTime(180.0) # Liquid retention [s] hp_design.setInletNozzleID(0.356) # 14-inch inlet [m] hp_design.setDemisterType("wire_mesh")
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub