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.

Quellinformationen

Repository
equinor/neqsim
Letzte Quellaktivität
15. Oktober 2026 um 13:45
Erkannte Sprache von SKILL.md
Englisch
Sterne
156
Forks
73

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
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")
Auf GitHub ansehen
Diese SKILL.md ist sehr gross, daher zeigt SkillsMP hier nur den ersten Abschnitt. Auf GitHub ansehen