Process Modeling with NeqSim in Python: A Reproducible, Project-Based Learning Path with Direct Java Access via jneqsim
Copyright
Copyright © 2026 Even Solbraa and the NeqSim Project.
This book is distributed under the Creative Commons Attribution 4.0 International License (CC BY 4.0). You are free to share and adapt the material for any purpose, including commercially, provided you give appropriate credit to the authors and the NeqSim Project, indicate if changes were made, and do not imply endorsement by the authors.
NeqSim is an open-source Java library for thermodynamic and process simulation released under the Apache License 2.0 and developed openly at https://github.com/equinor/neqsim. The companion Python package neqsim-python is released under the same license and developed at https://github.com/equinor/neqsim-python.
The code examples in this book are released under the MIT License. They may be copied, modified, and embedded into proprietary or open-source projects without restriction.
All trademarks are property of their respective owners.
The information in this book is provided "as is" and represents the author's understanding at the time of writing. Process engineering decisions based on this material remain the responsibility of the engineer of record.
Published by NeqSim Project and NTNU
Typeset using NeqSim PaperLab
To the engineers and students who pick up a Python prompt expecting numbers — and discover a process simulator behind them.
Preface
Process simulation in industry has historically belonged to dedicated desktop applications: HYSYS, UniSim, ProMax, Pro/II, and a handful of in-house tools. Each comes with its own user interface, its own scripting language, its own data model, and its own licensing model. Engineers who learn one of them are typically locked into that ecosystem for the lifetime of a project.
NeqSim takes a different path. It is an open-source Java library that grew from NTNU research into a mature industrial simulation toolkit, exposing its full thermodynamic and process simulation engine through plain Java method calls. Anything the Java code can do — create a fluid, run a flash, build a recompression train, calculate a hydrate temperature, run a transient depressurization, write a JSON report — can be done from Python using a single bridge package: neqsim-python.
The aim of this book is to be a state-of-the-art introduction to that stack: NeqSim as the calculation engine, Python as the engineering workbench, and notebooks, APIs, automation variables, validation checks, and AI-assisted tools as normal parts of the same workflow. The goal is not only to reproduce a desktop simulator in code. The goal is to make process models versioned, inspectable, automatable, and easy to defend in a technical review.
This is what makes NeqSim particularly powerful for industrial process modeling in 2026. A modern facility engineer rarely lives inside a single GUI. They work with Jupyter notebooks, plant historians, REST APIs, FastAPI services, LangChain agents, Excel dashboards, Power BI reports, MQTT brokers, and Git repositories. NeqSim slots into that ecosystem natively, because its Python interface is not a wrapper around a subset of features — it is a direct view into the same Java objects that drive the simulator.
What This Book Covers
This book teaches you to build industrial-grade process models in Python using NeqSim. It starts from the absolute basics:
- How NeqSim is built as a Java library, and why that matters when you call it from Python.
- The neqsim-python package, how it bridges Java and Python through JPype, and the four ways to use it.
- Direct Java access via
jneqsim, the recommended approach for serious process modeling because you get the complete API with full type information.
It then takes you through the full modeling stack:
- Fluid creation with all major equations of state (SRK, PR, CPA, UMR-PRU, GERG-2008, Electrolyte CPA).
- Fluid characterization, including TBP cuts, plus-fraction splitting, hypothetical components, and Eclipse/E300 fluid transfer.
- PVT simulations — CME, CVD, differential liberation, separator tests, swelling, saturation pressure, viscosity.
- Unit operations — separators, compressors, heat exchangers, valves, pipes, mixers, splitters, pumps, expanders, reactors.
- Process simulation — building flowsheets, recycles, adjusters, multi-area models with
ProcessModel. - Distillation and columns — tray numbering, staged specifications, homotopy, side draws, pumparounds, hydraulics, rate-based packed columns, and the current direct, damped, inside-out, matrix inside-out, sum-rates, Naphtali-Sandholm, MESH residual, and AUTO solver workflows.
- Dynamic process simulation — transient flowsheets, PID controllers, measurement devices, depressurization, surge analysis.
- The Automation API — string-addressable variable access, self-healing diagnostics, and plant-data integration via tagreader.
- Reporting and visualization — JSON state, Plotly, matplotlib, results.json, and the professional reporting toolkit.
- Web APIs — wrapping NeqSim in FastAPI services, NeqSimAPI patterns, and stateful operations-runtime interfaces.
Finally, it closes with a capstone path. You first assemble and audit a generic integrated offshore topside model with all inputs, reporting, and serialization made explicit. You then tour selected notebooks from the NeqSim-Colab collection as a reusable example library. The final chapter turns those converged process models into optimization and debottlenecking workflows grounded in the official NeqSim documentation and examples.
This book is meant to be read beside the living NeqSim documentation at https://equinor.github.io/neqsim/. The public site is the authoritative reference for API changes, quickstarts, examples, and package-specific guides. In particular, readers should keep these sections open while working through the book:
- Python quickstart: https://equinor.github.io/neqsim/quickstart/python-quickstart.html
- Reference manual: https://equinor.github.io/neqsim/REFERENCE_MANUAL_INDEX.html
- Process equipment: https://equinor.github.io/neqsim/process/README.html
- Recycle loops: https://equinor.github.io/neqsim/process/equipment/util/recycles.html
- Process recipes: https://equinor.github.io/neqsim/cookbook/process-recipes.html
- Process optimization: https://equinor.github.io/neqsim/process/optimization/OPTIMIZATION_OVERVIEW.html
- External optimizer integration: https://equinor.github.io/neqsim/integration/EXTERNAL_OPTIMIZER_INTEGRATION.html
- Compressors: https://equinor.github.io/neqsim/process/equipment/compressors.html
- Compressor curves: https://equinor.github.io/neqsim/process/equipment/compressor_curves.html
- Distillation equipment: https://equinor.github.io/neqsim/process/equipment/distillation.html
- Examples: https://equinor.github.io/neqsim/examples/
The chapters teach a stable modelling workflow; the documentation site gives the up-to-date class names, examples, and deeper reference pages.
How This Book Teaches
The book is built around a reproducible learning loop: read the concept, run the notebook, explain the output, change one input, validate the result, and package the evidence. That loop appears in the front-matter Learning Roadmap, in the chapter notebooks, in the expected-output sections, and in the Capstone Portfolio checklist at the end of the book.
This is intentional. NeqSim is powerful enough to produce numbers quickly, but industrial learning requires more than numbers. A state-of-the-art simulation workflow should leave behind the model code, input assumptions, figures, validation checks, saved state, machine-readable results, and a short engineering recommendation. The reader should not only know how to run NeqSim; they should know how to make a NeqSim result reviewable by another engineer, callable by a service, and understandable to an AI agent or downstream tool.
Each chapter therefore has three jobs:
- Teach the concept or API surface.
- Provide runnable code or a notebook-backed example.
- Point toward an artifact that belongs in a growing process-model portfolio.
What This Book Does Not Cover
This book is not a thermodynamics textbook. It assumes you know what a flash calculation is and what a cubic equation of state does. Compressor curves and distillation columns are now covered as NeqSim modelling workflows, but if you need deeper underlying theory, the companion online book Industrial Agentic Engineering with NeqSim and the standard texts of Prausnitz, Smith–Van Ness, and Kontogeorgis are good starting points.
It is also not a Java tutorial. You do not need to be a Java programmer to use NeqSim from Python — that is the entire point — but you will write code that calls Java classes, so a basic familiarity with object-oriented programming helps.
How to Read This Book
Start with the Learning Roadmap that follows this preface. It explains the book-level outcomes, the mastery loop, suggested reader paths, and the capstone portfolio rhythm.
Read Part I in order. The four chapters on the Java architecture, the Python package, and direct Java access are the conceptual foundation that makes everything else feel obvious instead of mysterious.
After that, the parts are independent. Part II is for thermodynamicists and PVT engineers. Part III is for process engineers building flowsheets. Part IV is for engineers who want to expose models to other tools — REST APIs, dashboards, agents, plant data. Part V deepens the equipment work: compressor maps, distillation diagnostics, LNG heat exchangers, coupled turboexpander-compressors, custom units, and parameter databases. Part VI then turns those pieces into study work: Chapter 18 optimizes converged models, Chapter 19 is the integrated capstone base case, and Chapter 20 points to the curated notebook library for smaller patterns.
The code in this book is written to be copy-pasteable. Every snippet imports the classes it uses, so you can drop any block into a Jupyter cell and run it. The accompanying notebooks under notebooks/ in each chapter directory contain the executed versions with figures.
If you are using the book for self-study or teaching, keep a small project folder as you read. Save one artifact from each part: an environment check, a fluid report, a process model, a saved state or report, and a final capstone study. By the end, that folder is stronger evidence of learning than a set of highlighted pages.
Acknowledgements
This book builds on twenty-four years of NeqSim development. Special thanks to Even Solbraa for the underlying Java engine, to the engineers and contributors who pressure-tested the Python interface in real process-modeling workflows, and to Even Sølbe for the NeqSim-Colab notebook collection that introduced thousands of engineers and students to NeqSim through a browser.
Learning Roadmap
This book is designed as a learning system, not only as a reference manual. The goal is that a reader can move from a first NeqSim notebook to a defensible industrial process model with validation evidence, generated figures, exported state files, and a clear engineering recommendation.
The Mastery Loop
Use the same loop in every chapter:
- Read the concept. Identify the physical or software idea being taught.
- Run the notebook. Execute the chapter notebook or code block before changing it.
- Explain the output. Write one or two sentences describing what changed, why it changed, and whether the number is physically plausible.
- Modify one input. Change pressure, temperature, composition, flow rate, efficiency, or a model option and rerun.
- Validate the result. Check mass balance, units, convergence, and whether the result agrees with a simpler hand estimate or reference case.
- Package the evidence. Save the figure, table,
results.json,.neqsimarchive, or lifecycle-state JSON that would let another engineer reproduce the conclusion.
This loop is the difference between running a simulation and learning process modeling. A notebook that produces a number is a calculation. A notebook that can be explained, modified, validated, and shared is engineering evidence.
Code and Case-Study Contract
Every chapter follows the same reproducibility contract. Runnable Python blocks are checked in chapter order, while short API fragments that require surrounding context are marked ` in the Markdown source. The refreshed code_block_report.json` is therefore a book-level health check: it should have zero failed runnable examples before a release.
The book also uses one recurring wet rich-gas export case. It begins as a fluid definition, becomes a PVT and process-simulation case, is exposed through automation and reporting, and ends as an optimization exercise. The back-matter appendix "Reproducibility, Running Case, and Engineering Interpretation" gives the ledger readers should maintain as the same case grows across chapters.
Book-Level Learning Outcomes
By the end of the book, you should be able to:
- Build thermodynamic fluids in NeqSim from Python and choose an equation of state that matches the engineering question.
- Run flash, PVT, and physical-property calculations while respecting NeqSim's initialization and unit conventions.
- Assemble steady-state and dynamic process models from streams and unit operations using direct Java access through
jneqsim. - Use
ProcessSystem,ProcessModel,ProcessAutomation, lifecycle-state JSON, and.neqsimarchives to make models reusable and inspectable. - Produce notebook-backed figures, tables, reports, and exports that support a design or operating decision.
- Compare NeqSim results with reference data, plant data, another simulator, or an independent calculation.
- Run parameter sweeps, batch studies, managed parallel scenario studies, and bounded optimization without losing traceability.
- Package a complete process-model study as a small reproducible portfolio.
Learning Paths
Different readers can take different routes through the same book.
| Reader | Recommended path | End product |
|---|---|---|
| New NeqSim user | Chapter 0, then Chapters 1-5 and 8-9 | A runnable separator or compressor notebook |
| Thermodynamics or PVT engineer | Chapter 0, then Chapters 1-7 and 13 | A fluid-characterization or PVT report notebook |
| Process engineer | Chapter 0, then Chapters 1-5, 8-11, 15-16, and 19 | A complete flowsheet with compressor and column validation checks |
| Digital-twin or automation engineer | Chapter 0, then Chapters 1-4, 9, and 12-14 | A model exposed through variables, JSON, or an API |
| Optimization or study engineer | Chapters 8-9, 13, 15-18, then 19-20 | A scenario table, equipment-envelope check, optimization result, and decision memo |
| Instructor | Use each chapter's objectives, notebooks, and exercises | A course module with reproducible labs |
Competency Matrix
| Part | Core competency | Practice asset | Evidence of mastery |
|---|---|---|---|
| Part I | Understand how Python reaches the Java simulator | Introductory code blocks and environment checks | A working notebook that imports jneqsim and runs a simple calculation |
| Part II | Build and validate fluids | Fluid, characterization, and PVT notebooks | A fluid report with composition, EOS, flash results, and a physical-property check |
| Part III | Assemble process systems | Unit-operation and flowsheet notebooks | A process model with mass balance, equipment KPIs, and a generated figure |
| Part IV | Expose, save, and report models | Automation, reporting, and API examples | results.json, lifecycle state, .neqsim, and a small report or API response |
| Part V | Check advanced equipment envelopes | Compressor-map, distillation-column, advanced-equipment, and custom-unit chapters | A compressor envelope table, column diagnostic report, or advanced-equipment checklist with solver and residual evidence |
| Part VI | Integrate, compare, and optimize | Optimization, capstone, and Colab comparison chapters | A reproducible study portfolio with scenarios, validation, figures, and recommendation |
What Counts as State of the Art
For this book, state of the art does not mean using the most complicated model available. It means using the smallest model that is technically defensible and leaving enough evidence that another engineer can reproduce the decision.
Use these habits throughout the book:
- Prefer direct Java access for serious process models so the Python code stays aligned with the current NeqSim API.
- Set a mixing rule for every EOS fluid and initialize properties before reading transport or physical-property values.
- Keep inputs and outputs structured: dictionaries, dataclasses, JSON, or
ProcessAutomationvariables are easier to audit than scattered notebook cells. - Design for automation from the start: stable unit names, explicit input and output variables, lifecycle-state files, and MCP/API boundaries make the same model usable by notebooks, dashboards, optimizers, and AI agents.
- Treat plots as engineering evidence: label axes, include units, and explain what the curve means for design or operation.
- Compare against a benchmark whenever possible: a hand estimate, a published case, plant data, another simulator, or a previous version of the same model.
- Use managed parallelism for independent studies. Build one process model per case and use the
runAsTask()/Futurepattern for large batches. - Save the final state. If a result matters, preserve the notebook, figure, input table, output JSON, and model archive.
Suggested Portfolio Rhythm
At the end of each part, keep one small artifact:
- After Part I: a one-cell environment check and a direct-access mini model.
- After Part II: a fluid notebook with an EOS choice and a reference check.
- After Part III: a steady-state process notebook with mass-balance closure.
- After Part IV: a saved model state and a machine-readable output file.
- After Part V: one equipment deep-dive artifact: compressor map envelope, anti-surge margin table, distillation solver diagnostic report, or advanced-equipment checklist.
- After Part VI: a complete capstone study with scenarios, validation, and a recommendation.
The Capstone Portfolio in the back matter gives a compact checklist and rubric for turning these artifacts into a finished learning project.
Task Decision Tree — "I Want to ___"
The reader-path matrix in the Learning Roadmap is organised by role. This page is organised by task — the question a working process engineer types into a search box on a Monday morning. Find your task, load the listed class, jump to the chapter.
Thermodynamics and Fluids
| I want to… | Use | Chapter |
|---|---|---|
| Build a natural-gas fluid | SystemSrkEos + setMixingRule("classic") |
5 |
| Build a polar / glycol / methanol fluid | SystemSrkCPAstatoil |
5 |
| Characterise a C7+ plus fraction | characterisePlusFraction |
6 |
| Import an Eclipse/E300 fluid | EclipseFluidReadWrite.read |
6 |
| Run a phase envelope | ThermodynamicOperations.calcPTphaseEnvelope |
7 |
| Run a CME / CVD / DL | ConstantMassExpansion etc. |
7 |
| Predict a hydrate-formation temperature | ThermodynamicOperations.hydrateFormationTemperature() |
7 |
Steady-State Process Modelling
| I want to… | Use | Chapter |
|---|---|---|
| Size a two-phase separator | Separator + SeparatorMechanicalDesign |
8 |
| Size a three-phase separator | ThreePhaseSeparator |
8 |
| Model a centrifugal compressor with a curve | Compressor.setCompressorChart |
15 |
| Check compressor surge, stonewall, and driver limits | CompressorChart, AntiSurge, CompressorDriver |
15 |
| Model a heat exchanger thermally and hydraulically | HeatExchanger + Bell–Delaware |
8 |
| Compute pipeline pressure drop | PipeBeggsAndBrills |
8 |
| Build a solvent, reactor, or recompression recycle | Recycle tear stream + convergence residuals |
9 |
| Close an anti-surge / lean-rich recycle | Recycle + compressor anti-surge margin |
9, 15 |
| Hit a product spec with a single variable | Adjuster |
9 |
| Build a multi-area plant (separation + compression + utilities) | ProcessModel |
9 |
| Build a deethaniser / debutaniser | DistillationColumn + solver diagnostics |
16 |
| Build an amine absorber | DistillationColumn or RateBasedPackedColumn |
16 |
| Model LNG exchangers, turboexpander-compressors, or custom unit operations | LNGHeatExchanger, TurboExpanderCompressor, custom ProcessEquipmentBaseClass units |
17 |
Dynamic and Safety
| I want to… | Use | Chapter |
|---|---|---|
| Run a blowdown / depressurisation | runTransient + open-valve schedule |
11 |
| Tune a PID controller | ControllerDeviceInterface + transmitters |
11 |
| Check MDMT after blowdown | DepressurizationSimulator (skill neqsim-depressurization-mdmt) |
11 |
| Size a PSV | ReliefValveSizing |
11 |
| Screen water hammer | WaterHammerStudy |
11 |
Industrial Integration
| I want to… | Use | Chapter |
|---|---|---|
| Read or write a variable by name | ProcessAutomation.getVariableValue |
12 |
| Save a model snapshot to JSON | ProcessSystemState.toJson |
12 |
| Compare two model versions | ProcessModelState.compare |
12 |
| Match the model to plant data | tagreader + ProcessAutomation (skill neqsim-plant-data) |
12 |
| Produce a Word/HTML engineering report | professional-reporting toolchain |
13 |
| Expose the model behind a REST API | FastAPI + ProcessSystem.fromJsonAndRun |
14 |
| Let an AI agent drive NeqSim | NeqSim MCP server | 1, 14 |
| Package a calculation for review or automation | results.json + lifecycle-state JSON |
12, 13 |
Optimisation and Studies
| I want to… | Use | Chapter |
|---|---|---|
| Minimise compressor power under constraints | SQPoptimizer |
18 |
| Run a parameter sweep | BatchStudy |
18 |
| Pareto-trade-off two objectives | MultiObjectiveOptimizer |
18 |
| Run an uncertainty / Monte Carlo study | MonteCarloSimulator |
18 |
| Bridge to SciPy / Pyomo / BoTorch | ProcessSimulationEvaluator |
18 |
When in Doubt
If your task is not in this table, search docs/development/TASK_LOG.md in the NeqSim repository — every solved engineering task is indexed there with keywords and solution paths.
Part 0: Quickstart
30-Minute Quickstart for Process Engineers
Learning Objectives
After this chapter you will:
- Install
neqsimand confirm the JVM starts. - Build a wet-gas fluid, run a flash, and read the key properties.
- Assemble a small flowsheet — feed → separator → cooler → compressor — and read engineering KPIs from it.
- Save a
results.jsonsnapshot you can hand to a colleague, report tool, service, or AI-assisted workflow.
The whole chapter is one notebook. It is the same wet rich-gas case used throughout the rest of the book (see Appendix Reproducibility, Running Case, and Engineering Interpretation).
0.1 Install and Smoke Test
pip install neqsim
python -c "from neqsim import jneqsim as J; print(J.thermo.system.SystemSrkEos(298.15, 60.0).getNumberOfComponents())"
If the second line prints 0 you have a working JVM bridge. If it raises No JVM shared library file, install a JDK 17 and set JAVA_HOME.
0.2 The Running Case
A wet rich-gas export stream. We use the same composition everywhere in the book.
from neqsim import jneqsim as J
fluid = J.thermo.system.SystemSrkEos(298.15, 60.0)
fluid.addComponent("nitrogen", 0.005)
fluid.addComponent("CO2", 0.020)
fluid.addComponent("methane", 0.830)
fluid.addComponent("ethane", 0.075)
fluid.addComponent("propane", 0.035)
fluid.addComponent("i-butane", 0.008)
fluid.addComponent("n-butane", 0.012)
fluid.addComponent("i-pentane", 0.004)
fluid.addComponent("n-pentane", 0.005)
fluid.addComponent("n-hexane", 0.003)
fluid.addComponent("water", 0.003)
fluid.setMixingRule("classic")
setMixingRule("classic") is mandatory. Forgetting it is the most common first-hour error — see Appendix First-Hour Troubleshooting.
0.3 First Flash — One Cell, One Result
ops = J.thermodynamicoperations.ThermodynamicOperations(fluid)
ops.TPflash()
fluid.initProperties() # initialises transport properties too
print(f"Phases: {fluid.getNumberOfPhases()}")
print(f"Density: {fluid.getDensity('kg/m3'):.2f} kg/m3")
print(f"Gas frac: {fluid.getPhase('gas').getMolarMass()/fluid.getMolarMass():.3f}")
print(f"Mu gas: {fluid.getPhase('gas').getViscosity('kg/msec')*1000:.4f} cP")
Read this carefully:
TPflash()does the equilibrium calculation at the fluid's current T,P.initProperties()is mandatory before reading viscosity, thermal conductivity, or density. Without it, transport properties may read as zero — a silent error.
0.4 A Complete Flowsheet — Feed, Separator, Cooler, Compressor
# --- Equipment -------------------------------------------------------------
feed = J.process.equipment.stream.Stream("Feed", fluid)
feed.setFlowRate(50_000.0, "kg/hr")
feed.setTemperature(40.0, "C")
feed.setPressure(60.0, "bara")
scrubber = J.process.equipment.separator.Separator("V-100", feed)
cooler = J.process.equipment.heatexchanger.Cooler(
"E-100", scrubber.getGasOutStream())
cooler.setOutTemperature(25.0, "C")
compressor = J.process.equipment.compressor.Compressor(
"K-100", cooler.getOutStream())
compressor.setOutletPressure(120.0, "bara")
compressor.setIsentropicEfficiency(0.78)
# --- Flowsheet -------------------------------------------------------------
proc = J.process.processmodel.ProcessSystem()
proc.add(feed); proc.add(scrubber); proc.add(cooler); proc.add(compressor)
proc.run()
# --- KPIs ------------------------------------------------------------------
print(f"Liquid knockout : {scrubber.getLiquidOutStream().getFlowRate('kg/hr'):8.1f} kg/h")
print(f"Cooler duty : {cooler.getDuty()/1e3:8.1f} kW")
print(f"Compressor power: {compressor.getPower()/1e3:8.1f} kW")
print(f"Discharge T : {compressor.getOutletStream().getTemperature('C'):8.1f} °C")
That is a complete steady-state flowsheet. It runs, it converges, and it prints four engineering numbers. Everything else in Part III of this book is a refinement of this pattern: better thermodynamics, richer equipment, recycles, dynamic behavior, automation variables, and a clearer evidence trail.
0.5 Mass Balance — Always Check
A process engineer never trusts a simulator that has not been checked. The five-line check below should appear at the bottom of every flowsheet notebook you write.
mass_in = feed.getFlowRate("kg/hr")
mass_out = (scrubber.getLiquidOutStream().getFlowRate("kg/hr")
+ compressor.getOutletStream().getFlowRate("kg/hr"))
err_pct = 100.0 * (mass_in - mass_out) / mass_in
assert abs(err_pct) < 0.01, f"Mass balance off by {err_pct:.4f}%"
print(f"Mass balance error: {err_pct:.5f}%")
If this assertion fails, do not interpret any other number from the model.
0.6 Save Evidence — results.json
A simulation result you cannot hand to a colleague is not engineering output. NeqSim's task-workflow convention is one results.json per study. Treat it as the first state-of-the-art habit: every important calculation should leave a machine-readable summary of inputs, key results, validation status, and method.
import json
results = {
"key_results": {
"liquid_knockout_kg_per_hr": scrubber.getLiquidOutStream().getFlowRate("kg/hr"),
"cooler_duty_kW": cooler.getDuty() / 1e3,
"compressor_power_kW": compressor.getPower() / 1e3,
"discharge_T_C": compressor.getOutletStream().getTemperature("C"),
},
"validation": {
"mass_balance_error_pct": err_pct,
"acceptance_criteria_met": abs(err_pct) < 0.01,
},
"approach": "SRK EOS, classic mixing, wet rich-gas case, no recycle.",
}
with open("results.json", "w") as f:
json.dump(results, f, indent=2)
Chapter 13 builds this idea out to figures, equations, and traceable references.
0.7 What You Just Learned (Without Knowing It)
In thirty minutes you have used:
- The direct Java access style (
jneqsim) — Chapter 4. - An SRK equation of state — Chapter 5.
- A TP flash — Chapter 7.
- Four unit operations — Chapter 8.
- A
ProcessSystem— Chapter 9. - The book's validation and evidence contract — Chapters 12-14.
That combination is the core of modern NeqSim-in-Python work: a physics model, a reproducible script, a validation check, and an output file that another tool can consume.
The rest of the book is "the same thing, with more rigour". When a chapter introduces a new API, it is usually replacing one line above with a richer one. If you ever feel lost, come back here.
0.8 Where to Go Next
| If you want to… | Go to |
|---|---|
| Understand why the code looks like this | Chapter 1 |
| Add a real plus-fraction characterisation | Chapter 6 |
| Add a recycle loop | Chapter 9 |
| Add a distillation column | Chapter 10 |
| Make the model transient | Chapter 11 |
| Bind it to historian tags | Chapter 12 |
| Build a Word/HTML report | Chapter 13 |
| Optimise discharge pressure | Chapter 18 |
| Just see more examples | Chapter 20 |
If you only have time for one more chapter, read Chapter 9.
Exercises
- Change the discharge pressure to 90 bara. By how much does the compressor power drop? Is the discharge temperature still acceptable for typical seal limits (~150 °C)?
- Increase the water content to 1 %. What happens to the liquid knockout flow? Is the cooler outlet still single-phase gas?
- Swap
SystemSrkEosforSystemPrEos. Do the four KPIs change by more than 1 %? If not, the EOS choice does not matter for this case. - Rewrite the flowsheet so the compressor outlet feeds a second cooler that brings the gas back to 40 °C before export. Repeat the mass balance check.
Reproducible Notebook Results
The notebook for this chapter is notebooks/00_quickstart.ipynb. It ships with cell outputs included. Re-running it on a fresh machine should reproduce all four KPIs to within 0.1 %.
Looking Ahead
Chapter 1 explains why the book is written this way and what makes this Python-first style different from a desktop simulator. If you found this chapter natural, you can read Chapter 1 as motivation and jump straight to Chapter 5. If parts felt magical (the jneqsim import, the unit strings, the ProcessSystem execution order), the next four chapters will demystify them.
NeqSim and Python
Introduction: Why NeqSim in Python
Learning Objectives
After reading this chapter, the reader will be able to:
- Explain what NeqSim is and how its open-source Java origin shapes the Python experience.
- Distinguish between the four ways of using NeqSim from Python and recognise when each is appropriate.
- Describe the typical industrial workflows that benefit from a scriptable process simulator embedded in Python.
- Decide whether the neqsim-python package, the jneqsim direct-access layer, the MCP server, or a REST wrapper is the right interface for a given project.
- Recognise the evidence trail expected from a modern process-simulation study: code, assumptions, validation checks, figures, saved state, and a machine-readable result package.
1.1 What This Book Is About
A process model is a structured representation of a physical process — streams, equipment, reactions, controls, and the constraints that link them — that can be evaluated to predict what the real process will do under specified conditions. For most of the history of computer-aided process engineering, that representation has lived inside a desktop application. The user opened the application, drew a flowsheet, picked components from a database, ran a flash, and read numbers back out of a tabular results window.
This book is about doing the same job in Python. Not in a Python wrapper that hides the simulator behind a thin layer of convenience functions, but in the simulator itself, manipulated as live objects in a Jupyter notebook, automation script, web service, or AI-assisted engineering workflow. The simulator is NeqSim, an open-source thermodynamic and process simulation engine written in Java. The bridge is neqsim-python, a small package that exposes every NeqSim Java class to Python through JPype.
The result is unusual. You write Python — concise, expressive, productive Python — that calls Java methods directly. There is no translation layer to get out of date, no subset of features that "isn't supported yet," and no parallel API to learn. When NeqSim adds a new equation of state, a new column solver, a new equipment model, or a new automation feature, it appears in Python the moment the underlying Java classes are available. When you read the NeqSim Javadoc, you are reading the Python API.
That makes NeqSim in Python more than a scripting convenience. It is a modern engineering platform: notebooks for exploration, version-controlled source for review, ProcessAutomation for string-addressable variables, lifecycle-state JSON and .neqsim archives for reproducibility, REST and MCP interfaces for tool integration, and results.json for report generation. A state-of-the-art NeqSim study should be executable, inspectable, and packaged so another engineer can reproduce the conclusion without reverse-engineering a GUI case.
1.2 The Industrial Context
The reason this matters in 2026 is that process engineering work no longer happens primarily inside a GUI. Consider a typical week in a topside process-engineering group at an oil and gas operator:
- A field engineer asks: "If we increase the inlet pressure by three bar, do we still meet the dew-point spec at the export meter?" The answer requires a flash, a pipeline pressure-drop calculation, and a hydrate margin check. The data sits in a historian and a STID export. The deliverable is a chat message and a one-page PDF.
- The reservoir team sends a new fluid composition and asks for a swelling test plot at three temperatures. The composition arrives in an Excel file. The plot is needed by Friday for a partner meeting.
- A digital-twin project needs a process model that exposes inlet conditions as inputs and predicted compressor power, discharge temperature, and recycle flow as outputs over a REST API, so that a dashboard can stream live predictions next to historian tags.
- A safety study needs a transient depressurization curve from 85 to 7 bar in 15 minutes, with end-of-blowdown temperature compared to the vessel MDMT, for ten different inventory cases.
- A training group wants a Jupyter notebook that students can open in Colab to see how an SRK fluid behaves around its critical point.
None of these workflows fit naturally into a desktop simulator. They all fit naturally into Python. They share data via pandas DataFrames, results via JSON, plots via Plotly and matplotlib, deployments via FastAPI and Docker, and reviews via Git pull requests. NeqSim in Python lives in this world because Python lives in this world.
The common thread is traceability. Modern process simulation is no longer just "run the case and copy the number." It is run the case, record the input basis, validate balances and convergence, save the model state, produce figures with units, expose the result to another tool if needed, and keep enough context for a colleague or agent to rerun the workflow later.
1.3 The Four Ways to Use NeqSim from Python
The neqsim-python package exposes four distinct entry points. They are not competitors — each is appropriate for a different audience and use case.
1.3.1 Convenience Python Modules
The neqsim.thermo, neqsim.process, and neqsim.pvt submodules wrap common operations in idiomatic Python functions:
from neqsim.thermo import fluid, TPflash
gas = fluid("srk")
gas.addComponent("methane", 0.9)
gas.addComponent("ethane", 0.1)
gas.setMixingRule("classic")
TPflash(gas)
This style is excellent for short scripts and teaching. It hides the JPype machinery and provides defaults that beginners expect. It covers many common operations but does not expose the full API.
1.3.2 Functional Wrappers Returning Pandas DataFrames
A second layer of helpers — dataFrame, phaseEnvelope, TPflashDataFrame — returns results as pandas DataFrames or matplotlib figures, ready to drop into a Jupyter notebook:
from neqsim.thermo import dataFrame
df = dataFrame(gas)
This is the data-science-friendly view. It is excellent for quick exploration and reporting, weaker for building complete process models because it operates on individual operations rather than on flowsheets.
1.3.3 NeqSim MCP Server
The third style, introduced in 2025, exposes NeqSim as a Model Context Protocol server. Large language models in VS Code, Claude Desktop, or any MCP-compatible host can call validated NeqSim tools through structured JSON schemas. This is the right choice for agentic workflows where an LLM is orchestrating a study and needs governance, logging, and provenance. It is covered in the companion book Industrial Agentic Engineering with NeqSim.
Sidebar — Letting an AI agent drive NeqSim. A working process engineer in 2026 can ask Copilot or Claude "size a two-phase scrubber for this gas at 60 bara and 25 °C, check liquid carry-over against NORSOK P-001/API 12J separator criteria, and write a results.json" and have a tool-augmented agent build the NeqSim model, run it, and return a validated report. The MCP server provides the verbs (
mcp_neqsim_runProcess,mcp_neqsim_runFlash,mcp_neqsim_solveTask,mcp_neqsim_runWaterHammer,mcp_neqsim_runRelief, ...) with schemas and validation. From the engineer's perspective this is a much faster path to a first model than learning the Python API by hand — and the agent's output is a runnable Python notebook you can then edit using the rest of this book. Chapter 14 shows the same idea on the service-provider side: wrapping a NeqSim model as a tool the agent can call.
1.3.4 Direct Java Access via jneqsim — The Focus of This Book
The fourth and most powerful style is direct Java access. Every NeqSim Java class is accessible through the jneqsim namespace:
from neqsim import jneqsim
fluid = jneqsim.thermo.system.SystemSrkEos(298.15, 60.0)
fluid.addComponent("methane", 0.9)
fluid.addComponent("ethane", 0.1)
fluid.setMixingRule("classic")
ops = jneqsim.thermodynamicoperations.ThermodynamicOperations(fluid)
ops.TPflash()
fluid.initProperties()
print(f"Density: {fluid.getDensity('kg/m3'):.3f} kg/m³")
The same five lines of "real" code, but you now have access to every class NeqSim defines: every solver option, every equipment subtype, every measurement device, every JSON serializer. When you build a serious process model — the kind in a full integrated process-model notebook, the kind that drives a production REST API, the kind that supports a digital twin — this is the interface you will use. This book teaches the direct-access style as the default, with brief mentions of the convenience helpers where they make sense.
1.4 Why Direct Java Access Is the Right Default for Modeling
Three arguments favour the direct-access style for industrial work.
Completeness. A serious process model touches dozens of classes: ProcessSystem, Stream, Separator, Compressor, HeatExchanger, ThrottlingValve, Recycle, Adjuster, Mixer, Splitter, Pump, Pipeline, DistillationColumn, MeasurementDeviceBaseClass, ProcessAutomation, ProcessSystemState, plus all the thermodynamic support classes. Wrapping every one of these in a Python convenience layer would double the maintenance burden of NeqSim and inevitably lag behind the Java code. The direct-access path is always current.
Type information. When you write jneqsim.process.equipment.compressor.Compressor, the Java package path maps directly to the Javadoc, and many IDEs can help with class discovery once the JVM is running. Python convenience wrappers strip much of this metadata.
Idiomatic Python around idiomatic Java. The Java classes hold the state and do the physics. The Python script holds the loop, the data shape, the plot, and the I/O. Each language does what it is best at. The result is faster to write than pure Java and faster to run than pure Python.
1.5 The Shape of a NeqSim Python Project
Most production-grade NeqSim projects in Python follow the same shape:
my_process_model/
├── pyproject.toml # depends on neqsim, pandas, plotly
├── src/my_model/
│ ├── fluids.py # fluid factories
│ ├── equipment.py # equipment builders
│ ├── flowsheet.py # assembles ProcessSystem
│ ├── runners.py # study cases, sensitivities
│ └── api.py # FastAPI app exposing run endpoints
├── notebooks/
│ ├── 01_exploration.ipynb
│ ├── 02_validation.ipynb
│ └── 03_sensitivity.ipynb
└── tests/
└── test_flowsheet.py
The patterns are not specific to NeqSim — they are how any modern Python engineering project looks — but NeqSim sits comfortably inside them because it does not impose its own project structure. There is no .hsc file, no proprietary case database, no GUI state to keep in sync. The model is the Python code, and the Python code is the model.
The code contract in this book. Runnable examples are ordinary python fences and are verified in chapter order. Short API fragments that need objects from a larger project are marked with a noexec source marker so the reader sees the pattern but the verifier does not pretend the fragment is a stand-alone script. This keeps the book honest: a chapter can contain both a copy-pasteable model and compact reference snippets without mixing the two.
The running case. The same wet rich-gas export problem appears repeatedly. Here it is a forty-line mini model. In Part II it becomes a database-backed fluid and PVT case; in Part III it becomes separators, compressors, recycles, and dynamic variants; in Part IV it becomes an automatable and reportable model; in Part V it gains equipment-envelope checks, and in Part VI it becomes the case to optimize, package, and defend. When in doubt, ask how a new technique would change that case.
1.6 A Two-Minute End-to-End Example
To set the tone for the rest of the book, here is a complete worked miniature: create a wet gas, compress it, separate condensed liquid, and report the hydrate equilibrium temperature.
from neqsim import jneqsim
# 1. Build a wet gas fluid
gas = jneqsim.thermo.system.SystemSrkEos(298.15, 60.0)
for comp, frac in [("nitrogen", 0.01), ("CO2", 0.02),
("methane", 0.85), ("ethane", 0.06),
("propane", 0.03), ("water", 0.03)]:
gas.addComponent(comp, frac)
gas.setMixingRule("classic")
gas.setMultiPhaseCheck(True)
# 2. Build a flowsheet: feed → compressor → cooler → separator
Stream = jneqsim.process.equipment.stream.Stream
Compressor = jneqsim.process.equipment.compressor.Compressor
Cooler = jneqsim.process.equipment.heatexchanger.Cooler
Separator = jneqsim.process.equipment.separator.Separator
ProcessSystem = jneqsim.process.processmodel.ProcessSystem
feed = Stream("Wet gas feed", gas)
feed.setFlowRate(10.0, "MSm3/day")
feed.setTemperature(25.0, "C")
feed.setPressure(60.0, "bara")
comp = Compressor("K-100", feed)
comp.setOutletPressure(100.0, "bara")
cool = Cooler("E-100", comp.getOutletStream())
cool.setOutTemperature(298.15)
sep = Separator("V-100", cool.getOutletStream())
process = ProcessSystem()
for u in (feed, comp, cool, sep):
process.add(u)
process.run()
# 3. Report
gas_out = sep.getGasOutStream()
print(f"Compressor power : {comp.getPower()/1e6:8.3f} MW")
print(f"Discharge temperature : {comp.getOutletStream().getTemperature('C'):8.2f} °C")
print(f"Separated water : {sep.getLiquidOutStream().getFlowRate('kg/hr'):8.2f} kg/h")
print(f"Hydrate eq. temp : {gas_out.getHydrateEquilibriumTemperature() - 273.15:8.2f} °C")
Forty lines. A complete, runnable, copy-pasteable industrial mini-model. The rest of the book is about scaling these forty lines up to the kind of model that runs a 200-MMSCFD topside compression train with recycles, anti-surge, PID control, and a REST endpoint.
1.7 Roadmap
Chapter 2 explains the Java side: how NeqSim is organised internally, what the seven base packages are, and which classes you will use most often. Chapter 3 covers the neqsim-python package — installation, the JVM lifecycle, JPype mechanics, and the layered API. Chapter 4 teaches the direct jneqsim style in depth: class discovery, Javadoc navigation, Java ↔ Python type mapping, error handling, and the idioms that make code feel natural rather than forced.
With those four chapters in hand, you can read the rest of the book in any order. Each part is self-contained and points back to Part I for the plumbing.
The front-matter Learning Roadmap gives the book-level study plan. Use it as a checklist rather than as decoration: after each part, save one reproducible artifact. By Chapter 19 you should have enough material to assemble the Capstone Portfolio described in the back matter.
Reproducible Notebook Results
The outputs below were captured from the companion Jupyter notebook generated from this chapter's code blocks. They show the expected figures and text results when the examples are run.
Example 3 line 166
Notebook: chapters/ch01_introduction/notebooks/chapter_scripts.ipynb
Density: 47.973 kg/m³
Example 4 line 261
Notebook: chapters/ch01_introduction/notebooks/chapter_scripts.ipynb
Compressor power : 5.638 MW
Discharge temperature : 60.27 °C
Separated water : 9401.74 kg/h
Hydrate eq. temp : 20.23 °C
Exercises
- Exercise 1.1: Install neqsim-python (
pip install neqsim) and run the forty-line example in this chapter. Vary the inlet pressure between 40 and 80 bar and plot compressor power versus inlet pressure.
- Exercise 1.2: Decide which of the four interfaces (convenience, dataframe, MCP, direct) you would use for each of these tasks: (a) a one-off student lab on dew points; (b) a microservice that returns compressor maps for a digital twin; (c) an LLM agent that drafts a hydrate screening report; (d) a Jupyter dashboard for a production engineer. Justify each choice in two sentences.
- Exercise 1.3: Read the NeqSim README on GitHub and list five classes mentioned there that you have never used. Locate each one in the Javadoc and write down its main constructor signature.
- Exercise 1.4: Create a folder named
neqsim_learning_portfolio. Add a shortREADME.mdwith your learning goal, your Python version, your NeqSim version, and the first notebook you ran successfully. This folder will grow into the portfolio described in the back matter.
How NeqSim Is Built — The Java Architecture
Learning Objectives
After this chapter you will:
- Map the NeqSim source tree to its seven base modules (thermo, thermodynamicoperations, physicalproperties, process, pvtsimulation, fluidmechanics, standards, util).
- Identify the cornerstone classes you will call most often from Python.
- Understand the inheritance hierarchy that links a
SystemSrkEosto a genericSystemInterfaceand aCompressorto aProcessEquipmentInterface. - Recognise the patterns NeqSim uses for serialization, copying, recycle handling, and unit conversion, and know which patterns matter when calling from Python.
2.1 NeqSim as an Open-Source Project
NeqSim — Non-Equilibrium Simulator — was started by Even Solbraa at NTNU in the early 2000s as part of his PhD work on CO₂ absorption with reactive solvents [<a href="#ref-1" class="cite">1</a>]. The code later matured into an industrial thermodynamic and process simulation tool, and in 2017 it was open-sourced under the Apache 2.0 license on GitHub at https://github.com/equinor/neqsim. As of 2026 the repository contains around 600,000 lines of Java code in roughly 2,500 source files, with a CI pipeline that runs more than 4,000 JUnit tests.
The fact that NeqSim is open source matters for the Python user in three ways. First, every method you call is visible — when something behaves unexpectedly, you can read the source. Second, the project accepts pull requests, so missing features can be added by the community. Third, releases are versioned and published to Maven Central; the Python package pulls specific JARs, so reproducibility is straightforward.
2.2 The Seven Base Modules
The Java source under src/main/java/neqsim/ is organised into seven base packages. Each is roughly self-contained, with a clean dependency direction from thermo upward. Table 2.1 summarises them.
| Package | Purpose | Used from Python as |
|---|---|---|
neqsim.thermo |
Fluids, components, phases, equations of state, mixing rules | jneqsim.thermo.* |
neqsim.thermodynamicoperations |
Flash calculations, phase envelopes, saturation points, regression | jneqsim.thermodynamicoperations.* |
neqsim.physicalproperties |
Density, viscosity, conductivity, surface tension, diffusion | jneqsim.physicalproperties.* |
neqsim.process |
Streams, equipment, controllers, measurement devices, process model | jneqsim.process.* |
neqsim.pvtsimulation |
CME, CVD, differential liberation, separator tests, swelling | jneqsim.pvtsimulation.* |
neqsim.fluidmechanics |
Pipe flow, two-phase flow, mechanistic models | jneqsim.fluidmechanics.* |
neqsim.standards |
ISO, AGA, GPA gas quality and metering standards | jneqsim.standards.* |
A supporting neqsim.util package contains helpers — unit conversion, logging adapters, JSON serialization, validation, an "agentic" automation layer — that the seven base modules depend on.
2.3 The thermo Package: The Heart of NeqSim
Everything in NeqSim ultimately operates on a SystemInterface. A SystemInterface represents a thermodynamic system: a collection of components, one or more phases, a temperature and pressure, and the equation of state and mixing rule that link them.
The concrete classes you will instantiate most often:
| Class | Equation of state | Typical use |
|---|---|---|
SystemSrkEos |
SRK | General hydrocarbon systems |
SystemPrEos |
Peng–Robinson | Reservoir fluids |
SystemSrkCPAstatoil |
SRK + CPA | Water, glycol, methanol, polar systems |
SystemUMRPRUMCEos |
UMR-PRU MC | Wide-range gas processing |
SystemGERG2008Eos |
GERG-2008 | Fiscal metering, custody transfer |
SystemElectrolyteCPAstatoil |
Electrolyte CPA | Brines, scale, acid gas with water |
All implement SystemInterface, so any code that operates on a system works with any EOS. Python idioms make this especially elegant:
from neqsim import jneqsim
def make_fluid(eos: str, T: float, P: float):
cls = {
"srk": jneqsim.thermo.system.SystemSrkEos,
"pr": jneqsim.thermo.system.SystemPrEos,
"cpa": jneqsim.thermo.system.SystemSrkCPAstatoil,
"umrpru": jneqsim.thermo.system.SystemUMRPRUMCEos,
"gerg": jneqsim.thermo.system.SystemGERG2008Eos,
}[eos]
return cls(T, P)
Inside a SystemInterface live one to four phases: gas, oil, aqueous, and optionally a solid (hydrate, wax, asphaltene). Phases implement PhaseInterface and expose composition, density, viscosity, fugacity coefficients, and partial molar properties. Components implement ComponentInterface and carry pure- component properties (critical constants, ideal-gas heat capacity, acentric factor, association parameters).
The relationships are summarised in the diagram below.
SystemInterface
├── PhaseInterface[] (gas, oil, water, hydrate, …)
│ └── ComponentInterface[] (methane, ethane, water, …)
├── mixingRule (classic, Huron–Vidal, CPA, MC)
├── eosName (SRK, PR, CPA, …)
└── thermodynamicOperations (flash, envelope, regression)
Why setMixingRule Is Mandatory
A frequent surprise for new users is that adding components without calling setMixingRule leaves the binary interaction parameters (k_ij) at default neutral values, often zero. This produces silently wrong phase behaviour for mixtures with significant non-ideality. The book treats setMixingRule as part of fluid construction:
fluid.addComponent("methane", 0.9)
fluid.addComponent("CO2", 0.1)
fluid.setMixingRule("classic") # never skip — populates k_ij from database
2.4 The thermodynamicoperations Package
ThermodynamicOperations is the dispatcher for all numerical solvers that operate on a SystemInterface. It is constructed around a fluid and then called for the specific operation:
ops = jneqsim.thermodynamicoperations.ThermodynamicOperations(fluid)
ops.TPflash() # isothermal flash
ops.PHflash(enthalpy) # isenthalpic flash
ops.PSflash(entropy) # isentropic flash
ops.dewPointTemperatureFlash()
ops.bubblePointPressureFlash()
ops.calcPTphaseEnvelope(True, 1.0)
ops.hydrateEquilibriumLine()
Internally these calls dispatch to Newton, accelerated-substitution, or stability-test algorithms that have been refined over twenty years and many tens of thousands of test cases. From Python, they are one-line calls.
The initProperties() Trap
After any flash, transport properties (viscosity, thermal conductivity) must be explicitly initialised by calling fluid.initProperties(). The init(3) call updates thermodynamic properties but not transport properties. Forgetting this is the single most common cause of "zero viscosity" issues in user code. The book uses initProperties() everywhere without exception.
2.5 The process Package
The process simulation side of NeqSim is structured around four interfaces that the Python user becomes intimately familiar with:
StreamInterface— a material stream carrying aSystemInterface.ProcessEquipmentInterface— any equipment with inlet and outlet streams.MeasurementDeviceInterface— non-state-changing sensors (PT, TT, LT, FT).ControllerDeviceInterface— PID and on/off controllers.
These all extend the common supertype ProcessElementInterface, which is what makes process.getAllElements() return a uniform list.
The ProcessSystem is the container. It holds equipment, controllers, measurement devices, and explicit connections. Its run() method evaluates the flowsheet, honouring topological order and converging recycles to a specified tolerance.
For models that span more than one functional area — a topside platform with separation, recompression, injection, and export, for example — multiple ProcessSystem objects are combined inside a ProcessModel. This pattern is mandatory for plant-scale models and is the entire subject of Chapter 9.
The Equipment Hierarchy
Equipment lives in neqsim.process.equipment.* with one sub-package per category. The structure mirrors what a process engineer expects:
process.equipment.separator Separator, GasScrubber, ThreePhaseSeparator
process.equipment.compressor Compressor, CompressorChartGenerator
process.equipment.heatexchanger HeatExchanger, Cooler, Heater, Reboiler
process.equipment.valve ThrottlingValve, SafetyValve, ControlValve
process.equipment.pipeline PipeBeggsAndBrills, TwoFluidPipe, AdiabaticPipe
process.equipment.pump Pump
process.equipment.expander Expander
process.equipment.mixer Mixer, StaticMixer
process.equipment.splitter Splitter
process.equipment.distillation DistillationColumn, TraySection, Reboiler, Condenser
process.equipment.reactor GibbsReactor, PlugFlowReactor, StirredTankReactor
process.equipment.stream Stream, EnergyStream
process.equipment.util Recycle, Adjuster, Calculator, SetPoint
Every equipment class extends ProcessEquipmentBaseClass, which provides shared services: name, inlet/outlet stream registration, serialization, copy semantics, and the new controller and measurement-device registries.
2.6 Serialization, Copy, and State
A property of NeqSim that is unfamiliar to engineers from the HYSYS world is that every class supporting ProcessSystem participates in Java serialization. This is what makes process.copy() work, what makes ProcessSystemState a portable JSON snapshot, and what makes a model survive a JVM restart through a round-trip to disk.
In practice the Python user does not write serialization code; they call high-level helpers:
ProcessSystemState = jneqsim.process.processmodel.lifecycle.ProcessSystemState
state = ProcessSystemState.fromProcessSystem(process)
state.saveToFile("model.json")
restored = ProcessSystemState.loadFromFile("model.json")
But it pays to know the constraint: any field you attach to a NeqSim class (through subclassing in Java, very rarely from Python) must be serializable or marked transient.
2.7 The util.unit System
Almost every NeqSim getter and setter accepts an optional unit string:
feed.setFlowRate(10.0, "MSm3/day")
feed.setTemperature(25.0, "C")
feed.setPressure(60.0, "bara")
T = stream.getTemperature("C")
rho = fluid.getDensity("kg/m3")
The unit conversion is handled inside neqsim.util.unit.UnitConverter. The Python user never instantiates this class, but the unit strings — "C", "K", "bara", "barg", "Pa", "kPa", "MPa", "kg/hr", "kg/sec", "MSm3/day", "MMSCFD", "m3/sec", "USGPM" — are part of the day-to-day vocabulary of the book.
Where a method omits a unit string, the default is SI for fluids (K, Pa, kg, m³, sec, J) and engineering for process equipment (°C, bara, kg/hr). Always pass the unit explicitly. The two-line cost in readability prevents the one-line cost in a unit-conversion bug.
2.8 The physicalproperties and fluidmechanics Packages
Transport properties live in neqsim.physicalproperties. Most users never touch this package directly — calling fluid.getViscosity() on a phase routes through it transparently — but for digital-twin work and unusual fluids it is worth knowing where to look. The package contains separate methods for density (LBC, Tait, COSTALD), viscosity (Lohrenz–Bray–Clark, Pedersen, friction-theory), thermal conductivity (Chung, Pedersen), surface tension (parachor, Linear Gradient Theory), and diffusion coefficients.
The fluidmechanics package contains the mechanistic two-phase pipe-flow solvers used inside PipeBeggsAndBrills and TwoFluidPipe. Again, most users access this through equipment, not directly.
2.9 The pvtsimulation Package
PVT laboratory experiments are scripted in neqsim.pvtsimulation.simulation: ConstantMassExpansion, ConstantVolumeDepletion, DifferentialLiberation, SeparatorTest, SwellingSimulation, SaturationPressure, SlimTubeSimulation. They take a fluid, accept the experimental conditions, run, and expose results as arrays ready to compare against lab data.
These are the classes that turn NeqSim into a usable replacement for a PVT package such as PVTsim or WinProp. They are covered in Chapter 7.
2.10 The standards Package
neqsim.standards contains implementations of industry standards for gas quality and custody transfer: ISO 6976 (heating value, Wobbe index, density), ISO 6578 (LNG), AGA 3 and 7 (flow measurement), GPA 2145/2172 (physical constants), EN 16723 and EN 16726 (European gas quality). They take a fluid and return numbers that map directly into a metering or sales spec sheet.
2.11 Where to Find Things — A Practical Map
When you don't know which class to look for, the following map shortens the search:
| You need to … | Look in package |
|---|---|
| Create a fluid | neqsim.thermo.system |
| Add a component | SystemInterface.addComponent |
| Run a flash | neqsim.thermodynamicoperations |
| Calculate density | phase.getDensity("kg/m3") after fluid.initProperties() |
| Build a compressor | neqsim.process.equipment.compressor |
| Build a column | neqsim.process.equipment.distillation |
| Run a CVD | neqsim.pvtsimulation.simulation.ConstantVolumeDepletion |
| Get Wobbe Index | neqsim.standards.gasquality.Standard_ISO6976 |
| Tune a controller | neqsim.process.controllerdevice |
| Save state to JSON | neqsim.process.processmodel.lifecycle |
| Look up an automation variable | neqsim.process.automation.ProcessAutomation |
2.12 Looking Ahead
The Java architecture you have just toured is what you are about to call from Python. The next chapter installs the neqsim-python package and shows how the JPype bridge maps Java classes into Python namespaces. Chapter 4 then shows the idioms — handling Java generics, exceptions, primitive arrays, and null — that make the calls feel natural.
Exercises
- Exercise 2.1: Browse
src/main/java/neqsim/process/equipmentin the GitHub repository and list one class from each sub-package that you have never used.
- Exercise 2.2: Open the Javadoc for
SystemInterfaceand identify five methods that are present on every fluid regardless of EOS.
- Exercise 2.3: Find the source file for
ProcessSystem.run()and read the recycle-convergence loop. How is the maximum number of iterations set? What residual is checked?
The neqsim-python Package
Learning Objectives
After this chapter you will:
- Install and verify the neqsim-python package on Windows, macOS, and Linux.
- Explain how JPype starts a JVM inside the Python process and how Java classes become Python objects.
- Navigate the layered API of neqsim-python and choose between
neqsim.thermo,neqsim.process, andneqsim.jneqsim. - Manage the JVM lifecycle: startup, classpath, shutdown, and the use of a custom JAR for local development.
3.1 Installation
In the simplest case:
pip install neqsim
This installs the Python package, downloads the bundled NeqSim JAR (neqsim-3.x.y.jar), and pulls in the JPype1 dependency. A working Java Development Kit (JDK 11 or later) must be present on the system path. On Windows the typical recipe is:
winget install Microsoft.OpenJDK.21
pip install neqsim
On macOS:
brew install openjdk@21
pip install neqsim
On Linux:
sudo apt install openjdk-21-jdk
pip install neqsim
After installation, verify in Python:
import neqsim
print(neqsim.__version__)
from neqsim.thermo import fluid, TPflash
g = fluid("srk", T=298.15, P=10.0)
g.addComponent("methane", 1.0)
g.setMixingRule("classic")
TPflash(g)
print(g.getDensity("kg/m3"))
A successful run prints a NeqSim version and a methane density around 6.9 kg/m³. If the first import fails with a JPype error, the most common cause is a missing or 32-bit JDK; install a 64-bit JDK 11 or later and try again.
3.2 What JPype Does
neqsim-python is built on JPype1, a mature Python/Java bridge that starts a JVM inside the same process as the Python interpreter and exposes Java classes as Python objects. Method calls cross the language boundary through JNI. Primitive arguments are converted automatically; Java collections are exposed as iterable Python-friendly proxies.
When you write:
from neqsim import jneqsim
fluid = jneqsim.thermo.system.SystemSrkEos(298.15, 60.0)
what happens is:
- On first import of
neqsim, the package locates the JAR file undersite-packages/neqsim/lib/java11/neqsim-3.x.y.jar(or the version appropriate for your installed JDK), assembles a classpath, and callsjpype.startJVM. - The
neqsim.jneqsimproxy module exposes the Java package tree (neqsim.thermo.system.*) directly underjneqsim.thermo.system.*. SystemSrkEos(298.15, 60.0)invokes the Java constructorpublic SystemSrkEos(double T, double P). JPype unboxes the Python floats into Javadoubleprimitives.- The returned object is a
jpype.JObjectproxy that forwards method calls —fluid.addComponent("methane", 0.9)— into JNI invocations on the underlying Java object.
For the most part you can ignore JPype and pretend the Java classes are Python classes. The cases where the abstraction leaks are predictable and covered in Chapter 4.
3.3 The Layered Python API
Open site-packages/neqsim/__init__.py and the layout becomes clear. The package exposes four kinds of names:
| Layer | Module | Purpose |
|---|---|---|
| 1. Convenience functions | neqsim.thermo, neqsim.process, neqsim.pvt |
Idiomatic Python wrappers for common tasks |
| 2. DataFrame helpers | neqsim.thermo.thermoTools |
Returns pandas-friendly tables and plots |
| 3. Direct Java access | neqsim.jneqsim |
The full NeqSim Java API |
| 4. JVM control | neqsim.javaSetup, neqsim.jpype |
Start/stop, classpath, custom JARs |
Choosing between them is straightforward once the trade-offs are clear.
Layer 1: Convenience Functions
from neqsim.thermo import fluid, TPflash, dewt
gas = fluid("srk", T=298.15, P=60.0)
gas.addComponent("methane", 0.9)
gas.addComponent("ethane", 0.1)
gas.setMixingRule("classic")
TPflash(gas)
print(f"Dew T at 60 bar: {dewt(gas) - 273.15:.2f} °C")
Pros: short, friendly, defaults already chosen. Cons: not every operation has a convenience function; the underlying Java object is still there, but you give up some autocomplete and have to remember which helpers exist.
Layer 2: DataFrame Helpers
from neqsim.thermo import dataFrame
import pandas as pd
df: pd.DataFrame = dataFrame(gas)
df.to_csv("phase_state.csv")
dataFrame formats the current phase state as a tabular DataFrame. Other helpers — TPflashDataFrame, phaseEnvelope, bubt, dewp — produce arrays and matplotlib figures. These are valuable for one-off analyses; they are not the right interface for a process model.
Layer 3: Direct Java Access — The Book's Default
from neqsim import jneqsim
gas = jneqsim.thermo.system.SystemSrkEos(298.15, 60.0)
gas.addComponent("methane", 0.9)
gas.addComponent("ethane", 0.1)
gas.setMixingRule("classic")
ops = jneqsim.thermodynamicoperations.ThermodynamicOperations(gas)
ops.TPflash()
gas.initProperties()
Slightly more verbose, but the entire NeqSim API is in scope. Every example in Parts III–V uses this style.
Layer 4: JVM Control
The JVM starts automatically on first import. For most users that is sufficient. Two situations require explicit control:
- Local development against a freshly built NeqSim JAR, e.g. you have cloned the NeqSim repository and want to test a new equipment class.
- Custom classpath or JVM arguments, e.g. you need a larger heap for a large transient simulation.
import jpype, jpype.imports
from pathlib import Path
custom_jar = Path("C:/Users/.../neqsim/target/neqsim-3.10.0.jar")
jpype.startJVM(
classpath=[str(custom_jar)],
convertStrings=False,
*["-Xmx4g"], # 4 GB heap
)
from neqsim import jneqsim
The devtools/neqsim_dev_setup.py script in the NeqSim repository automates this pattern for repository task notebooks; Chapter 12 explains it.
3.4 The JVM Lifecycle
The JVM lives for the lifetime of the Python process. You cannot stop and restart it within a single Python session, because once a JVM is shut down JPype cannot bring it back. Practical implications:
- Configure first, import later. If you need a custom classpath or heap size, call
jpype.startJVM(...)before importingneqsim. - One JAR per process. You cannot dynamically swap the NeqSim JAR; if you upgrade NeqSim, restart the Python process.
- Long-running services are fine. A FastAPI server can run NeqSim inside a single JVM for weeks. The Java garbage collector handles memory.
- Subprocess for isolation. For parametric sweeps where you want a guaranteed fresh state, spawn a Python subprocess per case. The NeqSim Runner pattern in
devtools/neqsim_runner/does exactly this.
3.5 Verifying the Installation Properly
Run this end-to-end smoke test after every install or upgrade:
from neqsim import jneqsim
print("NeqSim Java version:", jneqsim.NeqSim.getVersion())
# Thermo
fluid = jneqsim.thermo.system.SystemSrkEos(298.15, 50.0)
fluid.addComponent("methane", 0.9)
fluid.addComponent("ethane", 0.1)
fluid.setMixingRule("classic")
ops = jneqsim.thermodynamicoperations.ThermodynamicOperations(fluid)
ops.TPflash()
fluid.initProperties()
assert 30 < fluid.getDensity("kg/m3") < 80, "Suspicious gas density"
# Process
Stream = jneqsim.process.equipment.stream.Stream
Separator = jneqsim.process.equipment.separator.Separator
ProcessSystem = jneqsim.process.processmodel.ProcessSystem
feed = Stream("Feed", fluid)
feed.setFlowRate(100.0, "kg/hr")
sep = Separator("V-100", feed)
proc = ProcessSystem()
proc.add(feed); proc.add(sep)
proc.run()
assert sep.getGasOutStream().getFlowRate("kg/hr") > 0
print("Installation OK.")
If any assertion fires, the most likely causes are: the JAR is older than the one this book targets (upgrade with pip install -U neqsim), or the JVM and JDK do not match in bitness (force 64-bit by checking java -version).
3.6 Working Behind a Corporate Proxy or Air-Gapped
In many oil-and-gas IT environments pip install neqsim is blocked. Two fallback recipes:
Offline JAR placement. Download the NeqSim JAR from Maven Central or the GitHub Releases page and place it under <python>/site-packages/neqsim/lib/java11/neqsim-3.x.y.jar. The package will pick it up at import time.
Explicit classpath. Skip the pip-installed JAR entirely and start the JVM with your own classpath:
import jpype
jpype.startJVM(classpath=["C:/neqsim/neqsim-3.10.0.jar"])
from neqsim import jneqsim
3.7 Versioning Strategy
The neqsim-python package follows semantic versioning. The Java JAR carries its own version, often newer than the wrapper. To pin a specific Java JAR in a reproducible environment:
# requirements.txt
neqsim==3.10.0
JPype1>=1.5.0
For production deployments — REST APIs, scheduled jobs, dashboards — pin both the wrapper and the JAR by shipping the JAR alongside your code and starting the JVM with an explicit classpath.
3.8 First Look at jneqsim — Tab Completion in Jupyter
Drop the following into a Jupyter cell and press Tab:
from neqsim import jneqsim
jneqsim.process.equipment.<TAB>
You will see compressor, distillation, expander, heatexchanger, mixer, pipeline, pump, reactor, separator, splitter, stream, util, valve. Each sub-package contains the equipment classes that Chapter 8 covers in detail. The same trick works deeper:
jneqsim.process.equipment.separator.<TAB>
# → Separator, ThreePhaseSeparator, GasScrubber, Slugcatcher, …
This is the fastest way to discover what NeqSim offers without leaving the notebook. Pair it with help(jneqsim.process.equipment.separator.Separator) to see the constructor signature and Javadoc.
3.9 Anti-patterns
A short list of things newcomers do that this book recommends against:
- Importing the convenience layer and
jneqsimat the same time for the same fluid. Choose one. Mixing the layers is legal but confusing. - Caching JPype proxy objects across module reloads. If you reload a module that held a NeqSim object, the new module gets a new wrapper around the same Java instance — usually fine, occasionally surprising.
- Calling
System.exit(0)from Java code. This terminates the JVM and takes Python with it. Use exceptions for error signalling.
3.10 Looking Ahead
You now have a working neqsim-python installation and a mental model of the JPype bridge. Chapter 4 dives into the direct-access style — the dominant interface for the rest of the book — and walks through the practical idioms (arrays, generics, exceptions, type checks) that make Java-from-Python feel natural.
Exercises
- Exercise 3.1: Install neqsim-python in a fresh virtual environment. Print
jneqsim.NeqSim.getVersion()and confirm it matches the JAR you expect.
- Exercise 3.2: Time a single TPflash on a 10-component fluid using
time.perf_counter(). Do the same for 100 flashes. Where is the overhead — the flash itself, or the JPype call boundary?
- Exercise 3.3: Start the JVM with
-Xmx128mand try to run a 100-component CVD. Observe the OutOfMemoryError. What heap is needed for the same case to complete?
Direct Java Access: Full Control via jneqsim
Learning Objectives
After this chapter you will:
- Use
jneqsim.*to instantiate any NeqSim Java class from Python. - Read a Javadoc method signature and translate it into a Python call.
- Handle the practical bridge issues: Java arrays vs Python lists, generics, enums, exceptions, primitive overloads, and null.
- Recognise idiomatic patterns — factory dictionaries, builder helpers, context-managed JVM startup — that keep direct-access code readable.
4.1 The jneqsim Namespace
Every public Java package under neqsim.* is mirrored under jneqsim.* in Python. The mapping is mechanical:
| Java | Python |
|---|---|
neqsim.thermo.system.SystemSrkEos |
jneqsim.thermo.system.SystemSrkEos |
neqsim.process.equipment.compressor.Compressor |
jneqsim.process.equipment.compressor.Compressor |
neqsim.process.processmodel.ProcessSystem |
jneqsim.process.processmodel.ProcessSystem |
neqsim.standards.gasquality.Standard_ISO6976 |
jneqsim.standards.gasquality.Standard_ISO6976 |
There is no Python-side stub generation. The Java reflection metadata is introspected at runtime by JPype, so any class added to the JAR is immediately visible.
Three Equivalent Import Styles
# Style A — full path, no aliases
from neqsim import jneqsim
sep = jneqsim.process.equipment.separator.Separator("V-100", feed)
# Style B — package alias
from neqsim.jneqsim.process.equipment import separator
sep = separator.Separator("V-100", feed)
# Style C — class alias (most common in production code)
from neqsim.jneqsim.process.equipment.separator import Separator
sep = Separator("V-100", feed)
All three work. Style C reads best when a script uses a class more than once; Style A reads best when discovery is the priority. The book uses whichever is clearest in each example, with a slight preference for the factory dictionary pattern below.
Factory Dictionaries — A Readable Idiom
from neqsim import jneqsim as J
CLASSES = {
"stream": J.process.equipment.stream.Stream,
"compressor": J.process.equipment.compressor.Compressor,
"cooler": J.process.equipment.heatexchanger.Cooler,
"separator": J.process.equipment.separator.Separator,
"valve": J.process.equipment.valve.ThrottlingValve,
"recycle": J.process.equipment.util.Recycle,
"process": J.process.processmodel.ProcessSystem,
}
This top-of-file dictionary acts as a project-local import. The model body then reads in plain English:
feed = CLASSES["stream"]("Feed", gas)
comp = CLASSES["compressor"]("K-100", feed)
4.2 Reading a Javadoc Signature
The NeqSim Javadoc is published at https://htmlpreview.github.io/?https://github.com/equinor/neqsim/blob/master/docs/javadoc/index.html. Each class page lists constructors and methods with their full signatures. Translating one to Python is mechanical but worth practising once.
Consider Compressor. The constructor:
public Compressor(String name, StreamInterface inletStream)
Translates to:
comp = J.process.equipment.compressor.Compressor("K-100", feed)
The method:
public void setOutletPressure(double pressure, String unit)
Translates to:
comp.setOutletPressure(150.0, "bara")
The method:
public double getPower() // watts
Translates to:
power_W = comp.getPower()
Where Javadoc says "unit: K", you pass numbers in Kelvin. Where it accepts a unit string, pass the string. When in doubt, pass the unit string explicitly — it never hurts and removes ambiguity.
4.3 Java ↔ Python Type Mapping
JPype handles most conversions silently. The cases where you need to think:
| Java type | Python form | Notes |
|---|---|---|
int, long, short |
int |
Auto-converted |
double, float |
float |
Auto-converted |
boolean |
bool |
Auto-converted |
String |
str |
Auto-converted both directions |
char |
length-1 str |
Rare in NeqSim |
double[] |
list or numpy array | Convert with list(arr) for plotting |
String[] |
list of str | |
Object[] |
list | Heterogeneous |
List<X>, Map<K,V> |
iterable, indexable | Use list(...), dict(...) to copy |
enum |
attribute access | e.g. SolverType.INSIDE_OUT |
Java null |
Python None |
JPype maps these symmetrically |
A common operation: convert a Java double[] returned by a phase envelope into a numpy array:
import numpy as np
envelope.calcPTphaseEnvelope(True, 1.0)
T = np.asarray(envelope.get("dewT"))
P = np.asarray(envelope.get("dewP"))
np.asarray walks the JPype proxy once and produces a contiguous numpy array. From then on, all the usual numpy idioms apply.
Working with Enums
DistillationColumn = J.process.equipment.distillation.DistillationColumn
col = DistillationColumn("T-100", 20, True, False)
col.setSolverType(DistillationColumn.SolverType.INSIDE_OUT)
Enums are nested types on their owning class. Access them with attribute notation; JPype handles the lookup.
Java Method Overloading
setFlowRate is defined for several primitive overloads. JPype dispatches on the Python argument types; the unit string is the disambiguator:
feed.setFlowRate(100.0, "kg/hr") # uses (double, String) overload
feed.setFlowRate(100, "kg/hr") # int auto-widens to double — fine
The case where you must intervene is when Java has overloads on int and long and your Python value is large. In practice this never happens in NeqSim user code.
Java Generics
Java's List<StreamInterface> is exposed as a plain iterable:
for s in process.getAllUnitOperations():
print(s.getName(), type(s).__name__)
Generic type parameters are erased at runtime in both Java and Python, which means there is nothing to declare. Just iterate.
4.4 Exceptions
A Java exception thrown across the JPype boundary surfaces as a Python exception with a related type. Catch it as any other:
import jpype
try:
fluid.addComponent("methanesomething", 1.0)
except jpype.JException as e:
print("Java error:", e.message())
For most NeqSim methods, the meaningful exception classes live under neqsim.util.exception. They carry remediation hints via getRemediation():
RuntimeException = jpype.JClass("java.lang.RuntimeException")
try:
fluid.setMixingRule("nonsense")
except RuntimeException as e:
print(e.getMessage())
In notebook code, try/except jpype.JException is enough. In production services, catch more specific types and surface getRemediation() to the caller — the agentic infrastructure in neqsim.util.agentic is built on exactly this pattern.
4.5 The Null Trap
A NeqSim getter may return Java null, which JPype maps to None. The most common occurrence is asking for an outlet stream before run():
sep = Separator("V-100", feed)
gas = sep.getGasOutStream() # not yet populated
gas.getFlowRate("kg/hr") # AttributeError: NoneType has no getFlowRate
The rule of thumb: configuration before run(), results after run(). For paranoid code:
def safe_flow(stream, unit="kg/hr"):
return stream.getFlowRate(unit) if stream is not None else float("nan")
4.6 Long-Running Calls and the GIL
Java method calls release the Python Global Interpreter Lock while executing, so a long flash calculation does not block other Python threads. This makes it safe to run NeqSim inside a FastAPI request handler with multiple workers, inside a background thread for a UI, or inside a thread-pooled parameter sweep.
The flip side: if you want true parallelism inside a single Python process, use a concurrent.futures.ThreadPoolExecutor, not ProcessPoolExecutor, because the JVM is per process and the start cost is significant.
4.7 Idiomatic Builder Helpers
Direct API calls can grow verbose. Wrap the verbose parts in small helpers:
def feed_stream(name: str, comp: dict, T_C: float, P_bara: float,
flow_kg_hr: float, eos: str = "srk"):
cls = {
"srk": J.thermo.system.SystemSrkEos,
"pr": J.thermo.system.SystemPrEos,
"cpa": J.thermo.system.SystemSrkCPAstatoil,
}[eos]
fluid = cls(T_C + 273.15, P_bara)
for c, x in comp.items():
fluid.addComponent(c, x)
fluid.setMixingRule("classic")
s = J.process.equipment.stream.Stream(name, fluid)
s.setFlowRate(flow_kg_hr, "kg/hr")
s.setTemperature(T_C, "C")
s.setPressure(P_bara, "bara")
return s
A helper of this shape — twenty lines, project-local, no abstraction beyond what the project actually needs — is the right level. Avoid the temptation to build a "framework"; the direct API is already the framework.
4.8 Working with the NeqSim Build From a Clone
Engineers who contribute to NeqSim or extend it with custom equipment need to run Python against a freshly-built JAR. The recipe:
import os, sys
from pathlib import Path
PROJECT = Path(os.environ.get("NEQSIM_PROJECT_ROOT",
"C:/Users/ESOL/Documents/GitHub/neqsim"))
sys.path.insert(0, str(PROJECT / "devtools"))
from neqsim_dev_setup import neqsim_init
ns = neqsim_init(project_root=PROJECT, recompile=False, verbose=True)
fluid = ns.thermo.system.SystemSrkEos(298.15, 60.0)
neqsim_init builds the classpath from target/classes (or a custom JAR) and starts the JVM with sensible defaults. The returned ns object is the same jneqsim proxy you would otherwise import. This pattern is mandatory for repository task notebooks; for ordinary downstream projects, the pip install is fine.
4.9 Inspecting Java Objects from Python
Three reflection tricks that pay off constantly:
type(fluid).__name__ # 'SystemSrkEos'
[m for m in dir(fluid) if "Density" in m] # methods matching Density
help(jneqsim.process.equipment.compressor.Compressor) # constructor + Javadoc
In Jupyter, the question-mark trick — Compressor? — shows the constructor. Compressor?? shows the source if it is available.
4.10 When to Drop to Java
Almost never. The Java code under src/main/java/neqsim/ should be modified for two reasons only:
- A bug or missing feature in the physics or solver engine.
- A new equipment type or unit operation that does not already exist.
Everything else — flowsheet construction, study cases, sensitivities, reporting, dashboards, REST APIs — is properly Python work. The direct jneqsim interface is sufficient for it.
If you do find yourself reaching for Java, write the smallest possible class in src/main/java, add JUnit tests, contribute it upstream, and use it from Python through jneqsim like everything else.
4.11 A Worked Idiom Catalog
The patterns that recur in every Python NeqSim model:
# Discover what a class can do
print([m for m in dir(Cls) if not m.startswith("_")])
# Convert a Java array to numpy
import numpy as np
arr = np.asarray(envelope.get("dewT"))
# Catch a Java exception cleanly
import jpype
try:
ops.bubblePointPressureFlash()
except jpype.JException as e:
print("Flash failed:", e.message())
# Copy a fluid before perturbing
new = fluid.clone()
# Save / restore state
state = J.process.processmodel.lifecycle.ProcessSystemState.fromProcessSystem(process)
state.saveToFile("snap.json")
# String-addressable variable access
auto = process.getAutomation()
T = auto.getVariableValue("V-100.gasOutStream.temperature", "C")
Each is a one-liner; together they cover perhaps eighty percent of what day-to-day Python code does on top of NeqSim.
4.12 Looking Ahead
You have the bridge. Part II opens by building fluids properly — choosing the right EOS, adding components, setting mixing rules, and handling plus-fractions — and then runs the standard PVT laboratory experiments against them. From Chapter 8 onward we leave thermodynamics behind and build process flowsheets, with the direct-access style as the default.
Exercises
- Exercise 4.1: Translate this Java snippet into Python with
jneqsim:
SystemPrEos sys = new SystemPrEos(303.15, 100.0);
sys.addComponent("CO2", 0.95);
sys.addComponent("water", 0.05);
sys.setMixingRule("classic");
ThermodynamicOperations ops = new ThermodynamicOperations(sys);
ops.TPflash();
System.out.println(sys.getDensity("kg/m3"));
- Exercise 4.2: Catch a Java exception from a deliberately invalid mixing-rule name and print the remediation message.
- Exercise 4.3: Use reflection (
dir,type) to list every method on aSeparatorcontaining the word "Stream". How many are getters?
Part II: Thermodynamics in Python
Fluid Creation
Learning Objectives
After this chapter you will:
- Choose the right equation of state for a given fluid (hydrocarbon, polar, electrolyte, custody transfer).
- Build fluids component-by-component with correct units and consistent composition.
- Apply mixing rules and binary interaction parameters appropriately.
- Initialise fluids for multiphase work (gas–liquid–water, with optional hydrate or solid phases).
5.1 The Anatomy of a NeqSim Fluid
A NeqSim fluid — a SystemInterface — is built in three steps:
- Choose an EOS class and construct it with a default temperature and pressure.
- Add components with mole fractions, mass fractions, or absolute moles/mass.
- Set the mixing rule so binary interaction parameters from the database take effect.
from neqsim import jneqsim as J
fluid = J.thermo.system.SystemSrkEos(298.15, 60.0) # K, bara
fluid.addComponent("methane", 0.85)
fluid.addComponent("ethane", 0.10)
fluid.addComponent("propane", 0.05)
fluid.setMixingRule("classic")
These five lines build a 3-component natural gas at 25 °C and 60 bar. Everything else — flashing, properties, mechanical design — is downstream.
5.2 Choosing an Equation of State
The right EOS depends on what is in the fluid and what you want to predict. Table 5.1 is a decision aid.
| Fluid type | Recommended EOS | Class |
|---|---|---|
| Dry hydrocarbon gas | SRK or PR | SystemSrkEos, SystemPrEos |
| Wet hydrocarbon gas with water | CPA | SystemSrkCPAstatoil |
| Glycol/methanol systems (TEG, MEG, MeOH) | CPA | SystemSrkCPAstatoil |
| Reservoir oil with C7+ | PR with Peneloux | SystemPrEosVolumeCorrected |
| Natural gas for custody transfer | GERG-2008 | SystemGERG2008Eos |
| Brines, scale, acid gas in water | Electrolyte CPA | SystemElectrolyteCPAstatoil |
| Wide-range high-pressure gas processing | UMR-PRU MC | SystemUMRPRUMCEos |
| CO₂-rich streams | PR or GERG | SystemPrEos, SystemGERG2008Eos |
| Hydrogen-rich streams | GERG | SystemGERG2008Eos |
Choosing wrongly is rarely catastrophic — SRK on a dry gas produces results that are off by a few percent rather than wildly wrong — but the right choice removes a layer of doubt from every subsequent calculation. For polar systems (water, glycols, alcohols, hydrate inhibitors) always pick CPA; the cubic EOS underestimates hydrogen-bonded interactions and gets solubilities catastrophically wrong.
5.3 The Component Database
NeqSim ships with a database of more than 800 components. Their canonical names live in src/main/resources/neqsim_component_names.txt. The most-used ones:
# Permanent gases
"nitrogen", "oxygen", "argon", "helium", "hydrogen"
# Acid gases and inerts
"CO2", "H2S", "SO2"
# Hydrocarbons by carbon number
"methane", "ethane", "propane",
"i-butane", "n-butane", "i-pentane", "n-pentane",
"n-hexane", "n-heptane", "n-octane", "n-nonane", "n-decane"
# Aromatics and naphthenes
"benzene", "toluene", "ethyl-benzene", "p-xylene", "m-xylene", "o-xylene",
"cyclohexane", "methyl-cyclohexane"
# Polar
"water", "methanol", "MEG", "DEG", "TEG", "MDEA", "DEA"
# Ions (Electrolyte CPA only)
"Na+", "Cl-", "Ca++", "HCO3-"
To check whether a name is recognised:
names = list(fluid.getComponentNames())
print("methane" in names, "co2" in names, "CO2" in names)
Component names are case-sensitive. "CO2", "H2S", "MEG" are uppercase. Hydrocarbons use lowercase with hyphens for branched isomers ("i-butane").
The component-name list is only the front door. When a component is added, NeqSim looks up its pure component parameters in the COMP table stored in src/main/resources/data/COMP.csv. This compact property database is what makes a name usable inside an equation of state. It contains the values needed by ordinary process and PVT simulations: molar mass, normal boiling point, liquid density, critical temperature and pressure, acentric factor, critical volume, ideal-gas heat-capacity coefficients, vapor-pressure coefficients, volume-translation parameters, transport-property correlation parameters, hydrate flags, electrolyte reference-state information, and model parameters for CPA and PC-SAFT where they are available. For a cubic equation of state such as SRK or Peng-Robinson, the most visible entries are TC, PC, and ACSFACT; these determine the pure-component attraction and co-volume terms before the mixing rule combines components into a phase model.
This means that fluid.addComponent("methane", 1.0) is not just a symbolic operation. It is a database-backed operation: NeqSim retrieves a row from COMP, builds the component object, and then lets the selected thermodynamic system decide which subset of the row matters. A plain SRK gas model mainly uses critical constants, acentric factors, heat capacities, and binary interaction data. A CPA or electrolyte model may also depend on association sites, association energies, dielectric parameters, ionic charge, and reference-state settings. The same component name can therefore be used across several model families, but confidence in the result still depends on whether the relevant parameters for that model have been populated and validated.
NeqSim also includes an extended component database, src/main/resources/data/COMP_EXT.csv. The extended database is a much broader catalog intended for exploratory PVT work, early component screening, and cases where a named chemical is outside the standard oil-and-gas component set. It is useful when the alternative would be to stop at component lookup, but it should be treated with more engineering caution than the standard table: many extended entries are generic or estimated records, and a successful flash calculation does not by itself prove that vapor pressure, liquid density, heat capacity, association, or transport behavior is accurate enough for design. For design-quality work, compare the retrieved parameters against vendor data, literature data, or regressed PVT data before relying on the result.
The NeqSim-Colab notebooks notebooks/PVT/parameter_database.ipynb and notebooks/PVT/parameter_database2.ipynb are useful companions to this section. The first shows how to inspect the standard component list, enable the extended component database with NeqSimDataBase.useExtendedComponentDatabase(True), add a component that is only present in the extended catalog, and temporarily replace the COMP table with a custom CSV. The second goes one step further by replacing both COMP and INTER, which is the safer pattern when a custom component also needs reviewed binary interaction parameters.
That workflow is powerful, but it changes the foundation of every later flash and process calculation in that Python session, so it belongs near the start of a study notebook and should be recorded in the study assumptions. When you use a custom table, save the edited CSV files with the study, record which rows were changed, and run at least one independent benchmark before design use.
from pathlib import Path
from neqsim import jneqsim as J
DB = J.util.database.NeqSimDataBase
study_data = Path("study_parameter_database").resolve()
DB.replaceTable("COMP", str(study_data / "COMP_user.csv"))
DB.replaceTable("INTER", str(study_data / "INTER_user.csv"))
Direct component editing is different: it changes the component object in the current fluid, not the database table. Use it for sensitivity checks, not as a hidden replacement for a documented property basis.
component = fluid.getPhase(0).getComponent("example-component")
component.setTC(512.0, "K")
component.setPC(45.0, "bara")
component.setAcentricFactor(0.22)
component.setMolarMass(0.098, "kg/mol")
Running Case Checkpoint: Fluid Provenance
For the book's wet rich-gas export case, record the fluid basis before running any flash or process model. A minimal ledger row is:
| Item | Running-case entry |
|---|---|
| Component source | Lab or design-basis composition, normalized to mole fraction |
| Database mode | Standard COMP unless an extended-only component is required |
| EOS and mixing rule | SRK or CPA with the selected mixing rule stated explicitly |
| Property checks | Molar mass, phase count after TP flash, density, and one benchmark or hand estimate |
| Caveat | Extended-database entries require independent property review before design use |
This one table saves time later. If a PVT result, separator split, or optimizer choice looks surprising, the first question is whether the fluid basis changed.
5.4 Mole, Mass, or Absolute Composition
addComponent accepts two argument styles:
fluid.addComponent("methane", 0.85) # mole fraction (sums to 1)
fluid.addComponent("methane", 100.0, "kg/hr") # absolute mass flow
fluid.addComponent("methane", 1.0, "Sm3/hr") # absolute volumetric flow
When you mix styles in a single fluid, NeqSim normalises at the end. The clean recipe is to pick one style per fluid:
# Pure composition fluid (will be assigned a flow rate later via a Stream)
fluid = J.thermo.system.SystemSrkEos(298.15, 60.0)
for name, x in {"methane": 0.90, "ethane": 0.07, "propane": 0.03}.items():
fluid.addComponent(name, x)
fluid.setMixingRule("classic")
Mole fractions need not sum to exactly 1; NeqSim normalises. But getting them right at the source removes a class of bugs.
5.5 Mixing Rules
The mixing rule determines how pair-wise binary interaction parameters (BIPs, k_ij) enter the EOS. The relevant choices in NeqSim:
| Rule name | Use with |
|---|---|
"classic" |
All cubic EOS (SRK, PR), most NeqSim work |
"HV" |
Huron–Vidal — gE-based, for activity coefficient consistency |
"WongSandler" |
Wong–Sandler — high-pressure activity coefficient |
"CPA-EOS" |
Default for CPA when polar association is present |
10 (integer) |
Numeric mixing rule for advanced regression |
For 95% of work the recipe is setMixingRule("classic") for cubic EOS and nothing for CPA (it picks the CPA-specific rule automatically). The other rules are appropriate when regressing activity coefficient data, modelling glycol regeneration columns, or matching gas–water phase equilibria with unusual precision.
5.6 Multiphase Setup
A fresh fluid in NeqSim is initialised in two phases (gas + liquid) by default. To allow a separate aqueous phase, call:
fluid.setMultiPhaseCheck(True)
This adds an aqueous phase and the stability test that decides whether it appears in a flash. Always set it for fluids that contain water, polar inhibitors, or are likely to drop water at low temperature.
For hydrate, wax, or asphaltene checks, additional phase types are activated automatically by the corresponding flash methods (hydrateFormationTemperature, waxFormationTemperature, asphaltenePrecipitation). You do not have to add them by hand.
5.7 Setting Temperature, Pressure, and Flow
The constructor takes initial T and P. Updating them later:
fluid.setTemperature(303.15) # K
fluid.setTemperature(25.0, "C")
fluid.setPressure(50.0, "bara")
fluid.setPressure(7250.0, "psia")
fluid.setTotalFlowRate(100.0, "kg/hr")
fluid.setTotalFlowRate(1.0, "MSm3/day")
Flow rate is a property of the fluid in stand-alone use, and of a Stream in process-simulation use. For Part III the stream layer is the canonical place to set flow.
5.8 Worked Example: A Wet Reservoir Fluid
A reasonably realistic North-Sea gas-condensate at separator conditions:
from neqsim import jneqsim as J
fluid = J.thermo.system.SystemSrkCPAstatoil(298.15, 80.0)
composition = {
"nitrogen": 0.0152,
"CO2": 0.0306,
"methane": 0.7611,
"ethane": 0.0876,
"propane": 0.0438,
"i-butane": 0.0061,
"n-butane": 0.0152,
"i-pentane": 0.0046,
"n-pentane": 0.0050,
"n-hexane": 0.0061,
"n-heptane": 0.0050,
"n-octane": 0.0040,
"n-nonane": 0.0030,
"water": 0.0127,
}
for name, x in composition.items():
fluid.addComponent(name, x)
fluid.setMixingRule(10) # CPA mixing rule (numeric form)
fluid.setMultiPhaseCheck(True)
ops = J.thermodynamicoperations.ThermodynamicOperations(fluid)
ops.TPflash()
fluid.initProperties()
print(f"Number of phases : {fluid.getNumberOfPhases()}")
for i in range(fluid.getNumberOfPhases()):
ph = fluid.getPhase(i)
print(f" Phase {i:<2} {str(ph.getPhaseTypeName()):<8} "
f"x={float(fluid.getBeta(i)):6.4f} rho={float(ph.getDensity()):7.2f} kg/m3")
A typical output shows three phases — gas, oil, and a small aqueous phase — with sensible densities, all in twenty lines of Python.
5.9 Cloning, Resetting, and Sub-fluids
Three operations recur in every project.
Clone. Make an independent copy that can be perturbed without affecting the original:
hot = fluid.clone()
hot.setTemperature(353.15)
Reset. Wipe components and start over (rare, but useful in iterative regression):
fluid = J.thermo.system.SystemSrkEos(298.15, 60.0) # just rebuild
Sub-fluid. Extract one phase as a stand-alone fluid:
ops.TPflash()
gas_only = fluid.phaseToSystem("gas")
oil_only = fluid.phaseToSystem("oil")
This is how you decouple a separated gas from its parent fluid for downstream pipeline or compressor work.
5.10 Reading Properties Without Pitfalls
After every flash, call fluid.initProperties() before reading transport properties:
ops.TPflash()
fluid.initProperties()
mw = fluid.getMolarMass() * 1000 # kg/kmol
rho = fluid.getDensity("kg/m3")
cp = fluid.getCp("J/molK")
mu_g = fluid.getPhase("gas").getViscosity("kg/msec")
k_g = fluid.getPhase("gas").getThermalConductivity("W/mK")
Skipping initProperties() returns zero or stale viscosities — the single most-reported issue in user code, and the reason this book uses initProperties() after every flash without exception.
5.11 Common Composition Sources
In practice you read compositions from somewhere — a lab report, a STID export, a historian, a pandas DataFrame. The pattern:
import pandas as pd
df = pd.read_csv("composition.csv") # columns: name, mole_fraction
fluid = J.thermo.system.SystemSrkEos(298.15, 60.0)
for _, row in df.iterrows():
fluid.addComponent(row["name"], float(row["mole_fraction"]))
fluid.setMixingRule("classic")
Or from a JSON document such as a NeqSim process-model state:
import json
state = json.load(open("process_state.json"))
comp = state["fluids"]["FeedGas"]["composition"]
fluid = J.thermo.system.SystemSrkEos(298.15, 60.0)
for name, x in comp.items():
fluid.addComponent(name, x)
fluid.setMixingRule("classic")
The takeaway is that the fluid factory is a small, project-local function that takes external data and returns a SystemInterface. Once that function exists, the rest of the model is composition-agnostic.
5.12 Looking Ahead
This chapter covered well-defined components. Real reservoir fluids contain a heavy "plus" fraction (C7+, C10+, C20+) that is not a single molecule but a distribution. Chapter 6 covers characterisation: TBP cuts, plus-fraction splitting, hypothetical components, and the workflow for matching an experimental PVT report.
Reproducible Notebook Results
The outputs below were captured from the companion Jupyter notebook generated from this chapter's code blocks. They show the expected figures and text results when the examples are run.
Example 2 line 71
Notebook: chapters/ch05_fluid_creation/notebooks/chapter_scripts.ipynb
('Na+', 'Cl-', 'Ca++', 'HCO3-')
Example 8 line 252
Notebook: chapters/ch05_fluid_creation/notebooks/chapter_scripts.ipynb
Number of phases : 3
Phase 0 gas x=0.9274 rho= 83.71 kg/m3
Phase 1 oil x=0.0604 rho= 545.75 kg/m3
Phase 2 aqueous x=0.0122 rho=1005.89 kg/m3
Exercises
- Exercise 5.1: Build the same wet gas with SRK, PR, and CPA, run a TPflash at 20 °C and 50 bar, and compare the water phase content. Which EOS predicts more water in the gas phase, and why?
- Exercise 5.2: Write a
fluid_from_dict(eos, composition_dict)helper that returns a configuredSystemInterface. Use it to test five compositions in aforloop.
- Exercise 5.3: Clone a fluid, perturb the methane fraction by ±5%, re-flash, and plot dew-point pressure versus methane content.
Fluid Characterization and Plus Fractions
Learning Objectives
After this chapter you will:
- Characterise a reservoir fluid that contains a heavy plus-fraction (C7+, C10+, C20+).
- Use the TBP and Pedersen splitting methods to break a plus-fraction into a set of pseudo-components.
- Lump and de-lump components for performance versus accuracy trade-offs.
- Tune EOS parameters (volume shift, k_ij, plus-fraction MW) to match experimental PVT data.
- Transfer fluids between NeqSim and Eclipse / E300 / OLGA.
6.1 Why Characterise?
A reservoir fluid contains hundreds of distinct hydrocarbon species heavier than n-hexane — branched paraffins, naphthenes, aromatics, asphaltenes — that no laboratory measures individually. The PVT report summarises them as a "C7+ fraction" with three numbers: mass percent, molecular weight, and density at standard conditions. Characterisation turns that summary into a set of pseudo-components an EOS can use.
The result is never unique — many splittings reproduce the same lab data — but the calibrated EOS then predicts properties (saturation pressure, GOR, density, viscosity) far outside the lab envelope with usable accuracy.
6.2 The TBP Characterization Workflow
NeqSim implements the standard True Boiling Point characterisation. The flow is:
from neqsim import jneqsim as J
fluid = J.thermo.system.SystemSrkEos(298.15, 100.0)
# Light end (defined components)
defined = {
"nitrogen": 0.0080, "CO2": 0.0150, "methane": 0.4500,
"ethane": 0.0700, "propane": 0.0500, "i-butane": 0.0080,
"n-butane": 0.0220, "i-pentane": 0.0090, "n-pentane": 0.0110,
"n-hexane": 0.0160,
}
for c, x in defined.items():
fluid.addComponent(c, x)
# Plus fraction: 33.10 mol%, MW = 215 g/mol, density = 0.845 g/cm³
fluid.addTBPfraction("C7", 0.3310, 215.0, 0.845)
fluid.setMixingRule("classic")
# Generate pseudo-components from the plus fraction
fluid.getCharacterization().characterisePlusFraction()
fluid.setMixingRule("classic") # re-apply after characterisation
After characterisePlusFraction(), the single "C7" label is replaced with a series of pseudo-components (C7_PC, C8_PC, …, C36_PC typically) each with critical properties (Tc, Pc, ω) chosen so the cumulative distribution matches the input MW and density.
A few rules:
- Always set the mixing rule after characterisation. The expansion changes the component list, and BIPs need to be re-loaded.
- The plus-fraction name should be a clean identifier —
"C7"works,"C7+"does not because the+confuses downstream parsers. - Specifying the plus-fraction MW and density is mandatory; specifying more (such as a measured viscosity) lets the characterisation tune itself to match it.
6.3 Plus-Fraction Splitting Models
Three splitting models are available, accessed through getCharacterization().setSplitter(...):
| Splitter | Distribution | Best for |
|---|---|---|
| Pedersen | Exponential in carbon number | Reservoir oils, default |
| Whitson | Gamma distribution | Heavier oils, when shape parameter is known |
| Quadrature | Gauss–Laguerre quadrature | Performance, fewer pseudo-components |
char = fluid.getCharacterization()
char.setSplitter("Pedersen") # or "Whitson", "Quadrature"
char.characterisePlusFraction()
For most field development work, Pedersen with default parameters is the right starting point. Switch to Whitson when matching a heavy oil with a known gamma shape; switch to Quadrature when you need to run thousands of flashes and want the smallest acceptable component count.
6.4 Lumping Pseudo-Components
The TBP characterisation typically produces 20–40 pseudo-components. For many process calculations this is excessive. The lumping API merges neighbouring pseudo-components while preserving moments of the original distribution:
fluid.getCharacterization().getLumpingModel().setNumberOfLumpedComponents(6)
fluid.getCharacterization().getLumpingModel().runLumping()
The result is a 6-pseudo-component fluid that reproduces the original saturation pressure, density, and bulk MW to within a few tenths of a percent — fast enough for transient pipeline simulation, accurate enough for design.
The rule of thumb:
| Application | Pseudo-components |
|---|---|
| PVT matching, reserve estimates | 20–40 (no lumping) |
| Process flowsheet, steady-state | 8–12 |
| Transient pipeline, dynamic | 4–8 |
| Compositional reservoir grid | 5–7 |
6.5 Tuning the EOS to Lab Data
Even with a good characterisation, the bare EOS predictions deviate from measured saturation pressure and density. Tuning closes the gap by adjusting a handful of parameters:
- Volume shift (Peneloux) on each pseudo-component — corrects liquid density without disturbing equilibrium.
- Plus-fraction MW — within the ±5% uncertainty of the lab measurement, this is the most influential parameter.
- Critical properties of the heaviest pseudo-component — used as a last resort.
- Binary interaction parameters between methane and the heaviest pseudo-components — tunes saturation pressure of high-GOR oils.
The recipe is a constrained least-squares fit using scipy:
import numpy as np
from scipy.optimize import minimize
def residual(params, fluid, measured_psat, measured_rho):
plus_mw, kij = params
# apply
fluid.getComponent("C7").setMolarMass(plus_mw)
fluid.getPhase(0).getMixingRule().setBinaryInteractionParameter(
fluid.getComponent("methane").getComponentNumber(),
fluid.getComponent("C36_PC").getComponentNumber(),
kij)
# evaluate
ops = J.thermodynamicoperations.ThermodynamicOperations(fluid)
ops.bubblePointPressureFlash()
psat = fluid.getPressure()
ops.TPflash()
fluid.initProperties()
rho = fluid.getDensity("kg/m3")
return ((psat - measured_psat) / measured_psat) ** 2 + \
((rho - measured_rho) / measured_rho) ** 2
res = minimize(residual, x0=[215.0, 0.05],
args=(fluid, 220.0, 745.0),
bounds=[(195, 235), (-0.05, 0.15)])
In production work, the loss function compares the entire PVT report (CME pressures, CVD liquid drop-out, DL gas formation) rather than a single data point, and the optimisation is multi-objective. The neqsim-eos-regression skill covers the rigorous form; this chapter covers enough to recognise when tuning is needed.
6.6 Hypothetical Components
Sometimes a measured stream contains a component you cannot identify but whose Tc, Pc, ω, and MW are known (a heavy aromatic, a polymer additive, an unknown contaminant). Add it as a hypothetical:
fluid.addHypotheticalComponent("Hyp1", Tc=550.0, Pc=22.0, omega=0.45,
MW=180.0)
fluid.addComponent("Hyp1", 0.02)
Hypothetical components participate in the EOS like any other component; they just do not appear in the standard database.
6.7 Exporting to Eclipse / E300
Reservoir simulators expect a fluid definition in E300 format (critical properties, BIPs, acentric factor for each component). NeqSim writes it directly:
from neqsim.jneqsim.thermo.util.readwrite import EclipseFluidReadWrite
EclipseFluidReadWrite.write(fluid, "fluid_e300.txt")
The inverse — reading an existing E300 file into NeqSim — preserves critical properties exactly:
fluid = J.thermo.util.readwrite.EclipseFluidReadWrite.read("fluid_e300.txt")
This is essential when transferring a characterised reservoir fluid into a NeqSim process model: the E300 round-trip guarantees identical phase behaviour between the reservoir and the surface facilities.
6.8 Exporting to OLGA
For dynamic transient pipeline work in OLGA, NeqSim generates the PVT-table file format OLGA expects:
from neqsim.jneqsim.thermo.util.readwrite import OLGAFluidReadWrite
OLGAFluidReadWrite.writePVTTable(fluid,
T_range=(280, 360, 10), # K, step
P_range=(1, 200, 10), # bara, step
filename="fluid.tab")
The resulting fluid.tab is consumed by OLGA without modification. The reverse direction is less common — most projects own the characterisation in NeqSim and treat OLGA as a downstream consumer.
6.9 A Workflow Summary
1. Receive PVT report
↓
2. Build fluid in NeqSim with defined light end + plus-fraction
↓
3. characterisePlusFraction() (Pedersen, default)
↓
4. Compare against lab data (Psat, density, GOR, DL/CVD curves)
↓
5. If deviation > 5%, tune (plus-fraction MW, then k_ij)
↓
6. Lump to 8–12 pseudo-components for process work
↓
7. Export to E300 / OLGA for downstream simulators
The early steps are scientific; the later steps are engineering. The discipline of treating the characterised, tuned, lumped fluid as the "single source of truth" — and propagating it everywhere — is what makes field-development calculations consistent across reservoir, facilities, and pipeline groups.
6.10 Looking Ahead
A characterised fluid is the input to every PVT laboratory experiment. Chapter 7 walks through the standard tests — Constant Mass Expansion, Constant Volume Depletion, Differential Liberation, Separator Tests, Swelling — and shows how to reproduce them in NeqSim with a few dozen lines of Python.
Exercises
- Exercise 6.1: Characterise a fluid with 28% C7+ at MW=210 and density=0.845 using both Pedersen and Whitson splitters. Plot the resulting carbon-number distributions on the same axes.
- Exercise 6.2: Lump a 30-pseudo-component fluid down to 6. Re-run a TPflash at three different conditions. How much does the saturation pressure shift?
- Exercise 6.3: Tune the plus-fraction MW so the predicted bubble-point at 100 °C matches a measured 195 bara. Use
scipy.optimize.brentqand bracket the MW between 180 and 250 g/mol.
PVT Simulations
Learning Objectives
After this chapter you will:
- Reproduce the standard PVT laboratory experiments in NeqSim from Python: CME, CVD, Differential Liberation, Separator Test, Swelling Test.
- Compute saturation pressure and temperature with appropriate flash methods.
- Calculate viscosity and density curves across the operating envelope.
- Compare NeqSim predictions to lab data and use the comparison to drive EOS tuning.
7.1 The PVT Toolbox
PVT (Pressure–Volume–Temperature) experiments characterise a reservoir fluid's phase behaviour across pressure depletion. The four canonical tests:
| Test | What it measures | Reservoir analogue |
|---|---|---|
| Constant Mass Expansion (CME) | Saturation pressure, liquid volume vs P | Pressure depletion above bubble/dew point |
| Constant Volume Depletion (CVD) | Produced gas composition vs P, retrograde liquid | Gas condensate depletion |
| Differential Liberation (DL) | GOR, FVF, gas composition by stage | Solution gas drive of black oil |
| Separator Test | Surface volumes from a given reservoir fluid | Topside separation train |
All four are exposed under jneqsim.pvtsimulation.simulation.*. Each has a similar shape: construct the simulation with a fluid, set the pressure series, call runCalc(), read results.
7.2 Saturation Pressure and Temperature
The first quantity asked of any new fluid: at what pressure does it boil or condense?
from neqsim import jneqsim as J
fluid = J.thermo.system.SystemSrkEos(353.15, 200.0)
# ... add components, mixing rule ...
ops = J.thermodynamicoperations.ThermodynamicOperations(fluid)
# Bubble point (oils)
ops.bubblePointPressureFlash()
P_bub = fluid.getPressure()
print(f"Bubble-point pressure at 80 °C: {P_bub:.1f} bara")
# Dew point (gases, condensates)
fluid.setTemperature(353.15)
ops.dewPointPressureFlash()
P_dew = fluid.getPressure()
print(f"Dew-point pressure at 80 °C: {P_dew:.1f} bara")
# Cricondenbar/therm from full envelope
envelope = ops.calcPTphaseEnvelope(True, 1.0)
print(f"Cricondentherm: {envelope.get('cricondenTemperature'):.1f} K")
print(f"Cricondenbar : {envelope.get('cricondenPressure'):.1f} bara")
Note the phase envelope label trap from Chapter 2: always classify the two branches by their maximum temperature. The branch with the higher T_max is the dew curve.
7.3 Constant Mass Expansion
CME measures liquid volume and Z-factor as a single cell is depressurised at constant temperature with no fluid removed.
CME = J.pvtsimulation.simulation.ConstantMassExpansion
sim = CME(fluid)
sim.setTemperature(353.15)
sim.setPressures([400.0, 350.0, 300.0, 250.0, 220.0,
210.0, 200.0, 180.0, 150.0, 100.0])
sim.runCalc()
import numpy as np
P = np.asarray(sim.getPressures())
V = np.asarray(sim.getRelativeVolume())
Z = np.asarray(sim.getZ())
getRelativeVolume() is the cell volume normalised by the volume at the saturation pressure. Plot V against P and the kink at P_sat is visible; the slopes above and below the kink are the isothermal compressibilities of the single-phase fluid and the two-phase mixture respectively.
7.4 Constant Volume Depletion
CVD is the gas-condensate analogue. The cell is depressurised and the produced gas is removed each step to keep the cell volume constant.
CVD = J.pvtsimulation.simulation.ConstantVolumeDepletion
sim = CVD(fluid)
sim.setTemperature(353.15)
sim.setPressures([400.0, 350.0, 300.0, 250.0, 220.0,
200.0, 175.0, 150.0, 125.0, 100.0])
sim.runCalc()
import numpy as np
P = np.asarray(sim.getPressures())
gas_z = np.asarray(sim.getZGas())
liq_vol_pct = np.asarray(sim.getRelativeLiquidVolume()) * 100
gas_composition_at_each_step = sim.getGasComposition() # 2D Java array
Two outputs matter most: the retrograde liquid drop-out (cumulative percentage of the cell volume that becomes liquid) and the produced-gas composition (which becomes leaner over time as the heavies condense out).
7.5 Differential Liberation
DL is the black-oil counterpart. The cell is depressurised in steps; at each step the equilibrium gas is removed at constant pressure before the next depressurisation.
DL = J.pvtsimulation.simulation.DifferentialLiberation
sim = DL(fluid)
sim.setTemperature(353.15)
sim.setPressures([220.0, 200.0, 175.0, 150.0, 125.0, 100.0,
75.0, 50.0, 25.0, 1.013])
sim.runCalc()
P = np.asarray(sim.getPressures())
GOR = np.asarray(sim.getGOR()) # Sm3 gas / Sm3 oil at standard
Bo = np.asarray(sim.getBo()) # oil formation volume factor
rho_oil = np.asarray(sim.getOilDensity())
GOR and B_o are the inputs to material balance and reservoir simulation. For projects feeding a black-oil reservoir model, a NeqSim DL is the standard way to generate a PVT table.
7.6 Separator Test
The separator test predicts surface gas and oil volumes for a given reservoir fluid passed through a defined separation train. NeqSim builds this from a list of (P, T) stages:
ST = J.pvtsimulation.simulation.SeparatorTest
sim = ST(fluid)
sim.setSeparatorConditions(
pressures = [70.0, 10.0, 1.013], # bara
temperatures = [318.15, 313.15, 288.15] # K
)
sim.runCalc()
GOR_total = sim.getGOR() # Sm3/Sm3
oil_FVF = sim.getOilFVF()
stock_tank_density = sim.getOilDensity() # kg/m³
This is the formal name for the calculation every facility engineer does in their head: "if I take reservoir fluid into a three-stage train, what do I get out?" In Chapter 9 the same calculation is built as a process flowsheet — the separator test is the no-flowsheet shortcut.
7.7 Swelling Test
Used in gas-injection EOR studies. Inject a defined gas into a defined oil in incremental amounts; track the saturation pressure of the resulting mixture.
ST = J.pvtsimulation.simulation.SwellingSimulation
sim = ST(fluid)
sim.setInjectionGas(inj_gas_fluid)
sim.setInjectionGasMolePercent([5.0, 10.0, 20.0, 40.0, 60.0])
sim.runCalc()
mole_pct = np.asarray(sim.getInjectionGasMolePercent())
P_sat = np.asarray(sim.getSaturationPressures())
swelling = np.asarray(sim.getSwellingFactors())
A monotonically rising P_sat means the injection gas is miscible at reservoir pressure; a peak followed by a decline indicates first-contact miscibility was lost.
7.8 Viscosity
Viscosity is computed automatically by initProperties(). To produce a viscosity curve over pressure at fixed temperature:
import numpy as np, matplotlib.pyplot as plt
P_grid = np.linspace(10, 400, 40)
mu = []
for p in P_grid:
fluid.setPressure(float(p))
ops.TPflash()
fluid.initProperties()
# oil phase viscosity if present, else gas
if fluid.hasPhaseType("oil"):
mu.append(fluid.getPhase("oil").getViscosity("kg/msec") * 1000) # cP
else:
mu.append(fluid.getPhase("gas").getViscosity("kg/msec") * 1000)
mu = np.asarray(mu)
plt.semilogy(P_grid, mu); plt.xlabel("P [bara]"); plt.ylabel("μ [cP]")
The standard viscosity model in NeqSim is the LBC corresponding-states correlation; for heavy oils, switch to the Friction Theory model with fluid.getPhase("oil").setViscosityModel("frictionTheory").
7.9 Slim-Tube Simulation
Slim-tube is a 1-D simulation that estimates the Minimum Miscibility Pressure (MMP) for a gas–oil pair — a key number for miscible gas injection EOR.
slim = J.pvtsimulation.simulation.SlimTubeSimulation(fluid, inj_gas_fluid)
slim.setLength(20.0) # m
slim.setNumberOfCells(100)
slim.setTemperature(353.15)
slim.setPressures([200, 250, 300, 350, 400]) # bara
slim.runCalc()
recoveries = list(slim.getRecoveries())
The MMP is the pressure at which recovery exceeds ~95% after 1.2 PV injection. Slim-tube is more expensive than a swelling test (it solves a 1-D flow problem each step) but predicts MMP directly rather than inferring it.
7.10 Comparing to Lab Data
The end-to-end workflow:
import pandas as pd
lab = pd.read_csv("lab_dl.csv") # columns: P, GOR_measured, Bo_measured
sim = DL(fluid)
sim.setTemperature(353.15)
sim.setPressures(list(lab["P"]))
sim.runCalc()
lab["GOR_neqsim"] = np.asarray(sim.getGOR())
lab["Bo_neqsim"] = np.asarray(sim.getBo())
lab["GOR_dev_%"] = 100 * (lab["GOR_neqsim"] - lab["GOR_measured"]) / lab["GOR_measured"]
lab["Bo_dev_%"] = 100 * (lab["Bo_neqsim"] - lab["Bo_measured"]) / lab["Bo_measured"]
print(lab.describe())
A well-characterised, well-tuned EOS reproduces measured GOR to within 3–5% and B_o to within 1–2%. Larger deviations are a signal to retune the plus-fraction or revisit the splitting model.
Running Case Checkpoint: PVT Evidence
For the wet rich-gas case, do not carry only the tuned fluid into the process model. Carry the evidence that made the fluid credible:
| Evidence | Minimum check |
|---|---|
| Saturation point | Dew-point or bubble-point pressure at the design temperature |
| Phase envelope | Cricondentherm, cricondenbar, and operating envelope overlay if available |
| Density or Z-factor | One comparison against lab, NIST, vendor, or a previous simulator case |
| Liquid dropout | CVD or separator-test trend if the gas is condensate-rich |
| Acceptance note | Whether the EOS is ready for screening, design, or only exploratory use |
The process model in Chapters 8 and 9 should cite this evidence rather than silently inheriting a fluid object from an earlier notebook.
7.11 Regression Wrapped Together
When the gap is too wide, link Chapter 6's tuning machinery to the PVT loss function:
def loss(params, fluid, lab):
plus_mw = params[0]
fluid.getComponent("C7").setMolarMass(plus_mw)
sim = DL(fluid)
sim.setTemperature(353.15)
sim.setPressures(list(lab["P"]))
sim.runCalc()
pred = np.asarray(sim.getGOR())
return np.mean(((pred - lab["GOR_measured"]) / lab["GOR_measured"]) ** 2)
from scipy.optimize import minimize_scalar
res = minimize_scalar(loss, bounds=(195, 235), method="bounded",
args=(fluid, lab))
print("Best plus MW:", res.x)
The minimisation pattern generalises: replace DL with CVD or CME and the loss with the appropriate metric.
7.12 Looking Ahead
PVT closes Part II. Part III builds process flowsheets — strings of unit operations connected by streams. Chapter 8 inventories the equipment classes; Chapter 9 puts them together into a ProcessSystem.
Exercises
- Exercise 7.1: Run a CME on a 12-component fluid at three different temperatures. At which temperature is the bubble-point pressure highest, and why?
- Exercise 7.2: Reproduce a CVD experiment from a published PVT report. Plot measured vs predicted retrograde liquid drop-out.
- Exercise 7.3: Use a separator test to compare 2-stage and 3-stage separation schemes. Which produces more stable oil?
Part III: Process Modeling
Unit Operations
Learning Objectives
After this chapter you will:
- Instantiate and configure the major NeqSim equipment classes from Python: streams, separators, compressors, heat exchangers, valves, pumps, expanders, mixers, splitters, pipelines, recycles, adjusters.
- Connect equipment together by passing streams between constructors.
- Read the relevant outputs from each piece of equipment using the correct getter and unit.
- Apply equipment-specific design considerations — anti-surge margin, minimum approach temperature, NPSH — at the model level.
8.0 Process Engineer's One-Pager
If you do not yet care about the catalogue of equipment classes, this single runnable cell gets you a four-unit train you can paste into a notebook. It builds the book's running wet-gas case (see running_case.py), passes it through a scrubber, a cooler, and a compressor, and prints engineering KPIs.
from running_case import build_running_fluid, build_running_feed
from neqsim import jneqsim as J
fluid = build_running_fluid("srk")
feed = build_running_feed(fluid, flow_kg_hr=50_000.0)
scrubber = J.process.equipment.separator.Separator("V-100", feed)
cooler = J.process.equipment.heatexchanger.Cooler("E-100",
scrubber.getGasOutStream())
cooler.setOutTemperature(25.0, "C")
k100 = J.process.equipment.compressor.Compressor("K-100",
cooler.getOutStream())
k100.setOutletPressure(120.0, "bara")
k100.setIsentropicEfficiency(0.78)
proc = J.process.processmodel.ProcessSystem()
for unit in (feed, scrubber, cooler, k100):
proc.add(unit)
proc.run()
print(f"Liquid knockout: {scrubber.getLiquidOutStream().getFlowRate('kg/hr'):.0f} kg/h")
print(f"Cooler duty: {cooler.getDuty()/1e3:.1f} kW")
print(f"K-100 power: {k100.getPower()/1e3:.1f} kW")
print(f"Discharge T: {k100.getOutletStream().getTemperature('C'):.1f} °C")
The rest of this chapter explains each line. If a particular equipment family is the one you need, jump straight to its section using the table in §8.1.
8.1 The Equipment Inventory
Every NeqSim flowsheet is built from a small number of equipment families:
| Family | Package | Representative classes |
|---|---|---|
| Streams | equipment.stream |
Stream, EnergyStream |
| Separation | equipment.separator |
Separator, ThreePhaseSeparator, GasScrubber |
| Pressure change | equipment.compressor, equipment.pump, equipment.expander, equipment.valve |
Compressor (Chapter 15 for curves and anti-surge), Pump, Expander, ThrottlingValve, TurboExpanderCompressor |
| Heat transfer | equipment.heatexchanger |
Heater, Cooler, HeatExchanger, AirCooler, MultiStreamHeatExchanger2, LNGHeatExchanger |
| Mixing/Splitting | equipment.mixer, equipment.splitter |
Mixer, Splitter, ComponentSplitter |
| Pipelines | equipment.pipeline |
PipeBeggsAndBrills, TwoFluidPipe |
| Auxiliary | equipment.util |
Recycle, Adjuster, Calculator |
| Reactors | equipment.reactor |
GibbsReactor, PlugFlowReactor, StirredTankReactor |
| Columns | equipment.distillation |
DistillationColumn (Chapter 10 overview; Chapter 16 deep dive) |
This chapter covers the first seven at ordinary flowsheet level. Compressors, columns, and reactors get deeper treatment elsewhere: Chapter 15 covers compressor curves and anti-surge handling, Chapter 16 covers rigorous distillation columns, and Chapter 9 introduces reactors in flowsheets. The advanced-equipment chapter returns to the advanced members of the catalogue, including LNGHeatExchanger, TurboExpanderCompressor, custom unit-operation patterns, and parameter database overrides.
8.2 Streams
A Stream wraps a SystemInterface with a flow rate, T, P, and a name. Every flowsheet starts with one or more feed streams.
from neqsim import jneqsim as J
fluid = J.thermo.system.SystemSrkEos(298.15, 60.0)
fluid.addComponent("methane", 0.90)
fluid.addComponent("ethane", 0.10)
fluid.setMixingRule("classic")
feed = J.process.equipment.stream.Stream("Feed", fluid)
feed.setFlowRate(100.0, "kg/hr")
feed.setTemperature(25.0, "C")
feed.setPressure(60.0, "bara")
After run(), the relevant getters:
feed.run()
feed.getFlowRate("kg/hr")
feed.getTemperature("C")
feed.getPressure("bara")
feed.getFluid().getDensity("kg/m3")
An EnergyStream is the same idea applied to heat: it carries a duty value (in watts) and is used to link a heater to a cooler or to wire up a steam network. Most flowsheets use only material streams.
8.3 Separators
Two-Phase Separator
Separator performs an isothermal flash on the inlet and splits the result into gas and liquid outlets.
sep = J.process.equipment.separator.Separator("V-100", feed)
# Optional: explicit pressure (otherwise inherits from inlet)
# sep.setPressure(50.0, "bara")
sep.run()
gas = sep.getGasOutStream()
liq = sep.getLiquidOutStream()
print(gas.getFlowRate("kg/hr"), liq.getFlowRate("kg/hr"))
Three-Phase Separator
ThreePhaseSeparator adds an aqueous outlet — gas, oil, water.
sep3 = J.process.equipment.separator.ThreePhaseSeparator("V-101", wet_feed)
sep3.run()
gas = sep3.getGasOutStream()
oil = sep3.getOilOutStream()
water = sep3.getWaterOutStream()
The inlet fluid must have water added and setMultiPhaseCheck(True) for the water phase to materialise.
Gas Scrubber
GasScrubber is a vertical knockout — same physics as a two-phase separator but with a vertical orientation flag, used in compression suction service. The constructor and outputs are identical to Separator.
Slugcatcher
For multi-finger slugcatchers, NeqSim provides SlugcatcherFinger and a manifold class; for steady-state design these behave like a series of three-phase separators with shared liquid level.
8.4 Compressors
The compressor is the equipment family with the most options and the most ways to get wrong. The defaults work for screening; for design, configure the polytropic efficiency, outlet pressure, and (eventually) a performance curve.
comp = J.process.equipment.compressor.Compressor("K-100", feed)
comp.setOutletPressure(150.0, "bara")
comp.setPolytropicEfficiency(0.78)
comp.setCompressorChartType("polytropic")
comp.run()
power_kW = comp.getPower() / 1000.0
T_out_C = comp.getOutStream().getTemperature("C")
print(f"K-100: P_in={feed.getPressure('bara'):.1f} bara, "
f"P_out={comp.getOutStream().getPressure('bara'):.1f} bara, "
f"η_poly={comp.getPolytropicEfficiency():.2f}, "
f"shaft power={power_kW:.1f} kW")
Performance Curves
For real machines, attach a head/flow/efficiency curve:
chart = comp.getCompressorChart()
chart.addSurgeCurve(speed_rpm=10000,
flow_m3hr=[2000, 2500, 3000],
head_m=[12000, 11500, 11000])
chart.addOperationPoint(speed=10000, flow=2800, head=11200, efficiency=0.78)
comp.setUsePolytropicCalc(True)
A compressor with a real curve respects its surge boundary and produces a power and outlet temperature that match the manufacturer test. Without a curve, NeqSim assumes the user-specified efficiency is achievable at any flow.
Anti-surge Margin
Check the surge margin after run():
margin_pct = (comp.getFlowRate() - comp.getSurgeFlowRate()) / \
comp.getSurgeFlowRate() * 100
A margin below 10% is the usual trigger for an anti-surge recycle line.
8.5 Heat Exchangers
Four classes cover the common cases.
Heater and Cooler
Single-side specification — outlet temperature or duty.
cooler = J.process.equipment.heatexchanger.Cooler("E-100", feed)
cooler.setOutTemperature(283.15) # K
# or
cooler.setEnergyInput(-200_000.0) # W (negative = cooling)
cooler.run()
duty_kW = -cooler.getDuty() / 1000.0
Two-stream HeatExchanger
True counter/cocurrent exchanger between two process streams.
hx = J.process.equipment.heatexchanger.HeatExchanger("E-200", hot_in, cold_in)
hx.setFlowArrangement("counterCurrent")
hx.setUAvalue(5000.0) # W/K
# or specify temperature/approach
# hx.setOutTemperature(310.0) # hot outlet, K
# hx.setApproachTemperature(5.0) # min approach, K
hx.run()
hot_out = hx.getOutStream(0)
cold_out = hx.getOutStream(1)
duty_kW = hx.getDuty() / 1000.0
LMTD = hx.getLMTD()
The minimum approach temperature is the design constraint that matters most. A typical envelope is 10 °C for shell-and-tube on hydrocarbons, 3–5 °C for plate-fin or brazed aluminium.
Air Cooler
For atmospheric-air condensers and trim coolers:
ac = J.process.equipment.heatexchanger.AirCooler("E-300", feed)
ac.setOutTemperature(313.15)
ac.setAmbientTemperature(298.15)
ac.run()
8.6 Valves
Throttling Valve
Pressure drop with energy conservation — adiabatic expansion across an orifice. Used everywhere: pressure control, JT expansion, choke.
v = J.process.equipment.valve.ThrottlingValve("PV-100", feed)
v.setOutletPressure(20.0, "bara")
v.run()
T_out_C = v.getOutStream().getTemperature("C")
# Joule-Thomson cooling shows up here
For Cv-based sizing:
v.setCv(50.0)
v.setPercentValveOpening(60.0)
v.run()
# outlet pressure is now computed from Cv and opening
Safety Valve
SafetyValve is a sized PSV with set pressure, blowdown, and back-pressure correction. Chapter 14 in the agentic book and the neqsim-relief-flare-network skill cover sizing; from the process-model perspective it behaves like a valve that opens fully above set pressure.
8.7 Pumps and Expanders
pump = J.process.equipment.pump.Pump("P-100", liquid_feed)
pump.setOutletPressure(150.0, "bara")
pump.setIsentropicEfficiency(0.75)
pump.run()
shaft_kW = pump.getPower() / 1000.0
NPSH_required_m = pump.getNPSHrequired() # if a pump curve is loaded
exp = J.process.equipment.expander.Expander("EX-100", high_p_gas)
exp.setOutletPressure(20.0, "bara")
exp.setIsentropicEfficiency(0.85)
exp.run()
power_recovered_kW = exp.getPower() / 1000.0
8.8 Mixers and Splitters
mixer = J.process.equipment.mixer.Mixer("M-100")
mixer.addStream(stream_a)
mixer.addStream(stream_b)
mixer.addStream(stream_c)
mixer.run()
mixed_out = mixer.getOutletStream()
splitter = J.process.equipment.splitter.Splitter("SP-100", feed)
splitter.setSplitNumber(3)
splitter.setSplitFactors([0.5, 0.3, 0.2])
splitter.run()
s1, s2, s3 = (splitter.getSplitStream(i) for i in range(3))
A ComponentSplitter selectively diverts named components — used to model ideal separations or membrane units before a detailed mass-transfer model is built.
8.9 Pipelines
Beggs–Brill Two-Phase Pipe
The workhorse two-phase pipe model. Computes pressure drop, holdup, and flow regime over a pipe section.
pipe = J.process.equipment.pipeline.PipeBeggsAndBrills("Flowline", feed)
pipe.setLength(15_000.0) # m
pipe.setDiameter(0.305) # m (12 in)
pipe.setElevation(-50.0) # m, negative = downhill
pipe.setPipeWallRoughness(4.5e-5) # m
pipe.setNumberOfIncrements(50)
pipe.run()
dP_bar = (feed.getPressure("bara") - pipe.getOutStream().getPressure("bara"))
holdup = pipe.getAverageLiquidHoldup()
regime = pipe.getLastFlowRegime()
Single-Phase Adiabatic Pipe
For gas-only or oil-only service:
pipe = J.process.equipment.pipeline.AdiabaticPipe("Header", feed)
pipe.setLength(500.0); pipe.setDiameter(0.40)
pipe.run()
8.10 Recycles and Adjusters
A Recycle closes a loop by feeding the model an estimate of a downstream stream and iterating until the estimate matches the calculation.
rec = J.process.equipment.util.Recycle("R-100")
rec.addStream(downstream_stream) # the stream to be recycled back
rec.setOutletStream(recycle_estimate) # the placeholder feed
rec.setTolerance(1.0e-4)
An Adjuster solves a one-equation degree-of-freedom problem — vary an input until a target is met.
adj = J.process.equipment.util.Adjuster("ADJ-100")
adj.setAdjustedVariable(compressor, "outletPressure")
adj.setTargetVariable(downstream_temp_meas, "value")
adj.setTargetValue(40.0) # °C
adj.setMinValue(50.0); adj.setMaxValue(250.0)
Both are added to the ProcessSystem like any other equipment. The system's run() orchestrates iteration. Recycles and adjusters are covered in detail in Chapter 9.
8.11 Reading Connected Streams
Every piece of equipment knows its inlets and outlets:
inlets = list(sep.getInletStreams()) # [feed]
outlets = list(sep.getOutletStreams()) # [gasOut, liquidOut]
This pair of getters makes flowsheet introspection trivial: walk the graph by following streams.
8.12 Worked Example: Two-Stage Compression with Intercooling
from neqsim import jneqsim as J
fluid = J.thermo.system.SystemSrkEos(308.15, 5.0)
for c, x in {"methane":0.90, "ethane":0.07, "propane":0.03}.items():
fluid.addComponent(c, x);
fluid.setMixingRule("classic")
S = J.process.equipment.stream.Stream
K = J.process.equipment.compressor.Compressor
C = J.process.equipment.heatexchanger.Cooler
V = J.process.equipment.separator.Separator
P = J.process.processmodel.ProcessSystem
feed = S("Feed", fluid); feed.setFlowRate(50000.0, "kg/hr")
k1 = K("K-101", feed); k1.setOutletPressure(25.0, "bara"); k1.setPolytropicEfficiency(0.78)
e1 = C("E-101", k1.getOutStream()); e1.setOutTemperature(308.15)
v1 = V("V-101", e1.getOutStream())
k2 = K("K-102", v1.getGasOutStream()); k2.setOutletPressure(120.0, "bara"); k2.setPolytropicEfficiency(0.78)
e2 = C("E-102", k2.getOutStream()); e2.setOutTemperature(308.15)
v2 = V("V-102", e2.getOutStream())
proc = P()
for u in (feed, k1, e1, v1, k2, e2, v2): proc.add(u)
proc.run()
for k in (k1, k2):
print(f"{k.getName()}: power={k.getPower()/1000:.1f} kW, "
f"T_out={k.getOutStream().getTemperature('C'):.1f} °C, "
f"P_out={k.getOutStream().getPressure('bara'):.1f} bara")
This is a complete, runnable model in about twenty lines. Adding a recycle to handle anti-surge, or an adjuster to hit a delivery pressure precisely, is one more equipment object each.
8.13 Looking Ahead
Chapter 9 puts equipment into context: how ProcessSystem orchestrates the run, how to handle recycles and adjusters, how to debug a model that will not converge, and how ProcessModel composes multi-area plants.
Exercises
- Exercise 8.1: Build a single-stage compression with cooler and knockout. Compute the percentage of liquid knocked out as a function of cooler outlet temperature.
- Exercise 8.2: For the two-stage example, vary the inter-stage pressure between 15 and 40 bara. Plot total shaft power versus inter-stage pressure. Find the optimum.
- Exercise 8.3: Replace the simple
Coolerwith aHeatExchangerusing cooling water at 20 °C. What UA is required to hit a 40 °C process outlet?
Building Steady-State Process Models
Learning Objectives
After this chapter you will:
- Assemble equipment from Chapter 8 into a
ProcessSystemand execute the run. - Diagnose convergence problems — bad guesses, missing degrees of freedom, ordering issues.
- Close material recycles correctly with
Recycleand solve single-equation specs withAdjuster. - Compose multi-area plants with
ProcessModel.
9.0 Process Engineer's One-Pager — A Working Recycle
A complete anti-surge recycle that actually converges, in one cell. This is the smallest flowsheet that exercises every concept in this chapter: equipment registration, tearing, Recycle, and run()-driven iteration.
from running_case import build_running_fluid, build_running_feed
from neqsim import jneqsim as J
fluid = build_running_fluid("srk")
feed = build_running_feed(fluid, flow_kg_hr=50_000.0)
# Tear stream — initial guess for the recycle return
guess = J.process.equipment.stream.Stream("recycleGuess", fluid.clone())
guess.setFlowRate(5_000.0, "kg/hr")
guess.setTemperature(40.0, "C")
guess.setPressure(60.0, "bara")
mix = J.process.equipment.mixer.Mixer("M-100")
mix.addStream(feed); mix.addStream(guess)
k100 = J.process.equipment.compressor.Compressor("K-100", mix.getOutStream())
k100.setOutletPressure(120.0, "bara"); k100.setIsentropicEfficiency(0.78)
e100 = J.process.equipment.heatexchanger.Cooler("E-100", k100.getOutletStream())
e100.setOutTemperature(35.0, "C")
split = J.process.equipment.splitter.Splitter("SP-100", e100.getOutStream())
split.setSplitFactors([0.9, 0.1]) # 10 % recycle
valve = J.process.equipment.valve.ThrottlingValve(
"PV-100", split.getSplitStream(1))
valve.setOutletPressure(60.0, "bara")
rec = J.process.equipment.util.Recycle("REC-1")
rec.addStream(valve.getOutletStream())
rec.setOutletStream(guess)
rec.setTolerance(1e-4)
proc = J.process.processmodel.ProcessSystem()
for u in (feed, guess, mix, k100, e100, split, valve, rec):
proc.add(u)
proc.run()
m_in = feed.getFlowRate("kg/hr")
m_out = split.getSplitStream(0).getFlowRate("kg/hr")
print(f"Recycle iterations: {rec.getIterations()}")
print(f"Recycle solved: {rec.solved()}")
print(f"Recycle residuals: flow={rec.getErrorFlow():.2e}, "
f"T={rec.getErrorTemperature():.2e}, "
f"P={rec.getErrorPressure():.2e}, "
f"x={rec.getErrorComposition():.2e}")
print(f"Recycle flow: {valve.getOutletStream().getFlowRate('kg/hr'):.0f} kg/h")
print(f"Mass balance error: {100*(m_in - m_out)/m_in:.4f} %")
If this converges and the mass balance closes, you understand 80 % of what the rest of the chapter teaches. The remaining 20 % is about diagnosing what to do when it doesn't converge.
9.1 ProcessSystem: The Orchestrator
A ProcessSystem is a list of equipment plus an execution plan. Adding equipment with add() registers them; calling run() resolves the execution order, runs each unit in topological order, and iterates recycles until convergence.
from neqsim import jneqsim as J
proc = J.process.processmodel.ProcessSystem()
proc.add(feed)
proc.add(separator)
proc.add(compressor)
proc.add(cooler)
proc.run()
add() enforces unique names — naming two pieces of equipment "K-100" raises immediately. The execution order is derived from the inlet–outlet stream graph; the user does not need to add equipment in topological order.
9.2 Walking the Flowsheet
After run(), introspect by name or by traversal:
sep = proc.getUnit("V-100") # by name
for u in proc.getUnitOperations():
print(u.getName(), type(u).__name__)
for u in proc.getUnitOperations():
inlets = [s.getName() for s in u.getInletStreams()]
outlets = [s.getName() for s in u.getOutletStreams()]
print(f"{u.getName()}: {inlets} -> {outlets}")
This trio — name lookup, type-aware iteration, stream graph traversal — is everything needed to write topology-aware reporting code.
9.3 Recycles
Almost every industrial flowsheet has loops: anti-surge recycles, lean-rich amine loops, solvent regeneration, unreacted-gas reactor recycle, recompression from a lower-pressure separator, and small liquid returns from scrubbers or knockout drums. NeqSim solves these loops by tearing: pick one stream in the loop, replace it with an initial-guess stream, run the downstream equipment, then let a Recycle overwrite the guess until the upstream and downstream versions of the stream agree.
Use Recycle when a downstream material stream feeds back to an upstream unit in the same ProcessSystem. Do not use it just because two units are connected in series; direct stream references are enough for acyclic flowsheets. For a single design specification, use an Adjuster. For a large plant split into areas, use ProcessModel to compose the areas and use Recycle only for the actual material tear streams inside or between those areas.
| Situation | Use Recycle? |
Why |
|---|---|---|
| Compressor anti-surge gas returns to suction | Yes | Downstream discharge conditions affect suction flow and composition. |
| Lean solvent returns from regeneration to absorber | Yes | Absorber performance depends on regenerated solvent state. |
| Reactor effluent separator returns unreacted gas to reactor feed | Yes | Conversion and separator split define the next feed. |
| Distillation internal reflux | Usually no | DistillationColumn already solves its internal reflux and boilup. |
| Heater outlet feeds a downstream separator | No | This is a normal one-pass stream connection. |
| Vary compressor pressure to hit arrival pressure | No | Use Adjuster; there is no material feedback loop. |
The practical workflow is always the same:
- Choose the tear stream. Pick the quietest stream in the loop: single phase if possible, moderate flow, known pressure, and not immediately across a discontinuous valve or phase boundary.
- Create an initial guess. Make a
Streamwith the expected pressure, temperature, flow, and composition. A previous converged case is the best guess; a hand-estimate stream is usually good enough for a first run. - Feed the guess upstream. Add the guess to the upstream mixer or unit.
- Connect the computed return. Add the downstream return stream to the
Recycleand set the recycle outlet to the guess stream. - Set tolerances and acceleration. Start loose, converge the topology, then tighten only as far as the engineering decision needs.
- Check residuals. Treat
rec.solved()and the error getters as part of the model's acceptance criteria.
from neqsim import jneqsim as J
# Loop placeholder — properties match expected recycle stream
guess = J.process.equipment.stream.Stream("recycleGuess", fluid.clone())
guess.setFlowRate(5000.0, "kg/hr")
guess.setTemperature(40.0, "C")
guess.setPressure(60.0, "bara")
# Downstream unit consumes the guess
mixer = J.process.equipment.mixer.Mixer("M-100")
mixer.addStream(feed); mixer.addStream(guess)
# ... build the rest of the loop ...
loop_return = some_separator.getLiquidOutStream()
rec = J.process.equipment.util.Recycle("R-100")
rec.addStream(loop_return) # the actual computed stream
rec.setOutletStream(guess) # placeholder to be overwritten
rec.setTolerance(1.0e-4)
rec.setMaxIterations(30)
proc.add(rec)
proc.run()
The ProcessSystem detects the recycle, iterates the loop, and stops when consecutive iterations of loop_return agree within the tolerance.
Recycle Configuration
setTolerance(value) applies the same tolerance to flow, temperature, pressure, and composition. For real studies, it is clearer to set the four criteria explicitly and report the four residuals:
rec.setFlowTolerance(1.0e-3) # relative/absolute flow residual used by Recycle
rec.setCompositionTolerance(1.0e-5) # sum of mole-fraction differences
rec.setTemperatureTolerance(1.0e-4) # relative temperature residual
rec.setPressureTolerance(1.0e-4) # relative pressure residual
rec.setMaxIterations(50)
proc.run()
print("Recycle solved:", rec.solved())
print("Iterations:", rec.getIterations())
print("Flow residual:", rec.getErrorFlow())
print("Temperature residual:", rec.getErrorTemperature())
print("Pressure residual:", rec.getErrorPressure())
print("Composition residual:", rec.getErrorComposition())
Keep tolerances proportional to the decision. A screening case can often accept 1e-3 to 1e-2 residuals; a mass-balance report or optimizer case should be tighter. Very tight tolerances on a phase-splitting stream can waste iterations without changing the engineering conclusion.
Acceleration Methods
The current recycle API exposes three acceleration methods through AccelerationMethod:
| Method | When to use | Notes |
|---|---|---|
DIRECT_SUBSTITUTION |
Default for simple, well-behaved loops. | Most transparent; use first while debugging. |
WEGSTEIN |
Slow or mildly oscillating single recycle. | Low overhead; bounded q-factors prevent wild extrapolation. |
BROYDEN |
Multiple coupled variables or difficult recycle maps. | More stateful; useful after the topology is known to be physically feasible. |
AccelerationMethod = J.process.equipment.util.AccelerationMethod
rec.setAccelerationMethod(AccelerationMethod.WEGSTEIN)
rec.setWegsteinDelayIterations(2)
rec.setWegsteinQMin(-5.0)
rec.setWegsteinQMax(0.0)
# For tightly coupled cases:
rec.setAccelerationMethod(AccelerationMethod.BROYDEN)
Use acceleration to improve a plausible loop, not to hide a bad specification. If direct substitution diverges immediately, inspect the pressure levels, valve outlet pressure, phase boundary, and initial guess before turning on Broyden.
Multiple and Nested Recycles
Use priorities when one loop should converge before another. Lower priority numbers are solved first. This is useful for nested solvent loops, recompression trains with more than one anti-surge branch, or plant models where an inner equipment loop should settle before an outer plant recycle.
inner = J.process.equipment.util.Recycle("inner solvent recycle")
inner.addStream(regenerated_solvent)
inner.setOutletStream(lean_solvent_guess)
inner.setPriority(10)
inner.setAccelerationMethod(AccelerationMethod.WEGSTEIN)
outer = J.process.equipment.util.Recycle("outer gas recycle")
outer.addStream(export_recycle_gas)
outer.setOutletStream(gas_recycle_guess)
outer.setPriority(20)
outer.setAccelerationMethod(AccelerationMethod.DIRECT_SUBSTITUTION)
proc.add(inner)
proc.add(outer)
proc.run()
If two recycle loops strongly affect each other, avoid tuning both at once. First fix one loop with the other branch closed or held at a small estimate; then enable the second loop and switch one or both to BROYDEN only if needed.
What to Report
A recycle is part of the flowsheet convergence contract. A process calculation that hides recycle residuals is hard to review. For any model with a recycle, report at least:
- Recycle name and physical service: anti-surge, solvent return, reactor gas, recompression, liquid return, or heat-integration loop.
- Tear stream basis: where the guess is inserted and where the calculated return comes from.
- Initial guess: pressure, temperature, flow, and any composition assumption.
- Tolerances and acceleration method.
- Iteration count and the four residuals: flow, temperature, pressure, and composition.
- Whether the recycle converged with
rec.solved()and whether the overall mass balance closes.
When Recycles Diverge
Common causes:
- Bad initial guess. Recycle solvers are simple successive-substitution iterators. If the guess is in the wrong phase region or off by an order of magnitude, iterations may oscillate or stall. Fix: provide a guess from a hand calculation or a previous case.
- Underspecified downstream unit. If the loop contains a unit that does not have a unique solution (e.g. a separator with both T and P left unset), iteration cannot converge. Fix: pin every degree of freedom inside the loop.
- Physical infeasibility. If the loop demands removing more energy than the duty allows, or producing more liquid than is present, the loop diverges because there is no fixed point. Fix: revise the specification.
- Pressure mismatch across the tear. Anti-surge and recompression loops commonly fail because the recycle valve outlet pressure does not match the suction mixer pressure. Fix: align the valve outlet pressure and the guess stream pressure before tightening tolerances.
- Tearing at a bad location. A two-phase stream, a stream very close to a phase boundary, or a stream downstream of a discontinuous split is a harder tear than a stable single-phase stream. Fix: move the tear to the suction side of the mixer or another quieter part of the loop.
Acceleration via Wegstein is available on the Recycle object and helps slow loops:
rec.setAccelerationMethod(J.process.equipment.util.AccelerationMethod.WEGSTEIN)
For stubborn loops, loosen tolerances temporarily, reduce the recycle split, warm-start from a converged neighbouring case, then tighten the tolerances once the physical loop behaves.
9.4 Adjusters
An Adjuster solves the one-equation problem: vary X until Y equals target. Examples: vary a compressor outlet pressure until a downstream arrival pressure is hit; vary a heater duty until a stream temperature matches a setpoint.
adj = J.process.equipment.util.Adjuster("ADJ-OUTLET-T")
adj.setAdjustedVariable(cooler, "outTemperature") # vary this
adj.setTargetVariable(pipe.getOutStream(), "temperature") # observe this
adj.setTargetValue(30.0) # target value
adj.setMinValue(280.0); adj.setMaxValue(320.0) # bracket (K)
adj.setTolerance(0.1)
proc.add(adj)
proc.run()
Adjusters are bisection/secant solvers; they need a monotonic relationship between adjusted and target variable. If a compressor speed both increases and decreases the downstream pressure (across a surge curve, for example), the adjuster will fail.
Multiple adjusters are allowed in one model, but coupled adjusters (several variables driving several targets) are fragile — collapse them into a single multivariable optimisation when possible.
9.5 Calculators and Controllers
A Calculator runs arbitrary user logic each iteration; a controller device implements PID feedback for dynamic simulation. Both are covered in Chapter 11.
9.6 ProcessModel for Multi-Area Plants
Large plants (FPSOs, gas processing plants, refineries) are too big for a single ProcessSystem. Split the model by process area, each with its own ProcessSystem, then compose with ProcessModel.
from neqsim import jneqsim as J
PM = J.process.processmodel.ProcessModel
def build_separation():
proc = J.process.processmodel.ProcessSystem()
# ... HP/MP/LP separation with feed = well_fluid ...
return proc
def build_compression(gas_in):
proc = J.process.processmodel.ProcessSystem()
# ... compression train using gas_in as feed ...
return proc
sep_area = build_separation()
gas_to_comp = sep_area.getUnit("V-101").getGasOutStream()
comp_area = build_compression(gas_to_comp)
plant = PM()
plant.add("separation", sep_area)
plant.add("compression", comp_area)
plant.run()
The areas share streams by reference: the gas stream out of the HP separator is the same Java object the compression area uses as its feed. plant.run() iterates until all areas have converged.
A common error: adding a ProcessSystem to another ProcessSystem. Always use ProcessModel for composition; nesting ProcessSystems is not supported and raises a TypeError.
Convergence summary
After plant.run():
print(plant.getConvergenceSummary())
# Area 'separation': converged in 1 iter
# Area 'compression': converged in 4 iter (recycle R-100, residual 8.2e-5)
9.7 Naming Conventions
Naming matters more than it appears. The ProcessAutomation API (Chapter 12), the JSON serialisation, and the reporting tools all key on equipment names. A consistent scheme pays back tenfold.
Recommended:
| Equipment | Tag prefix | Example |
|---|---|---|
| Separator | V- | V-101 (HP), V-102 (MP), V-103 (LP) |
| Compressor | K- | K-101, K-102 |
| Cooler / heater | E- | E-101, E-102 |
| Pump | P- | P-101 |
| Valve | PV-, FV-, LV- | PV-101 |
| Pipe | PL- | PL-101 |
Use stream names that describe both origin and destination: "V-101_gas", "K-101_disch", "feed_well_A". This makes the JSON snapshots (Chapter 13) self-documenting.
9.8 Debugging Non-convergence
A flowsheet that does not converge usually fails in one of five ways.
| Symptom | Likely cause | Remedy |
|---|---|---|
| Flash failure on first unit | Wrong P, T, or phase region | Print feed state; check mixing rule was set |
| Compressor power negative | Inlet P > outlet P | Check setOutletPressure value |
| Separator returns zero liquid | Above dew point | Check T/P against envelope |
| Recycle oscillating | Bad guess or stiff loop | Use Wegstein; warm-start from previous run |
| Adjuster never converges | Non-monotonic or unbracketed | Narrow min/maxValue; manually find one feasible point first |
The single most useful debugging idiom: log every stream after run().
for s in proc.getStreams():
fl = s.getFlowRate("kg/hr")
T = s.getTemperature("C")
P = s.getPressure("bara")
print(f"{s.getName():20s} {fl:10.1f} kg/hr {T:7.1f} °C {P:7.1f} bara")
Three pieces of information per stream catch nine out of ten errors.
9.9 Running with the Run Manager
For long-running studies, the NeqSim Runner (devtools/neqsim_runner/) executes each simulation in an isolated subprocess with retry and checkpointing. This is the recommended way to run notebooks and scripts inside the task workflow:
from neqsim_runner.agent_bridge import AgentBridge
bridge = AgentBridge(task_dir="task_solve/2026-04-26_my_task")
job_ids = [bridge.submit_notebook("step2_analysis/01_basecase.ipynb")]
bridge.run_all(max_parallel=1)
The runner is useful when a single JVM cannot be reused across cases (e.g. parametric sweeps where each case mutates global state).
9.10 Worked Example: HP/MP/LP Separation
A three-stage separation train, with the MP recycle gas re-routed to the HP stage suction after compression:
from neqsim import jneqsim as J
# ... fluid creation omitted; see Chapter 5 ...
feed = J.process.equipment.stream.Stream("Well", well_fluid)
feed.setFlowRate(500_000.0, "kg/hr")
feed.setTemperature(80.0, "C"); feed.setPressure(70.0, "bara")
V = J.process.equipment.separator.ThreePhaseSeparator
hp = V("V-101", feed)
mp_inlet_valve = J.process.equipment.valve.ThrottlingValve("PV-101", hp.getOilOutStream())
mp_inlet_valve.setOutletPressure(20.0, "bara")
mp = V("V-102", mp_inlet_valve.getOutStream())
lp_inlet_valve = J.process.equipment.valve.ThrottlingValve("PV-102", mp.getOilOutStream())
lp_inlet_valve.setOutletPressure(1.5, "bara")
lp = V("V-103", lp_inlet_valve.getOutStream())
proc = J.process.processmodel.ProcessSystem()
for u in (feed, hp, mp_inlet_valve, mp, lp_inlet_valve, lp): proc.add(u)
proc.run()
print(f"HP gas: {hp.getGasOutStream().getFlowRate('kg/hr'):,.0f} kg/hr")
print(f"MP gas: {mp.getGasOutStream().getFlowRate('kg/hr'):,.0f} kg/hr")
print(f"LP gas: {lp.getGasOutStream().getFlowRate('kg/hr'):,.0f} kg/hr")
print(f"Stock-tank oil: {lp.getOilOutStream().getFlowRate('Sm3/hr'):,.1f} Sm³/hr")
To add a recompression: build a small two-stage compression area as a second ProcessSystem, connect its discharge stream back to a Mixer upstream of V-101, wrap the discharge–mixer link in a Recycle, and compose both areas with ProcessModel.
Running Case Checkpoint: Process Boundary
At this point the running case should have a clear model boundary. Write it down before adding optimization, reporting, or API layers:
| Boundary item | Example for the wet rich-gas case |
|---|---|
| Feed boundary | Rich gas or wellstream at stated T, P, and flow |
| Product boundary | Export gas pressure, liquid product condition, water disposal stream |
| Recycle boundary | Anti-surge or recompression stream with initial guess and tolerance |
| Constraint boundary | Compressor power, discharge temperature, separator pressure, product spec |
| Validation boundary | Mass-balance closure and one process KPI compared to a hand estimate or reference case |
This boundary is the contract used by the Automation API in Chapter 12 and by the optimizer in Chapter 18.
9.11 Looking Ahead
Chapter 10 covers distillation — a special class of unit operation big enough to warrant its own chapter, with its own solver options and its own failure modes. Chapter 11 introduces dynamics: the same equipment classes, run forward in time with controllers and disturbances.
Exercises
- Exercise 9.1: Build the HP/MP/LP train above. Add an adjuster that varies MP separator pressure to maximise oil volumetric flow at the stock tank. What MP pressure is optimal, and why?
- Exercise 9.2: Add a recompression area (MP gas + LP gas → compressor → cooler → recycle to HP suction). Use a
Recycleand verify convergence.
- Exercise 9.3: Use
ProcessModel.compare()(Chapter 13 preview) to diff your base case and your recompression case. Which equipment sees the largest parameter shift?
Distillation, Absorption, and Columns
Learning Objectives
After this chapter you will:
- Build a
DistillationColumnfrom Python, including condenser, reboiler, multiple feeds, and side draws. - Choose between the available solvers: direct, damped, inside-out, matrix inside-out, Wegstein, sum-rates, Newton, Naphtali-Sandholm, MESH residual, and AUTO.
- Diagnose and fix the most common column convergence failures.
- Size column internals with the column's built-in helpers and
SeparatorMechanicalDesignanalogues.
10.0 Process Engineer's One-Pager — A Converged Deethaniser
The minimum-viable distillation column: a deethaniser separating C2 from C3+, with the inside-out solver, that converges on the first try.
from running_case import build_running_fluid
from neqsim import jneqsim as J
fluid = build_running_fluid("srk")
feed = J.process.equipment.stream.Stream("Feed", fluid)
feed.setFlowRate(10_000.0, "kg/hr")
feed.setTemperature(-10.0, "C")
feed.setPressure(28.0, "bara")
col = J.process.equipment.distillation.DistillationColumn(
"T-100", 20, True, True) # 20 stages, reboiler, condenser
col.addFeedStream(feed, 10)
col.setCondenserTemperature(273.15 - 5.0)
col.setReboilerTemperature(273.15 + 120.0)
col.setSolverType(
J.process.equipment.distillation.DistillationColumn.SolverType.INSIDE_OUT)
proc = J.process.processmodel.ProcessSystem()
proc.add(feed); proc.add(col)
proc.run()
top = col.getGasOutStream(); bot = col.getLiquidOutStream()
print(f"Top C2 mole frac: {top.getFluid().getPhase(0).getComponent('ethane').getx():.4f}")
print(f"Bot C3 mole frac: {bot.getFluid().getPhase(0).getComponent('propane').getx():.4f}")
print(f"Reboiler duty: {col.getReboiler().getDuty()/1e3:.1f} kW")
print(f"Mass residual: {col.getLastMassResidual():.2e}")
If the mass residual is below 1e-6, the column is converged. The rest of the chapter is about what to do when it is not.
10.1 Why Columns Deserve a Chapter
A distillation column is conceptually a long chain of equilibrium stages with internal vapour and liquid traffic. In practice it is the hardest unit operation in any flowsheet to model: tightly coupled mass and energy balances, strongly non-linear vapour–liquid behaviour near the critical, and sensitivity to feed-tray placement that can swing convergence by factor-of-ten.
NeqSim's DistillationColumn is a single Java class that handles all of this — but it exposes solver choices and configuration parameters that the user needs to understand. This chapter is the compact introduction; Chapter 16 is the detailed reference-style treatment of the current distillation API.
10.2 Anatomy of a Column
A column object has:
- N simple trays, excluding optional condenser and reboiler.
- A reboiler at internal index 0 when present.
- Simple trays numbered upward above the reboiler.
- A condenser at the highest internal index when present.
- One or more feed streams, each connected to a specific stage.
- Zero or more side-draw streams, vapour or liquid, from any stage.
- Optional pumparounds — a heat-removal loop drawn from one stage and returned cooler to another, with a duty.
from neqsim import jneqsim as J
col = J.process.equipment.distillation.DistillationColumn(
"T-100", # name
20, # number of trays (excluding condenser/reboiler)
True, # has reboiler
True # has condenser
)
col.addFeedStream(feed, 10) # feed to tray 10
col.setCondenserTemperature(303.15) # K
col.setReboilerTemperature(393.15) # K
NeqSim's internal numbering is bottom-up. This is different from many operations drawings that count trays from the top. If the reboiler is present, index 0 is the reboiler; simple trays increase upward; the condenser, if present, is the last internal stage.
10.3 Specifying the Column
Distillation has more degrees of freedom than any other unit operation in this book. The minimum specification set:
| Specification | Typical use |
|---|---|
| Number of stages | Always |
| Feed location(s) | Always |
| Condenser pressure | Always |
| Reboiler pressure (or column ΔP) | Always |
| Two product specifications | Pick from: distillate rate, bottoms rate, reflux ratio, boilup ratio, distillate composition, bottoms composition, condenser duty, reboiler duty |
For a typical deethaniser:
col.setTopPressure(28.0) # bara
col.setBottomPressure(28.5) # bara
col.setCondenserRefluxRatio(2.5) # reflux ratio
col.setTopProductFlowRate(120.0, "kg/hr") # top product flow
Reflux ratio + distillate rate is one of the most stable pairings. Specifying both purities (top and bottom) gives the engineer the deliverable they want but converges harder.
10.4 Solver Choices
NeqSim ships several solver implementations. Choose by problem type and by the diagnostics you need:
ST = J.process.equipment.distillation.DistillationColumn.SolverType
col.setSolverType(ST.DIRECT_SUBSTITUTION)
col.setSolverType(ST.DAMPED_SUBSTITUTION)
col.setSolverType(ST.INSIDE_OUT)
col.setSolverType(ST.MATRIX_INSIDE_OUT)
col.setSolverType(ST.SUM_RATES)
col.setSolverType(ST.NAPHTALI_SANDHOLM)
col.setSolverType(ST.MESH_RESIDUAL)
col.setSolverType(ST.AUTO)
Direct and Damped Substitution
Tray-by-tray bubble-point / dew-point sweeps with energy-balance damping between iterations. Direct substitution is the classic tray sweep; damped substitution is a more conservative variant for overshooting or oscillating cases.
Sum-Rates
Designed for absorbers and strippers where the energy balance is weak and the composition profile is set by mass transfer alone. Use it for amine contactors and TEG dehydrators.
Inside-Out
The industry workhorse. Decouples thermo (computed in an outer loop with fixed K and H models) from material/energy (inner loop solved as a tridiagonal system). Fast and stable for production columns; the recommended choice for refinery and gas-plant work.
Naphtali–Sandholm
Full simultaneous-correction Newton method on the MESH equations. The solver of choice for narrow-boiling separations (e.g. C3/C4 splitters) and near-azeotrope systems. Expensive per iteration but quadratically convergent near the solution.
The current solver list also includes MATRIX_INSIDE_OUT, WEGSTEIN, NEWTON, MESH_RESIDUAL, and AUTO. Use Chapter 16 when the solver choice itself is part of the engineering deliverable.
After a run, check the diagnostics:
col.run()
print(col.getLastIterationCount(),
col.getLastMassResidual(),
col.getLastEnergyResidual())
Residuals below 1e-4 (mass) and 1e-3 (energy) indicate a converged solution.
10.5 Feed Tray Optimisation
Feed location is a discrete optimisation problem nested inside the continuous column solver. NeqSim provides an automatic helper:
candidate = col.estimateFeedTrayNumber(feed)
print("Temperature-profile feed tray estimate:", candidate)
result = col.findOptimalTrayConfiguration(0.95, "ethane", True, 30)
print("Optimization result:", result)
For a hand-tuned design, the rule of thumb is: feed at the tray whose temperature in the operating profile matches the feed temperature. estimateFeedTrayNumber(feed) gives that quick estimate; the tray optimization methods search tray count and feed tray against a product specification.
10.6 Side Draws and Pumparounds
A side draw extracts a vapour or liquid stream from an internal tray:
col.setLiquidSideDrawFraction(8, 0.05)
draw = col.getSideDrawStream(8, J.process.equipment.distillation.DistillationColumn.SideDrawPhase.LIQUID)
A pumparound is a heat-removal loop:
pa = col.addLiquidPumparound("PA-1", 8, 6, 0.15, 10.0)
col.setMaxPumparoundIterations(12)
col.setPumparoundTolerance(1.0e-4)
Crude distillation units typically have three pumparounds (top, middle, bottom); the duties shape the vapour traffic and the kerosene/diesel/AGO yields.
10.7 Absorbers and Strippers
The same DistillationColumn class models contactors and regenerators by disabling the condenser and/or reboiler:
absorber = J.process.equipment.distillation.DistillationColumn(
"T-200", 15, False, False # no condenser, no reboiler
)
absorber.addFeedStream(lean_amine, 1) # lean amine to top
absorber.addFeedStream(sour_gas, 15) # sour gas to bottom
absorber.setTopPressure(70.0)
absorber.setBottomPressure(70.5)
absorber.setSolverType(ST.SUM_RATES)
absorber.run()
treated_gas = absorber.getGasOutStream()
rich_amine = absorber.getLiquidOutStream()
A regenerator is the same class with both condenser and reboiler enabled, operating at low pressure.
10.8 Convergence Pathologies
Columns fail in distinctive ways:
| Symptom | Cause | Fix |
|---|---|---|
| Reboiler duty very large | Too few stages or wrong feed tray | Increase stages, use estimateFeedTrayNumber() or tray optimization |
| Compositions oscillate near solution | Numerically stiff, possibly azeotrope | Switch to Naphtali–Sandholm |
| Temperature profile flat across many trays | Dry stages (no liquid traffic) | Reduce reflux or distillate rate |
| Condenser temperature pinned at feed T | Reflux too high; total condensation impossible | Loosen reflux specification |
| Inside-out diverges | Inner-loop blowup near critical | Switch to DAMPED_SUBSTITUTION or AUTO; tighten initial profile |
For stubborn columns, warm-start from a converged neighbour case: save the temperature and composition profiles, perturb the specification by 5%, re-run from the saved profile.
col.setSeedTemperature(3, 273.15 + 45.0)
col.setRelaxationFactor(0.5)
col.setMaxNumberOfIterations(200)
10.9 Internals Sizing
After the equilibrium-stage solution converges, sizing internals requires diameter, height, and tray type. NeqSim's DistillationColumn exposes hydraulic rating through calcColumnInternals(...):
col.setInternalDiameter(2.5) # m
designer = col.calcColumnInternals("sieve")
print("Internal diameter:", col.getInternalDiameter())
print("Total pressure drop Pa:", designer.getTotalPressureDrop())
For trays, target 70–80% of flood at the design rate. For packed columns, use the rate-based packed-column model or internals-rating helpers described in Chapter 16.
For mechanical design (shell thickness, weight, cost), the column's MechanicalDesign follows the same pattern as separators (Chapter 8):
col.initMechanicalDesign()
md = col.getMechanicalDesign()
md.setMaxOperationPressure(35.0)
md.calcDesign()
shell_mass_kg = md.getTotalWeight()
10.10 Worked Example: Deethaniser
from neqsim import jneqsim as J
fluid = J.thermo.system.SystemSrkEos(298.15, 28.0)
for c, x in {"methane":0.10, "ethane":0.30, "propane":0.30,
"n-butane":0.20, "n-pentane":0.10}.items():
fluid.addComponent(c, x)
fluid.setMixingRule("classic")
feed = J.process.equipment.stream.Stream("Feed", fluid)
feed.setFlowRate(10_000.0, "kg/hr")
feed.setTemperature(40.0, "C"); feed.setPressure(28.0, "bara")
T = J.process.equipment.distillation.DistillationColumn
col = T("T-100", 25, True, True)
col.addFeedStream(feed, 12)
col.setCondenserTemperature(263.15) # -10 °C
col.setReboilerTemperature(383.15) # 110 °C
col.setTopPressure(28.0)
col.setBottomPressure(28.5)
col.setCondenserRefluxRatio(3.0)
col.setTopProductFlowRate(3500.0, "kg/hr")
col.setSolverType(T.SolverType.INSIDE_OUT)
proc = J.process.processmodel.ProcessSystem()
proc.add(feed); proc.add(col)
proc.run()
print(f"Reboiler duty: {col.getReboilerDuty()/1e6:.2f} MW")
print(f"Condenser duty: {col.getCondenserDuty()/1e6:.2f} MW")
print(f"Top C2 mole %: {col.getGasOutStream().getFluid().getPhase(0)"
f".getComponent('ethane').getx() * 100:.2f}")
print(f"Bot C2 mole %: {col.getLiquidOutStream().getFluid().getPhase(0)"
f".getComponent('ethane').getx() * 100:.2f}")
10.11 Looking Ahead
Chapter 11 leaves steady state behind: the same equipment classes can be driven forward in time. Dynamic simulation answers questions that steady-state cannot — startup, shutdown, depressurisation, control-loop tuning, anti-surge response.
Exercises
- Exercise 10.1: Vary the deethaniser feed tray location between stage 5 and stage 20. Plot reboiler duty versus feed-tray location. How well does
estimateFeedTrayNumber()compare with the best tray found byfindOptimalTrayConfiguration()?
- Exercise 10.2: Re-run the same column with
DAMPED_SUBSTITUTION,INSIDE_OUT,MATRIX_INSIDE_OUT, andAUTO. Compare iteration counts, wall-clock time, and final residuals.
- Exercise 10.3: Build a 20-tray amine absorber (MDEA solvent, sour natural gas feed). Target 4 ppmv H2S in treated gas. Use
SUM_RATES.
Dynamic Process Simulation
Learning Objectives
After this chapter you will:
- Convert a steady-state
ProcessSysteminto a dynamic simulation withrunTransient. - Add measurement devices (PT, TT, LT, FT) and PID controllers, and tune them.
- Model depressurisation and surge events.
- Read and store time-series results for downstream analysis.
11.0 Process Engineer's One-Pager — A 60-Second Blowdown
A minimum-viable depressurisation: a closed gas-filled vessel vents through a fixed orifice for one minute. The cell saves the pressure trace to a list so it can be plotted immediately.
from running_case import build_running_fluid
from neqsim import jneqsim as J
fluid = build_running_fluid("srk")
feed = J.process.equipment.stream.Stream("V-feed", fluid)
feed.setFlowRate(1.0, "kg/hr") # placeholder; vessel is closed
feed.setTemperature(25.0, "C")
feed.setPressure(85.0, "bara")
vessel = J.process.equipment.separator.Separator("V-100", feed)
vessel.setCalculateSteadyState(False)
vessel.setInternalDiameter(2.0); vessel.setSeparatorLength(6.0)
vessel.setLiquidLevel(0.0)
vent = J.process.equipment.valve.ThrottlingValve(
"BDV-100", vessel.getGasOutStream())
vent.setOutletPressure(1.0, "bara")
vent.setPercentValveOpening(100.0)
vent.setCv(50.0)
proc = J.process.processmodel.ProcessSystem()
for u in (feed, vessel, vent): proc.add(u)
proc.run() # steady-state init
trace = []
for step in range(60):
proc.runTransient(timestep_s=1.0, simulation_time_s=1.0)
trace.append((step + 1, vessel.getPressure("bara"),
vessel.getFluid().getTemperature("C")))
print("t[s] P[bara] T[°C]")
for row in trace[::10]:
print(f"{row[0]:5d} {row[1]:7.2f} {row[2]:6.2f}")
The MDMT check — comparing end-of-blowdown temperature to vessel design minimum metal temperature — is one extra line at the end. See §11.6 and the skill neqsim-depressurization-mdmt for API 521 rigour.
11.1 What Dynamic Means in NeqSim
Steady-state simulation answers where is the plant now; dynamic simulation answers how does it get from one state to another. The same equipment classes — separator, compressor, valve — are reused, but each now has volume (hold-up) and inertia (rotating mass). The system state evolves by integrating mass, energy, and momentum balances over time.
Dynamic simulation is invoked by calling runTransient instead of run:
proc.run() # steady-state initialisation
proc.runTransient(timestep_s=1.0, simulation_time_s=600.0)
The steady-state run sets the initial condition; the transient run integrates from there.
11.2 Dynamic-Aware Equipment
Equipment classes that have a meaningful dynamic mode override the default (which is to assume infinitely fast response). The most common:
| Equipment | Dynamic state | Typical setter |
|---|---|---|
| Separator | Liquid level, pressure | setLiquidVolume(m3), setGasVolume(m3) |
| Pipe | Pressure profile, holdup | setVolume(m3) |
| Compressor | Speed (RPM) | uses chart inertia |
| Valve | Cv, opening | setCv(.), setPercentValveOpening(0–100) |
To enable dynamic mode on a piece of equipment:
sep.setCalculateSteadyState(False)
sep.setLiquidLevel(0.5) # 50% level at t=0
sep.setInternalDiameter(2.0); sep.setSeparatorLength(6.0)
Without these volumetric parameters, the dynamic solver cannot integrate hold-up and the unit reverts to quasi-steady behaviour.
11.3 Measurement Devices
A measurement device samples a variable from a stream or equipment and publishes it as a tag value. Available types in neqsim.process.measurementdevice:
| Class | Measures |
|---|---|
PressureTransmitter |
Pressure of a stream |
TemperatureTransmitter |
Temperature of a stream |
LevelTransmitter |
Liquid level in a separator |
FlowTransmitter |
Mass or volumetric flow of a stream |
from neqsim import jneqsim as J
PT = J.process.measurementdevice.PressureTransmitter
LT = J.process.measurementdevice.LevelTransmitter
p_top = PT("PT-101", scrubber.getGasOutStream())
p_top.setUnit("bara")
l_sep = LT("LT-101", scrubber)
l_sep.setUnit("m")
proc.add(p_top); proc.add(l_sep)
After each timestep, p_top.getMeasuredValue() returns the latest reading. Measurement devices can have noise, lag, and span limits set — useful for testing how a controller copes with realistic instrumentation.
11.4 PID Controllers
NeqSim's controllers live in neqsim.process.controllerdevice. A PID controller takes a measurement device as input, computes a control action, and writes that action to a controlled equipment parameter.
PID = J.process.controllerdevice.ControllerDeviceBaseClass
pc = PID("PC-101")
pc.setMeasuredVariable(p_top) # the measurement
pc.setControllerSetPoint(50.0) # bara
pc.setControllerParameters(Kp=2.0, Ti=60.0, Td=0.0)
pc.setReverseActing(False)
# Wire the controller output to a valve
pv = J.process.equipment.valve.ThrottlingValve("PV-101", feed)
pv.addController("PC-101", pc)
Controllers respect Kp, Ti (integral time, seconds), Td (derivative time, seconds). The action is applied each timestep during runTransient.
Multiple controllers can be attached to a single piece of equipment with distinct tag names — a valve with both pressure and flow controllers, for instance, with a selector layer. This was Chapter 1's "named controllers" feature.
11.5 Tuning
Three approaches in increasing sophistication:
- Ziegler–Nichols — close the loop, increase
Kpuntil oscillation, read ultimate gainKuand periodTu, setKp = 0.6 Ku,Ti = Tu/2,Td = Tu/8. - IMC-based — pick a desired closed-loop time constant
τ_c, derive PID gains from the open-loop step response. - NeqSim's
ControllerTuningStudy— automated objective-function minimisation overKp/Ti/Tdfor a defined setpoint or disturbance response.
tuner = J.process.controllerdevice.ControllerTuningStudy(pc, proc)
tuner.setSetpointStep(50.0, 55.0, time_s=120.0)
tuner.setSimulationDuration(600.0)
tuner.setObjective("IAE") # integrated absolute error
result = tuner.run()
print(f"Tuned Kp={result.getKp():.2f} Ti={result.getTi():.1f} Td={result.getTd():.2f}")
The study runs many transient simulations under the hood — expensive but hands-off.
11.6 Depressurisation
Vessel depressurisation (blowdown) is the canonical dynamic safety study: open a valve to the flare, simulate temperature and pressure versus time for, say, 15 minutes, check minimum design metal temperature and end-state pressure.
NeqSim provides a high-level helper for the common API 521 §5.20 case:
DS = J.process.safety.depressurization.DepressurizationSimulator
sim = DS()
sim.setVessel(vessel_volume_m3=15.0,
vessel_metal_mass_kg=8500.0,
initial_pressure_bara=85.0,
initial_temperature_K=308.15,
fluid=fluid)
sim.setValveCv(85.0)
sim.setBackPressure(1.5)
sim.setSimulationTime(900.0) # 15 min
sim.setTimeStep(1.0)
sim.run()
t = list(sim.getTimes())
P = list(sim.getPressures())
Tg = list(sim.getGasTemperatures())
Tw = list(sim.getWallTemperatures())
The wall temperature feeds into MDMT checks; see the neqsim-depressurization-mdmt skill for the standards path.
For more complex networks — multiple vessels feeding a common flare header — build the dynamic flowsheet with Separator + SafetyValve + Recycle-free topology and run proc.runTransient() directly.
11.7 Surge and Anti-Surge
A compressor surge event is fast (sub-second), nonlinear, and dangerous. NeqSim's surge model interpolates the compressor chart, detects operation crossing the surge line, and triggers the configured response (anti-surge valve open).
asv = J.process.equipment.valve.ThrottlingValve("ASV-101",
compressor.getOutStream())
asv.setOutletPressure(compressor.getInletStream().getPressure("bara"))
asv.setCv(50.0)
asv.setPercentValveOpening(0.0) # initially closed
ascr = J.process.controllerdevice.AntiSurgeController("ASC-101")
ascr.setCompressor(compressor)
ascr.setSurgeMarginTarget(0.10) # 10%
ascr.setMeasuredVariable(flow_meas)
asv.addController("ASC-101", ascr)
In transient simulation, a feed flow disturbance pushes the compressor toward the surge line; the controller opens the ASV; flow stabilises in the safe region.
11.8 Saving Time-Series Results
For every dynamic run, capture the relevant tags into arrays or a dataframe:
import pandas as pd
rows = []
t = 0.0; dt = 1.0
while t < 600.0:
proc.runTransient(timestep_s=dt, simulation_time_s=dt)
rows.append({
"t": t,
"P": p_top.getMeasuredValue(),
"L": l_sep.getMeasuredValue(),
"Open": pv.getPercentValveOpening(),
})
t += dt
df = pd.DataFrame(rows)
df.to_csv("transient.csv", index=False)
For long runs, push the loop into Java by calling runTransient once with the full simulation time — but at the cost of less flexibility for event-driven analysis (alarms, trips, operator actions).
11.9 Worked Example: Separator Level Control
from neqsim import jneqsim as J
# Build steady-state model (HP separator + outlet valve)
# ... omitted; see Chapter 9 ...
# Enable dynamics
sep.setCalculateSteadyState(False)
sep.setInternalDiameter(2.0); sep.setSeparatorLength(6.0)
sep.setLiquidLevel(0.50) # 50% level
LT = J.process.measurementdevice.LevelTransmitter
lt = LT("LT-101", sep); lt.setUnit("m")
PID = J.process.controllerdevice.ControllerDeviceBaseClass
lc = PID("LC-101")
lc.setMeasuredVariable(lt)
lc.setControllerSetPoint(3.0) # 50% of 6 m vessel
lc.setControllerParameters(Kp=5.0, Ti=120.0, Td=0.0)
lv = J.process.equipment.valve.ThrottlingValve("LV-101", sep.getOilOutStream())
lv.setCv(40.0); lv.setPercentValveOpening(50.0)
lv.addController("LC-101", lc)
proc.add(lt); proc.add(lv)
# Steady-state init
proc.run()
# Disturbance: step up inlet flow by 20%
feed.setFlowRate(feed.getFlowRate("kg/hr") * 1.20, "kg/hr")
# Transient
proc.runTransient(timestep_s=1.0, simulation_time_s=900.0)
Plot lt.getMeasuredValue() and lv.getPercentValveOpening() over time to see the response. Tune Kp and Ti until the overshoot and settling time meet operability requirements.
11.10 Looking Ahead
Chapter 12 introduces the automation API — the bridge between a NeqSim model and the outside world (operators, optimisers, data historians). Once a model is parameterised by tag, the same model can be driven by plant data, used for what-if studies, or wrapped behind a REST service.
Exercises
- Exercise 11.1: Tune the level controller in §11.9 to achieve <5% overshoot and <300 s settling time for a 20% step disturbance.
- Exercise 11.2: Simulate a 5-minute blowdown of an 8 m³ scrubber from 80 bara through a Cv=60 valve. Find the minimum wall temperature and the time to reach 7 bara.
- Exercise 11.3: Add anti-surge control to a centrifugal compressor in a recompression train. Step the feed flow down by 40% and verify the anti-surge valve opens before the operating point crosses surge.
Part IV: Industrial Integration
The Automation API and Plant Data
Learning Objectives
After this chapter you will:
- Discover units and variables in any
ProcessSystemusingProcessAutomationwithout walking Java internals. - Read and write simulation variables using human-readable tag addresses, with optional unit conversion.
- Use the self-healing accessors (
getVariableValueSafe,setVariableValueSafe) to recover from typos and out-of-range values. - Save, load, and diff model state with
ProcessSystemStateandProcessModelState. - Connect a model to plant historian data via
tagreader.
12.1 Why Automation
By Chapter 11, models have grown to dozens of equipment objects. Driving a model from outside — a SciPy optimiser, a plant historian, a REST client — by writing proc.getUnit("V-101").getGasOutStream().getTemperature("C") is brittle. Names change, types change, getters get renamed in new versions.
The automation API solves three problems at once:
- Discovery. What equipment exists? What variables does it expose? Which are inputs, which are outputs?
- Addressing. A single string
"V-101.gasOutStream.temperature"identifies a variable. The same string works in JSON, REST, and CLI. - Self-healing. A typo like
"v101.temperature"gets corrected to"V-101.temperature"and the operation succeeds with a diagnostic.
The facade is ProcessAutomation. Get one with proc.getAutomation() (or plant.getAutomation() for a ProcessModel).
12.2 Discovery
from neqsim import jneqsim as J
auto = proc.getAutomation()
# What equipment exists?
for name in auto.getUnitList():
eq_type = auto.getEquipmentType(name)
print(f"{name} ({eq_type})")
# What variables on a unit?
for v in auto.getVariableList("V-101"):
print(f" {v.getAddress():40s} "
f"type={v.getType()} unit={v.getUnit()} "
f"desc={v.getDescription()}")
SimulationVariable describes each variable: address, INPUT vs OUTPUT, unit, and a human description. INPUT variables can be set; OUTPUT variables can only be read.
12.3 Reading and Writing
# Read with unit conversion
t = auto.getVariableValue("V-101.gasOutStream.temperature", "C")
p = auto.getVariableValue("V-101.pressure", "bara")
flow = auto.getVariableValue("V-101.gasOutStream.flowRate", "kg/hr")
# Write (INPUT-only)
auto.setVariableValue("K-101.outletPressure", 150.0, "bara")
proc.run() # propagate the change
The convention Unit.stream.property resolves dot-notation through the equipment graph. For variables of the equipment itself (not a stream), use Unit.property.
12.4 Multi-Area Plants
For ProcessModel, prefix the address with the area name:
plant_auto = plant.getAutomation()
t = plant_auto.getVariableValue("Separation::V-101.gasOutStream.temperature", "C")
plant_auto.setVariableValue("Compression::K-101.outletPressure", 170.0, "bara")
plant.run()
getAreaList() returns the registered area names; tags are disambiguated by the Area:: prefix.
12.5 Self-Healing Accessors
Production usage is messy. Tags get mistyped, units get omitted, values fall outside physical bounds. The "safe" accessors return JSON diagnostics instead of throwing:
import json
result_json = auto.getVariableValueSafe("hp separator.temperature", "C")
result = json.loads(result_json)
if result["status"] == "auto_corrected":
print(f"Original: {result['originalAddress']}")
print(f"Corrected: {result['correctedAddress']}")
print(f"Value: {result['value']} {result['unit']}")
elif result["status"] == "ok":
print(f"Value: {result['value']}")
elif result["status"] == "error":
print(f"Error: {result['message']}")
print(f"Suggestions: {result['suggestions']}")
Self-healing capabilities:
- Fuzzy address matching — edit distance ≤ 2 finds the right unit.
- Case and whitespace normalisation —
"hp separator"→"HP Sep". - Learned corrections — once a correction succeeds, it is cached; subsequent identical typos resolve without a search.
- Physical bounds —
setVariableValueSafechecks ranges before writing (e.g. polytropic efficiency must be in [0, 1]).
For diagnostics and learning insights:
diag = auto.getDiagnostics()
print(diag.getLearningReport())
# Lists successful/failed operations, common errors, learned corrections
12.6 Use Cases
The automation API turns a model into a building block for higher-level tools.
Optimisation
from scipy.optimize import minimize
def objective(x):
auto.setVariableValue("K-101.outletPressure", x[0], "bara")
auto.setVariableValue("E-101.outTemperature", x[1], "C")
proc.run()
power = auto.getVariableValue("K-101.power", "W")
duty = auto.getVariableValue("E-101.duty", "W")
return power + 3.0 * abs(duty) # weighted utility cost
res = minimize(objective, x0=[120.0, 40.0],
bounds=[(80, 160), (30, 50)])
The same auto.setVariableValue and proc.run calls would have required custom Java introspection in earlier chapters.
Plant data integration
import tagreader
client = tagreader.IMSClient("server", imstype="piwebapi")
client.connect()
mapping = {
"13TT001": "V-101.feedStream.temperature",
"13PT001": "V-101.pressure",
"13FT001": "V-101.feedStream.flowRate",
}
data = client.read(list(mapping.keys()), start_time="-1h", end_time="now",
ts=60, read_type=tagreader.ReaderType.SNAPSHOT)
for tag, address in mapping.items():
value = float(data[tag].iloc[-1])
auto.setVariableValueSafe(address, value, "default")
proc.run()
The model now reflects the latest plant state. Compare model outputs to plant tags for digital-twin validation; rerun under perturbed conditions for what-if analysis.
12.7 Lifecycle State
Models have versions, and versions need to be saved, diffed, and shared. NeqSim provides ProcessSystemState and ProcessModelState — portable JSON snapshots that capture every parameter needed to reconstruct a model.
Saving
PSS = J.process.processmodel.lifecycle.ProcessSystemState
state = PSS.fromProcessSystem(proc)
state.setName("Generic integrated base case")
state.setVersion("1.0.0")
state.setAuthor("J. Eng")
state.saveToFile("generic_integrated_v1.json")
The JSON is human-readable and Git-friendly:
{
"version": "1.0.0",
"name": "Generic integrated base case",
"equipment": [
{"name":"V-101","type":"ThreePhaseSeparator","pressure":70.0,...},
...
],
"streams": [...]
}
Loading and validating
loaded = PSS.loadFromFile("generic_integrated_v1.json")
result = loaded.validate()
assert result.isValid(), result.getErrors()
proc_reconstructed = loaded.toProcessSystem()
proc_reconstructed.run()
Comparing versions
v2 = PSS.fromProcessSystem(updated_proc)
v2.setVersion("2.0.0")
v2.saveToFile("generic_integrated_v2.json")
PMS = J.process.processmodel.lifecycle.ProcessModelState
diff = PMS.compare(loaded, v2)
for p in diff.getModifiedParameters():
print(f"{p.getEquipment()}.{p.getParameter()}: "
f"{p.getOldValue()} -> {p.getNewValue()}")
for added in diff.getAddedEquipment(): print("ADD:", added)
for removed in diff.getRemovedEquipment(): print("RM :", removed)
ModelDiff is the audit trail every change-management process wants: every modified parameter, every added or removed unit, attributed by version label.
Compressed transfer
For network transfer (REST APIs, message queues), use compressed bytes:
import base64
blob = bytes(state.toCompressedBytes())
encoded = base64.b64encode(blob).decode() # for JSON-over-HTTP
restored = PSS.fromCompressedBytes(blob)
.neqsim archives and .xip review packages
For large integrated models, use two complementary save formats. The .neqsim archive preserves the restartable Java object graph. The lifecycle JSON is lighter, reviewable in Git, and useful for comparing cases. A .xip package is not a separate NeqSim file format; it is a project convention for a zip-compatible review bundle that keeps results.json, the .neqsim archive, the state JSON, and selected reports together.
The official NeqSim documentation gives the API details in the process serialization guide: https://equinor.github.io/neqsim/simulation/process_serialization.html.
from pathlib import Path
import json
import zipfile
PMState = J.process.processmodel.lifecycle.ProcessModelState
case_dir = Path("case_archive")
case_dir.mkdir(exist_ok=True)
plant.run()
results_path = case_dir / "results.json"
with results_path.open("w", encoding="utf-8") as handle:
json.dump({"validation": {"acceptance_criteria_met": True}}, handle, indent=2)
neqsim_path = case_dir / "integrated_process_model.neqsim"
plant.saveToNeqsim(str(neqsim_path))
state_path = case_dir / "integrated_process_model_state.json"
state = PMState.fromProcessModel(plant)
state.saveToFile(str(state_path))
xip_path = case_dir / "integrated_process_model.xip"
with zipfile.ZipFile(xip_path, "w", compression=zipfile.ZIP_DEFLATED) as package:
package.write(results_path, arcname="results.json")
package.write(neqsim_path, arcname="model/integrated_process_model.neqsim")
package.write(state_path, arcname="model/integrated_process_model_state.json")
12.8 Convenience: Bridge Methods
For everyday Python work, the automation API is more verbose than direct getters. The convention this book recommends:
- Direct Python (
proc.getUnit("V-101").getGasOutStream().getTemperature("C")) for notebooks the engineer writes by hand. - Automation API (
auto.getVariableValue("V-101.gasOutStream.temperature", "C")) for code consumed by services, optimisers, or AI agents.
Both are first-class. Mixing is fine.
12.9 Worked Example: Driving a Model from JSON
A common pattern: receive a JSON state from a REST client, run a simulation, return outputs by tag.
import json
from neqsim import jneqsim as J
def run_what_if(base_state_path: str, overrides: dict, outputs: list[str]):
PSS = J.process.processmodel.lifecycle.ProcessSystemState
state = PSS.loadFromFile(base_state_path)
proc = state.toProcessSystem()
auto = proc.getAutomation()
for addr, (value, unit) in overrides.items():
auto.setVariableValueSafe(addr, float(value), unit)
proc.run()
return {addr: auto.getVariableValue(addr, "default") for addr in outputs}
result = run_what_if(
"generic_integrated_v1.json",
overrides={
"K-101.outletPressure": (165.0, "bara"),
"E-101.outTemperature": (35.0, "C"),
},
outputs=[
"K-101.power",
"V-103.oilOutStream.flowRate",
"V-103.gasOutStream.flowRate",
],
)
print(json.dumps(result, indent=2))
This is the kernel of a REST service. Chapter 14 wraps it in FastAPI.
12.10 Looking Ahead
Chapter 13 covers reporting and visualisation — taking the structured outputs of ProcessSystem and ProcessAutomation and turning them into plots, tables, and engineering reports the rest of the organisation can use.
Exercises
- Exercise 12.1: Use the automation API to scan every unit in your HP/MP/LP model and produce a one-line summary per unit (name, type, key input, key output) with units.
- Exercise 12.2: Save two versions of the same model with one parameter changed; use
ModelDiffto find the change automatically.
- Exercise 12.3: Wrap the
run_what_iffunction above in a CLI that takes JSON overrides on stdin and prints JSON outputs on stdout.
Reporting and Visualization
Learning Objectives
After this chapter you will:
- Produce publication-quality plots of process results with matplotlib and Plotly.
- Visualise phase envelopes, T–s diagrams, composition profiles, and transient responses.
- Generate engineering deliverables: heat & material balance tables, instrument schedules, results.json snapshots.
- Use the professional reporting toolchain to assemble Word/HTML reports from a NeqSim model.
13.1 Why Reporting Matters
A simulation that never leaves the engineer's notebook has no impact. Reporting is the bridge between simulation and decision: the colleagues who size the equipment, the operators who run the plant, and the management who approve the budget all need outputs in formats they can consume — figures, tables, narrative, and traceable references.
NeqSim provides three layers of reporting infrastructure:
- Low-level plotting — matplotlib and Plotly hooks directly off
SystemInterfaceandProcessSystemoutputs. - Structured snapshots —
ProcessSystemStateJSON (Chapter 12) andresults.jsonfor tasks (this book's task workflow). - Professional reporting — the
professional-reportingtoolchain (templates, generators, results-driven assembly).
13.2 Phase Envelopes
The canonical thermodynamic plot — pressure vs temperature with bubble and dew curves.
from neqsim import jneqsim as J
import numpy as np, matplotlib.pyplot as plt
ops = J.thermodynamicoperations.ThermodynamicOperations(fluid)
env = ops.calcPTphaseEnvelope(True, 1.0)
# Branch label trap: classify by max temperature
branch_A_T = np.asarray(env.getBubblePointTemperatures()) - 273.15
branch_A_P = np.asarray(env.getBubblePointPressures())
branch_B_T = np.asarray(env.getDewPointTemperatures()) - 273.15
branch_B_P = np.asarray(env.getDewPointPressures())
if branch_A_T.max() > branch_B_T.max():
dew_T, dew_P = branch_A_T, branch_A_P
bub_T, bub_P = branch_B_T, branch_B_P
else:
dew_T, dew_P = branch_B_T, branch_B_P
bub_T, bub_P = branch_A_T, branch_A_P
fig, ax = plt.subplots(figsize=(7, 5))
ax.plot(bub_T, bub_P, label="Bubble curve")
ax.plot(dew_T, dew_P, label="Dew curve")
ax.scatter([env.get("cricondenTemperature") - 273.15],
[env.get("cricondenPressure")], marker="*", s=80,
label="Cricondentherm")
ax.set_xlabel("Temperature [°C]"); ax.set_ylabel("Pressure [bara]")
ax.set_title("Phase envelope — reservoir gas"); ax.legend(); ax.grid(True)
fig.savefig("envelope.png", dpi=150, bbox_inches="tight")
Every phase-envelope figure should label the cricondentherm and cricondenbar; for transport studies, overlay the operating envelope (pipeline P–T trajectory) on top of the phase envelope to show hydrate or two-phase exposure.
13.3 Composition and Profile Plots
For columns, the composition profile per tray:
import pandas as pd
stages = list(range(col.getNumberOfStages() + 2))
profile = []
for s in stages:
phase = col.getFluid(s).getPhase(0)
row = {"stage": s, "T_C": col.getStageTemperature(s) - 273.15}
for c in ["methane", "ethane", "propane", "n-butane"]:
row[c] = phase.getComponent(c).getx()
profile.append(row)
df = pd.DataFrame(profile)
ax = df.plot(x="T_C", y=["methane", "ethane", "propane", "n-butane"],
figsize=(7, 5))
ax.set_xlabel("Stage temperature [°C]"); ax.set_ylabel("Liquid mole fraction")
ax.set_title("Deethaniser composition profile"); ax.grid(True)
For pipelines, the pressure–distance profile:
xs = np.linspace(0, pipe.getLength(), pipe.getNumberOfIncrements() + 1)
Ps = np.asarray(pipe.getPressureProfile())
Ts = np.asarray(pipe.getTemperatureProfile()) - 273.15
fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True, figsize=(8, 6))
ax1.plot(xs / 1000, Ps); ax1.set_ylabel("Pressure [bara]"); ax1.grid(True)
ax2.plot(xs / 1000, Ts); ax2.set_ylabel("Temperature [°C]")
ax2.set_xlabel("Distance [km]"); ax2.grid(True)
fig.suptitle("Subsea flowline profile")
13.4 Transient Plots
For dynamic results, a four-panel time-series figure is conventional: pressure, temperature, level, valve opening.
fig, axes = plt.subplots(2, 2, figsize=(10, 6), sharex=True)
axes[0,0].plot(df["t"]/60, df["P"]); axes[0,0].set_ylabel("P [bara]")
axes[0,1].plot(df["t"]/60, df["T"]); axes[0,1].set_ylabel("T [°C]")
axes[1,0].plot(df["t"]/60, df["L"]); axes[1,0].set_ylabel("Level [m]")
axes[1,1].plot(df["t"]/60, df["Open"]); axes[1,1].set_ylabel("Valve open [%]")
for ax in axes.flat:
ax.set_xlabel("Time [min]"); ax.grid(True)
13.5 Heat & Material Balance Tables
Every process design deliverable includes a stream table — one row per stream, columns for the standard properties.
def build_stream_table(proc):
rows = []
for s in proc.getAllStreams():
fl = s.getFluid()
rows.append({
"Stream": s.getName(),
"T [°C]": s.getTemperature("C"),
"P [bara]": s.getPressure("bara"),
"Flow [kg/h]": s.getFlowRate("kg/hr"),
"Vapour frac": fl.getNumberOfPhases() > 1 and fl.getPhase("gas").getMolarMass() / fl.getMolarMass() or "",
"ρ [kg/m³]": fl.getDensity("kg/m3"),
})
return pd.DataFrame(rows)
df = build_stream_table(proc)
df.to_csv("stream_table.csv", index=False)
df.to_html("stream_table.html", index=False, float_format="%.2f")
For NeqSim's engineering-deliverables toolchain (mechanicaldesign package), more elaborate tables — instrument schedules, line lists, spare parts inventories — are generated by dedicated classes (InstrumentScheduleGenerator, AlarmAndTripSchedule, ProcessFlowDiagramBuilder). These take a ProcessSystem and produce formatted Excel and Word outputs.
13.6 Interactive Visualisation with Plotly
For notebooks and dashboards, Plotly produces interactive HTML.
import plotly.graph_objects as go
fig = go.Figure()
fig.add_trace(go.Scatter(x=bub_T, y=bub_P, mode="lines", name="Bubble"))
fig.add_trace(go.Scatter(x=dew_T, y=dew_P, mode="lines", name="Dew"))
fig.add_trace(go.Scatter(x=[pipe_inlet_T, pipe_outlet_T],
y=[pipe_inlet_P, pipe_outlet_P],
mode="lines+markers", name="Operating",
line=dict(dash="dash", color="red")))
fig.update_layout(xaxis_title="Temperature [°C]",
yaxis_title="Pressure [bara]",
title="Phase envelope vs operating envelope")
fig.write_html("envelope_interactive.html")
Plotly figures embed cleanly in Jupyter, JupyterLab, and Quarto/MkDocs sites.
13.7 results.json Snapshots
The task workflow uses a single results.json per task as the canonical hand-off format. It carries everything a report needs:
import json
results = {
"key_results": {
"outlet_pressure_bara": 25.3,
"compressor_power_kW": 1248.5,
"minimum_approach_C": 5.2,
},
"validation": {
"mass_balance_error_pct": 0.003,
"energy_balance_error_pct": 0.012,
"acceptance_criteria_met": True,
},
"approach": "Two-stage centrifugal compression with intercooling...",
"conclusions": "The two-stage train meets the 25 bara delivery spec...",
"figure_captions": {
"envelope.png": "Phase envelope vs operating envelope",
"k_curve.png": "Compressor performance map with operating point",
},
"figure_discussion": [
{"figure": "envelope.png",
"observation": "Operating envelope stays in single-phase gas region",
"mechanism": "Inter-cooler T set above dew curve",
"implication": "No two-phase compression risk",
"recommendation": "Maintain inter-cooler T ≥ 30 °C"},
],
"references": [
{"id": "API617", "text": "API 617 9th Ed (2022). Axial and Centrifugal Compressors..."},
],
}
with open("results.json", "w") as f:
json.dump(results, f, indent=2)
The structured fields (key_results, validation, figure_discussion, references) are consumed by the professional-reporting toolchain to build a Word document automatically. Free-form text fields (approach, conclusions) round out the narrative.
Engineering Interpretation Blocks
The figure_discussion field should be written as a compact engineering argument, not as a caption restatement. Use four labels for every major figure or table:
| Label | Required content |
|---|---|
| Observation | The key trend or number with units |
| Mechanism | The physical reason or model assumption behind the trend |
| Implication | The consequence for design, operation, safety, or data quality |
| Recommendation | The next action and the evidence that would change it |
For the running case, an export-compressor plot might read: observation, power crosses 6 MW above 120 t/h; mechanism, required head rises at fixed outlet pressure while efficiency is fixed; implication, 120 t/h is the screening throughput limit; recommendation, add a compressor curve before treating the limit as design-quality.
13.8 The Professional Reporting Toolchain
NeqSim ships a Python reporting toolchain at professional-reporting/scripts/. The entry point is generate_report.py, which reads report_data.json (a wrapper around results.json plus metadata) and emits Word and HTML.
python professional-reporting/scripts/generate_report.py \
--input report_data.json \
--output Report.docx
The generator:
- Auto-builds the Results section from
key_results(with unit detection). - Auto-builds the Validation section from
validation(PASS/FAIL badges). - Auto-renders LaTeX equations to PNG in Word (KaTeX in HTML).
- Color-codes the risk register (5×5 ISO 31000 matrix).
- Numbers and captions every figure listed in
figure_captions. - Inserts a Discussion section from
figure_discussionblocks with traceability back tokey_resultsvialinked_results.
For a task, run consistency_checker.py first to catch numerical mismatches between notebooks and results.json before report generation.
13.8a Validation Recipes — Make the Numbers Defensible
A professional report is only as good as the validation that backs it. Four recipes cover almost every steady-state and dynamic study.
Recipe 1 — Mass Balance Closure
def mass_balance_error_pct(units_in, units_out):
m_in = sum(s.getFlowRate("kg/hr") for s in units_in)
m_out = sum(s.getFlowRate("kg/hr") for s in units_out)
return 100.0 * (m_in - m_out) / m_in
Acceptance: abs(err) < 0.01 % for any closed ProcessSystem. A larger error means a recycle did not converge or a stream is unaccounted for.
Recipe 2 — Energy Balance Closure
def energy_balance_error_pct(units_in, units_out, duties):
h_in = sum(s.getFluid().getEnthalpy() for s in units_in)
h_out = sum(s.getFluid().getEnthalpy() for s in units_out)
return 100.0 * (h_in + sum(duties) - h_out) / max(abs(h_in), 1.0)
duties is the signed sum of cooler, heater, and compressor work. The same < 0.05 % threshold applies to steady-state runs.
Recipe 3 — Cross-Check Against a Reference Simulator
For projects with legacy HYSYS / UniSim cases, the smallest defensible comparison is a four-row parity table:
| Stream / Duty | NeqSim | Reference | Deviation |
|---|---|---|---|
| Sales-gas flow [kg/hr] | |||
| LPG flow [kg/hr] | |||
| Reboiler duty [kW] | |||
| Compressor power [kW] |
Accept differences below 2 % without comment. Document anything between 2 % and 5 % (usually different EOS interaction parameters). Investigate anything above 5 %.
Recipe 4 — Plant Historian Comparison
For a calibrated model, pair each ProcessAutomation address with a historian tag and compute one row per pair:
for tag, address in tag_map.items():
measured = historian.get_value(tag, ts="2026-04-01 12:00")
simulated = auto.getVariableValue(address, units_for(tag))
err_pct = 100.0 * (simulated - measured) / measured if measured else None
rows.append((tag, measured, simulated, err_pct))
Keep the comparison rows in results.json under a plant_comparison key so the report's Validation section can render them automatically. See Chapter 12 and the skill neqsim-plant-data for the tagreader bridge.
13.9 P&ID-Style Diagrams
NeqSim provides DEXPI export via ProcessFlowDiagramBuilder. From Python:
PDB = J.process.diagram.ProcessFlowDiagramBuilder
builder = PDB(proc)
builder.setLayoutAlgorithm("hierarchical")
builder.exportSVG("flowsheet.svg")
builder.exportDEXPI("flowsheet.xml")
DEXPI is the open XML standard for P&ID exchange — useful for handing a NeqSim flowsheet to a P&ID tool like AVEVA Diagrams or Bentley OpenPlant.
For schematic-only diagrams (no full P&ID detail), networkx + graphviz on the stream graph gives a quick visual:
import networkx as nx
G = nx.DiGraph()
for u in proc.getUnitOperations():
G.add_node(u.getName(), type=type(u).__name__)
for u in proc.getUnitOperations():
for s in u.getOutletStreams():
for v in proc.getUnitOperations():
if s in v.getInletStreams():
G.add_edge(u.getName(), v.getName(), label=s.getName())
nx.drawing.nx_pydot.write_dot(G, "flowsheet.dot")
# render with: dot -Tpng flowsheet.dot -o flowsheet.png
13.10 Worked Example: A Compression Train Report
from neqsim import jneqsim as J
import json, matplotlib.pyplot as plt, pandas as pd
# ... build proc as in Chapter 8 two-stage compression ...
proc.run()
# Stream table
df = build_stream_table(proc)
df.to_csv("figures/stream_table.csv", index=False)
# Power figure
import numpy as np
stages = ["K-101", "K-102"]
powers = [proc.getUnit(s).getPower() / 1000.0 for s in stages]
fig, ax = plt.subplots()
ax.bar(stages, powers); ax.set_ylabel("Shaft power [kW]"); ax.grid(True)
fig.savefig("figures/power.png", dpi=150, bbox_inches="tight")
# results.json
results = {
"key_results": {
"total_power_kW": sum(powers),
"K-101_power_kW": powers[0],
"K-102_power_kW": powers[1],
"final_pressure_bara": proc.getUnit("V-102").getPressure("bara"),
},
"validation": {"acceptance_criteria_met": sum(powers) < 5000.0},
"approach": "Two-stage centrifugal compression with knock-out drums.",
"conclusions": f"Train delivers gas at {results['key_results']['final_pressure_bara']:.1f} bara with {results['key_results']['total_power_kW']:.0f} kW total.",
"figure_captions": {"power.png": "Shaft power per stage"},
}
with open("results.json", "w") as f:
json.dump(results, f, indent=2)
Pass to generate_report.py and a 4–6 page Word document drops out automatically.
13.11 Looking Ahead
Chapter 14 wraps these techniques behind APIs and services. A model in Python is one consumer; the same model behind FastAPI is consumable by thousands of clients — operators, optimisers, dashboards, AI agents.
Exercises
- Exercise 13.1: Generate a phase envelope figure and overlay your subsea flowline operating profile. Identify the maximum hydrate risk point (lowest T above the envelope) along the route.
- Exercise 13.2: Build a stream table for your HP/MP/LP separator train. Export to both CSV and HTML.
- Exercise 13.3: Write a
results.jsonfor a single-compressor case study and rungenerate_report.py. Inspect the output Word document.
Web APIs and Operations Runtime Interfaces
Learning Objectives
After this chapter you will:
- Wrap a NeqSim model behind a FastAPI REST service.
- Understand the architecture of NeqSimAPI and stateful operations-runtime interfaces.
- Containerise a NeqSim service with Docker.
- Apply caching, statelessness, and async patterns to scale NeqSim services.
- Decide when a service is the right abstraction (and when it is not).
14.1 Why Services
A Python script is consumed by one engineer. A REST service is consumed by everyone in the organisation — operators via dashboards, optimisers via batch jobs, AI agents via tool-use calls, external partners via HTTP. The service boundary turns NeqSim from a tool into infrastructure.
Two reference services demonstrate the patterns:
- NeqSimAPI (https://github.com/equinor/NeqSimAPI) — a public REST API for thermodynamic and PVT calculations.
- Operations-runtime NeqSim interface — a deeper, model-driven interface for full process simulations.
Both build on the principles below.
14.2 A Minimal FastAPI Service
The simplest meaningful service: given a fluid composition and (T, P), return density, viscosity, and phase fractions.
# app.py
from fastapi import FastAPI
from pydantic import BaseModel
from neqsim import jneqsim as J
import jpype
app = FastAPI(title="NeqSim Flash Service")
class FlashRequest(BaseModel):
components: dict[str, float] # name -> mole fraction
temperature_C: float
pressure_bara: float
eos: str = "SRK"
class FlashResponse(BaseModel):
density_kg_m3: float
viscosity_Pas: float
vapour_fraction: float
phases: list[str]
@app.post("/flash", response_model=FlashResponse)
def flash(req: FlashRequest):
Eos = {"SRK": J.thermo.system.SystemSrkEos,
"PR": J.thermo.system.SystemPrEos}[req.eos]
fluid = Eos(req.temperature_C + 273.15, req.pressure_bara)
for c, x in req.components.items():
fluid.addComponent(c, x)
fluid.setMixingRule("classic")
J.thermodynamicoperations.ThermodynamicOperations(fluid).TPflash()
fluid.initProperties()
return FlashResponse(
density_kg_m3 = fluid.getDensity("kg/m3"),
viscosity_Pas = fluid.getViscosity("kg/msec"),
vapour_fraction = fluid.getBeta(0) if fluid.hasPhaseType("gas") else 0.0,
phases = [fluid.getPhase(i).getPhaseTypeName()
for i in range(fluid.getNumberOfPhases())],
)
Run it:
uvicorn app:app --host 0.0.0.0 --port 8000
A POST to /flash returns JSON in tens of milliseconds.
14.3 The JVM Lifecycle Trap
The JVM is process-wide and started once. The pattern above relies on JPype starting a JVM lazily when neqsim is first imported — fine for a single worker. But under uvicorn --workers 4, each worker is a separate process, each starts its own JVM, and start-up takes 1–2 seconds.
Practical guidance:
- One worker per process. Use multiple processes for concurrency, not multiple JVMs per process.
- Warm up. Run a dummy flash on startup so the first user request doesn't pay JVM and class-loading cost.
- Don't restart the JVM. JPype's
shutdownJVM()is broken with some JVMs; once a JVM is started, leave it running until the process exits.
@app.on_event("startup")
def warmup():
fluid = J.thermo.system.SystemSrkEos(298.15, 10.0)
fluid.addComponent("methane", 1.0); fluid.setMixingRule("classic")
J.thermodynamicoperations.ThermodynamicOperations(fluid).TPflash()
14.4 Statelessness and Idempotency
Every NeqSim object holds state — composition, temperature, pressure, last-converged solution. In a service, sharing state across requests breaks isolation: request A's composition leaks into request B's result.
The rule: construct fresh NeqSim objects per request. This sounds expensive but isn't — a SystemSrkEos instantiation takes microseconds. What is expensive is the flash itself; cache that result if needed.
from functools import lru_cache
import hashlib, json
def fingerprint(req: FlashRequest) -> str:
payload = json.dumps(req.dict(), sort_keys=True)
return hashlib.sha256(payload.encode()).hexdigest()
_cache: dict[str, FlashResponse] = {}
@app.post("/flash", response_model=FlashResponse)
def flash(req: FlashRequest):
key = fingerprint(req)
if key in _cache:
return _cache[key]
response = _do_flash(req)
_cache[key] = response
return response
For production, use Redis instead of an in-process dict.
14.5 Heavy Endpoints: Async and Background Tasks
Some calculations are slow — column convergence, dynamic simulations, optimisations. Don't hold an HTTP connection for 30 seconds. Use background tasks and a job-id pattern:
from uuid import uuid4
from fastapi import BackgroundTasks
_jobs: dict[str, dict] = {}
@app.post("/simulate")
def submit(req: SimRequest, bg: BackgroundTasks):
jid = str(uuid4())
_jobs[jid] = {"status": "pending"}
bg.add_task(_run_sim, jid, req)
return {"job_id": jid}
@app.get("/jobs/{jid}")
def status(jid: str):
return _jobs.get(jid, {"status": "unknown"})
def _run_sim(jid: str, req: SimRequest):
_jobs[jid]["status"] = "running"
try:
result = expensive_sim(req)
_jobs[jid] = {"status": "done", "result": result}
except Exception as e:
_jobs[jid] = {"status": "failed", "error": str(e)}
For production, use a real job queue (Celery, RQ, Dramatiq) backed by Redis or RabbitMQ.
14.6 NeqSimAPI Architecture
NeqSimAPI exposes a curated set of thermodynamic calculations as REST endpoints. Its design choices are instructive:
- One endpoint per calculation type.
/flash/TP,/flash/PH,/properties/saturation,/phaseenvelope. Each endpoint maps to oneThermodynamicOperationsmethod. - Standard fluid model. The request schema is uniform across endpoints — components + composition + EOS + mixing rule. Reuse means consumers learn one model.
- JSON-first. No XML, no proprietary binary formats. Standard Pydantic schemas mean client SDKs in any language generate automatically from OpenAPI.
- Containerised. A single Docker image, deployed on Azure Container Apps. Stateless replicas scale horizontally.
When to use it: ad-hoc property lookups, integrating NeqSim into non-Python applications (Excel via Power Query, .NET clients, embedded controls), or as a sandbox before adopting NeqSim more deeply.
14.7 Operations-Runtime NeqSim Interface Architecture
Where NeqSimAPI is calculation-focused, an operations-runtime NeqSim interface is model-focused. Its job is to drive an existing NeqSim ProcessSystem from an operations digital-twin runtime.
The architectural shift from NeqSimAPI:
- Long-lived models. A
ProcessSystemis constructed once, hydrated fromProcessSystemStateJSON, and re-run as tags change. The cost of re-construction is amortised over hundreds of plant data refreshes. - Automation API on the wire. The REST surface mirrors
ProcessAutomation: discover units, read variables, write variables, run. The client is plant historian software that already has a tag abstraction. - State persistence.
ProcessSystemStatesnapshots are versioned in object storage; rollback to a previous model is just a state load. - Convergence is the API. Every "run" call returns a convergence diagnostic alongside the outputs.
The two services are complementary. NeqSimAPI for stateless calculations; an operations-runtime interface for stateful, model-driven twin operation.
14.8 Containerisation
Both reference services ship as Docker containers. A minimal Dockerfile for the FastAPI app above:
FROM python:3.11-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
openjdk-17-jre-headless && rm -rf /var/lib/apt/lists/*
ENV JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
EXPOSE 8000
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
requirements.txt:
fastapi==0.115.*
uvicorn[standard]==0.32.*
neqsim==3.10.*
JPype1==1.5.*
pydantic==2.*
Build and run:
docker build -t neqsim-flash .
docker run -p 8000:8000 neqsim-flash
For production, multi-stage builds reduce image size, and a distroless or Alpine base further trims it.
14.9 Scaling
For a service with N concurrent users, two scaling axes apply:
- Vertical (per replica). A replica is one process with one JVM. CPU-bound flashes scale with cores via async workers (
uvicorn --workers W). RAM is dominated by the JVM heap (default 512 MB is fine for thermodynamic work; raise to 2–4 GB for process simulations). - Horizontal (replicas). Stateless replicas behind a load balancer scale linearly with N. Container orchestrators (Kubernetes, Azure Container Apps) handle replica counts automatically.
For operations-runtime style stateful services, sticky sessions or external state stores are needed so the same model is hit for every tag-driven update.
14.10 Observability
Three signals for a NeqSim service:
- Convergence rate. What fraction of flashes / simulations converge? A drop indicates bad inputs or a regression.
- P50/P99 latency. Single flashes should be ≤ 50 ms; process simulations 1–10 s; column convergence 10–60 s. Set SLOs.
- JVM health. Heap usage, GC pause times. Long pauses (> 100 ms) indicate the heap is too small.
Standard tools — Prometheus + Grafana, OpenTelemetry — work unchanged. Wrap NeqSim calls in a decorator that emits histograms.
14.11 When Not to Build a Service
Services have overhead. Don't build one for:
- A single engineer's notebook — direct Python is faster and more flexible.
- One-off studies — script it.
- High-fidelity dynamic simulations needing ms response — keep them in-process.
Build a service when:
- Multiple consumers need the same calculation.
- The consumers are not Python.
- The calculation is expensive enough to warrant caching.
- The model is operational (digital twin).
14.12 Looking Ahead
Chapter 19 grounds everything in a generic integrated offshore process flowsheet, walked end-to-end.
Exercises
- Exercise 14.1: Extend the FastAPI app above with a
/envelopeendpoint that returns the phase envelope as JSON arrays.
- Exercise 14.2: Wrap your Chapter 9 HP/MP/LP train behind a REST endpoint that accepts the feed flow rate and returns the gas and oil product rates.
- Exercise 14.3: Add Redis-backed caching to a
/flashendpoint and benchmark cache-hit vs cache-miss latency.
Part V: Equipment Deep Dives
Compressor Calculations, Curves, and Anti-Surge Handling
Learning Objectives
After this chapter you will be able to:
- Build a compressor calculation from suction conditions, discharge pressure, flow rate, and efficiency assumptions.
- Distinguish isentropic efficiency, polytropic efficiency, polytropic head, shaft power, and discharge temperature in NeqSim outputs.
- Generate and attach compressor performance curves with speed, head, efficiency, surge, and stonewall limits.
- Interpret
getDistanceToSurge(), surge flow, stonewall margin, and speed range checks as operating-envelope indicators. - Model anti-surge handling as both a steady-state recycle path and a dynamic controller object with valve-position logic.
- Package compressor results in a way that a machinery engineer, process engineer, and control engineer can review together.
15.1 Process Engineer's One-Pager
The compressor workflow has two levels. First run the process compressor with a specified pressure rise and an efficiency. Then attach a performance chart and use it to check whether the operating point is inside the compressor map.
from running_case import build_running_fluid, build_running_feed
from neqsim import jneqsim as J
fluid = build_running_fluid("srk")
feed = build_running_feed(fluid, flow_kg_hr=50_000.0, name="K-101 suction")
feed.setTemperature(25.0, "C")
feed.setPressure(45.0, "bara")
k101 = J.process.equipment.compressor.Compressor("K-101", feed)
k101.setUsePolytropicCalc(True)
k101.setPolytropicEfficiency(0.78)
k101.setOutletPressure(95.0, "bara")
k101.setSpeed(8000.0)
process = J.process.processmodel.ProcessSystem()
process.add(feed)
process.add(k101)
process.run()
# Generate a map around the design point and activate it for future runs.
generator = J.process.equipment.compressor.CompressorChartGenerator(k101)
generator.setChartType("interpolate and extrapolate")
chart = generator.generateCompressorChart("normal curves", 5)
k101.setCompressorChart(chart)
k101.getCompressorChart().setUseCompressorChart(True)
k101.getAntiSurge().setActive(True)
k101.getAntiSurge().setSurgeControlFactor(1.10)
process.run()
print(f"Power: {k101.getPower('kW'):.1f} kW")
print(f"Discharge pressure: {k101.getOutletStream().getPressure('bara'):.1f} bara")
print(f"Discharge temp.: {k101.getOutletStream().getTemperature('C'):.1f} C")
print(f"Speed: {k101.getSpeed():.0f} rpm")
print(f"Polytropic head: {k101.getPolytropicHead('kJ/kg'):.2f} kJ/kg")
print(f"Distance to surge: {100.0 * k101.getDistanceToSurge():.1f} %")
print(f"Surge flow: {k101.getSurgeFlowRate():.1f} m3/hr")
Read this cell as a compressor screening model, not as a vendor guarantee. The chart is generated from the calculated design point, so it is useful for teaching and early envelope work. A real project should replace the generated curves with vendor map data, corrected-flow conventions, anti-surge valve Cv, recycle piping pressure drop, and driver limits.
15.2 Why Compressors Deserve Their Own Chapter
A compressor is a pressure-raising unit operation, but that phrase hides the engineering work. A process model must calculate discharge temperature and power. A machinery model must check speed, head, efficiency, surge, stonewall, driver power, and mechanical limits. A controls model must keep the operating point away from surge during startup, shutdown, turndown, recycle, trips, and large upstream disturbances.
Those views overlap but they are not the same. A shortcut compressor with a fixed efficiency can answer: "How much power is needed for this pressure rise?" A compressor with curves can answer: "At this inlet flow and gas density, can this machine make that head at an allowed speed?" An anti-surge model asks: "If the process flow falls, how much recycle must open before the operating point crosses the surge control line?"
For process engineers, the safe pattern is:
- Solve the thermodynamics and power balance with simple specifications.
- Add chart data and check the operating envelope.
- Add recycle and anti-surge handling for turndown or transient studies.
- Add driver, mechanical, and instrumentation details when the calculation is used for design or operations.
Skipping the first step makes the map hard to debug. Skipping the second step makes the result look precise while ignoring the machine envelope.
15.3 The Calculation Basis
For a steady adiabatic compressor, the process simulator is solving the steady-flow energy balance:
$$ \dot W = \dot m (h_2 - h_1) $$
where $h_1$ and $h_2$ are specific enthalpies. The hard part is determining $h_2$. In a simple isentropic calculation, NeqSim finds the ideal outlet state at the target pressure and inlet entropy, then divides the ideal enthalpy rise by the isentropic efficiency:
$$ \eta_s = \frac{h_{2s} - h_1}{h_2 - h_1} $$
For centrifugal compressors, polytropic efficiency is often a better fit to vendor performance data because compression happens over many small stages:
$$ \eta_p = \frac{\text{ideal polytropic head}}{\text{actual polytropic head}} $$
NeqSim can run either style. Use setUsePolytropicCalc(True) and setPolytropicEfficiency(value) when you are matching a centrifugal compressor map. Use setIsentropicEfficiency(value) for simple screening or when the data source is explicitly isentropic.
The outputs worth logging every time are:
| Quantity | NeqSim call | Why it matters |
|---|---|---|
| Power | getPower("kW") |
Driver size, energy use, emissions |
| Discharge temperature | getOutletStream().getTemperature("C") |
Cooler duty, material limits, hydrate or liquid risk |
| Polytropic head | getPolytropicHead() |
Map ordinate and stage loading |
| Polytropic fluid head | getPolytropicFluidHead() |
Surge and stonewall calculations |
| Speed | getSpeed() |
Map interpolation and mechanical limit |
| Distance to surge | getDistanceToSurge() |
Turndown and anti-surge margin |
| Stonewall margin | getDistanceToStoneWall() |
High-flow/choke limit |
When any of these numbers are missing from a compressor study, the reviewer has to guess what was actually constrained.
15.4 Basic Compressor Setup
The minimal compressor needs an inlet stream, an outlet pressure, and an efficiency. In the direct Java API, temperatures in constructors are Kelvin and pressures are bara, while stream setters can carry explicit units.
from neqsim import jneqsim as J
fluid = J.thermo.system.SystemSrkEos(273.15 + 30.0, 40.0)
for component, amount in [("methane", 0.90), ("ethane", 0.07), ("propane", 0.03)]:
fluid.addComponent(component, amount)
fluid.setMixingRule("classic")
suction = J.process.equipment.stream.Stream("suction gas", fluid)
suction.setFlowRate(100_000.0, "kg/hr")
suction.setTemperature(30.0, "C")
suction.setPressure(40.0, "bara")
compressor = J.process.equipment.compressor.Compressor("K-200", suction)
compressor.setOutletPressure(100.0, "bara")
compressor.setUsePolytropicCalc(True)
compressor.setPolytropicEfficiency(0.76)
compressor.setSpeed(9000.0)
process = J.process.processmodel.ProcessSystem()
process.add(suction)
process.add(compressor)
process.run()
The model is intentionally small. Do not add curves, coolers, or recycles until this base calculation gives plausible discharge temperature and power. If the base case is wrong, the performance map only makes the wrong answer harder to understand.
15.5 Power, Head, and Temperature Checks
A compressor result should be checked three ways before being trusted.
First, compare the pressure ratio:
$$ PR = \frac{P_2}{P_1} $$
Single centrifugal casings often live in moderate pressure-ratio ranges. A very large ratio may be possible for special service, but it should trigger a check that the model is not silently representing a multi-casing train as one stage.
Second, check discharge temperature. High discharge temperature may require intercooling, lower pressure ratio per stage, different metallurgy, or a polytropic efficiency review. A gas compressor that appears feasible on pressure alone can fail a discharge-temperature limit.
Third, compare calculated power with the rough estimate:
$$ \dot W \approx \dot m C_p (T_2 - T_1) $$
This estimate is not the design method, but it is a useful sanity check. If the NeqSim power differs by an order of magnitude, inspect units before inspecting thermodynamics.
power_kW = compressor.getPower("kW")
outlet = compressor.getOutletStream()
ratio = outlet.getPressure("bara") / suction.getPressure("bara")
print(f"Pressure ratio: {ratio:.2f}")
print(f"Power: {power_kW:.1f} kW")
print(f"Discharge T: {outlet.getTemperature('C'):.1f} C")
15.6 Compressor Curves in NeqSim
A compressor curve relates inlet volumetric flow, speed, polytropic head, and polytropic efficiency. The map also carries boundary curves:
- Surge line: minimum stable flow for a given head or speed.
- Surge control line: surge line plus a safety offset used by anti-surge control.
- Stonewall/choke line: high-flow boundary where the compressor cannot add the required head.
- Speed limits: minimum and maximum allowed speed curves.
NeqSim represents this through CompressorChartInterface. A compressor has a chart object by default, but the chart affects the calculation only after setUseCompressorChart(True).
The generated-chart workflow is useful for study models:
generator = J.process.equipment.compressor.CompressorChartGenerator(compressor)
generator.setChartType("interpolate and extrapolate")
generator.enableAdvancedCorrections(3) # Reynolds, Mach, multistage surge correction
chart = generator.generateCompressorChart("normal curves", 7)
compressor.setCompressorChart(chart)
compressor.getCompressorChart().setUseCompressorChart(True)
process.run()
generateCompressorChart("normal curves", n) distributes speed curves around the compressor's current speed. For early studies this is a defensible way to explore map behavior. For design, replace it with vendor data.
15.7 Manual Vendor Curves
Vendor data usually arrives as corrected flow, polytropic head, efficiency, and speed. Before loading it into NeqSim, convert it to the flow and head basis used by the chart. In the current compressor chart interface, flow is read as inlet volumetric flow in m3/hr, speed is rpm, and the head unit is controlled by setHeadUnit(...).
chart = compressor.getCompressorChart()
chart.setHeadUnit("kJ/kg")
# One speed curve. Replace these values with vendor-map data on the same basis.
speed = 9000.0
flow = [2600.0, 3200.0, 3800.0, 4400.0, 5000.0]
head = [92.0, 89.0, 84.0, 76.0, 65.0]
eff = [70.0, 76.0, 80.0, 78.0, 70.0]
chart.addCurve(speed, flow, head, eff)
# Boundary curves normally come from vendor surge/choke maps.
chart.getSurgeCurve().setCurve([0.0], [2550.0, 3000.0, 3600.0], [92.0, 84.0, 70.0])
chart.getStoneWallCurve().setCurve([0.0], [5200.0, 5600.0, 6000.0], [90.0, 78.0, 60.0])
chart.setUseCompressorChart(True)
Do not mix vendor corrected flow with NeqSim actual inlet flow without an explicit conversion. A map that looks smooth but uses the wrong flow basis is a classic source of false surge margins.
15.8 Speed Solving
In normal process calculations the compressor speed is an input and the outlet pressure follows from the map. Sometimes the process engineer wants the reverse: find the speed required to hit a discharge pressure. NeqSim exposes this with setSolveSpeed(True).
compressor.setOutletPressure(110.0, "bara")
compressor.setSolveSpeed(True)
process.run()
print("Required speed:", compressor.getSpeed())
print("Within map range:", compressor.isSpeedWithinRange())
print("Above max speed:", compressor.isHigherThanMaxSpeed())
print("Below min speed:", compressor.isLowerThanMinSpeed())
Use speed solving for envelope studies, but add guardrails. A required speed outside the chart is a design finding, not a number to report as if the machine could deliver it.
15.9 Surge and Stonewall Interpretation
NeqSim's getDistanceToSurge() returns a fractional distance from the surge line. In practical terms:
- Positive value: current flow is above the surge line.
- Zero: current flow is on the surge line.
- Negative value: current flow is below the surge line, i.e. in surge region.
The value is a fraction. Multiply by 100 for percent margin. A result of 0.12 means roughly 12 percent flow margin to surge on the active chart basis.
margin = compressor.getDistanceToSurge()
if margin < 0.0:
status = "SURGE"
elif margin < 0.10:
status = "LOW_MARGIN"
else:
status = "OK"
print(status, 100.0 * margin)
print("Surge flow:", compressor.getSurgeFlowRate(), "m3/hr")
print("Distance to stonewall:", compressor.getDistanceToStoneWall())
A low surge margin does not automatically mean the steady-state flowsheet is wrong. It means the compressor cannot be treated as a passive pressure source. You need anti-surge handling, operating-envelope limits, or a different machine selection.
15.10 Anti-Surge Handling as Equipment Topology
In a steady-state process model, anti-surge handling is a recycle path. The usual topology is:
- Compressor discharge.
- Aftercooler and scrubber if present.
- Splitter or recycle valve branch.
- Anti-surge valve back to suction pressure.
- Recycle block closing the loop to the suction mixer.
mixer = J.process.equipment.mixer.Mixer("K-200 suction mixer")
mixer.addStream(fresh_feed)
mixer.addStream(recycle_guess)
compressor = J.process.equipment.compressor.Compressor("K-200", mixer.getOutletStream())
compressor.setOutletPressure(100.0, "bara")
splitter = J.process.equipment.splitter.Splitter("K-200 discharge splitter", compressor.getOutletStream())
splitter.setSplitFactors([0.15, 0.85]) # recycle, export
anti_surge_valve = J.process.equipment.valve.ThrottlingValve("K-200 anti-surge valve", splitter.getSplitStream(0))
anti_surge_valve.setOutletPressure(fresh_feed.getPressure("bara"), "bara")
recycle = J.process.equipment.util.Recycle("K-200 anti-surge recycle")
recycle.addStream(anti_surge_valve.getOutletStream())
recycle.setOutletStream(recycle_guess)
recycle.setTolerance(1e-3)
This is the model you need for steady-state turndown studies. You vary the split fraction or valve opening, run the process, and check whether the final compressor operating point is above the surge control line.
15.11 Anti-Surge Handling as Controller State
The compressor also owns an AntiSurge object. It stores controller settings, valve-position state, control strategy, warning margin, predictive horizon, and trip-cycle logic. This is useful for dynamic simulations or for exporting the compressor's protection configuration.
anti = compressor.getAntiSurge()
anti.setActive(True)
anti.setControlStrategy(J.process.equipment.compressor.AntiSurge.ControlStrategy.PID)
anti.setSurgeControlFactor(1.10)
anti.setSurgeControlLineOffset(0.10)
anti.setPIDParameters(2.0, 0.5, 0.1)
anti.setPIDSetpoint(0.12)
anti.setValveResponseTime(2.0)
anti.setValveRateLimit(0.5)
anti.setMinimumRecycleFlow(500.0)
anti.setMaximumRecycleFlow(15_000.0)
surge_margin = compressor.getDistanceToSurge()
valve_position = anti.updateController(surge_margin, 1.0)
recycle_flow = anti.getRecycleFlow(15_000.0)
print(valve_position, recycle_flow, anti.shouldTrip())
Do not confuse the two representations. The AntiSurge object tells you what the controller wants. The recycle topology tells you what the process actually does with recycled gas. A complete anti-surge study needs both.
15.12 Multi-Stage Compression and Intercooling
High pressure ratios are usually split across several compressor stages with intercoolers and scrubbers. The process reason is simple: cooling between stages reduces specific volume and power, while scrubbers protect the next impeller from condensed liquid.
A reusable pattern is:
stage1 = J.process.equipment.compressor.Compressor("K-101A", feed)
stage1.setOutletPressure(70.0, "bara")
stage1.setUsePolytropicCalc(True)
stage1.setPolytropicEfficiency(0.78)
cooler1 = J.process.equipment.heatexchanger.Cooler("E-101A", stage1.getOutletStream())
cooler1.setOutTemperature(30.0, "C")
scrubber1 = J.process.equipment.separator.Separator("V-101A", cooler1.getOutStream())
stage2 = J.process.equipment.compressor.Compressor("K-101B", scrubber1.getGasOutStream())
stage2.setOutletPressure(120.0, "bara")
stage2.setUsePolytropicCalc(True)
stage2.setPolytropicEfficiency(0.76)
Report per-stage pressure ratio, discharge temperature, cooler duty, scrubber liquid rate, speed, and surge margin. A total train power without these details is hard to review.
15.13 Curves, Optimization, and Constraints
Compressor maps become especially important in optimization. If the optimizer only sees power, it may reduce flow or increase discharge pressure into a region where the compressor is in surge or beyond maximum speed. Use the chart status as an explicit constraint.
A simple study table should include:
| Case | Flow | Pout | Speed | Power | Surge margin | Stonewall margin | Status |
|---|---|---|---|---|---|---|---|
| Base | kg/hr | bara | rpm | kW | % | % | OK / warning / infeasible |
For each candidate case:
process.run()
margin = compressor.getDistanceToSurge()
speed_ok = compressor.isSpeedWithinRange()
stonewall = compressor.getDistanceToStoneWall()
feasible = margin > 0.10 and speed_ok and stonewall > 0.05
This turns compressor curves from a plot into a calculation gate. It is the difference between optimizing a flowsheet and optimizing an impossible machine.
15.14 Molecular Weight and Composition Effects on Curves
Vendor compressor maps are measured at reference conditions. The reference fluid has a temperature, pressure, density, compressibility, and molecular weight. If the actual operating gas is heavier or lighter than the reference gas, the same physical impeller does not simply keep the same head and capacity. The map shifts because sound speed, density, Mach number, and corrected flow all change.
The official NeqSim curve documentation describes two ways to handle this when the gas composition varies.
The first is CompressorChartKhader2015. It converts the reference map to a dimensionless basis using sound-speed similarity, then regenerates real curves for the actual stream fluid. Use it when you have one reference map and need a physics-based correction for moderate changes in gas composition.
CompressorChartKhader2015 = J.process.equipment.compressor.CompressorChartKhader2015
impeller_diameter_m = 0.35
chart = CompressorChartKhader2015(compressor.getInletStream(), impeller_diameter_m)
chart_conditions = [25.0, 50.0, 50.0, 20.0] # T C, P bara, density kg/m3, MW g/mol
speeds = [9000.0, 10000.0, 11000.0]
flows = [[3200.0, 4000.0, 4800.0], [3500.0, 4400.0, 5300.0], [3800.0, 4800.0, 5800.0]]
heads = [[85.0, 78.0, 66.0], [105.0, 96.0, 82.0], [128.0, 118.0, 100.0]]
effs = [[75.0, 80.0, 77.0], [76.0, 82.0, 78.0], [75.0, 81.0, 77.0]]
chart.setCurves(chart_conditions, speeds, flows, heads, flows, effs)
chart.setHeadUnit("kJ/kg")
compressor.setCompressorChart(chart)
compressor.setSpeed(10000.0)
process.run()
The second is CompressorChartMWInterpolation. It is more data-driven. If you have maps measured at several molecular weights, NeqSim interpolates head, efficiency, surge, and stonewall between the nearest maps. By default, the compressor updates the chart with the inlet stream during run(), so the chart can use the actual inlet gas molecular weight automatically.
CompressorChartMWInterpolation = J.process.equipment.compressor.CompressorChartMWInterpolation
mw_chart = CompressorChartMWInterpolation()
mw_chart.setHeadUnit("kJ/kg")
mw_chart.setAutoGenerateSurgeCurves(True)
mw_chart.setAutoGenerateStoneWallCurves(True)
# Add complete speed/head/efficiency maps at each reference molecular weight.
mw_chart.addMapAtMW(18.0, chart_conditions, speeds, flows_18, heads_18, effs_18)
mw_chart.addMapAtMW(22.0, chart_conditions, speeds, flows_22, heads_22, effs_22)
compressor.setCompressorChart(mw_chart)
process.run()
print("Operating MW used by chart:", mw_chart.getOperatingMW(), "g/mol")
Use the multi-map approach when the project has measured or vendor-approved map families. Use the Khader-style correction when you only have one base map and need a transparent approximation. Do not use either method for two-phase inlet flow. Both are compressor gas-map tools; liquid carryover is a suction-scrubber or upstream-process problem.
15.15 Loading Vendor Curves from JSON and CSV
The official compressor-curve page recommends JSON for map exchange because it can carry metadata, speed-curve arrays, units, and optional design power. CSV is also supported for spreadsheet workflows.
The JSON map has this shape:
{
"compressorName": "K-101 export compressor",
"headUnit": "kJ/kg",
"maxDesignPower_kW": 16619.42,
"speedCurves": [
{
"speed_rpm": 7382.55,
"flow_m3h": [19852.05, 21679.87, 23507.69],
"head_kJkg": [256.69, 253.67, 249.29],
"polytropicEfficiency_pct": [81.74, 82.99, 83.95]
}
]
}
Load JSON or CSV through the compressor. The loader activates the chart and reinitializes capacity constraints so min and max speed limits reflect the file.
compressor.loadCompressorChartFromJson("compressor_curves/k101_export.json")
# or
compressor.loadCompressorChartFromCsv("compressor_curves/k101_export.csv")
compressor.setUsePolytropicCalc(True)
compressor.setSpeed(7382.55)
process.run()
print(compressor.getCompressorChart().isUseCompressorChart())
print(compressor.getPower("kW"), compressor.getDistanceToSurge())
For CSV, use semicolon-delimited columns named speed, flow, head, and polyEff. Each row is one point, and rows with the same speed form a speed curve. Keep these quality checks with the file:
| Check | Practical rule |
|---|---|
| Flow basis | Use m3/hr at actual inlet conditions unless the conversion is documented. |
| Head basis | Use polytropic head and set headUnit consistently, usually kJ/kg. |
| Efficiency scale | Use percent in curve files, typically 70-90 for centrifugal maps. |
| Speed coverage | Include at least three speed curves for variable-speed machinery. |
| Range coverage | Include points from surge side to stonewall side for each speed. |
| Provenance | Store source, date, map revision, reference gas, and conversion assumptions. |
The loader can only protect syntax and basic structure. It cannot know whether a vendor corrected-flow coordinate was converted correctly. That remains an engineering responsibility.
15.16 Templates and Automatic Curve Generation
When manufacturer data is not available, CompressorChartGenerator can create reasonable curves around the current operating point. The official docs list three template families:
| Family | Templates | Typical use |
|---|---|---|
| Basic centrifugal | CENTRIFUGAL_STANDARD, CENTRIFUGAL_HIGH_FLOW, CENTRIFUGAL_HIGH_HEAD |
Generic early studies. |
| Application | PIPELINE, EXPORT, INJECTION, GAS_LIFT, REFRIGERATION, BOOSTER |
Oil and gas process contexts. |
| Machine type | SINGLE_STAGE, MULTISTAGE_INLINE, INTEGRALLY_GEARED, OVERHUNG |
Mechanical architecture screening. |
The selection logic is simple. Use PIPELINE for large gas-transmission duty, EXPORT for offshore export compressors, INJECTION for high discharge pressure, GAS_LIFT for wide surge margin, and REFRIGERATION for LNG or process-cooling service. If you do not know, start with CENTRIFUGAL_STANDARD and label the chart as generated.
generator = J.process.equipment.compressor.CompressorChartGenerator(compressor)
generator.setChartType("interpolate and extrapolate")
generator.enableAdvancedCorrections(6) # six stages: Reynolds, Mach, surge shift corrections
chart = generator.generateFromTemplate("EXPORT", 5)
compressor.setCompressorChart(chart)
process.run()
Generated curves are valuable for education, early screening, and sensitivity analysis. They are not a substitute for a vendor selection. In reports, use clear language: generated map, vendor map, or measured map. Never call a generated NeqSim template a vendor curve.
15.17 Driver, Speed, and Power Limits
A compressor map tells you what the aerodynamic stage can do. The driver tells you whether the shaft can deliver the required power at the current speed. NeqSim's CompressorDriver model covers electric motors, VFD motors, gas turbines, steam turbines, and reciprocating engines. It can represent maximum power, speed range, speed-dependent maximum power, VFD efficiency, and gas turbine ambient-temperature derating.
For a VFD motor, a common approximation is constant torque over much of the range, so maximum power scales roughly with speed. The official docs express the speed-dependent maximum power as:
$$ P_{max}(N) = P_{max,rated}\left(a + b\frac{N}{N_{rated}} + c\left(\frac{N}{N_{rated}}\right)^2\right) $$
CompressorDriver = J.process.equipment.compressor.CompressorDriver
DriverType = J.process.equipment.compressor.DriverType
driver = CompressorDriver(DriverType.VFD_MOTOR, 8000.0) # kW rated power
driver.setRatedSpeed(10000.0)
driver.setMinSpeed(3000.0)
driver.setMaxSpeed(11000.0)
driver.setMaxPower(8800.0)
driver.setMaxPowerCurveCoefficients(0.0, 1.0, 0.0)
compressor.setDriver(driver)
process.run()
required = compressor.getPower("kW")
speed = compressor.getSpeed()
print("Driver can deliver:", driver.canDeliverPowerAtSpeed(required, speed))
print("Power margin kW:", driver.getPowerMarginAtSpeed(required, speed))
Gas turbines need ambient-temperature derating. A hot day can reduce available power at exactly the moment the gas is warmer and compressor power rises. If the process study has a summer design case, the driver model should be part of the same case table as the compressor power.
15.18 Mechanical Losses, Seal Gas, Fouling, and Washing
The process compressor gives gas power and outlet state. The shaft and package add losses and auxiliary demands. NeqSim's compressor documentation points to CompressorMechanicalLosses for dry gas seals, bearings, and lube-oil estimates, with API 692, API 617, and API 614 as the organizing standards.
losses = compressor.initMechanicalLosses(120.0) # shaft diameter in mm
losses.setSealType(losses.SealType.DRY_GAS_TANDEM)
losses.setBearingType(losses.BearingType.TILTING_PAD)
compressor.updateMechanicalLosses()
print("Seal gas Nm3/hr:", compressor.getSealGasConsumption())
print("Bearing loss kW:", compressor.getBearingLoss())
print("Mechanical efficiency:", compressor.getMechanicalEfficiency())
Use these results as screening values. Seal systems, bearing design, and lube oil systems require vendor and mechanical-engineering review before procurement or operating limits are set.
NeqSim also separates direct fouling derate from washing-event modeling. A direct compressor fouling factor is suitable when you only need to reduce head or efficiency for a case. CompressorWashing is useful when the study needs fouling growth, wash scheduling, recovery effectiveness, water consumption, or production loss.
| Mechanism or method | Official model use |
|---|---|
setFoulingFactor(value) |
Direct compressor performance derate. |
CompressorWashing.FoulingType.SALT |
High built-in fouling rate and high washability. |
HYDROCARBON, PARTICULATE, CORROSION, BIOLOGICAL |
Different fouling growth and recovery assumptions. |
ONLINE_WET |
Online partial recovery with no downtime. |
OFFLINE_SOAK, CRANK_WASH, CHEMICAL_CLEAN, DRY_ICE_BLAST |
Offline methods with increasing recovery and downtime. |
Report fouling assumptions separately from clean-case performance. This keeps maintenance decisions from being hidden inside a single compressor efficiency.
15.19 Dynamic Anti-Surge, Events, and Operating History
The steady-state recycle loop in Section 19.10 answers where gas goes. The dynamic compressor features answer when protection actions happen and whether the compressor state changes. The official compressor-curve documentation lists states such as STOPPED, STARTING, RUNNING, SURGE_PROTECTION, SPEED_LIMITED, SHUTDOWN, DEPRESSURIZING, TRIPPED, and STANDBY.
A dynamic screening model can enable operating history, rotational inertia, acceleration limits, surge warning thresholds, and an anti-surge controller.
AntiSurge = J.process.equipment.compressor.AntiSurge
compressor.enableOperatingHistory()
compressor.setRotationalInertia(15.0)
compressor.setMaxAccelerationRate(100.0)
compressor.setMaxDecelerationRate(200.0)
compressor.setSurgeWarningThreshold(0.15)
compressor.setSurgeCriticalThreshold(0.05)
anti = compressor.getAntiSurge()
anti.setControlStrategy(AntiSurge.ControlStrategy.PID)
anti.setPIDParameters(2.0, 0.5, 0.1)
anti.setPIDSetpoint(0.10)
anti.setValveResponseTime(2.0)
anti.setValveRateLimit(0.5)
anti.setMinimumRecycleFlow(500.0)
anti.setMaximumRecycleFlow(3000.0)
compressor.startCompressor(10000.0)
for step in range(600):
process.run()
compressor.updateDynamicState(0.1)
anti.updateController(compressor.getDistanceToSurge(), 0.1)
compressor.recordOperatingPoint(step * 0.1)
Treat this as a screening and educational model. Real anti-surge protection requires control-system details, valve Cv and response, recycle piping pressure drop, cooler dynamics, compressor vendor limits, and site trip philosophy. The NeqSim state and history tools are useful because they force the process model to expose the same operating variables a control engineer needs: flow, head, speed, margin, valve demand, and driver power.
15.20 Mechanical, Electrical, and Instrument Views
After a compressor process run, NeqSim can attach mechanical, electrical, and instrument design objects. Use these to collect design-adjacent information, but keep the responsibilities separate:
- The process compressor calculates thermodynamic head, power, and outlet state.
- The compressor chart checks aerodynamic envelope and speed range.
- The driver model checks power availability at speed.
- The mechanical-loss model estimates seal gas, bearing loss, and lube oil screening quantities.
- The instrument design lists suction/discharge pressure, temperature, flow, vibration, and anti-surge instrumentation.
compressor.initMechanicalDesign()
mechanical = compressor.getMechanicalDesign()
mechanical.calcDesign()
compressor.initInstrumentDesign()
instruments = compressor.getInstrumentDesign()
instruments.setIncludeAntiSurge(True)
instruments.calcDesign()
For machinery design, a compressor study should state whether the chart is vendor-supplied, generated from NeqSim, estimated from a similar machine, or interpolated between molecular-weight maps. Those cases carry very different confidence levels.
15.21 Troubleshooting Compressor Models
| Symptom | Likely cause | Practical response |
|---|---|---|
| Discharge temperature too high | Too much pressure ratio in one casing or low efficiency | Split stages, intercool, review efficiency |
| Power far too large | Flow-rate unit error or wet gas entering compressor | Check kg/hr vs Sm3/day, add suction scrubber |
| Chart gives odd efficiency | Flow basis mismatch | Convert vendor corrected flow to chart basis |
| Generated chart looks too optimistic | Template chosen for wrong application | Try EXPORT, INJECTION, or REFRIGERATION as appropriate and label assumptions |
| Negative distance to surge | Low flow relative to surge curve | Open recycle, lower pressure ratio, or change machine |
| Required speed outside map | Outlet pressure target outside machine envelope | Report infeasible; do not extrapolate silently |
| Driver overloaded | Power required exceeds speed-dependent driver power | Lower duty, change driver, split stages, or evaluate ambient derating |
| Recycle loop fails to converge | Tight tolerance, poor initial recycle stream, valve pressure mismatch | Start with small recycle, relax tolerance, inspect mixer pressure |
| Sudden liquid at discharge cooler | Heavy gas crosses dew point after compression/cooling | Add scrubber and include liquid handling in KPIs |
The first debugging question is always: does the compressor work without a chart? If not, fix the thermodynamic process model first. The second question: does the chart use the same flow, head, speed, and gas-property basis as the operating point? The third question is whether the chart is being extrapolated outside the data range.
15.22 What to Put in a Compressor Calculation Report
A useful compressor report is compact but complete:
- Fluid basis: composition, EOS, mixing rule, water/heavy handling.
- Suction conditions: T, P, flow, phase state, molar mass, Z if available.
- Discharge target: pressure or pressure ratio, and any temperature limit.
- Efficiency basis: isentropic or polytropic, fixed value or curve-derived.
- Power, discharge temperature, and cooler duty if staged.
- Chart basis: generated, vendor, CSV/JSON file, template, Khader correction, or multi-MW interpolation; include head and flow units.
- Operating envelope: speed, surge margin, stonewall margin, and speed range.
- Anti-surge basis: static recycle, controller object, dynamic state, or both.
- Driver and utility impact: shaft power, driver margin, fuel/electric load.
- Mechanical screening: seal gas, bearing loss, lube oil, fouling or washing assumptions when used.
- Residual risks: liquid carryover, map extrapolation, missing vendor data, unvalidated gas properties, and control-valve assumptions.
For early studies, state the assumptions clearly. For design, attach the vendor map and the conversion sheet that turns vendor coordinates into NeqSim chart coordinates.
15.23 Exercises
- Single-stage screening. Change the one-pager outlet pressure from 95 to 120 bara. Record power, discharge temperature, speed, and surge margin. Decide whether a single casing is still plausible.
- Efficiency sensitivity. Sweep polytropic efficiency from 0.68 to 0.82 and plot power and discharge temperature. Explain why both move in the direction they do.
- Map sensitivity. Generate charts with 3, 5, and 9 speed curves. Compare distance to surge at the same operating point.
- Template selection. Generate
PIPELINE,EXPORT, andINJECTIONmaps for the same design point. Explain how each template changes turndown and head behavior. - Molecular-weight case. Create two gas compositions with different MW and compare a standard chart with an MW-interpolation chart.
- Anti-surge recycle. Add a splitter, valve, and recycle around the one-pager compressor. Find the smallest recycle fraction that gives at least 10 percent surge margin.
- Two-stage compression. Split the pressure ratio over two stages with an intercooler at 30 C. Compare total power with the single-stage case.
- Driver limit. Add a VFD driver with a linear power curve and find the highest discharge pressure that still has positive driver power margin.
- Portfolio item. Save a compressor envelope table with flow, discharge pressure, speed, power, surge margin, stonewall margin, and driver margin. Include one figure and one short recommendation.
Distillation Columns: Rigorous Stages, Specifications, Solvers, and Diagnostics
Learning Objectives
After this chapter you will be able to:
- Build a NeqSim
DistillationColumnwith correct tray numbering, feeds, condenser/reboiler hardware, pressures, and product specifications. - Choose between direct, damped, inside-out, matrix inside-out, Wegstein, sum-rates, Newton, Naphtali-Sandholm, MESH residual, and AUTO solvers.
- Use product purity, component recovery, reflux, duty, product-flow, condenser, and reboiler specifications without over-constraining the model.
- Interpret convergence diagnostics, solve status, MESH residuals, homotopy steps, and outer tear-variable residuals.
- Add side draws, pumparounds, Murphree efficiencies, hydraulic rating, shortcut initialization, and tray optimization in the right order.
- Decide when an equilibrium-stage column is enough and when to use a rate-based packed column, simple absorber, or reactive distillation model.
16.1 Process Engineer's One-Pager
The safest distillation workflow in NeqSim is staged. First make the feed stream flash correctly. Then build a simple column with clear pressure and thermal specifications. Then tighten product specifications, add internals, and only after that optimize trays or feed location.
from running_case import build_running_fluid
from neqsim import jneqsim as J
fluid = build_running_fluid("srk")
fluid.setTemperature(35.0, "C")
fluid.setPressure(28.0, "bara")
fluid.setTotalFlowRate(25_000.0, "kg/hr")
feed = J.process.equipment.stream.Stream("deethanizer feed", fluid)
feed.setTemperature(10.0, "C")
feed.setPressure(27.0, "bara")
feed.run()
DistillationColumn = J.process.equipment.distillation.DistillationColumn
column = DistillationColumn("T-101 deethanizer", 12, True, True)
column.addFeedStream(feed, 7)
column.setTopPressure(25.0)
column.setBottomPressure(26.0)
column.setCondenserTemperature(273.15 - 12.0)
column.setReboilerTemperature(273.15 + 95.0)
column.setSolverType(DistillationColumn.SolverType.AUTO)
process = J.process.processmodel.ProcessSystem()
process.add(feed)
process.add(column)
process.run()
print("Solved:", column.solved())
print("Solver used:", column.getLastSolverTypeUsed())
print("Status:", column.getLastSolveStatus(), column.getLastSolveStatusReason())
print("Iterations:", column.getLastIterationCount())
print("Top flow kg/hr:", column.getGasOutStream().getFlowRate("kg/hr"))
print("Bottom flow kg/hr:", column.getLiquidOutStream().getFlowRate("kg/hr"))
print("Mass residual:", column.getLastMassResidual())
print("Energy residual:", column.getLastEnergyResidual())
Read this as an executable skeleton, not a final design. A real column study also reports light and heavy key recoveries, product compositions, condenser and reboiler duties, pressure drop, internal diameter, flooding margin, tray or packing efficiency, side products, and convergence diagnostics.
16.2 What the NeqSim Distillation Package Contains
The official NeqSim distillation documentation describes a broad column package, not just a single simple unit operation. The main package is neqsim.process.equipment.distillation, and it includes:
| Area | Main APIs | Engineering role |
|---|---|---|
| Rigorous staged columns | DistillationColumn, SimpleTray, Condenser, Reboiler |
Equilibrium-stage MESH-style column with tray-by-tray flash calculations. |
| Solver selection | DistillationColumn.SolverType |
Robustness and diagnostics for different column structures. |
| Formal specifications | ColumnSpecification, convenience setters |
Product purity, recovery, product flow, reflux, and duty targets. |
| Side products | setGasSideDrawFraction, setLiquidSideDrawFraction, addSideDrawFlowSpecification |
External vapor or liquid draws from internal stages. |
| Pumparounds | addLiquidPumparound |
Internal liquid draw/return circuits with temperature change. |
| Hardware modes | CondenserMode, ReboilerMode |
Partial/total condenser, fixed liquid reflux split, equilibrium reboiler, vapor boilup. |
| Hydraulics and sizing | calcColumnInternals, enableHydraulicPressureDropCoupling |
Tray or packing hydraulics and pressure-drop coupling. |
| Efficiency | setMurphreeEfficiency, setMurphreeEfficiencies |
Column-wide and per-stage vapor efficiency. |
| Shortcut design | ShortcutDistillationColumn, initializeFromShortcut |
Fenske-Underwood-Gilliland estimates and rigorous initialization. |
| Tray optimization | findOptimalNumberOfTrays, findEconomicOptimalTrayConfiguration |
Tray count and feed tray search. |
| Dynamics | runTransient, DynamicColumnModel.EXPERIMENTAL_EULER |
Explicit-Euler holdup screening, not a rigorous DAE simulator. |
| Rate-based packed columns | RateBasedPackedColumn |
Segment-based packed absorber/stripper with film transfer and hydraulics. |
That list matters because it prevents a common modeling mistake. A column is not only a stream splitter with a temperature profile. It is a connected system of phase equilibrium, component balances, energy balances, internal flows, terminal heat devices, product constraints, and hydraulics. Add these pieces in layers.
16.3 Tray Numbering: Bottom-Up Internal Indices
The constructor is:
column = J.process.equipment.distillation.DistillationColumn(
"T-100", simple_tray_count, has_reboiler, has_condenser
)
The simple_tray_count excludes optional terminal equipment. If the reboiler is present, internal index 0 is the reboiler. Simple trays are numbered upward from the reboiler. If a condenser is present, the highest internal index is the condenser.
For example, DistillationColumn("T-100", 10, True, True) creates 12 internal stages: reboiler, 10 simple trays, and condenser. A feed added to tray 5 is not the fifth physical tray from the top; it is the internal bottom-up index. The same convention applies to feed locations, side draws, pumparounds, and per-stage Murphree efficiency.
A good notebook should include a small tray map before the first run:
| Internal index | Meaning when reboiler/condenser exist |
|---|---|
| 0 | Reboiler |
| 1 | Lowest simple tray |
| 2..N | Simple trays upward |
| N+1 | Condenser |
If a column result looks physically inverted, check tray numbering before changing thermodynamics.
16.4 The Physics: MESH Equations in Plain Language
NeqSim's rigorous staged column is an equilibrium-stage model. Each tray is a small separator that exchanges vapor with the tray above, liquid with the tray below, and feed or side products where configured. The governing relationships are the familiar MESH equations:
| Letter | Meaning | Practical question |
|---|---|---|
| M | Material balance | Did every component entering the tray leave in vapor, liquid, or product draw? |
| E | Equilibrium | Do vapor and liquid compositions satisfy K-values from the EOS? |
| S | Summation | Do vapor and liquid mole fractions each sum to one? |
| H | Heat balance | Does enthalpy entering plus tray heat equal enthalpy leaving? |
The thermodynamic system supplies fugacity-based K-values and enthalpies. The column solver supplies the internal-flow iteration. If the EOS, mixing rule, or feed flash is wrong, no solver choice can make the physical answer reliable.
The most useful process-engineer habit is to validate the feed before touching column settings:
feed.run()
fluid = feed.getFluid()
print("Feed T C:", feed.getTemperature("C"))
print("Feed P bara:", feed.getPressure("bara"))
print("Feed phases:", fluid.getNumberOfPhases())
print("Feed vapor fraction:", fluid.getBeta())
Columns are sensitive to feed enthalpy. A vapor feed and subcooled liquid feed with the same composition and pressure can require different reflux, boilup, and feed location.
16.5 Building the Base Column
Start with the smallest model that has the correct hardware. For a deethanizer or depropanizer, that usually means both condenser and reboiler. For a stripper, use a reboiler and no condenser. For an absorber, use neither condenser nor reboiler and feed gas and solvent at opposite ends.
DistillationColumn = J.process.equipment.distillation.DistillationColumn
column = DistillationColumn("T-201", 15, True, True)
column.addFeedStream(feed, 8)
column.setTopPressure(22.0)
column.setBottomPressure(23.0)
column.setCondenserTemperature(273.15 - 20.0)
column.setReboilerTemperature(273.15 + 110.0)
column.setMaxNumberOfIterations(100)
column.setSolverType(DistillationColumn.SolverType.DAMPED_SUBSTITUTION)
column.run()
Do not start with sharp product purities, side draws, pumparounds, hydraulic coupling, and tray optimization in the same model. Those are all valid features, but they make debugging harder. A base column should answer four questions:
- Does it converge?
- Are overhead and bottoms phases plausible?
- Is the temperature profile monotonic in the expected direction?
- Are condenser and reboiler duties plausible in sign and magnitude?
Only after those answers are stable should you add product specifications.
16.6 Operating Specifications
Direct operating specifications are applied before the inner column solver. The official docs show these as endpoint pressures, condenser/reboiler temperatures, reflux ratio, heat input, and boilup ratio.
column.setTopPressure(25.0)
column.setBottomPressure(26.0)
column.setCondenserTemperature(263.15)
column.setReboilerTemperature(378.15)
column.setCondenserRefluxRatio(3.0)
column.getCondenser().setHeatInput(-5.0e6)
column.getReboiler().setHeatInput(6.0e6)
column.setReboilerBoilupRatio(2.5)
Use operating specifications when you know how the column is controlled or when you are reproducing a plant operating point. Use product specifications when you need the solver to adjust terminal temperatures to meet purity, recovery, or flow targets.
Product-quality and recovery targets are dimensionless fractions from 0 to 1:
column.setTopProductPurity("ethane", 0.95)
column.setBottomProductPurity("propane", 0.98)
column.setTopComponentRecovery("ethane", 0.99)
column.setBottomComponentRecovery("propane", 0.99)
column.setBottomProductFlowRate(1000.0, "mol/hr")
A sharp product target should be treated as a design constraint, not just a number. Ask whether the specified component inventory exists in the feed, the number of trays is sufficient, and the condenser/reboiler hardware can actually manipulate the required product.
16.7 Condenser and Reboiler Modes
Condenser and reboiler hardware modes change the physical meaning of the top and bottom stages.
column.setCondenserMode(DistillationColumn.CondenserMode.PARTIAL)
column.setCondenserMode(DistillationColumn.CondenserMode.TOTAL)
column.setCondenserLiquidReflux(500.0, "kg/hr")
column.setReboilerMode(DistillationColumn.ReboilerMode.EQUILIBRIUM)
column.setReboilerVaporBoilupRatio(1.8)
Use setCondenserLiquidReflux(value, unit) instead of setting the liquid reflux split mode directly. The fixed reflux flow is required for that mode, and the convenience method sets both the mode and the value.
A partial condenser can produce both vapor and liquid top products. A total condenser collapses the top vapor into liquid. For cryogenic NGL columns, this choice is not cosmetic; it changes the product stream phases and material balance.
16.8 Solver Selection
The official distillation page lists the current solver options. A useful way to remember them is to group by purpose.
| Solver | Strategy | Typical use |
|---|---|---|
DIRECT_SUBSTITUTION |
Classic tray-by-tray substitution. | Simple, well-posed columns. |
DAMPED_SUBSTITUTION |
Sequential substitution with initial fixed relaxation. | Stiff or oscillating cases. |
INSIDE_OUT |
Flow correction with K-value tracking and polishing. | General deethanizer/depropanizer work. |
MATRIX_INSIDE_OUT |
Matrix warm start plus rigorous inside-out polish. | Larger hydrocarbon fractionators. |
WEGSTEIN |
Accelerated fixed-point iteration after warm-up. | Well-conditioned fixed-point problems. |
SUM_RATES |
Flow-corrected tearing method. | Absorbers, strippers, and flow-sensitive columns. |
NEWTON |
Tray-temperature Newton accelerator. | Difficult temperature convergence; not full simultaneous MESH Newton. |
NAPHTALI_SANDHOLM |
Guarded simultaneous correction after inside-out warm start. | Residual-driven hydrocarbon fractionators. |
MESH_RESIDUAL |
Inside-out initialization plus full residual audit. | Auditing material, equilibrium, summation, energy, and spec residuals. |
AUTO |
Feasibility screen plus candidate solver tracing. | Agent workflows and uncertain cases. |
For a process engineer, the default decision tree is:
- Start with
AUTOwhen you want robust selection and diagnostics. - Use
DAMPED_SUBSTITUTIONif the case oscillates or specs are aggressive. - Use
INSIDE_OUTfor common hydrocarbon fractionators after a base case runs. - Use
SUM_RATESfor absorber or stripper style columns. - Use
MESH_RESIDUALorNAPHTALI_SANDHOLMwhen you need residual auditing.
column.setSolverType(DistillationColumn.SolverType.AUTO)
column.run()
print("Concrete solver:", column.getLastSolverTypeUsed())
print(column.getLastAutoSolverSummary())
getLastSolverTypeUsed() reports the concrete solver that completed the latest run.
16.9 Product Specifications and Homotopy
Product purities and recoveries can be numerically hard because the solver must move from the current product split to a target split that may be far away. NeqSim can stage these targets through specification homotopy.
column.setTopProductPurity("ethane", 0.98)
column.setBottomProductPurity("propane", 0.98)
column.setSpecificationHomotopySteps(5)
column.setSolverType(DistillationColumn.SolverType.AUTO)
column.run()
print("Homotopy stages completed:", column.getLastSpecificationHomotopyStepCount())
print("Top spec residual:", column.getLastTopSpecificationResidual())
print("Bottom spec residual:", column.getLastBottomSpecificationResidual())
When AUTO is selected and an adjustable product specification is active, NeqSim uses three homotopy stages by default unless you configure another stage count. Homotopy changes the effective intermediate target during solving; it does not change the final stored product specification.
Use homotopy when a purity target is realistic but difficult. Do not use it to force an impossible split. If the feed does not contain enough of the key component, the target remains impossible regardless of continuation steps.
16.10 Diagnostics: Solve Status, Residuals, and What to Log
The distillation docs expose a large diagnostic surface. Use it. A column that prints only overhead and bottoms flows is hard to review.
| Getter | What it tells you |
|---|---|
solved() |
Whether the current convergence flag is true. |
getLastSolveStatus() |
Strict status: rigorous convergence, reconciled products, fallback products, failure, or not run. |
getLastSolveStatusReason() |
Concise explanation for fallback or rejection. |
getLastSolverTypeUsed() |
Concrete solver, especially useful after AUTO. |
getLastIterationCount() |
Inner solver iteration count. |
getLastSolveTimeSeconds() |
Latest wall time. |
getLastTemperatureResidual() |
Average tray-temperature residual. |
getLastMassResidual() |
Relative mass-balance residual. |
getLastEnergyResidual() |
Relative energy residual. |
getLastSpecificationResidual() |
Maximum endpoint specification residual. |
getLastColumnTearResidual() |
Maximum side-draw, pumparound, or hydraulic tear residual. |
getLastMeshResidualNorm() |
Full scaled MESH residual infinity norm. |
A reporting helper should print the key metrics every time:
print("Solved:", column.solved())
print("Status:", column.getLastSolveStatus())
print("Reason:", column.getLastSolveStatusReason())
print("Solver:", column.getLastSolverTypeUsed())
print("Iterations:", column.getLastIterationCount())
print("Mass residual:", column.getLastMassResidual())
print("Energy residual:", column.getLastEnergyResidual())
print("MESH norm:", column.getLastMeshResidualNorm())
The MESH residual gate is diagnostic-only for legacy sequential solvers by default. It is effective by default for NAPHTALI_SANDHOLM and MESH_RESIDUAL. If you need it to be part of the convergence contract for other solvers, use setEnforceMeshResidualTolerance(True).
16.11 Shortcut Initialization and FUG Thinking
Shortcut methods are not a replacement for rigorous simulation, but they are excellent initializers and sense checks. The official docs describe ShortcutDistillationColumn and initializeFromShortcut as Fenske-Underwood- Gilliland style tools. The workflow is:
- Identify light key and heavy key.
- Estimate target recoveries or purities.
- Use shortcut results for tray count, reflux, and feed location hints.
- Initialize the rigorous column.
- Run and audit rigorous residuals.
init = column.initializeFromShortcut(feed, "ethane", "propane", 0.98, 0.98, 1.3)
if init.isFeasible():
column.setSolverType(DistillationColumn.SolverType.INSIDE_OUT)
column.run()
The shortcut estimate should be compared against physical expectations. If a light/heavy key pair has relative volatility close to one, the shortcut will already warn you that the separation needs many stages or high reflux. Do not hide that warning by forcing a small rigorous column.
16.12 Feed Tray and Tray Optimization
Feed tray placement controls the balance between rectifying and stripping sections. A feed too high overloads the stripping section; a feed too low overloads the rectifying section. In hydrocarbon columns, a first guess often puts the feed around the temperature-matching tray or the middle of the active separation zone.
NeqSim provides search utilities for tray count, feed tray, and economic ranking. These methods mutate the column to the selected candidate, so copy the model first if you need to preserve the original.
column.setMaxTrayOptimizationCandidates(200)
column.setMaxTrayOptimizationTimeSeconds(20.0)
trays = column.findOptimalNumberOfTrays(0.95, "ethane", True, 30)
economic = column.findEconomicOptimalTrayConfiguration(0.95, "ethane", True, 30)
print("Selected trays:", trays)
print("Economic result:", economic)
Do not let tray optimization run before the base specs are physically sensible. If the product target is impossible, optimization will spend time searching a bad problem.
16.13 Side Draws
Side draws withdraw vapor or liquid from internal stages. They are true external product streams. The official docs emphasize that getOutletStreams() includes non-zero side draws and that mass-balance reporting subtracts them from the feed-product balance.
For simple fractional side draws:
column.setGasSideDrawFraction(6, 0.05)
column.setLiquidSideDrawFraction(4, 0.10)
gas_draw = column.getSideDrawStream(6, DistillationColumn.SideDrawPhase.GAS)
all_draws = column.getSideDrawStreams()
For target side-product flow, use side-draw flow specifications. The column adjusts the corresponding tray split fraction as an outer tear variable.
spec = column.addSideDrawFlowSpecification(
6, DistillationColumn.SideDrawPhase.GAS, 100.0, "kg/hr"
)
spec.setTolerance(1.0e-4)
spec.setMaxIterations(15)
column.setMaxColumnTearIterations(20)
column.setColumnTearTolerance(1.0e-4)
column.run()
print("Actual draw:", spec.getLastActualFlowRate())
print("Draw residual:", spec.getLastRelativeResidual())
print("Outer tear converged:", column.isLastColumnTearConverged())
If the requested draw is larger than the available tray traffic, NeqSim bounds the split and reports non-convergence in the tear diagnostics. That is a useful engineering result: the requested side product is not available at that tray.
16.14 Pumparounds
A liquid pumparound withdraws liquid from one tray, changes its temperature, and returns it to another tray. It is an internal heat-removal or heat-addition circuit, not an external product. It does not appear in getOutletStreams().
pa = column.addLiquidPumparound("PA-1", 4, 6, 0.15, 10.0)
column.setMaxPumparoundIterations(12)
column.setPumparoundTolerance(1.0e-4)
column.run()
print("Return stream:", pa.getReturnStream().getName())
print("Pumparound change:", column.getLastPumparoundRelativeChange())
The temperatureDrop argument is in Kelvin. Positive values cool the returned liquid; negative values heat it. A non-finite or below-zero-K return temperature fails explicitly.
Use pumparounds for crude-style heat integration and large fractionators where internal heat removal shapes the vapor and liquid traffic. For a simple binary teaching column, leave them out.
16.15 Hydraulics, Internals, and Pressure-Drop Coupling
A thermodynamically converged column can still be mechanically impossible. Flooding, weeping, entrainment, downcomer backup, and pressure drop matter. NeqSim exposes tray and packing hydraulic rating through calcColumnInternals() and optional pressure-drop coupling.
column.setInternalDiameter(2.5)
designer = column.calcColumnInternals("sieve")
print("Total pressure drop Pa:", designer.getTotalPressureDrop())
column.enableHydraulicPressureDropCoupling("sieve")
column.run()
print("Coupled pressure drop Pa:", column.getLastHydraulicPressureDropPa())
print("Hydraulic residual:", column.getLastHydraulicPressureDropResidual())
Supported internals names include common tray types such as sieve, valve, bubble-cap, and packed-column mode packed, depending on available data. Hydraulic coupling is opt-in. The recommended sequence is:
- Run without hydraulic coupling.
- Set a positive internal diameter.
- Confirm
calcColumnInternals(...)succeeds. - Enable coupling and rerun.
- Inspect hydraulic residual and pressure profile.
Hydraulic coupling changes the pressure profile, and pressure profile changes phase behavior. That is why it belongs after the base thermodynamic case is stable.
16.16 Murphree Efficiency and Actual Trays
The rigorous column solves theoretical equilibrium stages unless efficiency corrections are applied. Murphree vapor efficiency can be set globally or per stage:
column.setMurphreeEfficiency(0.70)
column.setMurphreeEfficiency(3, 0.65)
print(column.getMurphreeEfficiency(3))
column.clearPerStageMurphreeEfficiency()
Efficiency links the theoretical model to real internals. Low efficiency means more actual trays or more packing height for the same separation. The official docs note that mechanical design and tray optimization can use tray efficiency to convert theoretical stages to actual trays for cost ranking.
Use realistic efficiency ranges:
| Internals | Typical design thought |
|---|---|
| Sieve trays | Simple, economical, general hydrocarbon service. |
| Valve trays | Better turndown than sieve trays. |
| Bubble-cap trays | Robust at low liquid rate but higher pressure drop. |
| Random packing | Low pressure drop, useful for corrosive or smaller services. |
| Structured packing | High efficiency and low pressure drop, often vacuum or specialty service. |
Treat these as screening choices. Final internals selection needs vendor and mechanical review.
16.17 Absorbers, Strippers, and Rate-Based Packed Columns
The official distillation page explicitly points to absorbers and RateBasedPackedColumn. Do not force every mass-transfer problem into a reboiled distillation column.
Use DistillationColumn without condenser and reboiler for simple equilibrium- stage absorber or stripper work:
absorber = DistillationColumn("Equilibrium absorber", 10, False, False)
absorber.addFeedStream(gas_stream, 0)
absorber.addFeedStream(lean_solvent, 9)
absorber.setSolverType(DistillationColumn.SolverType.SUM_RATES)
absorber.run()
Use RateBasedPackedColumn when packed-column mass transfer, film transfer, segment profiles, pressure drop, or flood fraction are the point of the study:
RateBasedPackedColumn = J.process.equipment.distillation.RateBasedPackedColumn
packed = RateBasedPackedColumn("CO2 absorber", gas_stream, lean_solvent)
packed.setColumnDiameter(1.2)
packed.setPackedHeight(6.0)
packed.setNumberOfSegments(12)
packed.setPackingType("Pall-Ring-50")
packed.setTransferComponents("CO2")
packed.run()
print(packed.toJson())
The absorber documentation also describes SimpleTEGAbsorber, SimpleAmineAbsorber, WaterStripperColumn, and other specialized models. Use those when the engineering question is dehydration, amine sweetening, water stripping, or a simplified removal-efficiency study rather than rigorous tray-by-tray distillation.
16.18 Reactive Distillation
Reactive distillation combines separation and reaction. NeqSim implements this by replacing standard tray PH flashes with reactive PH flashes on selected trays. The current reactive model is equilibrium-based: it does not solve kinetic rate expressions.
column = DistillationColumn("Reactive column", 6, True, True)
column.setReactive(True, 2, 4) # only internal trays 2-4 are reactive
column.addFeedStream(feed, 3)
column.run()
for tray_index in range(column.getNumberOfTrays()):
print(tray_index, column.getTray(tray_index).isUseReactiveFlash())
Use reactive distillation only when the reaction chemistry and thermodynamics are suitable for an equilibrium assumption. For hydrocarbon-only systems with no independent reactions, NeqSim detects the non-reactive case and delegates to the standard flash path, preserving consistency with non-reactive column results.
16.19 Dynamic Screening Model
Distillation dynamics in the current package are explicitly experimental. The official docs state that getDynamicColumnModel() returns DynamicColumnModel.EXPERIMENTAL_EULER and that the transient model uses explicit-Euler tray holdup updates with simplified hydraulics.
column.setDynamicColumnEnabled(True)
column.setDynamicEnergyEnabled(True)
column.setTrayWeirHeight(0.05)
column.setTrayWeirLength(1.0)
column.setTrayDryPressureDrop(200.0)
print(column.getDynamicColumnModel())
print(column.isDynamicColumnModelExperimental())
Use this as a qualitative screening tool for inventory response. Do not present it as a rigorous DAE dynamic column model for control-system design or safety- critical trip studies.
16.20 Common Workflows
NGL Fractionation
A practical NGL fractionation workflow is:
- Build a hydrocarbon feed with an EOS and mixing rule suitable for the range.
- Run and inspect feed phase state.
- Initialize with shortcut methods if light/heavy keys are clear.
- Run
AUTOorINSIDE_OUTfor the base case. - Add product purity or recovery specs with homotopy if needed.
- Audit with
MESH_RESIDUALorNAPHTALI_SANDHOLMfor residual confidence. - Add hydraulics only after the base thermodynamic case is stable.
- Report product compositions, duties, tray profiles, and residuals.
Absorber or Stripper
For equilibrium-stage absorber or stripper models:
- Use no condenser and no reboiler for absorber.
- Use reboiler and no condenser for stripper.
- Feed gas and solvent at physically meaningful ends of the column.
- Use
SUM_RATESif flow-corrected updates help. - Switch to
RateBasedPackedColumnif packing hydraulics or film mass transfer matters.
Design Screening
For design screening:
- Use shortcut initialization and tray optimization.
- Apply Murphree efficiency or HETP assumptions.
- Rate internals and pressure drop.
- Convert theoretical stages to actual trays or packed height.
- State whether the result is conceptual, preliminary, or vendor-supported.
16.21 Troubleshooting Distillation Models
| Symptom | Recommended checks |
|---|---|
| No convergence | Verify tray numbering, feed condition, endpoint temperatures, pressure profile, and component list. Start with DIRECT_SUBSTITUTION, DAMPED_SUBSTITUTION, or AUTO, then inspect diagnostics. |
| Oscillating temperatures | Reduce aggressive condenser/reboiler specs, lower relaxation, or use DAMPED_SUBSTITUTION. |
| Product spec does not close | Check spec residuals, feasible product split, component inventory, and whether required terminal hardware exists. Use homotopy if the target is realistic but sharp. |
| Side draw non-converged | Target may exceed tray traffic or feed inventory. Inspect the side-draw stream and outer tear residual. |
| Pumparound fails | Check draw tray, return tray, draw fraction, and return temperature. |
| Hydraulic coupling fails | Run without coupling, set positive diameter, verify internals calculation, then enable coupling. |
| Unexpected dynamic result | Remember the dynamic model is experimental explicit-Euler screening. |
| Light key appears in bottoms | Could be normal leakage, insufficient stages, too low reflux, wrong feed tray, or impossible target. |
| Perfect split | Usually suspicious. Check tolerances, missing heavy components, or over-constrained specs. |
Most column debugging is sequencing. Remove advanced features, converge the base case, and add one feature back at a time.
16.22 What to Put in a Distillation Calculation Report
A professional column report should include:
- Fluid basis: EOS, components, mixing rule, feed phase state, and key component definitions.
- Column hardware: number of simple trays, condenser/reboiler presence, tray numbering convention, feed tray, side draws, pumparounds.
- Pressure profile: top pressure, bottom pressure, pressure drop basis, and whether hydraulic pressure-drop coupling was used.
- Specifications: temperatures, duties, reflux, boilup, product purities, recoveries, product flows, and homotopy steps.
- Solver: requested solver, concrete solver used, iteration count, solve time, solve status, and status reason.
- Products: top and bottom flow, phase, composition, key recoveries, and any side-product flows.
- Energy: condenser duty, reboiler duty, pumparound heat removal/addition.
- Profiles: tray temperature, pressure, vapor/liquid traffic, key compositions.
- Hydraulics: internals type, diameter, pressure drop, flooding/weeping or packing diagnostics when available.
- Residuals: mass, energy, specification, outer tear, and MESH residuals.
- Limitations: equilibrium-stage assumption, efficiency assumptions, missing vendor internals, and experimental dynamic-model caveat if used.
A useful appendix includes the exact book.yaml or notebook environment, the code used to build the column, and a table of all solver diagnostics.
16.23 Exercises
- Tray numbering. Build a 10-simple-tray column with condenser and reboiler. Create a table mapping internal indices to hardware. Place the feed three different ways and explain the result.
- Solver comparison. Run the same base column with
DAMPED_SUBSTITUTION,INSIDE_OUT,MATRIX_INSIDE_OUT, andAUTO. Compare iterations, solve time, and residuals. - Product specification. Add top ethane purity and bottom propane purity. Compare direct solving with three and five homotopy steps.
- Side draw. Add a gas side draw and then a target side-draw flow specification. Report actual draw, residual, and mass balance.
- Pumparound. Add a liquid pumparound with a 10 K temperature drop. Plot tray temperature before and after.
- Hydraulics. Run
calcColumnInternals("sieve"), then enable hydraulic coupling and compare pressure profiles. - Efficiency. Compare theoretical stages with 70 percent Murphree efficiency. Explain how that changes actual tray requirements.
- Absorber alternative. Model a simple equilibrium absorber with no condenser/reboiler, then repeat with
RateBasedPackedColumnif suitable streams are available. - Reactive section. Enable reactive trays for a small water-gas-shift example and confirm which trays use reactive flash.
- Portfolio item. Produce a column design note with a tray-temperature plot, product-composition table, energy-duty table, solver diagnostics, and one recommendation about internals or feed location.
Advanced Equipment, Custom Unit Operations, and Parameter Databases
Learning Objectives
After this chapter you will:
- Know when to move from shortcut heaters, coolers, expanders, and compressors to advanced equipment classes.
- Build a runnable
LNGHeatExchangerexample and read cryogenic design KPIs such as MITA and exergy destruction. - Build a runnable
TurboExpanderCompressorexample with performance curves, speed matching, and power balance checks. - Decide whether a custom calculation belongs in a Python prototype unit, a Java unit operation, or a reusable helper function.
- Use component and interaction parameter database overrides in a controlled, auditable way.
17.1 Why This Chapter Exists
The first process chapters deliberately used familiar equipment: separators, coolers, compressors, valves, mixers, splitters, and columns. Those are enough for many screening studies, but industrial models often need equipment that carries more physics than a shortcut unit:
| Need | Shortcut class | Advanced class or pattern |
|---|---|---|
| Multi-stream cryogenic heat exchange | Cooler, Heater, HeatExchanger |
LNGHeatExchanger, MultiStreamHeatExchanger2 |
| Expander mechanically coupled to a recompressor | Expander plus Compressor |
TurboExpanderCompressor |
| Notebook-only experiment with non-standard equipment logic | Helper function | Python unitop prototype pattern |
| Production-quality new equipment | Python class | Java class extending the process equipment base classes |
| New pure-component or binary interaction data | Hard-coded assumptions | Controlled COMP and INTER database override |
A process engineer should not reach for these classes just because they are more advanced. Use them when the engineering question asks for their extra state: pinch location, exergy destruction, freeze-out flags, pressure-drop allocation, expander/compressor speed, power balance, or custom behavior that must participate in a flowsheet.
17.2 LNGHeatExchanger for Cryogenic Multi-Stream Service
LNGHeatExchanger is the cryogenic exchanger model to use when the question is not just "what outlet temperature do I want?" but "where is the internal pinch, how much exergy is destroyed, and do any zones approach freeze-out or thermal stress limits?" It extends the multi-stream heat exchanger framework and adds zone-by-zone flash calculations, composite curves, minimum internal temperature approach, exergy metrics, pressure-drop effects, and design-oriented checks.
The basic setup has three ideas:
- Add every stream that participates in the exchanger.
- Classify streams as hot or cold explicitly, or let the exchanger classify them from inlet temperatures.
- Specify the discretization and any stream pressure drops before running.
from neqsim import jneqsim as J
def make_srk_stream(name, temp_c, pressure_bara, flow_kg_hr, composition):
fluid = J.thermo.system.SystemSrkEos(273.15 + temp_c, pressure_bara)
for component, amount in composition.items():
fluid.addComponent(component, amount)
fluid.setMixingRule("classic")
stream = J.process.equipment.stream.Stream(name, fluid)
stream.setTemperature(temp_c, "C")
stream.setPressure(pressure_bara, "bara")
stream.setFlowRate(flow_kg_hr, "kg/hr")
return stream
warm_feed = make_srk_stream(
"warm feed gas",
30.0,
50.0,
100_000.0,
{"methane": 0.90, "ethane": 0.05, "propane": 0.03, "nitrogen": 0.02},
)
mixed_refrigerant = make_srk_stream(
"cold mixed refrigerant",
-33.0,
3.0,
150_000.0,
{"methane": 0.40, "ethane": 0.30, "propane": 0.30},
)
mche = J.process.equipment.heatexchanger.LNGHeatExchanger("MCHE-100")
mche.addInStream(warm_feed)
mche.addInStream(mixed_refrigerant)
mche.setStreamIsHot(0, True)
mche.setStreamIsHot(1, False)
mche.setNumberOfZones(8)
mche.setExchangerType("BAHX")
mche.setStreamPressureDrop(0, 1.5) # hot stream pressure drop, bar
mche.setStreamPressureDrop(1, 0.3) # refrigerant pressure drop, bar
mche.setReferenceTemperature(15.0)
process = J.process.processmodel.ProcessSystem()
for unit in (warm_feed, mixed_refrigerant, mche):
process.add(unit)
process.run()
print(f"MITA: {mche.getMITA('K'):.2f} K")
print(f"MITA zone index: {mche.getMITAZoneIndex()}")
print(f"Second-law efficiency: {mche.getSecondLawEfficiency():.3f}")
print(f"Exergy destruction: {mche.getTotalExergyDestruction():.1f} kW")
print(f"Freeze-out risk: {mche.hasFreezeOutRisk()}")
Read the results as exchanger diagnostics, not as final vendor design. MITA is where the model tells you the exchanger is closest to violating temperature approach. Exergy destruction tells you where thermodynamic irreversibility is being paid for. A freeze-out flag is a prompt to revisit CO2, water, heavy hydrocarbon, mercury, or cold-end design assumptions before using the result in a design basis.
For LNG work, a minimum reporting table should include inlet and outlet conditions for every stream, duty, MITA, MITA zone, per-stream pressure drop, phase state at the cold end, freeze-out flag, thermal-stress warning, and the EOS/database basis for the feed and refrigerant.
17.3 TurboExpanderCompressor for Coupled Shaft Power
A turboexpander train is not just an expander followed by a compressor. The expander and compressor share shaft speed, and the compressor head depends on map-like curve data. TurboExpanderCompressor captures that coupling. Use it when you need expander outlet temperature, recompressor outlet pressure, shaft-speed consistency, power balance, and curve-ratio diagnostics in one object.
The pattern below is intentionally close to a unit test in the NeqSim source. The curve arrays are dimensionless map ratios, while the design settings anchor those ratios to a design speed, impeller diameter, head, and efficiency.
from neqsim import jneqsim as J
base_gas = J.thermo.system.SystemSrkEos(273.15 + 42.0, 10.0)
for component, amount in [
("nitrogen", 0.006),
("CO2", 0.014),
("methane", 0.862),
("ethane", 0.080),
("propane", 0.030),
("i-butane", 0.0024),
("n-butane", 0.0040),
("n-hexane", 0.0015),
]:
base_gas.addComponent(component, amount)
base_gas.setMixingRule("classic")
base_gas.init(0)
expander_feed = J.process.equipment.stream.Stream("expander feed", base_gas.clone())
expander_feed.setFlowRate(456_000.0, "kg/hr")
expander_feed.setTemperature(-23.0, "C")
expander_feed.setPressure(60.95, "bara")
expander_feed.run()
compressor_feed = J.process.equipment.stream.Stream("compressor feed", base_gas.clone())
compressor_feed.setFlowRate(423_448.0, "kg/hr")
compressor_feed.setTemperature(17.0, "C")
compressor_feed.setPressure(42.0, "bara")
compressor_feed.run()
turbo = J.process.equipment.expander.TurboExpanderCompressor(
"TurboExpanderCompressor", expander_feed
)
turbo.setCompressorFeedStream(compressor_feed)
turbo.setUCcurve(
[0.9965, 0.7591, 0.9843, 0.8828, 0.9552, 1.0],
[0.9841, 0.7966, 0.9932, 0.9364, 0.9943, 1.0],
)
turbo.setQNEfficiencycurve(
[0.5, 0.7, 0.85, 1.0, 1.2, 1.4, 1.6],
[0.88, 0.91, 0.95, 1.0, 0.97, 0.85, 0.60],
)
turbo.setQNHeadcurve(
[0.5, 0.8, 1.0, 1.2, 1.4, 1.6],
[1.10, 1.05, 1.0, 0.90, 0.70, 0.40],
)
turbo.setImpellerDiameter(0.424) # m
turbo.setDesignSpeed(6850.0) # rpm
turbo.setExpanderDesignIsentropicEfficiency(0.88)
turbo.setDesignUC(0.7)
turbo.setDesignQn(0.03328)
turbo.setExpanderOutPressure(42.0)
turbo.setCompressorDesignPolytropicEfficiency(0.81)
turbo.setCompressorDesignPolytropicHead(20.47) # kJ/kg
turbo.setMaximumIGVArea(1.637e4) # mm2
turbo.run()
print(f"Speed: {turbo.getSpeed():.0f} rpm")
print(f"Expander power: {turbo.getPowerExpander('MW'):.3f} MW")
print(f"Compressor power: {turbo.getPowerCompressor('MW'):.3f} MW")
print(f"Expander outlet T: {turbo.getExpanderOutletStream().getTemperature('C'):.1f} C")
print(f"Compressor outlet P: {turbo.getCompressorOutletStream().getPressure('bara'):.2f} bara")
print(f"Compressor efficiency: {turbo.getCompressorPolytropicEfficiency():.3f}")
For a real train, the curve arrays should come from vendor performance maps or project compressor/expander datasheets. Do not tune them only to make a model converge. A useful sanity check is that expander power and compressor power are nearly equal after losses, the computed speed is inside the mechanical operating window, and the compressor outlet pressure is inside the expected map envelope.
17.4 Choosing the Right Extension Pattern
Sooner or later you will need a unit operation that is not in the standard catalogue. There are three levels of extension, and they serve different purposes.
| Extension level | Best use | Trade-off |
|---|---|---|
| Helper function | One notebook, no process-system integration needed | Simple, but not a unit operation |
| Python unit prototype | Teaching, early experiments, Colab workflows | Easy to change, but less robust for production |
| Java unit operation | Shared library code, production models, serialization, automation API | More work, but testable and reusable |
A Python prototype can be useful when the purpose is to test process logic quickly. The Colab notebook notebooks/process/newunitoperation.ipynb uses this idea: define a Python class with an inlet stream, an outlet stream, a run method, and optional JSON reporting. Treat this as a notebook pattern, not as a replacement for a Java class when the behavior will be shared by several users.
from neqsim import jneqsim as J
from neqsim.process.unitop import unitop
class TemperatureLiftUnit(unitop):
def __init__(self, name="temperature lift"):
super().__init__(name)
self.inlet = None
self.outlet = None
self.delta_t_c = 0.0
def setInletStream(self, stream):
self.inlet = stream
self.outlet = stream.clone()
def setTemperatureLift(self, delta_t_c):
self.delta_t_c = delta_t_c
def getOutletStream(self):
return self.outlet
def run(self, id=None):
fluid = self.inlet.getFluid().clone()
fluid.setTemperature(self.inlet.getTemperature("C") + self.delta_t_c, "C")
self.outlet.setFluid(fluid)
self.outlet.run()
def toJson(self):
return {
"name": self.getName(),
"type": "TemperatureLiftUnit",
"delta_t_C": self.delta_t_c,
}
The production Java version should follow the same behavioral idea but use the NeqSim process equipment interfaces. That gives you serialization, copy support, validation hooks, process-system execution, unit introspection, and a place for JUnit tests. When you add a public Java unit, also add JavaDoc, Java 8 compatible code, validateSetup() checks where applicable, and at least one physical regression test.
17.5 Parameter Databases: Powerful, Global, and Easy to Misuse
The parameter database examples in NeqSim-Colab are worth studying before you edit any component data:
notebooks/PVT/parameter_database.ipynbshows the standardCOMPtable, the extended component database, and a simple custom component table.notebooks/PVT/parameter_database2.ipynbgoes further by replacing bothCOMPandINTER, so pure-component properties and binary interaction parameters are changed together.
That second pattern matters. Adding a new pure component without checking its binary interaction parameters can give deceptively smooth but unvalidated phase behavior. If you change TC, PC, or acentric factor, you should also decide whether each important binary pair uses a known, regressed, estimated, or zero interaction parameter.
Use this workflow for custom parameter work:
- Copy the baseline
COMP.csvand, when relevant,INTER.csvinto the study folder with a date and source note. - Edit the copied files, never the installed package resources.
- Replace the tables at the start of the notebook, before any fluid is built.
- Build a small benchmark: vapor pressure, density, saturation pressure, or a binary VLE point from independent data.
- Record a provenance table with filenames, row changes, sources, units, and validation deviations.
- Restart the Python session when switching back to the standard database.
from pathlib import Path
from neqsim import jneqsim as J
DB = J.util.database.NeqSimDataBase
study_data = Path("study_parameter_database").resolve()
DB.replaceTable("COMP", str(study_data / "COMP_user.csv"))
DB.replaceTable("INTER", str(study_data / "INTER_user.csv"))
fluid = J.thermo.system.SystemSrkEos(273.15 + 20.0, 50.0)
fluid.addComponent("methane", 0.90)
fluid.addComponent("my-new-component", 0.10)
fluid.setMixingRule("classic")
For short investigations, direct component edits can be convenient. They change the component object in the current fluid, not the database table. That makes them useful for sensitivity checks, but dangerous if they are hidden in the middle of a notebook.
component = fluid.getPhase(0).getComponent("my-new-component")
component.setTC(512.0, "K")
component.setPC(45.0, "bara")
component.setAcentricFactor(0.22)
component.setMolarMass(0.098, "kg/mol")
fluid.init(0)
A good rule is simple: database replacement is a study-basis decision, while direct component editing is a sensitivity experiment. Both must be visible in the notebook narrative and in any report assumptions.
17.6 Advanced Model Checklist
Before handing an advanced process model to another engineer, check the items below.
| Topic | Minimum check |
|---|---|
| Equipment physics | Class choice matches the engineering question, not just the available API |
| Thermodynamics | EOS, mixing rule, component database, and interaction database are recorded |
| Advanced exchanger | Stream roles, MITA, pressure drops, freeze/fouling/stress flags, and curves are reported |
| Turboexpander | Vendor map source, speed range, power balance, outlet T/P, and curve ratios are reported |
| Custom units | Prototype or Java implementation choice is stated, with tests for shared code |
| Verification | At least one benchmark, one mass/energy check, and one sensitivity case are included |
| Reproducibility | All custom CSVs, curves, and assumptions are saved with the model or task folder |
Advanced equipment makes a model more expressive. It also makes hidden assumptions more expensive. The way to keep control is to make every added piece of physics leave an auditable trace: a curve, a source, a benchmark, and a clear reason for being in the model.
Part VI: Optimization, Capstone, and Further Study
Process Optimization and Reproducible Advanced Models
Learning Objectives
After this chapter you will:
- Use the official NeqSim optimization documentation to choose the right optimizer for a process-modeling task.
- Turn a converged
ProcessSystemorProcessModelinto a bounded optimization problem with decision variables, objectives, and constraints. - Reproduce optimization examples from the NeqSim docs and example notebooks.
- Apply the workflow to a large integrated offshore topside model without losing traceability or reproducibility.
18.1 Start from the Official Docs
This chapter is the final step in the book's modelling arc. By this point you have built fluids, unit operations, steady-state flowsheets, integrated ProcessModel cases, reports, and restartable archives. Optimization should come after that foundation: first make the model converge and explain its base case, then decide which variables can move and which constraints must remain protected.
The public NeqSim documentation is the living reference for process optimization:
- Optimization overview: https://equinor.github.io/neqsim/process/optimization/OPTIMIZATION_OVERVIEW.html
- Process optimization landing page: https://equinor.github.io/neqsim/process/optimization/README.html
- Practical examples: https://equinor.github.io/neqsim/process/optimization/PRACTICAL_EXAMPLES.html
- Batch studies: https://equinor.github.io/neqsim/process/optimization/batch-studies.html
- Multi-objective optimization: https://equinor.github.io/neqsim/process/optimization/multi-objective-optimization.html
- External optimizers: https://equinor.github.io/neqsim/integration/EXTERNAL_OPTIMIZER_INTEGRATION.html
- Python optimization notebook: https://equinor.github.io/neqsim/examples/NeqSim_Python_Optimization.html
- Production optimizer tutorial: https://equinor.github.io/neqsim/examples/ProductionOptimizer_Tutorial.html
This chapter does not replace those pages. It shows how to read them as a process engineer: start from a converged flowsheet, define what can move, define what must stay safe, run a bounded search, and report the engineering decision.
18.2 The Optimization Ladder
NeqSim optimization is not one algorithm. It is a ladder of increasingly powerful methods:
| Task | Use | Typical decision |
|---|---|---|
| Parameter sweep | Plain Python loop or BatchStudy |
Which cooler outlet temperature is best? |
| Maximum throughput | ProcessOptimizationEngine |
What is max export flow at fixed inlet and outlet pressure? |
| General objective | ProductionOptimizer |
Minimize compression power or maximize net value. |
| Capacity bottlenecks | ProcessConstraintEvaluator and equipment constraints |
Which unit stops the next increment of flow? |
| Multi-objective trade-off | Pareto optimization | Throughput vs power vs emissions. |
| External optimizer | ProcessSimulationEvaluator with SciPy, NLopt, Pyomo, or BoTorch |
Custom research or machine-learning workflows. |
| Lifecycle uncertainty | Monte Carlo and tornado analysis | P10/P50/P90 value and risk ranking. |
The most common mistake is starting too high on the ladder. If a five-point sweep answers the engineering question, use the sweep. If the decision is multi-variable or constrained, escalate to the built-in optimizers.
18.3 A Runnable Mini Optimization
This example uses a small export-compressor flowsheet and searches for the largest feasible feed flow. Feasibility is defined by two simple limits: compressor power below 6 MW and discharge temperature below 140 degC. A real study would use compressor curves, surge margin, separator K-factor, pipeline velocity, and utility constraints; the loop shape is the same.
from pathlib import Path
from neqsim import jneqsim
import matplotlib.pyplot as plt
Path("../figures").mkdir(parents=True, exist_ok=True)
SystemSrkEos = jneqsim.thermo.system.SystemSrkEos
Stream = jneqsim.process.equipment.stream.Stream
Compressor = jneqsim.process.equipment.compressor.Compressor
Cooler = jneqsim.process.equipment.heatexchanger.Cooler
ProcessSystem = jneqsim.process.processmodel.ProcessSystem
def make_export_gas(pressure_bara=45.0):
gas = SystemSrkEos(303.15, pressure_bara)
for name, amount in [
("nitrogen", 0.015),
("CO2", 0.020),
("methane", 0.855),
("ethane", 0.065),
("propane", 0.030),
("n-butane", 0.015),
]:
gas.addComponent(name, amount)
gas.setMixingRule("classic")
return gas
def run_export_case(flow_kg_h, outlet_pressure_bara=150.0):
feed = Stream("export feed", make_export_gas())
feed.setFlowRate(float(flow_kg_h), "kg/hr")
feed.setPressure(45.0, "bara")
feed.setTemperature(30.0, "C")
compressor = Compressor("K-500 export", feed)
compressor.setOutletPressure(outlet_pressure_bara, "bara")
compressor.setPolytropicEfficiency(0.78)
aftercooler = Cooler("E-500 aftercooler", compressor.getOutletStream())
aftercooler.setOutTemperature(313.15)
process = ProcessSystem()
for unit in (feed, compressor, aftercooler):
process.add(unit)
process.run()
power_mw = compressor.getPower() / 1.0e6
discharge_c = compressor.getOutletStream().getTemperature("C")
feasible = power_mw <= 6.0 and discharge_c <= 140.0
return {
"flow_kg_h": float(flow_kg_h),
"power_MW": power_mw,
"discharge_C": discharge_c,
"feasible": feasible,
}
The search is deliberately transparent. It is the same calculation you would show in a design review before introducing a black-box optimizer.
flows = [40000, 60000, 80000, 100000, 120000, 140000, 160000, 180000]
rows = [run_export_case(flow) for flow in flows]
feasible_rows = [row for row in rows if row["feasible"]]
best = max(feasible_rows, key=lambda row: row["flow_kg_h"])
print("Flow kg/h | Power MW | Discharge C | Feasible")
for row in rows:
print(
f"{row['flow_kg_h']:9.0f} | {row['power_MW']:8.2f} | "
f"{row['discharge_C']:11.1f} | {row['feasible']}"
)
print(
f"Selected case: {best['flow_kg_h']:.0f} kg/h at "
f"{best['power_MW']:.2f} MW"
)
A plot makes the operating limit visible.
fig, ax1 = plt.subplots(figsize=(7.0, 4.2))
flow = [row["flow_kg_h"] / 1000.0 for row in rows]
power = [row["power_MW"] for row in rows]
temperature = [row["discharge_C"] for row in rows]
ax1.plot(flow, power, marker="o", label="Compressor power")
ax1.axhline(6.0, color="tab:red", linestyle="--", label="Power limit")
ax1.set_xlabel("Feed flow (t/h)")
ax1.set_ylabel("Power (MW)")
ax1.grid(True, alpha=0.3)
ax2 = ax1.twinx()
ax2.plot(flow, temperature, marker="s", color="tab:orange", label="Discharge T")
ax2.axhline(140.0, color="tab:purple", linestyle=":", label="Temperature limit")
ax2.set_ylabel("Discharge temperature (degC)")
lines, labels = ax1.get_legend_handles_labels()
lines2, labels2 = ax2.get_legend_handles_labels()
ax1.legend(lines + lines2, labels + labels2, loc="upper left")
ax1.set_title("Export-compressor throughput screening")
fig.tight_layout()
fig.savefig("../figures/ch18_throughput_optimization.png", dpi=150, bbox_inches="tight")
print("Saved ../figures/ch18_throughput_optimization.png")
This is optimization in its most audit-friendly form: every case is visible, every constraint is explicit, and the selected point can be defended.
18.4 When to Use the Built-In Optimizers
Once the problem moves beyond a simple sweep, use the NeqSim optimizer stack. The official docs map the choice:
- Use
ProcessOptimizationEngine.findMaximumThroughput(...)for throughput at fixed pressure boundaries and equipment limits. - Use
ProductionOptimizerwhen you need custom objectives, several decision variables, or Pareto trade-offs. - Use
BatchStudywhen you want a structured design-of-experiments table. - Use
MonteCarloSimulatorwhen the result must be reported as P10/P50/P90. - Use
ProcessSimulationEvaluatorwhen a Python optimizer should drive a NeqSim model through a clean black-boxevaluate(x)interface. - Use
SQPoptimizerfor constrained nonlinear programs when variables are well scaled and bounds are physically meaningful.
The Python pattern for the current API is:
from scipy.optimize import minimize
from neqsim import jneqsim
ProcessSimulationEvaluator = jneqsim.process.util.optimizer.ProcessSimulationEvaluator
evaluator = ProcessSimulationEvaluator(process)
evaluator.addParameter("K-500 export", "outletPressure", 120.0, 170.0, "bara")
evaluator.addObjective("power", lambda p: p.getUnit("K-500 export").getPower("kW"))
evaluator.addConstraintUpperBound(
"temperature",
lambda p: p.getUnit("K-500 export").getOutletStream().getTemperature("C"),
140.0,
)
bounds = [tuple(pair) for pair in evaluator.getBounds()]
x0 = list(evaluator.getInitialValues())
def objective(x):
result = evaluator.evaluate(x)
return float(result.getObjectives()[0])
solution = minimize(objective, x0=x0, bounds=bounds, method="L-BFGS-B")
The advantage is separation of concerns: NeqSim owns the process model and unit conversions; SciPy owns the numerical search.
Batch and Parallel Scenario Studies
The Colab notebook notebooks/process/comparesimulations.ipynb shows a useful optimization pre-step: run many independent process cases, collect the KPIs, and only then decide whether to optimize, fit a surrogate model, or stop with a plain sensitivity table. In that notebook the workflow is:
- Define a typed
ProcessInputobject and a typedProcessOutputobject. - Wrap the flowsheet in
getprocess,updateinput,run_simulation, andgetoutputhelper functions. - Run a base case and save/reopen the model as
.neqsim. - Run a 100-case randomized parametric study.
- Use the generated case table for optimization or machine-learning examples.
For a small number of cases, a Python loop is easiest to audit. For hundreds or thousands of independent cases, use NeqSim's managed thread pool. The current parallel-simulation documentation recommends runAsTask(), which returns a Java Future, rather than older unmanaged runAsThread() calls. The important engineering rule is that each worker gets its own independent process model; do not share a mutable ProcessSystem, stream, or fluid object across running cases.
from neqsim import jneqsim
NeqSimThreadPool = jneqsim.util.NeqSimThreadPool
# Optional: keep CPU-intensive work close to the number of physical cores.
NeqSimThreadPool.setPoolSize(8)
def build_case(case_id, feed_pressure_bara):
"""Return a fresh, independent ProcessSystem for one scenario."""
process = build_integrated_process_model(case_id=case_id)
auto = process.getAutomation()
auto.setVariableValueSafe("Feed.pressure", feed_pressure_bara, "bara")
return process
case_pressures = [35.0, 40.0, 45.0, 50.0, 55.0, 60.0]
processes = [build_case(i, pressure) for i, pressure in enumerate(case_pressures)]
futures = [process.runAsTask() for process in processes]
# Wait for completion. For production studies, use future.get(timeout, unit)
# and handle cancellation or failed cases explicitly.
for future in futures:
future.get()
rows = []
for pressure, process in zip(case_pressures, processes):
auto = process.getAutomation()
rows.append(
{
"feed_pressure_bara": pressure,
"export_flow_kg_h": auto.getVariableValue(
"Export.outletStream.flowRate", "kg/hr"
),
"compression_power_kW": auto.getVariableValue("ExportCompressor.power", "kW"),
}
)
Use this pattern for sensitivity studies, Monte Carlo batches, and training-data generation. Use a bounded batch size when each process model is large, collect results immediately after each batch, and record failures as rows with status and diagnostics rather than silently dropping them.
18.5 Examples Worth Including in Your Study Folder
The local repository already contains examples that form a good learning sequence. Treat them as templates, not as black boxes.
| Example | What to learn |
|---|---|
docs/examples/NeqSim_Python_Optimization.ipynb |
Python/SciPy wrapper around a NeqSim process, constraints, Pareto, global optimization. |
docs/examples/ProductionOptimizer_Tutorial.ipynb |
Built-in production optimizer, search modes, constraints, and reporting. |
examples/notebooks/process_optimization_enhancements.ipynb |
Multi-variable adjustment and optimizer enhancements. |
examples/notebooks/capacity_constraints_optimization_demo.ipynb |
Equipment utilization, bottleneck analysis, and constraint visualization. |
examples/notebooks/pipeline_network_optimization.ipynb |
Network/choke optimization, Pareto trade-off, sparse solver comparison. |
examples/notebooks/production_optimization_topside_coupling.ipynb |
Coupled reservoir/network/topside decisions and debottlenecking. |
examples/notebooks/full_field_optimization_reservoirs_to_export.ipynb |
Full-field optimization from reservoirs through export with lifecycle re-optimization. |
For this book, the most useful path is:
- Run the Python optimization tutorial.
- Run the capacity-constraints notebook and inspect the bottleneck dashboard.
- Run the topside-coupling notebook.
- Run the full-field notebook.
- Apply the same optimizer wrapper to the integrated process model from Chapter 19.
18.6 Applying Optimization to the Large Integrated Model
The Chapter 19 notebook is large because the plant is large. Optimization should not make it harder to reason about. Wrap the model with a small function that accepts decision variables and returns a compact result dictionary.
Good first decision variables are:
- HP separator pressure.
- MP separator pressure.
- LP separator pressure.
- Export-compressor discharge pressure.
- Intercooler outlet temperature.
- Recycle or anti-surge valve opening.
- Feed split between manifolds or trains.
Good first objectives are:
- Maximize export gas flow.
- Maximize stabilized oil production.
- Minimize compression power per tonne of export gas.
- Minimize cooling duty.
- Maximize value after power and emissions penalties.
Good first constraints are:
- Compressor power and discharge temperature limits.
- Compressor surge and stonewall margins.
- Separator gas load factor and liquid residence time.
- Pipeline pressure drop or arrival pressure.
- Product specifications: dew point, RVP/TVP, water content, or CO2.
- Mass-balance closure and model convergence.
A production-grade wrapper looks like this:
def evaluate_integrated_case(x, base_config):
hp_pressure, mp_pressure, export_pressure, intercooler_c = x
plant = build_integrated_process_model(base_config)
auto = plant.getAutomation()
auto.setVariableValueSafe("Separation::V-101 HP.pressure", hp_pressure, "bara")
auto.setVariableValueSafe("Separation::V-102 MP.pressure", mp_pressure, "bara")
auto.setVariableValueSafe("Export::K-301.outletPressure", export_pressure, "bara")
auto.setVariableValueSafe("Export::E-302.outletTemperature", intercooler_c, "C")
plant.run()
export_flow = auto.getVariableValue("Export::E-302.outletStream.flowRate", "kg/hr")
power_kw = sum(
auto.getVariableValue(address, "W") / 1000.0
for address in [
"Recompression::K-201.power",
"Recompression::K-202.power",
"Export::K-301.power",
]
)
feasible = check_integrated_constraints(plant)
return {
"export_flow_kg_h": export_flow,
"power_kW": power_kw,
"specific_power_kWh_t": power_kw / max(export_flow / 1000.0, 1.0),
"feasible": feasible,
}
Notice the boundary: the full model stays in build_integrated_process_model, while the optimizer sees a simple vector in and a simple dictionary out.
18.7 Full-Model Throughput-to-Bottleneck Workflow
The natural Chapter 19 optimization question is: how much can one or more producer feeds be increased before fixed topside equipment reaches a bottleneck? That is a good practical task, provided the model is wrapped at the same level as the engineering decision.
In the Chapter 19 structure, producer rates are not a single global feed. They enter through the ProcessInput contract as forecast rates and scale factors, then become named streams in the well-feed ProcessSystem: examples include cluster_a hc feed, main reference_stream_b hc feed, Offshore Area East Feed, Offshore Area C Feed, and the calibration or third-party feeds. The optimizer should therefore vary producer multipliers in ProcessInput, rebuild or refresh the full ProcessModel, run it, and evaluate all capacity constraints across all process areas.
With equipment sizes already set, do not call auto-sizing inside the optimization. Treat the installed equipment as fixed constraints. For each separator, compressor, pump, heat exchanger, valve, pipe, and column, make sure the relevant capacity constraints are populated from mechanical design or design data. NeqSim's capacity framework can then report utilization, margin, and the active bottleneck.
The current capability picture is:
| Need | What NeqSim can do now | Practical limitation for Chapter 19 |
|---|---|---|
| Run the full plant model | ProcessModel composes many ProcessSystem areas, exposes area-qualified automation addresses, and supports fast large-model execution. |
The main optimizer helper classes are mostly typed around ProcessSystem or ProcessModule, not ProcessModel. |
| Vary one feed stream | ProcessOptimizationEngine can vary one named feed stream and find maximum throughput. |
Chapter 19 has many producer feeds and scale factors, so one-stream throughput search is too narrow. |
| Evaluate bottlenecks | ProcessConstraintEvaluator, ProcessOptimizationEngine.evaluateAllConstraints(), and DebottleneckAnalyzer rank equipment constraints. |
ProcessConstraintEvaluator and DebottleneckAnalyzer evaluate one ProcessSystem at a time; a full ProcessModel needs aggregation over all areas. |
| Use external optimizers | ProcessSimulationEvaluator provides a clean black-box bridge for SciPy, NLopt, Pyomo, and similar tools. |
It currently accepts ProcessSystem; full-model studies need a Python wrapper or a Java ProcessModel equivalent. |
| Fixed equipment limits | CapacityConstraint and CapacityConstrainedEquipment can hold design values, current values, utilization, severity, and margins. |
The Chapter 19 wrapper must load real design capacities before optimization, otherwise the bottleneck report only reflects defaults. |
A practical first implementation is an auditable Python search around the ProcessModel, using NeqSim's capacity evaluators per area:
import copy
from neqsim import jneqsim
ProcessConstraintEvaluator = jneqsim.process.util.optimizer.ProcessConstraintEvaluator
def apply_producer_multipliers(inp, multipliers):
"""Apply producer multipliers to the Chapter 19 input object."""
inp.cluster_a_scale_factor_intermediate = multipliers["cluster_a"]
inp.cluster_b_scale_factor_intermediate = multipliers["cluster_b"]
inp.offshore_east_scale_factor_intermediate = multipliers["offshore_east"]
inp.condensate_area_scale_factor_intermediate = multipliers["condensate_area"]
return inp
def evaluate_chapter15_capacity(base_input, multipliers):
"""Return objective and active bottleneck for one full-model case."""
inp = apply_producer_multipliers(copy.deepcopy(base_input), multipliers)
plant = build_chapter15_process_model(inp)
plant.enableFastLargeModelMode()
plant.run()
bottlenecks = []
for area_name in list(plant.getProcessSystemNames()):
area = plant.get(area_name)
result = ProcessConstraintEvaluator(area).evaluate()
bottlenecks.append(
{
"area": str(area_name),
"equipment": str(result.getBottleneckEquipment()),
"constraint": str(result.getBottleneckConstraint()),
"utilization": float(result.getBottleneckUtilization()),
"feasible": bool(result.isFeasible()),
}
)
active = max(bottlenecks, key=lambda row: row["utilization"])
auto = plant.getAutomation()
export_gas = auto.getVariableValue("export gas::export gas stream.flowRate", "MSm3/day")
return {
"multipliers": multipliers,
"export_gas_MSm3_day": float(export_gas),
"max_utilization": active["utilization"],
"active_bottleneck": active,
"feasible": active["utilization"] <= 1.0,
"all_bottlenecks": bottlenecks,
}
Start with a scalar multiplier applied to all selected producers and use binary search to find the first infeasible case. Then move to a coordinate search or ProductionOptimizer/SciPy wrapper for individual producer multipliers. For a large model, save every evaluated case to CSV or JSON with multipliers, export rate, model convergence status, active bottleneck, and every constraint margin. That table is often more valuable than the final optimum because it explains why the model stopped increasing production.
Several NeqSim improvements would make this workflow practical for routine studies:
- Add a
ProcessModelSimulationEvaluatorparallel toProcessSimulationEvaluator. It should accept area-qualified automation addresses, run aProcessModel, expose objective and constraint vectors, and export the optimization problem as JSON. - Extend
ProcessConstraintEvaluator,DebottleneckAnalyzer, andProcessOptimizationEnginewithProcessModelconstructors. The result should prefix bottlenecks with area names, for exampleExport train A::K-301/power. - Add a ready-made throughput helper for multi-producer studies: inputs are a base
ProcessModel, producer variables with bounds and units, fixed equipment capacities, and an objective such as export gas or total value. - Add a capacity-data loader that maps installed equipment sizes to
CapacityConstraintobjects. This is the step that turns a converged model into an operations bottleneck model. - Add an optimization trace object with case rows, failed-case diagnostics, convergence summaries, active bottlenecks, and saved
ProcessModelStatereferences. This would make the result directly reportable.
Until those helpers exist, the recommended production workflow is: build the full ProcessModel, vary producer multipliers through ProcessInput, run each case as an independent model, aggregate capacity utilization across all areas, and use the first active bottleneck as the stopping criterion.
18.8 Reproducibility Rules for Advanced Optimization
Optimization can create impressive numbers quickly. The discipline is to make those numbers reproducible:
- Save the base-case state before optimization.
- Record every decision variable with unit, lower bound, upper bound, and base value.
- Record every constraint with unit, limit, current value, and margin.
- Keep the objective formula in code and in prose.
- Export every evaluated case to CSV or JSON.
- Save the selected case as a
ProcessModelStatefile. - Add a figure showing the objective and the active constraint.
- Add an engineering recommendation: operate here, debottleneck this, or do not proceed.
For large integrated offshore work, also include the source-data manifest: process-condition spreadsheets, E300 fluid files, production forecast inputs, and any plant-data snapshots. A reader should be able to rebuild the exact base case before they trust the optimization result.
For the running case, the optimization result is complete only when the selected throughput, active constraint, saved model state, and engineering interpretation all point to the same decision. The usual final sentence should not be "the optimizer selected 120 t/h". It should be closer to: "Use 120 t/h as the screening throughput until a compressor curve, surge margin, and product-spec check are added; the active screening constraint is 6 MW shaft power."
Reproducible Notebook Results
The outputs below were captured from the companion Jupyter notebook generated from this chapter's code blocks. They show the expected figures and text results when the examples are run.
Example 2 line 130
Notebook: chapters/ch18_process_optimization/notebooks/chapter_scripts.ipynb
Flow kg/h | Power MW | Discharge C | Feasible
40000 | 1.86 | 123.3 | True
60000 | 2.80 | 123.3 | True
80000 | 3.73 | 123.3 | True
100000 | 4.66 | 123.3 | True
120000 | 5.59 | 123.3 | True
140000 | 6.53 | 123.3 | False
160000 | 7.46 | 123.3 | False
180000 | 8.39 | 123.3 | False
Selected case: 120000 kg/h at 5.59 MW
Example 3 line 150
Notebook: chapters/ch18_process_optimization/notebooks/chapter_scripts.ipynb
Saved ../figures/ch18_throughput_optimization.png
Example 3 line 150
Notebook: chapters/ch18_process_optimization/notebooks/chapter_scripts.ipynb
Figure 18.1: Export-compressor throughput screening generated from Example 3.
Observation. Figure 18.1 shows compressor power crossing the 6 MW screening limit between 120 and 140 t/h, while discharge temperature stays below the 140 degC screening limit across the tested flow range.
Mechanism. At fixed suction and discharge pressure, power rises almost linearly with mass flow because each additional tonne per hour requires the same approximate compression head in this simplified screening model.
Implication. The selected 120 t/h point is power-limited rather than temperature-limited, so compressor duty is the active bottleneck for this screening case.
Recommendation. Use 120 t/h as the auditable screening throughput and replace the fixed-efficiency compressor with a vendor curve before making a design commitment.
18.9 Exercises
- Exercise 18.1: Extend the mini optimization above with a second decision variable: compressor outlet pressure from 120 to 170 bara. Plot power and feasible flow as a contour map.
- Exercise 18.2: Run
examples/notebooks/capacity_constraints_optimization_demo.ipynband rewrite its utilization chart for the Chapter 19 separation train. - Exercise 18.3: Wrap the Chapter 19 model in an
evaluate_case(x)function and run a five-case separator-pressure sweep. Report the selected case, active constraint, and mass-balance error. - Exercise 18.4: Read the official optimization overview and write a one-page justification for whether your integrated-process study should use
ProcessOptimizationEngine,ProductionOptimizer,BatchStudy, or an external SciPy optimizer.
- Exercise 18.5: Assemble your Capstone Portfolio. Include the problem statement, fluid definition, runnable notebook, validation evidence, figures,
results.json, saved model state, and a decision memo. Grade it against the back-matter rubric before sharing it.
Worked Example: Generic Integrated Offshore Process Model
Learning Objectives
After this chapter you will:
- Reconstruct the full structure of an advanced multi-area offshore process-model notebook without relying on site-specific names.
- Understand how scenario controls, yearly input data, feed allocation, process-area builders, reporting, and serialization connect.
- Use
ProcessSystemfor area-level models andProcessModelfor integrated plant execution. - Write engineering results to text files,
results.json,.neqsimmodel archives, and portable.xipreview packages.
19.0 How to Read This Chapter
This chapter is roughly 80 pages. If you read it linearly from front to back in one sitting you will lose the thread. The chapter is structured as a reference build — a working full-plant model with every detail exposed — and most readers should sample it.
The 30-Minute Path
If you only have 30 minutes, read:
- §19.1 — model scope and the anonymisation convention.
- §19.2 and §19.3 — scenario controls and the
ProcessInputdataclass. These are the two patterns you will copy into your own project. - §19.6 — the calibration separator and main train. This is the pedagogical core: it shows how the eight equipment families from Chapter 8 compose into a real train.
- §19.11 (last section) — the result hand-off,
results.json, and the.neqsimarchive.
Skip the inter-area routing, fractionation, and utilities sections on a first pass; they repeat the same patterns at larger scale.
The Full Path
If you are building your own multi-area model, read the whole chapter in order with the notebook open. Treat each section as a template: copy the pattern, rename the streams, replace the composition.
The Areas at a Glance
The full model has the following process areas; each lives in its own builder function and returns a ProcessSystem that is composed into a plant-level ProcessModel:
| Area | Inputs | Outputs | Section |
|---|---|---|---|
| Calibration sep. | Reservoir feed | Tuned splits | 19.6 |
| Main separation A | Manifold A | Oil, gas, water | 19.6 |
| Main separation B | Manifold B | Oil, gas, water | 19.6 |
| Inter-area routing | Train liquid outlets | Stabiliser feed | 19.7 |
| Gas conditioning | Stage compressors’ gas | Treated export gas | 19.8 |
| Fractionation | Stabilised condensate | LPG, naphtha | 19.9 |
| Utilities | Heat & power demand | Steam, cooling | 19.10 |
| Reporting / export | All areas | results.json, .neqsim |
19.11 |
What You Will Not Find Here
No new NeqSim API surface. Every major class used in this chapter has appeared in Chapters 5-18. If a line confuses you, the prerequisite map in §19.0 below tells you exactly which chapter to revisit.
19.1 Model Scope and Anonymisation
This chapter expands the advanced process-model notebook into a complete, generic worked example. The original notebook is an industrial multi-area offshore model; the version below removes field, operator, and facility-specific names while preserving the modelling structure, section order, and code. Treat names such as Area A, Gas conditioning, TerminalAsset, ReferenceStreamA, and EXPORT_COMPRESSOR as placeholders for your own asset, plant area, and equipment tags.
This is the capstone chapter of the book. The intent is not to introduce new API families, but to assemble the earlier material into one auditable model: fluids from Part II, equipment and ProcessSystem patterns from Part III, and automation, reporting, serialization, and service-facing ideas from Part IV, equipment-envelope checks from Part V, and optimization discipline from Chapter 18. Chapter 20 then points to smaller runnable notebooks for individual patterns.
Prerequisite and Documentation Map
Chapter 19 is deliberately a synthesis chapter: it should not introduce a major NeqSim operation without an earlier explanation. Use this map when a cell in the worked model uses an object or method you want to revisit.
| Chapter 19 topic | Earlier book coverage | NeqSim docs for details |
|---|---|---|
jneqsim imports and direct Java class access |
Chapters 3-4 | https://equinor.github.io/neqsim/java-getting-started.html |
| SRK fluids, components, plus fractions, and phase envelopes | Chapters 5-7 | https://equinor.github.io/neqsim/thermo/fluid_creation_guide.html and https://equinor.github.io/neqsim/thermo/pvt_fluid_characterization.html |
| Streams, separators, scrubbers, valves, pumps, compressors, coolers, mixers, splitters, and pipelines | Chapter 8 | https://equinor.github.io/neqsim/process/equipment/ |
ProcessSystem, stream wiring, recycles, adjusters, and ProcessModel area composition |
Chapter 9 | https://equinor.github.io/neqsim/process/processmodel/process_system.html and https://equinor.github.io/neqsim/process/processmodel/process_model.html |
| NGL recovery, fractionation, absorbers, and distillation columns | Chapter 10 | https://equinor.github.io/neqsim/process/equipment/distillation.html |
| Dynamic control ideas such as controllers, anti-surge loops, and transient checks | Chapter 11 | https://equinor.github.io/neqsim/simulation/dynamic_simulation_guide.html |
Automation addresses, state snapshots, .neqsim archives, and .xip review packages |
Chapter 12 | https://equinor.github.io/neqsim/simulation/automation/automation_foundations.html and https://equinor.github.io/neqsim/simulation/process_serialization.html |
Results tables, mass-balance checks, plots, results.json, and report-ready outputs |
Chapter 13 | https://equinor.github.io/neqsim/simulation/process_automation.html |
| Web/API style execution and JSON process exchange | Chapter 14 | https://equinor.github.io/neqsim/integration/web_api_json_process_builder.html |
The process-optimization workflow for this kind of model is developed just before the worked example in Chapter 18. Chapter 19 itself focuses on creating a converged, auditable base case that can be swept, optimized, or packaged for review.
19.2 Scenario, Case, and Result Folder Controls
The first notebook cells define the simulation year, case label, result directory, and scenario switches. These values are deliberately simple strings and integers because they are used everywhere else: Excel row selection, production-forecast lookup, conditional feed activation, result-file naming, and model archive naming. A professional notebook keeps these controls at the top so a reader can rerun one case without editing later process functions.
19.3 Input Data Object and Process Conditions
The ProcessInput dataclass is the contract between data sources and model-building functions. It collects pressures, temperatures, split factors, efficiencies, bypass fractions, route dimensions, and result paths in one object. The Excel update function fills that object for the selected year. This is the central decoupling move: the process code reads attributes from inp, while the data-loading code decides where those attributes came from.
19.4 Production Forecast and Feed Preparation
The production-data section reads forecasted oil, gas, and water rates and converts those rates into NeqSim streams. The notebook creates default hydrocarbon and water streams using an E300-style characterization, then uses small helper functions to clone fluids, set flow rates, and keep pressure/temperature consistent. In a live study this is where reservoir profiles, well allocation, and composition updates enter the process model.
19.5 Manifolds, Splits, and Well Feed Model
The manifold functions split the total feed into named train feeds and satellite or third-party feeds. The important pattern is object-reference wiring: each output stream is a NeqSim stream object that can be passed directly into later ProcessSystem builders. Split factors are explicit, so sensitivity studies can change train loading without rewriting the separation model.
19.6 Calibration Separator and Main Separation Trains
The calibration-stage model is a small process used to tune separator assumptions and inlet conditioning before the full train is assembled. The main liquid-processing train then creates heaters, throttling valves, separators, pumps, mixers, recompression equipment, and recycle links. Two parallel train instances are built from the same function, which keeps the model maintainable while still representing different feed streams and operating points.
19.7 Inter-Area Routing and Produced-Water Handling
The route model connects liquid-processing and gas-processing areas. In the full notebook this includes a manifold/pipe calculation and simplified water-treatment sections. The pipe calculation is more than a pressure drop: it checks arrival pressure, temperature, velocity, Reynolds number, viscosity, density, and volumetric flow. Those values decide whether the downstream process receives a physically plausible feed.
19.8 Gas Conditioning, Turboexpander, and Dew-Point Control
The gas-conditioning section builds a dew-point-control process with cooling, separation, turboexpansion, recompression, bypasses, fuel-gas splitting, and recycle or routing streams. The model evaluates whether rich gas should be cooled through the low- temperature section or bypassed to compression. That makes it useful for both product- specification checks and power/cooling-duty studies.
19.9 NGL Recovery and Fractionation
The NGL section connects gas-conditioning liquid recovery to a column model. The code builds mixers, splitters, valves, pumps, heat exchangers, and distillation-column objects to divide LPG or NGL products between export, injection, and liquid-product blending. The section is intentionally parameterized because column pressure, reflux, reboiler duty, and split ratios are common optimization variables.
19.10 Export Compression and Gas Allocation
The export-compressor section combines gas from multiple trains, compresses to export or injection pressure, cools and scrubs between stages, and splits gas to export, lift, injection, fuel, and bypass destinations. The code reports Wobbe index, component composition, cricondenbar, and compressor power because these are the quantities normally checked against gas-quality and compressor-envelope requirements.
19.11 Liquid Product Mixing and Export Route
The liquid-export section mixes stabilized liquid, NGL return, condensate or third-party feeds, and terminal-bound streams. It then pumps and routes the combined liquid through a pipeline model. Arrival pressure and temperature are checked before a terminal or downstream-processing model is run.
19.12 Full ProcessModel Composition
The full model is a ProcessModel containing multiple named ProcessSystem areas. Each area is still a normal process system, but the model-level object gives one run command, one convergence summary, one automation facade, and one serialization boundary. This is the right pattern for large plants because each area can be tested independently before integration.
19.13 Reporting, Results Files, and JSON
The reporting cells are not cosmetic. They extract the values that turn a simulation into a reusable engineering result: export rates, product qualities, compressor power, cooler duties, mass-balance closure, gas composition, Wobbe index, vapor-pressure checks, and selected operating temperatures and pressures. The notebook writes both human-readable text files and structured case tables. The additional generic results.json pattern below is the preferred interchange format for reports, dashboards, and automated review.
19.14 Terminal Processing Model
The terminal section models downstream liquid handling: cavern or storage conditioning, pumping, water wash, heat integration, hot-oil heating, stabilization, gas compression, condensation, and fractionation. Even if a reader does not need this exact terminal layout, the pattern is useful: downstream processing is another ProcessSystem that receives the upstream liquid export stream and is added to the same model or run as a linked model.
19.15 Serialization, Tags, and Final Mass-Balance Audit
The last notebook cells save the model, assign readable tag names, and run mass-balance checks over initialized unit operations. The serialized .neqsim archive preserves the full Java object graph. The .xip package shown below is a portable engineering package: it bundles .neqsim, results.json, text reports, and selected inputs into one zip-compatible file for transfer or review.
19.16 Generic Reporting and Serialization Pattern
The source notebook contains text-file reporting functions and model-save cells. The code below is the generic, preferred end-of-notebook pattern: run the full model, extract values through the automation API or direct unit access, write a stable results.json, save the model as .neqsim, save a Git-friendly state JSON, and package the evidence as a zip-compatible .xip file.
from pathlib import Path
import json
import zipfile
ProcessModelState = neqsim.process.processmodel.lifecycle.ProcessModelState
CASE_DIR = Path(input_parameters.results_path)
CASE_DIR.mkdir(parents=True, exist_ok=True)
integrated_process.run()
auto = integrated_process.getAutomation()
def safe_value(address, unit):
try:
return auto.getVariableValue(address, unit)
except Exception:
return None
results = {
"case": {
"year": int(input_parameters.Year),
"case": str(input_parameters.simulation_case),
"model_scope": "generic integrated offshore process model",
},
"key_results": {
"gas_export_MSm3_day": safe_value("Gas Export::export gas stream.flowRate", "MSm3/day"),
"liquid_export_idSm3_hr": safe_value("Liquid Export::oil pump.outletStream.flowRate", "idSm3/hr"),
"lpg_export_idSm3_hr": safe_value("NGL Recovery::lpg production stream.flowRate", "idSm3/hr"),
"gas_injection_MSm3_day": safe_value("Gas Export::gas injection stream.flowRate", "MSm3/day"),
"gas_lift_MSm3_day": safe_value("Gas Export::gas lift stream.flowRate", "MSm3/day"),
"mass_balance_error_pct": float(mass_balance),
},
"quality_checks": {
"wobbe_index_MJ_m3": safe_value("Gas Export::export gas stream.WI", "MJ/m3"),
"liquid_rvp_37_8C_bara": None,
"liquid_tvp_30C_bara": None,
},
"validation": {
"acceptance_criteria_met": abs(float(mass_balance)) < 1.0,
"mass_balance_limit_pct": 1.0,
},
"approach": (
"Multi-area ProcessModel assembled from feed preparation, separation, "
"gas conditioning, NGL recovery, export compression, liquid export, "
"transport, and terminal processing ProcessSystems."
),
"conclusions": "Use the active constraints and product-quality checks to select the operating case.",
}
results_path = CASE_DIR / "results.json"
with results_path.open("w", encoding="utf-8") as handle:
json.dump(results, handle, indent=2)
# Full Java object graph. This is the restartable model archive.
neqsim_path = CASE_DIR / "integrated_process_model.neqsim"
integrated_process.saveToNeqsim(str(neqsim_path))
# Lightweight lifecycle snapshot. This is useful for diffs and review.
state_path = CASE_DIR / "integrated_process_model_state.json"
state = ProcessModelState.fromProcessModel(integrated_process)
state.saveToFile(str(state_path))
# Portable engineering exchange package. The .xip extension is a project convention;
# the container is a standard zip archive with results, model state, and reports.
xip_path = CASE_DIR / "integrated_process_model.xip"
with zipfile.ZipFile(xip_path, "w", compression=zipfile.ZIP_DEFLATED) as package:
package.write(results_path, arcname="results.json")
package.write(neqsim_path, arcname="model/integrated_process_model.neqsim")
package.write(state_path, arcname="model/integrated_process_model_state.json")
for text_report in CASE_DIR.glob("results_*.txt"):
package.write(text_report, arcname=f"reports/{text_report.name}")
print(f"Wrote {results_path}")
print(f"Wrote {neqsim_path}")
print(f"Wrote {state_path}")
print(f"Wrote {xip_path}")
19.17 Runnable Anonymous Companion Notebook
The full generalized listing below preserves the structure of the source process model, but it is intentionally marked no-execute because the original implementation expects project workbooks, fluid-characterization exports, and case-specific result folders. For a reader who wants to run the pattern immediately, this chapter also includes the self-contained companion notebook anonymous_integrated_process_model.ipynb in the chapter's notebooks folder.
That notebook embeds all information it needs directly in its cells: a synthetic production case, a component table, separator pressures, compressor efficiencies, cooler temperatures, reporting paths, and product-accounting checks. Running it builds generic separation, recompression, gas-export, liquid-export, and terminal-stabilization ProcessSystem areas, adds them to one ProcessModel, executes the model, prints the expected rates and compressor powers, and writes these artifacts under notebooks/results/anonymous_integrated_case/:
results.jsonwith embedded inputs, key results, validation status, figure captions, and figure discussion metadata.anonymous_integrated_process_model.neqsimwith the restartable Java object graph.anonymous_integrated_process_model_state.jsonwith a lightweight lifecycle snapshot.anonymous_integrated_process_model.xip, a zip-compatible package containing the JSON results, model files, and generated figure PNG files.figures/product_flow_summary.pngandfigures/compression_power_summary.pngunder the notebook results folder, with book-facing copies in this chapter'sfigures/directory.
The companion notebook is anonymous and reproducible by design: it does not read Excel files, CSV files, absolute paths, private helper modules, or asset-specific names.
Expected main cell outputs from the executed anonymous notebook are:
| Notebook cell | Main output to expect |
|---|---|
| 3 | Results directory: results/anonymous_integrated_case |
| 5 | Embedded case anonymous_base, year 2026, feed 120000 kg/h at 60 C and 70 bara; 11 synthetic components from nitrogen to n-nonane |
| 7 | No printed output; defines the area-builder functions |
| 9 | ProcessModel converged in 2 iterations; all five process areas solved |
| 11 | Product flow 120000 kg/h, mass-balance error about 0.0%, export gas 1.125 MSm3/day, terminal liquid 87.06 idSm3/h, total compression power 2.283 MW |
| 13 | Displays and saves product_flow_summary.png and compression_power_summary.png |
| 15 | Writes results.json, .neqsim, lifecycle-state JSON, .xip, and the two PNG figures |
The executed notebook figures are included directly in the book. They are not static illustrations: both are generated from the simulated stream rates and compressor powers in the preceding notebook cells.
Figure 19.1: Product and drain flow distribution. The plot is generated from the calculated export-gas, terminal-liquid, terminal-gas, and drain-liquid mass flows. It is a quick visual check that the model accounting includes all material leaving the integrated process.
Observation. Figure 19.1 shows all calculated product and drain streams on the same mass-flow basis, so the reader can see whether a material outlet has been missed before trusting the integrated case.
Mechanism. The bars are generated from the simulated stream rates after the five process areas have converged, not from static illustration data.
Implication. A missing or unexpectedly small stream would immediately point to a wiring, split-factor, or phase-separation error in the model boundary.
Recommendation. Keep this figure in every rerun of the capstone case and compare it with the mass-balance error reported in results.json.
Figure 19.2: Compression power summary. The final export compressor dominates the power demand because it raises recombined gas from separator pressure to export pressure, while the recompressors only lift low- and medium-pressure flash gas back into the high-pressure gas path.
Observation. Figure 19.2 separates recompression power from final export power, making the dominant power consumer visible without reading the full stream table.
Mechanism. The export compressor handles the largest pressure ratio and the combined gas flow, while the recompressors only recover flash gas from lower pressure stages.
Implication. The export-compressor specification is the first place to check when the integrated case approaches power, discharge-temperature, or emissions limits.
Recommendation. Treat this figure as the starting point for Chapter 18 optimization: vary throughput and export pressure, then track which compressor sets the active constraint.
The validation flag in results.json is true because the calculated mass-balance error is far below the 1.0% acceptance limit.
19.18 Full Implementation Listing
The full no-execute generalized source listing has been moved out of the main reading flow and preserved in the backmatter appendix Full Integrated Process Model Code Listing. Keep this chapter open for the engineering narrative, the runnable anonymous notebook, the figures, and the portfolio-style reporting pattern; use the appendix when you need to compare the compact teaching model with the complete source-notebook structure.
Exercises
- Replace the generic stream and train names with names from your own study and run the feed-preparation section only. Check the feed mass balance before building process units.
- Run one separation train and write a small
results.jsoncontaining gas, liquid, and water outlet rates plus separator pressures and temperatures. - Save a converged
ProcessModelto.neqsim, reload it withProcessModel.loadFromNeqsim, and confirm that the key result values are unchanged. - Build a
.xippackage containingresults.json, the.neqsimfile, the state JSON, and text reports. Send it to a colleague and ask them to inspect the archive without running the model.
Learning from the NeqSim-Colab Notebooks
Learning Objectives
After this chapter you will:
- Know the structure and scope of the NeqSim-Colab notebook collection (github.com/EvenSol/NeqSim-Colab).
- Recognise the most useful notebooks for thermodynamics, PVT, process simulation, and dynamic analysis.
- Use Colab as a teaching, prototyping, and sharing platform for NeqSim work.
- Adapt Colab notebooks to your own local environment.
20.1 What NeqSim-Colab Is
NeqSim-Colab is a community-maintained collection of Google Colab notebooks demonstrating NeqSim capabilities. It is the largest single source of NeqSim usage examples — hundreds of notebooks covering thermodynamics, PVT, process equipment, and applied case studies.
In the structure of this book, Chapter 20 is the further-study chapter after the optimization and capstone work. Chapter 18 showed how to turn a converged model into a bounded optimization or debottlenecking study, and Chapter 19 assembled a large integrated model in one place. This chapter helps you find smaller notebooks that isolate one pattern at a time.
The notebooks share a structure:
- Install NeqSim Python in the first cell (
!pip install neqsim). - Import
neqsimor specific subpackages. - Build a fluid or process.
- Run a calculation.
- Plot and discuss results.
Most notebooks are 5–20 cells long — a single concept per notebook. This makes them excellent starting points for prototypes and teaching.
20.2 Running a Notebook Locally
To take a Colab notebook into your local environment:
- Click "Open in Colab", then File → Download → Download .ipynb.
- Open in JupyterLab or VS Code.
- Replace the first cell (
!pip install neqsim) with theneqsim_dev_setup.pycell from this book's Chapter 4 if you are developing against a Java source build; otherwise leave the install in place. - Run cells in order.
Most notebooks Just Work outside Colab. Occasional dependencies (e.g., yfinance for an energy-market example) install via pip without issues.
20.3 Thermodynamics Highlights
A handful of notebooks introduce the SystemInterface and flash calls that the rest of the collection builds on. Use repository paths rather than display titles; filenames in the collection are intentionally short and sometimes historical.
notebooks/process/fluidsandneqsim.ipynb— a compact entry point for EOS selection, component addition, mixing rule setup, TPflash, and property reads. Equivalent to the Chapter 5 material here.notebooks/thermodynamics/Phase_envelopes_of_oil_and_gas.ipynb— phase-envelope construction and plotting. Pair it with this book's phase-envelope notes before interpreting bubble and dew branches.notebooks/gasquality/hydrocarbon_dew_point_of_natural_gas.ipynb— point calculations for gas-quality and transport checks.notebooks/thermodynamics/TPflashRachRice.ipynb— a focused look at flash-calculation mechanics and the Rachford-Rice equation.
20.4 PVT Highlights
The PVT notebooks expose the pvtsimulation and fluid-characterization workflows directly — no wrappers, just the same Java classes reached through Python.
notebooks/PVT/parameter_database.ipynb— pure component parameter database usage, the extended component database, and temporaryCOMPtable replacement for custom component tests.notebooks/PVT/parameter_database2.ipynb— the same database idea taken further by replacing bothCOMPandINTER, so custom pure-component data and binary interaction parameters can be managed together.notebooks/PVT/PVTexperiments.ipynb— laboratory-style PVT experiments and pressure-step output tables.notebooks/PVT/PVTreports.ipynb— report-oriented extraction of PVT results.notebooks/PVT/fluidcharacterization.ipynb— plus-fraction and characterization workflow, matching the Chapter 6 model-building path.notebooks/PVT/eclipseFluidCharNeqSim.ipynb— reading and using Eclipse-style fluid definitions.notebooks/PVT/OilProperties.ipynb— black-oil style property calculations useful for quick checks before a full PVT study.
20.5 Process Simulation Highlights
The process notebooks parallel Chapters 8–9 of this book, with additional applied cases.
notebooks/process/gas_oil_separation.ipynbandnotebooks/process/Separators.ipynb— compact separator examples that are easier to inspect than the multi-train model in Chapter 19.notebooks/process/GasCompressorTrain.ipynbandnotebooks/process/compressor_curves_and_compressor_calculations.ipynb— compressor trains, compressor curves, and performance calculations.notebooks/process/TEGdehydration.ipynbandnotebooks/process/TEGprocessSim2.ipynb— gas drying with triethylene glycol; useful references for non-trivial process flowsheets.notebooks/process/comparesimulations.ipynb— a larger applied process-model notebook that compares a NeqSim offshore separation and compression case with published HYSYS/DWSIM reference results. It is worth reading after Chapter 19 because it combines typed input/output objects,.neqsimsave/reopen, detailed equipment KPI extraction, 100-case random parametric studies, surrogate-model data generation, and a parallel simulation discussion in one workflow.notebooks/process/newunitoperation.ipynb— a practical Python-side custom unit-operation prototype pattern. Use it after the advanced-equipment and custom-units chapter when you want to test unit behavior in a notebook before deciding whether to implement a production Java unit operation.notebooks/thermodynamics/thermodynamics_of_natural_gas_hydrates.ipynbandnotebooks/fluidflow/gaspipeline.ipynb— hydrate and pipeline checks that couple thermodynamics and flow assurance.
20.6 Dynamic and Control Highlights
These notebooks show runTransient in action with PID controllers and generated time-series output. The Chapter 11 patterns are demonstrated on smaller systems for teaching.
notebooks/process/dynsep.ipynb— separator dynamics and small transient examples.notebooks/process/dynamiccompressor.ipynb— dynamic compressor behaviour and compressor-train response.notebooks/process/process_control_with_neqsim.ipynbandnotebooks/process/process_control_with_neqsim_v2.ipynb— PID and process-control patterns on smaller systems.notebooks/process/transient_multiphase_flow_tutorial.ipynb— transient flow behaviour for pipeline-style cases.
20.7 Field-Development Highlights
Beyond unit-level simulation, several notebooks string everything together into field-development calculations.
notebooks/LNG/LNGprocesses2.ipynb— gas-processing and LNG process examples.notebooks/fielddevelopment/npv.ipynbandnotebooks/fielddevelopment/economy.ipynb— discounted cash-flow and field-economics calculations.notebooks/reservoir/fieldDevelopment1.ipynb— field-development workflow example using reservoir and production inputs.notebooks/reservoir/optimizationofoilandgasproduction.ipynb— reservoir and production optimization patterns that lead naturally into Chapter 18.
20.8 Patterns Worth Stealing
A few idioms recur across the collection:
- Pandas DataFrames as the result format. Notebooks build a list of dicts inside a parameter sweep and
pd.DataFrame(rows)at the end. The DataFrame goes straight to.plot()and.to_csv(). - Typed case boundaries. Larger notebooks often define a single input object, a single output object, and helper functions such as
getprocess,updateinput,run_simulation, andgetoutput. That pattern turns a notebook into something that can later become an API endpoint, a batch job, or an optimizer callback. - Scenario studies before optimizers. The comparison notebook first runs a random 100-case parametric study before moving to optimization and machine learning. That ordering is useful: the sweep reveals scale, failures, and active constraints before a numerical optimizer starts making decisions.
- Managed parallelism for independent cases. When the cases are independent, build one fresh process model per case and submit the cases to NeqSim's managed thread-pool API. The current repository docs prefer
runAsTask()andFuture.isDone()/Future.get()over older unmanagedrunAsThread()polling. - Side-by-side plots with
subplots(1, 2). Many notebooks show a before/after or two related variables (P and T profiles, gas and oil rates) in twin axes. - One assertion at the end. Even teaching notebooks often finish with an
assertthat the final result is in a sensible range. This is good practice — a notebook that runs but silently produces nonsense is a notebook that breaks production downstream.
20.9 Limitations and Adaptation
Colab notebooks have constraints that production work doesn't share:
- Colab installs every time. Slow first cell. For repeated work, copy the notebook locally.
- No persistent state. Each Colab session restarts from scratch.
- Limited Java memory. Colab's JVM gets default heap; large simulations may need
JPype.startJVM("-Xmx4g")explicitly. - No tagreader. Plant historian integration requires local installation. The Chapter 12 examples won't run in Colab.
For prototyping, learning, and sharing — Colab is unbeatable. For production engineering work, your local environment from Chapter 3 is the right tool.
20.10 Where to Go Next
The official NeqSim documentation at https://equinor.github.io/neqsim/ is the living index for the topics below: quickstarts, process recipes, the reference manual, optimization, examples, safety, field development, and integration guides. Use this chapter as a curated reading list, then use the docs site as the current source of API details.
The next steps depend on your interests:
- Refinery and petrochemicals — distillation patterns of Chapter 10 plus reaction modelling (
process.equipment.reactor). - CCS and hydrogen —
process.equipment.pipelineincludesCO2InjectionWellAnalyzerandTransientWellbore; theneqsim-ccs-hydrogenskill packages the patterns. - Process safety —
process.safetycovers depressurisation, PSV sizing, fire scenarios, dispersion, QRA. Pair with theneqsim-process-safetyskill. - Field development —
process.fielddevelopmentandprocess.economicsfor concept selection, screening, NPV. Pair with theneqsim-field-developmentskill. - Digital twin operation — combine
ProcessAutomation,ProcessModelState, and a tagreader integration to run a live twin. Pair with theneqsim-model-calibration-and-data-reconciliationskill. - Optimization and debottlenecking — the process optimization docs,
ProductionOptimizer,ProcessOptimizationEngine, capacity constraints, and the notebooks in Chapter 18 show how to turn a converged flowsheet into an operating decision.
Each of these is a book in itself; the foundations in this book apply in all of them.
20.11 Closing
A process model is a hypothesis about how the real plant behaves. The better the model, the better the decisions. NeqSim's job is to give you the thermodynamic and engineering machinery to encode that hypothesis in code that runs, converges, and can be reasoned about. Python's job is to make that machinery usable — from notebooks for one engineer to services for an entire fleet.
Build models. Validate them against the plant. Share them.
Exercises
- Exercise 20.1: Pick a Colab notebook of interest, download it, and run it locally with the
neqsim_dev_setup.pycell from Chapter 4 substituted for thepip install.
- Exercise 20.2: Choose a Colab process notebook and refactor it to use the direct-Java-access pattern (
from neqsim import jneqsim as J) of this book. Compare line counts and clarity.
- Exercise 20.3: Write your own teaching notebook — one concept, 8–15 cells, ending with an assertion. Share it with a colleague.
Glossary
Adjuster — A ProcessSystem element that varies one variable to drive a target to a setpoint by iteration; the steady-state equivalent of a PID controller.
Anti-surge controller — Control loop that opens a recycle valve around a compressor when the operating point approaches the surge line.
Approach temperature — Smallest temperature difference between hot and cold streams in a heat exchanger. Sub-5 °C approaches imply very large area; sub-1 °C is usually a model error.
Automation API — ProcessAutomation. String-addressable variable interface to a ProcessSystem or ProcessModel.
Bubble point — The pressure (at fixed T) or temperature (at fixed P) at which the first bubble of vapour appears in a liquid mixture.
Capstone portfolio — A compact set of reproducible study artifacts: problem statement, fluid definition, runnable notebook, validation evidence, figures or tables, machine-readable output, saved model state, and decision memo.
CME — Constant Mass Expansion. PVT laboratory test stepping pressure down at constant temperature and composition.
CPA — Cubic-Plus-Association equation of state. Adds an association term to a cubic EOS for hydrogen-bonding systems (water, glycols, methanol, hydrate inhibitors).
Cricondenbar — The highest pressure on the phase envelope above which the mixture is single-phase regardless of temperature.
Cricondentherm — The highest temperature on the phase envelope above which the mixture is single-phase regardless of pressure.
CVD — Constant Volume Depletion. PVT test simulating gas-condensate reservoir depletion.
Dew point — The pressure or temperature at which the first drop of liquid appears in a vapour mixture.
Dew-point spec — A sales-gas quality requirement that the hydrocarbon (or water) dew point must lie below a specified temperature at a stated pressure. NeqSim computes both via calcPTphaseEnvelope and hydrateFormationTemperature.
Differential Liberation (DL) — PVT test releasing equilibrium gas in stages, simulating a black-oil reservoir.
Direct Java Access — The book's recommended NeqSim-from-Python style: from neqsim import jneqsim as J; J.subpackage.Class(...). Full access to the Java API with JPype type conversion.
E300 / Eclipse fluid — A NeqSim text format compatible with Schlumberger Eclipse's E300 compositional simulator; carries Tc, Pc, acentric factor, MW, and BIPs.
EOS — Equation of state. A mathematical relationship between P, V, and T for a fluid. NeqSim ships SRK, PR, CPA, GERG-2008, and others.
Factory dictionary — Python idiom in this book: {"SRK": J.thermo.system.SystemSrkEos} — table-driven constructor lookup.
Flash — A thermodynamic calculation that determines phase split and composition given two state variables (TP, PH, PS, UV, etc.).
Future — Java concurrency object returned by runAsTask() or NeqSimThreadPool.submit(...). It represents a running or completed calculation and supports get(), isDone(), cancellation, and timeout handling.
GERG-2008 — Reference EOS for natural gas mixtures, accurate to custody-transfer specs across wide T and P ranges.
HMB — Heat and Material Balance. The reference table of stream properties used to validate process models.
Inside-out solver — A column-convergence algorithm using linearised "inside" tray equations with periodic "outside" property updates. Robust for highly non-ideal systems.
JPype — Python library that starts a JVM and exposes Java classes as Python objects.
K-value — The ratio $K_i = y_i / x_i$ between a component's vapour and liquid mole fractions at equilibrium. The fundamental output of a flash calculation; light components have $K > 1$, heavy components $K < 1$.
Lifecycle state — ProcessSystemState / ProcessModelState. JSON snapshots of a model for save, load, version, and diff.
MDMT — Minimum Design Metal Temperature. The lowest temperature at which a vessel may safely be pressurised, set by the material's impact-toughness curve. Checked against the end-of-blowdown temperature in every depressurisation study (see skill neqsim-depressurization-mdmt).
Mixing rule — Combining rule for pure-component EOS parameters in a mixture. NeqSim's default is "classic" (van der Waals one-fluid). Required for every EOS-based fluid.
Multi-area model — A ProcessModel composed of multiple ProcessSystem objects, each representing a process area.
Naphtali-Sandholm solver — A simultaneous-correction column solver solving all equilibrium and mass-balance equations as one Newton system.
NeqSimThreadPool — Managed Java thread pool used to run independent process simulations concurrently. Prefer runAsTask() with Future objects over unmanaged runAsThread() calls.
Pinch analysis — Heat-integration technique that targets minimum utility demand subject to a chosen ΔTmin.
Plus-fraction — A pseudo-component representing all hydrocarbons heavier than a cut (e.g., C7+). Characterised into sub-components by a splitter algorithm (Pedersen, Whitson).
Polytropic efficiency — Compressor efficiency defined for an infinitesimal compression step; preferred over isentropic for multi-stage machines.
Process model — ProcessModel. A container of named ProcessSystem objects that iterate to convergence as a unit.
Process system — ProcessSystem. A NeqSim flowsheet — equipment, streams, controllers, recycles, adjusters.
results.json — Machine-readable result file used in this book's notebook-backed workflow. It should contain key results, validation flags, figure captions, assumptions, and references needed to regenerate reports.
Recycle — A ProcessSystem element that closes a loop by passing its outlet back to a target equipment's inlet. Iterates with damping (Wegstein) until convergence.
Sequential solver — A column solver that converges trays one-at-a- time. Fast for ideal systems, slow for non-ideal.
Stream introspection — Walking a ProcessSystem graph via getInletStreams() and getOutletStreams() on each equipment object.
Surge margin — Distance, expressed as a percentage of flow, between a centrifugal compressor's operating point and its surge line. Anti-surge controllers typically open the recycle valve at a 10 – 15 % margin.
TBP — True Boiling Point. A laboratory crude assay producing a boiling-point curve used to characterise a heavy oil.
Three-phase separator — Separator vessel producing three product streams: gas, oil, and water.
TPflash — Constant-pressure, constant-temperature flash. The default and most common operation.
Wegstein damping — A relaxation method accelerating fixed-point iteration; used by Recycle to suppress oscillation.
WET process — A process system that includes a water phase throughout — common for produced fluids and gas treatment.
First-Hour Troubleshooting
This appendix collects the errors a process engineer is most likely to hit in the first hour of using NeqSim from Python. Each entry follows the same shape: symptom → cause → fix → why it happened.
E.1 The Big Six
1. NaN density or zero viscosity
Symptom. fluid.getDensity("kg/m3") returns nan or fluid.getPhase("gas").getViscosity("kg/msec") returns exactly 0.0.
Cause. You called TPflash() (or any flash) but forgot fluid.initProperties() afterwards. The flash initialises thermodynamic properties but not transport properties.
Fix.
ops.TPflash()
fluid.initProperties() # ← MANDATORY before getViscosity/getThermalConductivity
Why. init(3) alone does not initialise physical-property models; initProperties() calls both init(2) and initPhysicalProperties(). This is documented in the NeqSim agent skill neqsim-api-patterns but trips up almost every new user.
---
2. Mixing rule not set or strange flash results
Symptom. Either an exception about mixing rules, or a flash that produces obviously wrong K-values (e.g., methane K-value far from unity at 60 bara, 25 °C).
Cause. You created an EOS fluid but forgot fluid.setMixingRule("classic").
Fix. Always call it before adding the last component or before flashing. For CPA fluids use fluid.setMixingRule(10).
---
3. Component xxx not found in database
Symptom. addComponent("C20+", 0.01) raises a database lookup error.
Cause. Plus-fraction names contain the + character which is not a valid component-database key. Use "C20" and characterise via characterisePlusFraction() afterwards.
Fix.
fluid.addComponent("C20", 0.01)
fluid.setHeavyTBPfractionAsPlusFraction()
fluid.characterisePlusFraction()
---
4. Phase-envelope branches look swapped
Symptom. getBubblePointTemperatures() and getDewPointTemperatures() return data that visually looks the wrong way around — the "bubble" branch contains the cricondentherm.
Cause. When calcPTphaseEnvelope(True, 1.0) is called with bubblePointFirst=True, the getter method names are physically swapped. This is a long-standing quirk preserved for backward compatibility.
Fix. Always classify by physics, never by getter name:
A_T = np.asarray(env.getBubblePointTemperatures())
B_T = np.asarray(env.getDewPointTemperatures())
if A_T.max() > B_T.max():
dew_T, bub_T = A_T, B_T # "bubble" getter actually returned dew data
else:
dew_T, bub_T = B_T, A_T
The branch containing the highest temperature is the dew branch (it owns the cricondentherm).
---
5. JPype JVM already started / cannot change classpath
Symptom. A second import neqsim in the same Python process fails or ignores classpath changes.
Cause. JPype starts the JVM once per process. You cannot swap JARs mid-session.
Fix. Restart the Python kernel before changing the NeqSim installation. In notebooks: Kernel → Restart. If you are running parametric studies that mutate the classpath, use the NeqSim Runner (devtools/neqsim_runner/) which isolates each job in its own JVM.
---
6. Mass balance does not close on a recycle
Symptom. ProcessSystem.run() returns but Σ(in) ≠ Σ(out) by more than rounding.
Cause. Either the recycle did not converge (default tolerance is loose) or you forgot to register the Recycle element on the ProcessSystem.
Fix.
recycle = J.process.util.recycle.Recycle("REC-1")
recycle.addStream(loop_return)
recycle.setOutletStream(loop_guess)
recycle.setTolerance(1e-4)
proc.add(recycle) # ← easy to forget
proc.run()
---
E.2 Unit-String Gotchas
NeqSim uses unit strings on almost every getter and setter. The most frequent mistakes:
| What you wrote | What you meant | Why it bit you |
|---|---|---|
setFlowRate(100.0) |
setFlowRate(100.0, "kg/hr") |
Default is mol/sec — almost always wrong for industry data |
getTemperature() |
getTemperature("C") |
Default is Kelvin |
getPressure() |
getPressure("bara") |
Default is Pa |
setOutTemperature(25) |
setOutTemperature(25.0, "C") |
Without unit string defaults to Kelvin → frozen fluid |
getDuty() |
getDuty() (returns W) |
Then divide by 1e3 for kW |
Habit to form. Always pass an explicit unit string. The Java API will accept the default, but your code will be unreadable in six months.
E.3 Python ↔ Java Type Surprises
Java intvsPython int. Some setters requireint; passingnumpy.int64raisesNo matching overloads. Cast withint(x).Java double[]vsnumpy array. Most NeqSim methods accept lists or numpy arrays and copy them. Returned arrays areJArray— wrap innp.asarray(...)if you need numpy semantics.nullhandling. Javanullbecomes PythonNone. Check withif stream is Nonebefore calling getters.
E.4 When the Solver Diverges
See the dedicated skill neqsim-troubleshooting (in the NeqSim agent skills directory) for a ranked recovery checklist:
- Run a TP flash alone first — if that fails, the flowsheet is irrelevant.
- Loosen the recycle tolerance temporarily, then tighten back once convergence is found.
- Provide an explicit
Recycleguess close to the expected solution. - For distillation columns: switch solver (
DistillationColumn.SolverType.INSIDE_OUTis the most robust), double the tray count, or check feed-tray placement. - For dynamic runs that blow up: reduce the timestep by 10× and re-check.
E.5 When Numbers Look "Almost Right"
Process-engineering intuition catches errors a unit test never will. A 2 %-off compressor power for a wet gas at 60 bara is almost certainly a wrong EOS, a missing component, or a missing initProperties(). When in doubt:
- Re-run with
SystemPrEosorSystemSrkCPAstatoiland compare. - Compare to a hand calculation with $W = \dot m \cdot c_p \cdot \Delta T / \eta$.
- Compare to a literature value or a previous HYSYS/UniSim case.
The Capstone Portfolio backmatter expects every result to carry one such independent check.
E.6 Quick "Is Anything Working?" Script
If you hit anything weird, run this five-line sanity check before investigating further:
from neqsim import jneqsim as J
f = J.thermo.system.SystemSrkEos(298.15, 60.0)
f.addComponent("methane", 1.0); f.setMixingRule("classic")
J.thermodynamicoperations.ThermodynamicOperations(f).TPflash()
f.initProperties()
print(f.getDensity("kg/m3")) # expect ~44 kg/m3
If this prints a reasonable number, NeqSim is fine and the bug is in your model.
Reproducibility, Running Case, and Engineering Interpretation
This appendix turns the book's three practical promises into a checklist: code that can be rerun, one process-modeling case that connects the chapters, and results that are interpreted as engineering evidence rather than left as raw numbers.
Code Execution Contract
The source chapters use two kinds of Python blocks.
- Runnable chapter examples are ordinary
pythonfences. They are verified in chapter order because later cells often depend on objects created earlier in the same chapter. - Reference fragments are preceded by the
noexecmarker. These show API calls, patterns, or placeholders that need surrounding project context. They are preserved for readers but skipped by the verifier.
The book-level quality gate is:
python neqsim-paperlab/tools/verify_book_code_blocks.py \
neqsim-paperlab/books/process_modeling_with_neqsim_python_2026 \
--mode concat \
--report neqsim-paperlab/books/process_modeling_with_neqsim_python_2026/code_block_report.json
Use --mode concat for this book because the reader experience is chapter based: import NeqSim once, create a fluid, run the next calculation, and then plot or report the result. Isolated snippets are still useful while editing, but they are stricter than the way the chapter examples are meant to be read.
The Running Case
The recurring case is a wet rich-gas export system. It starts as a fluid, grows into a PVT case, becomes a separator and compression train, is exposed through the Automation API, and finally becomes an optimization and reporting exercise.
| Chapter range | Running-case artifact | What the reader should save |
|---|---|---|
| Chapters 1-4 | Environment and direct Java-access smoke test | A notebook that imports jneqsim and runs one flash or mini flowsheet |
| Chapters 5-7 | Rich-gas fluid, component provenance, PVT checks | Composition table, EOS choice, database notes, saturation or lab-data comparison |
| Chapters 8-11 | Separator, compression, recycle, and dynamic variants | ProcessSystem notebook, stream table, mass-balance check, key equipment KPIs |
| Chapters 12-14 | Automation, state, report, and API boundary | Variable list, results.json, lifecycle-state JSON, and API-style input/output schema |
| Chapters 15-17 | Integrated case, scenario table, and optimization | Selected case, active constraints, figures, saved state, and decision memo |
The case is intentionally generic. Readers can replace the composition, pressure levels, and equipment tags with their own project data while keeping the structure: fluid factory, model builder, run function, validation function, and reporting function.
Running-Case Ledger
Keep a small ledger as the case moves through the book.
| Ledger item | Minimum content |
|---|---|
| Fluid basis | Component list, source, EOS, mixing rule, database mode, plus-fraction assumptions |
| Operating envelope | Inlet and outlet pressure, temperature, flow, equipment limits, product specs |
| Validation evidence | Mass balance, phase count, benchmark value, lab-data deviation, or hand estimate |
| Model state | Notebook path, input JSON or CSV, generated figures, .neqsim or lifecycle-state file |
| Decision basis | Selected case, active constraint, uncertainty or sensitivity, recommendation |
The ledger prevents a common failure mode: by Chapter 18 the optimization result looks precise, but nobody remembers which component database, feed composition, or process limits produced it.
Engineering Interpretation Blocks
After every important figure, table, flash, PVT result, process result, or optimization result, write four short sentences.
| Label | Question to answer |
|---|---|
| Observation | What changed, and what are the key numbers with units? |
| Mechanism | Which physical effect or model assumption explains the change? |
| Implication | What does it mean for design, operation, safety, or data quality? |
| Recommendation | What should the engineer do next, and what evidence would change the decision? |
This structure is deliberately compact. It makes a notebook easier to review and gives the reporting toolchain enough material to build a useful discussion section from figure_discussion entries in results.json.
When Not to Trust the Result Yet
Treat a result as provisional when any of these are true:
- The component comes from the extended database and no independent property data has been checked.
- A flash converges but the phase count contradicts the expected envelope.
- A process model meets the target only because a hidden utility, recycle, or equipment limit was unconstrained.
- A plotted curve has the right shape but no benchmark point, hand estimate, or previous-case comparison.
- An optimization selects a point on a boundary without reporting the active constraint and margin.
The remedy is not always a larger model. Often it is a simpler cross-check: a mass balance, a single NIST point, a vendor curve, a manual compressor power estimate, or rerunning the same model after saving and restoring its state.
Full Integrated Process Model Code Listing
This appendix preserves the long, no-execute generalized code listing that used to sit inside the integrated offshore process-model chapter. It remains available for implementation comparison and audit, but it no longer interrupts the main teaching path through the capstone narrative.
The runnable, anonymous companion notebook remains linked from the capstone chapter and should be the first place readers go when they want executable code. Use this appendix when you need to inspect how a larger project notebook was organized cell by cell.
Original Full Generalized Notebook Code Listing
The listing below contains every code cell from the source process-model notebook, in notebook order. It is marked no-execute in the book because it depends on project- specific input files such as process-condition spreadsheets and fluid-characterization exports. The code is anonymized: replace generic asset, train, route, and tag names with your own project names before running it.
Code cell 1: Content of notebook
Notebook cell 3. This code belongs to the Content of notebook section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid- characterization files, and case-specific result folders.
simulation_year: int = 2025 # 1, 2, 2025, 2026, 2027, 2028, 2029, 2030, 2031, 2032, 2033, 2034, 2035, 2036, 2037, 2038, 2039, 2040, 2041, 2042, 2043, 2044, 2045
Code cell 2: Content of notebook
Notebook cell 4. This code belongs to the Content of notebook section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid- characterization files, and case-specific result folders.
simulation_case: str = 'base' # 'low', 'high', 'base','benchmark'
Code cell 3: Content of notebook
Notebook cell 5. This code belongs to the Content of notebook section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid- characterization files, and case-specific result folders.
results_path: str = 'results/basecase' #/highN2' basecase highcase # path to the results folder
Code cell 4: Content of notebook
Notebook cell 6. This code belongs to the Content of notebook section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid- characterization files, and case-specific result folders.
third_party_feed_case: str = 'unstable' #stable / unstable
Code cell 5: 1. Define input parameters
Notebook cell 9. This code belongs to the 1. Define input parameters section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid- characterization files, and case-specific result folders.
from dataclasses import dataclass, field
from typing import List
import math
from IPython.display import clear_output
@dataclass
class ProcessInput:
# Core
# parameters
Year: int = 2029 # Current year of the simulation
rich_gas_bypass: bool = False # Flag for rich gas pass
#lpg_injection_flow: float = 20000.0 # LPG injection flow rate in kg/hr
lpg_injection_fraction: float = 0.001
third_party_feed_case:str='unstable'#"unstable" #unstable" #"stable" # "no third_party_feed"
results_path:str='results'
heater_start_year: int = 2026 # Year when the heater starts operation
year_pre_compression: int = 2028 # Year before compression starts
tex_start_year: int = 2029 # Year when TEX starts operation
cold_gas_bypass_multidunk = 0.05
# Scenario flags
simulation_case:str='base' # Simulation case: 'low', 'high', 'base'
# Process parameters
new_compressor_pressure: float = 62.0 # Pressure of the new compressor in bar
# Outlet temperature of the new compressor cooler in °C
new_compressor_cooler_out_temperature: float = 30.0
# CalibrationStream separation pressure tuning parameter in bar
reference_stream_c_calibration_stream_separator_pressure: float = 29.11
# Gas fraction to CALIB_STAGE tuning parameter the rest is sent to recompressor
reference_stream_c_gas_fraction_to_CALIB_STAGE = 0.999999
reference_stream_c_CALIB_STAGE_compressor_cooler_temperature = 30.0
# Low-pressure well feed temperature in °C
satellite_heater_temperature: float = 70.0
first_stage_pressure = 62.0 # Pressure of the first stage in bar
first_stage_heater_temperature: float = 80.0 # Temperature of the second stage in °C
second_stage_pressure: float = 20.0 # Pressure of the second stage in bar
third_stage_pressure: float = 7.0 # Pressure of the third stage in bar
fourth_stage_pressure: float = 3.0 # Pressure of the fourth stage in bar
flare_gas_recovery_pressure: float = 1.96 # Flare gas recovery pressure in bar
first_stage_scrubber_temperature: float = 30.0 # Temperature of the first stage scrubber in °C
second_stage_suction_cooler_temperature : float = 30.0
third_stage_suction_cooler_temperature : float = 30.0
fourth_stage_suction_cooler_temperature : float = 30.0
fifth_stage_suction_cooler_temperature : float = 30.0
use_both_gas_recompression_trains_1_2_stage : int = 0 # Use both gas recompression trains (1) or not (0)
use_both_gas_recompression_trains_3_stage : int = 0
fuel_gas_rate: float = 0.2 # Fuel gas rate in MSm3/day
dew_point_scrubber_temperature: float = 24.0 # Dew point scrubber temperature in °C
expander_out_pressure: float = 42.5 # Expander outlet pressure in bar
pre_flash_drum_pressure: float = 20.0 # Pre-flash drum pressure in bar
# NGL column reboiler temperature in °C
ngl_column_reboiler_temperature: float = 42.0
ngl_column_pressure: float = 7.0 # NGL column pressure in bar
ngl_routing_to_oil: float = 0.999 # NGL routing to oil fraction
export_compressor_pressure: float = 150 # Export compressor pressure in bar
# Injection compressor pressure in bar
injection_compressor_pressure: float = 250
injection_gas_rate_ht: float = 0.0 # Injection gas rate in MSm3/day
injection_gas_rate_ht_split_to_train_A: float = 0.0 # Injection gas rate split to Train A in MSm3/day
speed_EXPORT_COMPRESSOR: int = 8000 # Speed of compressor EXPORT_COMPRESSOR in RPM
speed_EXPORT_COMPRESSOR: int = 8000 # Speed of compressor EXPORT_COMPRESSOR in RPM
injection_gas_rate_dx : float = 2.3 # Injection gas rate in MSm3/day
gas_lift_rate : float = 0.6 # Gas lift rate in MSm3/day
# Reservoir production parameters
calibration_stream_scale_factor_low: float = 0.9 # Low calibration_stream scale factor
calibration_stream_scale_factor_intermediate: float = 1.0 # Intermediate calibration_stream scale factor
calibration_stream_scale_factor_high: float = 1.1 # High calibration_stream scale factor
calibration_stream_temperature: float = 80.0 # CalibrationStream temperature in °C
calibration_stream_water: float = 10000.0 # CalibrationStream water in m3/day
reference_stream_c_reference_stream_a_temperature: float = 80.0 # ReferenceStreamA temperature tuning in °C
reference_stream_c_reference_stream_a_water: float = 10000.0 # ReferenceStreamA water tuning in m3/day
reference_stream_c_reference_stream_b_temperature: float = 80.0 # ReferenceStreamB temperature tuning in °C
reference_stream_c_reference_stream_b_water: float = 10000.0 # ReferenceStreamB water tuning in m3/day
cluster_d_temperature: float = 80.0 # Cluster D temperature in °C
cluster_d_water: float = 10000.0 # Cluster D water in m3/day
cluster_c_temperature: float = 80.0 # Cluster C temperature in °C
cluster_c_water: float = 10000.0 # Cluster C water in m3/day
reference_stream_b_scale_factor_low: float = 0.9 # Low ReferenceStreamB scale factor
reference_stream_b_scale_factor_intermediate: float = 1.0 # Intermediate ReferenceStreamB scale factor
reference_stream_b_scale_factor_high: float = 1.1 # High ReferenceStreamB scale factor
reference_stream_b_temperature: float = 80.0 # ReferenceStreamB temperature in °C
reference_stream_b_water: float = 10000.0 # ReferenceStreamB water in m3/day
cluster_a_scale_factor_low: float = 0.9 # Low Cluster A scale factor
cluster_a_scale_factor_intermediate: float = 1.0 # Intermediate Cluster A scale factor
cluster_a_scale_factor_high: float = 1.1 # High Cluster A scale factor
cluster_a_temperature: float = 80.0 # Cluster A temperature in °C
cluster_a_water: float = 10000.0 # Cluster A water in m3/day
condensate_area_scale_factor_low: float = 0.9 # Low Condensate Area scale factor
condensate_area_scale_factor_intermediate: float = 1.0 # Intermediate Condensate Area scale factor
condensate_area_scale_factor_high: float = 1.1 # High Condensate Area scale factor
condensate_area_temperature: float = 20.0 # Condensate Area temperature in °C
condensate_area_water: float = 10000.0 # Condensate Area water in m3/day
third_party_feed_temperature: float = 20.0 # ThirdPartyFeed temperature in °C
third_party_feed_water: float = 1.0 # ThirdPartyFeed water in m3/day
offshore_asset_east_temperature: float = 80.0 # Offshore Area East temperature in °C
offshore_asset_east_water: float = 10000.0 # Offshore Area East water in m3/day
offshore_east_scale_factor_low: float = 0.9 # Low Offshore Area East scale factor
offshore_east_scale_factor_intermediate: float = 1.0 # Intermediate Offshore Area East scale factor
offshore_east_scale_factor_high: float = 1.1 # High Offshore Area East scale factor
offshore_asset_south_temperature: float = 80.0 # Offshore Area South temperature in °C
offshore_asset_south_water: float = 10000.0 # Offshore Area South water in m3/day
cluster_b_scale_factor_low: float = 0.9 # Low Cluster B scale factor
# Intermediate Cluster B scale factor
cluster_b_scale_factor_intermediate: float = 1.0
cluster_b_scale_factor_high: float = 1.1 # High Cluster B scale factor
cluster_b_temperature: float = 80.0 # Cluster B temperature in °C
cluster_b_water: float = 10000.0 # Cluster B water in m3/day
# Example hydrocarbon flow rates and compositions
hc_flow_rate_cluster_a: float = 2.931e6 # Hydrocarbon flow rate for Cluster A in Sm3/day
hc_composition_cluster_a: List[float] = field(default_factory=lambda: [ # Hydrocarbon composition for Cluster A
0.2427, 1.4284, 78.9641, 8.9389, 4.7052, 0.6935, 1.4287, 0.2067,
0.2469, 0.3546, 0.5473, 0.6345, 0.1843, 0.7085, 0.5630, 0.1528, 0.0
])
hc_flow_rate_calibration_stream: float = 1.843e6 # Hydrocarbon flow rate for CalibrationStream in Sm3/day
hc_composition_calibration_stream: List[float] = field(default_factory=lambda: [ # Hydrocarbon composition for CalibrationStream
0.6417, 1.8505, 83.9189, 7.6914, 3.1650, 0.3587, 0.6610, 0.1740,
0.1930, 0.2134, 0.3393, 0.3568, 0.1782, 0.2254, 0.0319, 0.0007, 0.0
])
# Hydrocarbon flow rate for Main ReferenceStreamB in Sm3/day
hc_flow_rate_main_reference_stream_b: float = 2.931e6
hc_composition_main_reference_stream_b: List[float] = field(default_factory=lambda: [ # Hydrocarbon composition for Main ReferenceStreamB
0.2427, 1.4284, 78.9641, 8.9389, 4.7052, 0.6935, 1.4287, 0.2067,
0.2469, 0.3546, 0.5473, 0.6345, 0.1843, 0.7085, 0.5630, 0.1528, 0.0
])
# Hydrocarbon flow rate for Cluster B in Sm3/day
hc_flow_rate_cluster_b: float = 2.931e6
hc_composition_cluster_b: List[float] = field(default_factory=lambda: [ # Hydrocarbon composition for Cluster B
0.2427, 1.4284, 78.9641, 8.9389, 4.7052, 0.6935, 1.4287, 0.2067,
0.2469, 0.3546, 0.5473, 0.6345, 0.1843, 0.7085, 0.5630, 0.1528, 0.0
])
hc_flow_rate_condensate_area: float = 2.931e6 # Hydrocarbon flow rate for Condensate Area in Sm3/day
hc_composition_condensate_area: List[float] = field(default_factory=lambda: [ # Hydrocarbon composition for Condensate Area
0.2427, 1.4284, 78.9641, 8.9389, 4.7052, 0.6935, 1.4287, 0.2067,
0.2469, 0.3546, 0.5473, 0.6345, 0.1843, 0.7085, 0.5630, 0.1528, 0.0
])
# Hydrocarbon flow rate for ReferenceStreamC ReferenceStreamA in Sm3/day
hc_flow_rate_reference_stream_creference_stream_a: float = 2.931e6
hc_composition_reference_stream_creference_stream_a: List[float] = field(default_factory=lambda: [ # Hydrocarbon composition for ReferenceStreamC ReferenceStreamA
0.2427, 1.4284, 78.9641, 8.9389, 4.7052, 0.6935, 1.4287, 0.2067,
0.2469, 0.3546, 0.5473, 0.6345, 0.1843, 0.7085, 0.5630, 0.1528, 0.0
])
# Hydrocarbon flow rate for ReferenceStreamC ReferenceStreamB in Sm3/day
hc_flow_rate_reference_stream_creference_stream_b: float = 2.931e6
hc_composition_reference_stream_creference_stream_b: List[float] = field(default_factory=lambda: [ # Hydrocarbon composition for ReferenceStreamC ReferenceStreamB
0.2427, 1.4284, 78.9641, 8.9389, 4.7052, 0.6935, 1.4287, 0.2067,
0.2469, 0.3546, 0.5473, 0.6345, 0.1843, 0.7085, 0.5630, 0.1528, 0.0
])
# Hydrocarbon flow rate for Cluster D in Sm3/day
hc_flow_rate_cluster_d: float = 2.931e6
hc_composition_cluster_d: List[float] = field(default_factory=lambda: [ # Hydrocarbon composition for Cluster D
0.2427, 1.4284, 78.9641, 8.9389, 4.7052, 0.6935, 1.4287, 0.2067,
0.2469, 0.3546, 0.5473, 0.6345, 0.1843, 0.7085, 0.5630, 0.1528, 0.0
])
# Hydrocarbon flow rate for Cluster C in Sm3/day
hc_flow_rate_cluster_c: float = 2.931e6
hc_composition_cluster_c: List[float] = field(default_factory=lambda: [ # Hydrocarbon composition for Cluster C
0.2427, 1.4284, 78.9641, 8.9389, 4.7052, 0.6935, 1.4287, 0.2067,
0.2469, 0.3546, 0.5473, 0.6345, 0.1843, 0.7085, 0.5630, 0.1528, 0.0
])
# Hydrocarbon flow rate for Offshore Area East in Sm3/day
hc_flow_rate_offshore_east: float = 2.931e6
hc_composition_offshore_east: List[float] = field(default_factory=lambda: [ # Hydrocarbon composition for Offshore Area East
0.2427, 1.4284, 78.9641, 8.9389, 4.7052, 0.6935, 1.4287, 0.2067,
0.2469, 0.3546, 0.5473, 0.6345, 0.1843, 0.7085, 0.5630, 0.1528, 0.0
])
# Hydrocarbon flow rate for Offshore Area South in Sm3/day
hc_flow_rate_offshore_south: float = 2.931e6
hc_composition_offshore_south: List[float] = field(default_factory=lambda: [ # Hydrocarbon composition for Offshore Area South
0.2427, 1.4284, 78.9641, 8.9389, 4.7052, 0.6935, 1.4287, 0.2067,
0.2469, 0.3546, 0.5473, 0.6345, 0.1843, 0.7085, 0.5630, 0.1528, 0.0
])
hc_flow_rate_third_party_feed: float = 100.0 # Hydrocarbon flow rate for ThirdPartyFeed in Sm3/day
hc_composition_third_party_feed: List[float] = field(default_factory=lambda: [ # Hydrocarbon composition for ThirdPartyFeed
0.2427, 1.4284, 0.9641, 8.9389, 4.7052, 0.6935, 1.4287, 0.2067,
0.2469, 0.3546, 0.5473, 0.6345, 0.1843, 0.7085, 0.5630, 0.1528, 0.0
])
expander_efficiency = 77.0
chartConditions = [] #used to set molecular weight etc.
headunit = 'kJ/kg'
surgeflow_first_stage_compressor = [5500, 6269.85, 6878.75, 7488.05, 7979.18, 8567.67, 8811, 9097.54] #single speed
surgehead_first_stage_compressor = [93.2, 92.54, 91.66, 90.27, 88.18, 84.87, 83.2, 80.61]
surgeflow_second_stage_compressor = [9758.49, 9578.11, 9397.9, 9248.64, 9006.93, 8749.97, 8508.5, 8179.81, 7799.81, 7111.75, 6480.26, 6007.91, 5607.45] #single speed
surgehead_second_stage_compressor = [112.65, 121.13, 127.56, 132.13, 137.29, 140.73, 142.98, 144.76, 146.14, 148.05, 148.83, 149.54, 150]
surgeflow_third_stage_compressor = [2451.6782, 2834.65, 3720.0688, 4394.420, 5384.2417, 5825.17]
surgehead_third_stage_compressor = [78.8811, 105.519, 141.39, 158.5963, 180.320, 189.53]
surgeflow_DX1_stage_compressor = [5115.3633, 5848.66, 6663.91, 7842.191, 8288.40, 8736.8]
surgehead_DX1_stage_compressor = [ 63.519, 82.8421, 106.11, 124.987, 129.369, 142.5]
surgeflow_DX2_stage_compressor = [2061.8, 2356.74, 2848.3, 3398.8, 3634.83]
surgehead_DX2_stage_compressor = [41.4, 51.52, 63.6, 84.2, 92.6]
surgeflow_DX3_stage_compressor = [2061.8, 2356.74, 2848.3, 3398.8, 3634.83]
surgehead_DX3_stage_compressor = [41.4, 51.52, 63.6, 84.2, 92.6]
surgeflow_htA_stage_compressor = [2173.346, 2537.7532, 3150.335, 3591.540, 4220.19,4506.92]
surgehead_htA_stage_compressor = [81.888, 108.4406, 144.835, 162.46066,185.808, 195.6]
surgeflow_htB_stage_compressor = [809.729, 920, 1036.75, 1153.513, 1166.48, 1244.3243]
surgehead_htB_stage_compressor = [35.31, 46.25, 59.03, 70.81, 73.17, 80.9134]
surgeflow_new_stage_compressor = [7000, 12000, 16000]
surgehead_new_stage_compressor = [40, 100, 160]
surgeflow_CALIB_STAGE__compressor_1st_stage = [4200, 6000, 8100]
surgehead_CALIB_STAGE_compressor_1st_stage = [90, 150, 200]
surgeflow_CALIB_STAGE__compressor_2nd_stage = [1100, 1500, 1750]
surgehead_CALIB_STAGE_compressor_2nd_stage = [45, 80, 100]
surgeflow_third_stage_compressor_speed_new_boundle = [11571]
surgeflow_third_stage_compressor_flow_new_boundle = [3419.61, 3464.15, 3959.03, 4453.91, 4948.79, 5078.89, 5443.67, 5938.54, 6433.42, 6567.04]
surgeflow_third_stage_compressor_head_new_boundle = [191.3385288, 191.1080725, 188.1582322, 183.592256, 176.9227533, 174.5269887, 167.8221821, 157.3555446, 131.6895802, 125.1710999]
reference_stream_c_CALIB_STAGE_compressor_pressure = 200.0 # Tuning parameter for CALIB_STAGE compressor pressure in bar
speed_export_compressor_stage_1 = 8000 # Speed of compressor EXPORT_COMPRESSOR in RPM
speed_export_compressor_stage_2 = 8000
offshore_asset_transport_diameter = 0.67
offshore_asset_transport_length = 105000
input_parameters = ProcessInput()
Code cell 6: 2. Read process conditions from parameters.xlsx file
Notebook cell 11. This code belongs to the 2. Read process conditions from parameters.xlsx file section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid-characterization files, and case-specific result folders.
import pandas as pd
from IPython.display import clear_output
def read_process_conditions(file_path: str, simulation_year: int, inp: ProcessInput):
"""
Reads process conditions from an Excel file for the given year and updates the `inp` object
with corresponding attribute values.
Parameters
----------
file_path : str
Path to the Excel file (e.g. 'parameters.xlsx').
simulation_year : int
The year for which to select data.
inp : ProcessInput
An object whose attributes will be set to the values from the Excel row.
Returns
-------
ProcessInput
The updated input object.
"""
try:
df = pd.read_excel(file_path, sheet_name=0, header=0)
except Exception as e:
raise IOError(f"Error reading Excel file at {file_path}: {e}")
# Select the row for the chosen year
selected = df.loc[df['Year'] == simulation_year]
if selected.empty:
raise ValueError(
f"No data found for year {simulation_year} in {file_path}.")
# If multiple rows exist, pick the first one
row = selected.iloc[0]
# Dynamically update attributes of the input object
for column in df.columns:
attribute_name = column.replace(" ", "_")
if hasattr(inp, attribute_name):
setattr(inp, attribute_name, float(row[column]))
else:
print(f"Warning: {attribute_name} not found in ProcessInput class.")
return inp
input_parameters = ProcessInput()
# Path to your Excel file
if simulation_case == "high":
file_path = "./parameters_highcase.xlsx"
elif simulation_case == "low":
file_path = "./parameters_lowcase.xlsx"
else:
file_path = "./parameters.xlsx"
# Read process conditions and update the input object
input_parameters = read_process_conditions(
file_path, simulation_year, input_parameters)
input_parameters.Year = int(simulation_year)
input_parameters.simulation_case = simulation_case
input_parameters.third_party_feed_case = third_party_feed_case
input_parameters.results_path = results_path
# Dynamically print all attributes and their values
for attr, value in vars(input_parameters).items():
print(f"{attr} = {value}")
Code cell 7: 3. Read reservoir production data from FluidMagic
Notebook cell 13. This code belongs to the 3. Read reservoir production data from FluidMagic section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid-characterization files, and case-specific result folders.
import pandas as pd
def read_production_file(inp: ProcessInput, file_path, simulation_year:int):
"""
Reads a production file with the known format, filters it by the given year,
and returns the relevant values in a convenient dictionary.
"""
df = pd.read_csv(file_path, sep=r"\s+", header=0)
# Filter DataFrame for the chosen year
data_for_year = df.loc[df['Year'] == simulation_year].copy()
if data_for_year.empty:
raise ValueError(
f"No data found for year {simulation_year} in file '{file_path}'.")
# Build the composition list. We take columns from index 6 onward.
composition_columns = data_for_year.columns[6:]
data_for_year['composition'] = data_for_year[composition_columns].values.tolist()
# Append [0] to the end of the composition list
data_for_year['composition'] = data_for_year['composition'].apply(
lambda x: x + [0])
# Extract what we need (assuming only one row for that year)
oil_rate = data_for_year['Oilrate'].values[0]
gas_rate = data_for_year['Gasrate'].values[0]
mass_rate = data_for_year['Massrate'].values[0]
composition = data_for_year['composition'].values[0]
# Return as a dictionary (or you could return a namedtuple or a custom class if you prefer)
return {
'year': simulation_year,
'oil_rate': oil_rate,
'gas_rate': gas_rate,
'mass_rate': mass_rate,
'composition': composition
}
files_and_attrs = {
'cluster_a_north': "./well production from fluid magic/ClusterANorthProfile.txt",
'main_reference_stream_b': "./well production from fluid magic/main_reference_stream_b.txt",
'cluster_b': "./well production from fluid magic/cluster_b.txt",
'calibration_stream': "./well production from fluid magic/calibration_streamrates.txt",
'condensate_area': "./well production from fluid magic/condensate_area.txt",
'third_party_feed': "./well production from fluid magic/third_party_feed_unstable.txt",
'reference_stream_creference_stream_a': "./well production from fluid magic/reference_stream_creference_stream_a.txt",
'reference_stream_creference_stream_b': "./well production from fluid magic/reference_stream_creference_stream_b.txt",
'cluster_c': "./well production from fluid magic/cluster_c.txt",
'cluster_d': "./well production from fluid magic/cluster_d.txt",
'offshore_south': "./well production from fluid magic/offshore_asset_south.txt",
'offshore_east': "./well production from fluid magic/offshore_asset_east.txt",
}
if(input_parameters.results_path == "results/highN2"):
files_and_attrs['main_reference_stream_b'] = "./results/highN2/main_reference_stream_b.txt"
if(input_parameters.simulation_case == "high"):
files_and_attrs['offshore_south'] = "./well production from fluid magic/offshore_asset_south_high.txt"
if(input_parameters.simulation_case == "low"):
files_and_attrs['offshore_south'] = "./well production from fluid magic/offshore_asset_south_low.txt"
if(input_parameters.third_party_feed_case == "stable"):
files_and_attrs['third_party_feed'] = "./well production from fluid magic/third_party_feed.txt"
if(input_parameters.results_path == "results/no_oss"):
files_and_attrs['offshore_south'] = "./results/no_oss/offshore_asset_south.txt"
def read_production_file_all(inp: ProcessInput, file_path:str, simulation_year:int):
data_dict = {}
for name, path in files_and_attrs.items():
try:
data_dict[name] = read_production_file(inp, path, inp.Year)
except ValueError as e:
# Here we assume that the only reason for ValueError is "No data found"
# If you'd like to differentiate among multiple error messages, you can parse 'e' or do more specific checks.
print(f"Warning: {name}")
print(f"Warning: {e}")
data_dict[name] = None
# Then pick out the data, e.g.:
if data_dict['cluster_a_north'] is None:
inp.hc_flow_rate_cluster_a = 1.0
inp.cluster_a_water = 1.0e-3
else:
inp.hc_composition_cluster_a = data_dict['cluster_a_north']['composition']
inp.hc_flow_rate_cluster_a = data_dict['cluster_a_north']['mass_rate']
if data_dict['main_reference_stream_b'] is None:
inp.hc_flow_rate_main_reference_stream_b = 1.0
inp.main_reference_stream_b_water = 1.0e-3
else:
inp.hc_flow_rate_main_reference_stream_b = data_dict['main_reference_stream_b']['mass_rate']
inp.hc_composition_main_reference_stream_b = data_dict['main_reference_stream_b']['composition']
if data_dict['cluster_b'] is None:
inp.hc_flow_rate_cluster_b = 1.0
inp.cluster_b_water = 1.0e-3
else:
inp.hc_flow_rate_cluster_b = data_dict['cluster_b']['mass_rate']
inp.hc_composition_cluster_b = data_dict['cluster_b']['composition']
if data_dict['calibration_stream'] is None:
inp.hc_flow_rate_calibration_stream = 1000.0
inp.calibration_stream_water = 1.0
else:
inp.hc_flow_rate_calibration_stream = data_dict['calibration_stream']['mass_rate']
inp.hc_composition_calibration_stream = data_dict['calibration_stream']['composition']
if data_dict['condensate_area'] is None:
inp.hc_flow_rate_condensate_area = 1.0
inp.condensate_area_water = 1.0e-3
else:
inp.hc_flow_rate_condensate_area = data_dict['condensate_area']['mass_rate']
inp.hc_composition_condensate_area = data_dict['condensate_area']['composition']
if data_dict['third_party_feed'] is None:
inp.hc_flow_rate_third_party_feed = 1.0
inp.third_party_feed_water = 1.0e-3
else:
inp.hc_flow_rate_third_party_feed = data_dict['third_party_feed']['mass_rate']
inp.hc_composition_third_party_feed = data_dict['third_party_feed']['composition']
if data_dict['reference_stream_creference_stream_a'] is None:
inp.hc_flow_rate_reference_stream_creference_stream_a = 1.0
inp.reference_stream_c_reference_stream_a_water = 1.0e-3
else:
inp.hc_flow_rate_reference_stream_creference_stream_a = data_dict['reference_stream_creference_stream_a']['mass_rate']
inp.hc_composition_reference_stream_creference_stream_a = data_dict['reference_stream_creference_stream_a']['composition']
if data_dict['reference_stream_creference_stream_b'] is None:
inp.hc_flow_rate_reference_stream_creference_stream_b = 1.0
inp.reference_stream_c_reference_stream_b_water = 1.0e-3
else:
inp.hc_flow_rate_reference_stream_creference_stream_b = data_dict['reference_stream_creference_stream_b']['mass_rate']
inp.hc_composition_reference_stream_creference_stream_b = data_dict['reference_stream_creference_stream_b']['composition']
if data_dict['cluster_d'] is None:
inp.hc_flow_rate_cluster_d = 1.0
inp.cluster_d_water = 1.0e-3
else:
inp.hc_flow_rate_cluster_d = data_dict['cluster_d']['mass_rate']
inp.hc_composition_cluster_d = data_dict['cluster_d']['composition']
if data_dict['cluster_c'] is None:
inp.hc_flow_rate_cluster_c = 1.0
inp.cluster_c_water = 1.0e-3
else:
inp.hc_flow_rate_cluster_c = data_dict['cluster_c']['mass_rate']
inp.hc_composition_cluster_c = data_dict['cluster_c']['composition']
if data_dict['offshore_east'] is None:
inp.hc_flow_rate_offshore_east = 1.0
inp.offshore_asset_east_water = 1.0e-3
else:
inp.hc_flow_rate_offshore_east = data_dict['offshore_east']['mass_rate']
inp.hc_composition_offshore_east = data_dict['offshore_east']['composition']
if data_dict['offshore_south'] is None:
inp.hc_flow_rate_offshore_south = 1.0
inp.offshore_asset_south_water = 1.0e-3
else:
inp.hc_flow_rate_offshore_south = data_dict['offshore_south']['mass_rate']
inp.hc_composition_offshore_south = data_dict['offshore_south']['composition']
read_production_file_all(
input_parameters, files_and_attrs, input_parameters.Year)
Code cell 8: Create default well and water stream using E300 characterization
Notebook cell 15. This code belongs to the Create default well and water stream using E300 characterization section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid-characterization files, and case- specific result folders.
# Improved fluid and stream creation utilities for Offshore Area field
from neqsim.process.processTools import stream, mixer
from neqsim import jneqsim as neqsim
# Load base well fluid
def create_fluid(composition=None):
wellFluid = neqsim.thermo.util.readwrite.EclipseFluidReadWrite.read('generic_offshore_fluid_water.e300')#.autoSelectModel() #use autoSelectModel if CPA is going to be used
if composition is not None:
wellFluid.setMolarComposition(composition)
wellFluid.setMultiPhaseCheck(True)
wellFluid.init(0)
return wellFluid
def create_water_fluid():
"""Create a pure water fluid based on the base fluid."""
water_fluid = create_fluid()
n = water_fluid.getNumberOfComponents()
composition = [0] * n
composition[-1] = 1 # Assume last component is water
water_fluid.setMolarComposition(composition)
water_fluid.init(0)
return water_fluid
def create_stream(name, composition=None):
"""Create a stream with a given fluid and optional composition."""
fluid = create_fluid(composition)
return neqsim.process.equipment.stream.Stream(name, fluid)
def create_gas_stream(name):
"""Create a gas stream (e.g., pure methane)."""
n = create_fluid().getNumberOfComponents()
composition = [0] * n
composition[2] = 1.0 # Assume index 2 is methane
return create_stream(name, composition)
def create_flare_gas_stream(name):
"""Create a flare gas stream with a specified composition."""
n = create_fluid().getNumberOfComponents()
composition = [0] * n
composition[2] = 0.5 # Methane
composition[3] = 0.1 # Ethane
composition[4] = 0.1 # Propane
composition[5] = 0.1 # Butane
return create_stream(name, composition)
def create_water_stream(name):
wstream = create_stream(name)
water_fluid = wstream.getFluid()
n = water_fluid.getNumberOfComponents()
composition = [0] * n
composition[-1] = 1 # Assume last component is water
water_fluid.setMolarComposition(composition)
water_fluid.init(0)
return wstream
Code cell 9: 4. Create a NeqSim process model for well feed and splitting streams into process feed streams and manifolds
Notebook cell 17. This code belongs to the 4. Create a NeqSim process model for well feed and splitting streams into process feed streams and manifolds section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid- characterization files, and case-specific result folders.
def manifold_model(inp: ProcessInput, feedstream, split_factors=None):
manifold_process = neqsim.process.processmodel.ProcessSystem()
manifold = neqsim.process.equipment.manifold.Manifold('manifold')
manifold.addStream(feedstream)
if split_factors is not None:
manifold.setSplitFactors(split_factors)
else:
manifold.setSplitFactors([0.5, 0.5])
manifold.run()
manifold_process.add(manifold)
return manifold_process
Code cell 10: Offshore Area Well Feed Model Function
Notebook cell 19. This code belongs to the Offshore Area Well Feed Model Function section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid-characterization files, and case-specific result folders.
def create_offshore_asset_well_feed_model(inp: ProcessInput):
well_process = neqsim.process.processmodel.ProcessSystem()
if (inp.simulation_case == 'high'):
reference_stream_b_scale_factor = inp.reference_stream_b_scale_factor_high
cluster_a_scale_factor = inp.cluster_a_scale_factor_high
condensate_area_scale_factor = inp.condensate_area_scale_factor_high
cluster_b_scale_factor = inp.cluster_b_scale_factor_high
calibration_stream_scale_factor = inp.calibration_stream_scale_factor_high
offshore_east_scale_factor = inp.offshore_east_scale_factor_high
elif (inp.simulation_case == 'base'):
reference_stream_b_scale_factor = inp.reference_stream_b_scale_factor_intermediate
cluster_a_scale_factor = inp.cluster_a_scale_factor_intermediate
condensate_area_scale_factor = inp.condensate_area_scale_factor_intermediate
cluster_b_scale_factor = inp.cluster_b_scale_factor_intermediate
calibration_stream_scale_factor = inp.calibration_stream_scale_factor_intermediate
offshore_east_scale_factor = inp.offshore_east_scale_factor_intermediate
elif (inp.simulation_case == 'low'):
reference_stream_b_scale_factor = inp.reference_stream_b_scale_factor_low
cluster_a_scale_factor = inp.cluster_a_scale_factor_low
condensate_area_scale_factor = inp.condensate_area_scale_factor_low
cluster_b_scale_factor = inp.cluster_b_scale_factor_low
calibration_stream_scale_factor = inp.calibration_stream_scale_factor_low
offshore_east_scale_factor = inp.offshore_east_scale_factor_low
feedstreamgas_lift = create_stream('gas lift feed')
feedstreamgas_lift.setTemperature(inp.cluster_b_temperature, 'C')
feedstreamgas_lift.setPressure(inp.first_stage_pressure, 'bara')
feedstreamgas_lift.setFlowRate(
0.5, 'MSm3/day')
feedstreamgas_lift.run()
well_process.add(feedstreamgas_lift)
gaslift_splitter = neqsim.process.equipment.splitter.Splitter(
'lift gas splitter')
gaslift_splitter.setInletStream(feedstreamgas_lift)
gaslift_splitter.setSplitFactors(
[0.5, 0.5])
gaslift_splitter.run()
well_process.add(gaslift_splitter)
feedstream_main_reference_stream_b = create_stream('main reference_stream_b hc feed')
feedstream_main_reference_stream_b.setTemperature(inp.reference_stream_b_temperature, 'C')
feedstream_main_reference_stream_b.setPressure(inp.first_stage_pressure, 'bara')
feedstream_main_reference_stream_b.getFluid().setMolarComposition(inp.hc_composition_main_reference_stream_b)
feedstream_main_reference_stream_b.setFlowRate(
inp.hc_flow_rate_main_reference_stream_b*reference_stream_b_scale_factor, 'kg/day')
feedstream_main_reference_stream_b.run()
well_process.add(feedstream_main_reference_stream_b)
waterfeed_main_reference_stream_b = create_water_stream('main reference_stream_b water feed')
waterfeed_main_reference_stream_b.setTemperature(inp.reference_stream_b_temperature, 'C')
waterfeed_main_reference_stream_b.setPressure(inp.first_stage_pressure, 'bara')
waterfeed_main_reference_stream_b.setFlowRate(
inp.reference_stream_b_water*1e3*reference_stream_b_scale_factor, 'kg/day')
waterfeed_main_reference_stream_b.run()
well_process.add(waterfeed_main_reference_stream_b)
main_reference_stream_b_mixer = neqsim.process.equipment.mixer.Mixer('main reference_stream_b mixer')
main_reference_stream_b_mixer.addStream(feedstream_main_reference_stream_b)
main_reference_stream_b_mixer.addStream(waterfeed_main_reference_stream_b)
main_reference_stream_b_mixer.run()
well_process.add(main_reference_stream_b_mixer)
main_reference_stream_b_heater = neqsim.process.equipment.heatexchanger.Heater(
'main reference_stream_b heater')
main_reference_stream_b_heater.setInletStream(main_reference_stream_b_mixer.getOutStream())
main_reference_stream_b_heater.setOutTemperature(inp.reference_stream_b_temperature, 'C')
main_reference_stream_b_heater.run()
well_process.add(main_reference_stream_b_heater)
feedstream_main_reference_stream_b_splitter = neqsim.process.equipment.splitter.Splitter(
'main reference_stream_b splitter')
feedstream_main_reference_stream_b_splitter.setInletStream(
main_reference_stream_b_heater.getOutStream())
feedstream_main_reference_stream_b_splitter.setSplitFactors(
[0.65, 0.35, 0.00001, 0.00001, 0.00001])
feedstream_main_reference_stream_b_splitter.run()
well_process.add(feedstream_main_reference_stream_b_splitter)
# Adding cluster_b to first stage
feedstream_main_cluster_b = create_stream('cluster_b hc feed')
feedstream_main_cluster_b.setTemperature(
inp.cluster_b_temperature, 'C')
feedstream_main_cluster_b.setPressure(inp.first_stage_pressure, 'bara')
feedstream_main_cluster_b.getFluid().setMolarComposition(
inp.hc_composition_cluster_b)
feedstream_main_cluster_b.setFlowRate(
inp.hc_flow_rate_cluster_b*cluster_b_scale_factor, 'kg/day')
feedstream_main_cluster_b.run()
well_process.add(feedstream_main_cluster_b)
waterfeed_cluster_b = create_water_stream('cluster_b water feed')
waterfeed_cluster_b.setTemperature(inp.cluster_b_temperature, 'C')
waterfeed_cluster_b.setPressure(inp.first_stage_pressure, 'bara')
waterfeed_cluster_b.setFlowRate(inp.cluster_b_water*1e3, 'kg/day')
waterfeed_cluster_b.run()
well_process.add(waterfeed_cluster_b)
cluster_b_mixer = neqsim.process.equipment.mixer.Mixer(
'cluster_b mixer')
cluster_b_mixer.addStream(feedstream_main_cluster_b)
cluster_b_mixer.addStream(waterfeed_cluster_b)
cluster_b_mixer.run()
well_process.add(cluster_b_mixer)
cluster_b_heater = neqsim.process.equipment.heatexchanger.Heater(
'cluster_b heater')
cluster_b_heater.setInletStream(cluster_b_mixer.getOutStream())
cluster_b_heater.setOutTemperature(inp.cluster_b_temperature, 'C')
cluster_b_heater.run()
well_process.add(cluster_b_heater)
feedstream_main_cluster_b_splitter = neqsim.process.equipment.splitter.Splitter(
'main cluster_b splitter')
feedstream_main_cluster_b_splitter.setInletStream(
cluster_b_heater.getOutStream())
feedstream_main_cluster_b_splitter.setSplitFactors([0.001, 0.999])
feedstream_main_cluster_b_splitter.run()
well_process.add(feedstream_main_cluster_b_splitter)
# Adding Cluster A to first stage
feedstream_cluster_a = create_stream('cluster_a hc feed')
feedstream_cluster_a.setTemperature(inp.cluster_a_temperature, 'C')
feedstream_cluster_a.setPressure(inp.first_stage_pressure, 'bara')
feedstream_cluster_a.getFluid().setMolarComposition(inp.hc_composition_cluster_a)
feedstream_cluster_a.setFlowRate(
inp.hc_flow_rate_cluster_a*cluster_a_scale_factor, 'kg/day')
feedstream_cluster_a.run()
well_process.add(feedstream_cluster_a)
waterfeed_cluster_a = create_water_stream('cluster_a water feed')
waterfeed_cluster_a.setTemperature(inp.cluster_a_temperature, 'C')
waterfeed_cluster_a.setPressure(inp.first_stage_pressure, 'bara')
waterfeed_cluster_a.setFlowRate(inp.cluster_a_water*1e3, 'kg/day')
waterfeed_cluster_a.run()
well_process.add(waterfeed_cluster_a)
cluster_a_well_mixer = neqsim.process.equipment.mixer.Mixer(
'cluster_a hc water mixer')
cluster_a_well_mixer.addStream(feedstream_cluster_a)
cluster_a_well_mixer.addStream(waterfeed_cluster_a)
cluster_a_well_mixer.run()
well_process.add(cluster_a_well_mixer)
cluster_a_heater = neqsim.process.equipment.heatexchanger.Heater(
'cluster_a heater')
cluster_a_heater.setInletStream(cluster_a_well_mixer.getOutStream())
cluster_a_heater.setOutTemperature(inp.cluster_a_temperature, 'C')
cluster_a_heater.run()
well_process.add(cluster_a_heater)
feedstream_cluster_a_splitter = neqsim.process.equipment.splitter.Splitter(
'cluster_a splitter')
feedstream_cluster_a_splitter.setInletStream(cluster_a_heater.getOutStream())
feedstream_cluster_a_splitter.setSplitFactors([0.25, 0.25, 0.25, 0.25, 0.00001])
feedstream_cluster_a_splitter.run()
well_process.add(feedstream_cluster_a_splitter)
HP_manifold_A = neqsim.process.equipment.mixer.Mixer('HP manifold A')
HP_manifold_A.addStream(feedstream_main_reference_stream_b_splitter.getSplitStream(0))
HP_manifold_A.addStream(
feedstream_main_cluster_b_splitter.getSplitStream(0))
HP_manifold_A.addStream(feedstream_cluster_a_splitter.getSplitStream(0))
HP_manifold_A.addStream(gaslift_splitter.getSplitStream(0))
HP_manifold_A.run()
well_process.add(HP_manifold_A)
HP_manifold_B = neqsim.process.equipment.mixer.Mixer('HP manifold B')
HP_manifold_B.addStream(feedstream_main_reference_stream_b_splitter.getSplitStream(1))
HP_manifold_B.addStream(
feedstream_main_cluster_b_splitter.getSplitStream(1))
HP_manifold_B.addStream(feedstream_cluster_a_splitter.getSplitStream(1))
HP_manifold_B.addStream(gaslift_splitter.getSplitStream(1))
HP_manifold_B.run()
well_process.add(HP_manifold_B)
pressurereduction_reference_stream_b1 = neqsim.process.equipment.valve.ThrottlingValve(
"pressurereduction_reference_stream_b1", feedstream_main_reference_stream_b_splitter.getSplitStream(2))
pressurereduction_reference_stream_b1.setOutletPressure(
inp.second_stage_pressure, 'bara')
pressurereduction_reference_stream_b1.run()
well_process.add(pressurereduction_reference_stream_b1)
pressurereduction_reference_stream_b2 = neqsim.process.equipment.valve.ThrottlingValve(
"pressurereduction_reference_stream_b2", feedstream_main_reference_stream_b_splitter.getSplitStream(3))
pressurereduction_reference_stream_b2.setOutletPressure(
inp.second_stage_pressure, 'bara')
pressurereduction_reference_stream_b2.run()
well_process.add(pressurereduction_reference_stream_b2)
pressurereduction_reference_stream_b3 = neqsim.process.equipment.valve.ThrottlingValve(
"pressurereduction_reference_stream_b3", feedstream_main_reference_stream_b_splitter.getSplitStream(4))
pressurereduction_reference_stream_b3.setOutletPressure(
inp.second_stage_pressure, 'bara')
pressurereduction_reference_stream_b3.run()
well_process.add(pressurereduction_reference_stream_b3)
pressurereduction_cluster_a1 = neqsim.process.equipment.valve.ThrottlingValve(
"pres red cluster_a1", feedstream_cluster_a_splitter.getSplitStream(2))
pressurereduction_cluster_a1.setOutletPressure(
inp.second_stage_pressure, 'bara')
pressurereduction_cluster_a1.run()
well_process.add(pressurereduction_cluster_a1)
pressurereduction_cluster_a2 = neqsim.process.equipment.valve.ThrottlingValve(
"pres red cluster_a2", feedstream_cluster_a_splitter.getSplitStream(3))
pressurereduction_cluster_a2.setOutletPressure(
inp.second_stage_pressure, 'bara')
pressurereduction_cluster_a2.run()
well_process.add(pressurereduction_cluster_a2)
pressurereduction_cluster_a3 = neqsim.process.equipment.valve.ThrottlingValve(
"pres red cluster_a3", feedstream_cluster_a_splitter.getSplitStream(4))
pressurereduction_cluster_a3.setOutletPressure(
inp.second_stage_pressure, 'bara')
pressurereduction_cluster_a3.run()
well_process.add(pressurereduction_cluster_a3)
LP_manifold_A = neqsim.process.equipment.mixer.Mixer('LP manifold A')
LP_manifold_A.addStream(pressurereduction_reference_stream_b1.getOutStream())
LP_manifold_A.addStream(pressurereduction_cluster_a1.getOutStream())
LP_manifold_A.run()
well_process.add(LP_manifold_A)
LP_manifold_B = neqsim.process.equipment.mixer.Mixer('LP manifold B')
LP_manifold_B.addStream(pressurereduction_reference_stream_b2.getOutStream())
LP_manifold_B.addStream(pressurereduction_cluster_a2.getOutStream())
LP_manifold_B.run()
well_process.add(LP_manifold_B)
test_manifold = neqsim.process.equipment.mixer.Mixer('test manifold')
test_manifold.addStream(pressurereduction_reference_stream_b3.getOutStream())
test_manifold.addStream(pressurereduction_cluster_a3.getOutStream())
test_manifold.run()
well_process.add(test_manifold)
# Adding calibration_stream to reference_stream_c calibration_stream separator
feedstream_calibration_stream = create_stream('calibration_stream hc feed')
feedstream_calibration_stream.setTemperature(inp.calibration_stream_temperature, 'C')
feedstream_calibration_stream.setPressure(inp.reference_stream_c_calibration_stream_separator_pressure, 'bara')
feedstream_calibration_stream.getFluid().setMolarComposition(inp.hc_composition_calibration_stream)
feedstream_calibration_stream.setFlowRate(
inp.hc_flow_rate_calibration_stream*calibration_stream_scale_factor, 'kg/day')
feedstream_calibration_stream.run()
well_process.add(feedstream_calibration_stream)
waterfeed_calibration_stream = create_water_stream('calibration_stream water feed')
waterfeed_calibration_stream.setTemperature(inp.calibration_stream_temperature, 'C')
waterfeed_calibration_stream.setPressure(inp.reference_stream_c_calibration_stream_separator_pressure, 'bara')
waterfeed_calibration_stream.setFlowRate(inp.calibration_stream_water*1e3, 'kg/day')
waterfeed_calibration_stream.run()
well_process.add(waterfeed_calibration_stream)
calibration_stream_well_mixer = neqsim.process.equipment.mixer.Mixer(
'calibration_stream hc water mixer')
calibration_stream_well_mixer.addStream(feedstream_calibration_stream)
calibration_stream_well_mixer.addStream(waterfeed_calibration_stream)
calibration_stream_well_mixer.run()
well_process.add(calibration_stream_well_mixer)
feedstream_calibration_stream_splitter = neqsim.process.equipment.splitter.Splitter(
'calibration_stream splitter')
feedstream_calibration_stream_splitter.setInletStream(calibration_stream_well_mixer.getOutStream())
feedstream_calibration_stream_splitter.setSplitFactors([1.0])
feedstream_calibration_stream_splitter.run()
well_process.add(feedstream_calibration_stream_splitter)
# Adding calibration_stream to second stage separator
feedstream_cluster_d = create_stream('cluster_d hc flow')
feedstream_cluster_d.setTemperature(inp.cluster_d_temperature, 'C')
feedstream_cluster_d.setPressure(inp.reference_stream_c_calibration_stream_separator_pressure, 'bara')
feedstream_cluster_d.getFluid().setMolarComposition(inp.hc_composition_cluster_d)
feedstream_cluster_d.setFlowRate(inp.hc_flow_rate_cluster_d, 'kg/day')
feedstream_cluster_d.run()
well_process.add(feedstream_cluster_d)
waterfeed_cluster_d = create_water_stream('cluster_d water feed')
waterfeed_cluster_d.setTemperature(inp.cluster_d_temperature, 'C')
waterfeed_cluster_d.setPressure(inp.reference_stream_c_calibration_stream_separator_pressure, 'bara')
waterfeed_cluster_d.setFlowRate(inp.cluster_d_water*1e3, 'kg/day')
waterfeed_cluster_d.run()
well_process.add(waterfeed_cluster_d)
cluster_d_well_mixer = neqsim.process.equipment.mixer.Mixer(
'cluster_d hc water mixer')
cluster_d_well_mixer.addStream(feedstream_cluster_d)
cluster_d_well_mixer.addStream(waterfeed_cluster_d)
cluster_d_well_mixer.run()
well_process.add(cluster_d_well_mixer)
feedstream_cluster_d_splitter = neqsim.process.equipment.splitter.Splitter(
'cluster_d splitter')
feedstream_cluster_d_splitter.setInletStream(cluster_d_well_mixer.getOutStream())
feedstream_cluster_d_splitter.setSplitFactors([1.0])
feedstream_cluster_d_splitter.run()
well_process.add(feedstream_cluster_d_splitter)
# Adding calibration_stream to second stage separator
feedstream_cluster_c = create_stream('cluster_c hc flow')
feedstream_cluster_c.setTemperature(inp.cluster_c_temperature, 'C')
feedstream_cluster_c.setPressure(inp.reference_stream_c_calibration_stream_separator_pressure, 'bara')
feedstream_cluster_c.getFluid().setMolarComposition(inp.hc_composition_cluster_c)
feedstream_cluster_c.setFlowRate(inp.hc_flow_rate_cluster_c, 'kg/day')
feedstream_cluster_c.run()
well_process.add(feedstream_cluster_c)
waterfeed_cluster_c = create_water_stream('cluster_c water feed')
waterfeed_cluster_c.setTemperature(inp.cluster_c_temperature, 'C')
waterfeed_cluster_c.setPressure(inp.reference_stream_c_calibration_stream_separator_pressure, 'bara')
waterfeed_cluster_c.setFlowRate(inp.cluster_c_water*1e3, 'kg/day')
waterfeed_cluster_c.run()
well_process.add(waterfeed_cluster_c)
cluster_c_well_mixer = neqsim.process.equipment.mixer.Mixer(
'cluster_c hc water mixer')
cluster_c_well_mixer.addStream(feedstream_cluster_c)
cluster_c_well_mixer.addStream(waterfeed_cluster_c)
cluster_c_well_mixer.run()
well_process.add(cluster_c_well_mixer)
feedstream_cluster_c_splitter = neqsim.process.equipment.splitter.Splitter(
'cluster_c splitter')
feedstream_cluster_c_splitter.setInletStream(cluster_c_well_mixer.getOutStream())
feedstream_cluster_c_splitter.setSplitFactors([1.0])
feedstream_cluster_c_splitter.run()
well_process.add(feedstream_cluster_c_splitter)
# Adding reference_stream_c reference_stream_a to second stage separator
feedstream_reference_stream_creference_stream_a = create_stream('reference_stream_creference_stream_a hc flow')
feedstream_reference_stream_creference_stream_a.setTemperature(
inp.reference_stream_c_reference_stream_a_temperature, 'C')
feedstream_reference_stream_creference_stream_a.setPressure(inp.reference_stream_c_calibration_stream_separator_pressure, 'bara')
feedstream_reference_stream_creference_stream_a.getFluid().setMolarComposition(
inp.hc_composition_reference_stream_creference_stream_a)
feedstream_reference_stream_creference_stream_a.setFlowRate(
inp.hc_flow_rate_reference_stream_creference_stream_a, 'kg/day')
feedstream_reference_stream_creference_stream_a.run()
well_process.add(feedstream_reference_stream_creference_stream_a)
waterfeed_reference_stream_creference_stream_a = create_water_stream('reference_stream_creference_stream_a water feed')
waterfeed_reference_stream_creference_stream_a.setTemperature(inp.reference_stream_c_reference_stream_a_temperature, 'C')
waterfeed_reference_stream_creference_stream_a.setPressure(inp.reference_stream_c_calibration_stream_separator_pressure, 'bara')
waterfeed_reference_stream_creference_stream_a.setFlowRate(inp.reference_stream_c_reference_stream_a_water*1e3+1, 'kg/day')
waterfeed_reference_stream_creference_stream_a.run()
well_process.add(waterfeed_reference_stream_creference_stream_a)
reference_stream_creference_stream_a_well_mixer = neqsim.process.equipment.mixer.Mixer(
'reference_stream_c reference_stream_a hc water mixer')
reference_stream_creference_stream_a_well_mixer.addStream(feedstream_reference_stream_creference_stream_a)
reference_stream_creference_stream_a_well_mixer.addStream(waterfeed_reference_stream_creference_stream_a)
reference_stream_creference_stream_a_well_mixer.run()
well_process.add(reference_stream_creference_stream_a_well_mixer)
feedstream_reference_stream_creference_stream_a_splitter = neqsim.process.equipment.splitter.Splitter(
'reference_stream_c reference_stream_a splitter')
feedstream_reference_stream_creference_stream_a_splitter.setInletStream(
reference_stream_creference_stream_a_well_mixer.getOutStream())
feedstream_reference_stream_creference_stream_a_splitter.setSplitFactors([1.0])
feedstream_reference_stream_creference_stream_a_splitter.run()
well_process.add(feedstream_reference_stream_creference_stream_a_splitter)
# Adding reference_stream_c reference_stream_a to second stage separator
feedstream_reference_stream_creference_stream_b = create_stream('reference_stream_c reference_stream_b hc flow')
feedstream_reference_stream_creference_stream_b.setTemperature(inp.reference_stream_c_reference_stream_b_temperature, 'C')
feedstream_reference_stream_creference_stream_b.setPressure(inp.reference_stream_c_calibration_stream_separator_pressure, 'bara')
feedstream_reference_stream_creference_stream_b.getFluid().setMolarComposition(inp.hc_composition_reference_stream_creference_stream_b)
feedstream_reference_stream_creference_stream_b.setFlowRate(inp.hc_flow_rate_reference_stream_creference_stream_b, 'kg/day')
feedstream_reference_stream_creference_stream_b.run()
well_process.add(feedstream_reference_stream_creference_stream_b)
waterfeed_reference_stream_creference_stream_b = create_water_stream('reference_stream_c reference_stream_b water feed')
waterfeed_reference_stream_creference_stream_b.setTemperature(inp.reference_stream_c_reference_stream_b_temperature, 'C')
waterfeed_reference_stream_creference_stream_b.setPressure(inp.reference_stream_c_calibration_stream_separator_pressure, 'bara')
waterfeed_reference_stream_creference_stream_b.setFlowRate(inp.reference_stream_c_reference_stream_b_water*1e3, 'kg/day')
waterfeed_reference_stream_creference_stream_b.run()
well_process.add(waterfeed_reference_stream_creference_stream_b)
reference_stream_creference_stream_b_well_mixer = neqsim.process.equipment.mixer.Mixer(
'reference_stream_c reference_stream_b hc water mixer')
reference_stream_creference_stream_b_well_mixer.addStream(feedstream_reference_stream_creference_stream_b)
reference_stream_creference_stream_b_well_mixer.addStream(waterfeed_reference_stream_creference_stream_b)
reference_stream_creference_stream_b_well_mixer.run()
well_process.add(reference_stream_creference_stream_b_well_mixer)
feedstream_reference_stream_creference_stream_b_splitter = neqsim.process.equipment.splitter.Splitter(
'reference_stream_c reference_stream_b splitter')
feedstream_reference_stream_creference_stream_b_splitter.setInletStream(
reference_stream_creference_stream_b_well_mixer.getOutStream())
feedstream_reference_stream_creference_stream_b_splitter.setSplitFactors([1.0])
feedstream_reference_stream_creference_stream_b_splitter.run()
well_process.add(feedstream_reference_stream_creference_stream_b_splitter)
manifold_reference_stream_ccalibration_stream = neqsim.process.equipment.mixer.Mixer(
'reference_stream_c calibration_stream manifold')
manifold_reference_stream_ccalibration_stream.addStream(feedstream_calibration_stream_splitter.getSplitStream(0))
manifold_reference_stream_ccalibration_stream.addStream(
feedstream_reference_stream_creference_stream_b_splitter.getSplitStream(0))
if inp.simulation_case == 'high' or inp.Year <= 100: # added to include in benchmark cases
manifold_reference_stream_ccalibration_stream.addStream(
feedstream_cluster_d_splitter.getSplitStream(0))
manifold_reference_stream_ccalibration_stream.addStream(
feedstream_cluster_c_splitter.getSplitStream(0))
manifold_reference_stream_ccalibration_stream.addStream(
feedstream_reference_stream_creference_stream_a_splitter.getSplitStream(0))
manifold_reference_stream_ccalibration_stream.run()
well_process.add(manifold_reference_stream_ccalibration_stream)
offshore_asset_south_feed = create_stream('Offshore Area South Feed')
offshore_asset_south_feed.setTemperature(inp.offshore_asset_south_temperature, 'C')
offshore_asset_south_feed.setPressure(inp.second_stage_pressure, 'bara')
offshore_asset_south_feed.getFluid().setMolarComposition(inp.hc_composition_offshore_south)
offshore_asset_south_feed.setFlowRate(inp.hc_flow_rate_offshore_south, 'kg/day')
offshore_asset_south_feed.run()
well_process.add(offshore_asset_south_feed)
offshore_asset_east_feed = create_stream('Offshore Area East Feed')
offshore_asset_east_feed.setTemperature(inp.offshore_asset_east_temperature, 'C')
offshore_asset_east_feed.setPressure(inp.second_stage_pressure, 'bara')
offshore_asset_east_feed.getFluid().setMolarComposition(inp.hc_composition_offshore_east)
offshore_asset_east_feed.setFlowRate(inp.hc_flow_rate_offshore_east*offshore_east_scale_factor, 'kg/day')
offshore_asset_east_feed.run()
well_process.add(offshore_asset_east_feed)
satelitemixer = neqsim.process.equipment.mixer.Mixer('satellite mixer')
satelitemixer.addStream(offshore_asset_south_feed)
satelitemixer.addStream(offshore_asset_east_feed)
satelitemixer.run()
well_process.add(satelitemixer)
lp_feed_pressure_temperature_setter = neqsim.process.equipment.heatexchanger.Heater(
"satellite heater", satelitemixer.getOutStream())
lp_feed_pressure_temperature_setter.setOutTemperature(
inp.satellite_heater_temperature, 'C')
lp_feed_pressure_temperature_setter.setOutPressure(
inp.second_stage_pressure, 'bara')
lp_feed_pressure_temperature_setter.run()
well_process.add(lp_feed_pressure_temperature_setter)
manifold_south_east = neqsim.process.equipment.manifold.Manifold(
'south east manifold')
manifold_south_east.addStream(lp_feed_pressure_temperature_setter.getOutStream())
manifold_south_east.setSplitFactors([0.5, 0.5])
manifold_south_east.run()
well_process.add(manifold_south_east)
offshore_asset_condensate_feed = create_stream('Offshore Area C Feed')
offshore_asset_condensate_feed.setTemperature(inp.condensate_area_temperature, 'C')
offshore_asset_condensate_feed.setPressure(inp.second_stage_pressure, 'bara')
offshore_asset_condensate_feed.getFluid().setMolarComposition(inp.hc_composition_condensate_area)
offshore_asset_condensate_feed.setFlowRate(inp.hc_flow_rate_condensate_area*condensate_area_scale_factor, 'kg/day')
offshore_asset_condensate_feed.run()
well_process.add(offshore_asset_condensate_feed)
third_party_feedFeed = create_stream('ThirdPartyFeed Feed')
third_party_feedFeed.setTemperature(inp.third_party_feed_temperature, 'C')
third_party_feedFeed.setPressure(inp.second_stage_pressure, 'bara')
third_party_feedFeed.getFluid().setMolarComposition(inp.hc_composition_third_party_feed)
third_party_feedFeed.setFlowRate(inp.hc_flow_rate_third_party_feed, 'kg/day')
third_party_feedFeed.run()
well_process.add(third_party_feedFeed)
return well_process
Code cell 11: Offshore Area Well Feed Model Function
Notebook cell 21. This code belongs to the Offshore Area Well Feed Model Function section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid-characterization files, and case-specific result folders.
%%script false --no-raise-error uncomment if model should be saved to file
offshore_asset_well_feed_model = create_offshore_asset_well_feed_model(input_parameters)
offshore_asset_well_feed_model.run()
clear_output(wait=True)
print("Calculation finished")
multiplier = 0.0
if input_parameters.simulation_case == 'high' or input_parameters.Year <= 100: # added to include in benchmark cases
multiplier = 1.0
# Corrected feedflow calculation
feedflow = (
offshore_asset_well_feed_model.getUnit('gas lift feed').getFlowRate('kg/hr') +
offshore_asset_well_feed_model.getUnit('main reference_stream_b mixer').getOutletStream().getFlowRate('kg/hr') +
offshore_asset_well_feed_model.getUnit('cluster_b mixer').getOutletStream().getFlowRate('kg/hr')+
offshore_asset_well_feed_model.getUnit('cluster_a hc water mixer').getOutletStream().getFlowRate('kg/hr') +
offshore_asset_well_feed_model.getUnit('calibration_stream hc water mixer').getOutletStream().getFlowRate('kg/hr') +
offshore_asset_well_feed_model.getUnit('cluster_d hc water mixer').getOutletStream().getFlowRate('kg/hr')*multiplier +
offshore_asset_well_feed_model.getUnit('cluster_c hc water mixer').getOutletStream().getFlowRate('kg/hr')*multiplier +
offshore_asset_well_feed_model.getUnit('reference_stream_c reference_stream_a hc water mixer').getOutletStream().getFlowRate('kg/hr')*multiplier +
offshore_asset_well_feed_model.getUnit('reference_stream_c reference_stream_b hc water mixer').getOutletStream().getFlowRate('kg/hr') +
offshore_asset_well_feed_model.getUnit('Offshore Area South Feed').getFlowRate('kg/hr') +
offshore_asset_well_feed_model.getUnit('Offshore Area East Feed').getFlowRate('kg/hr') +
offshore_asset_well_feed_model.getUnit('Offshore Area C Feed').getFlowRate('kg/hr') +
offshore_asset_well_feed_model.getUnit('ThirdPartyFeed Feed').getFlowRate('kg/hr')
)
# Corrected feedflow calculation
exitflow = (
offshore_asset_well_feed_model.getUnit('south east manifold').getSplitStream(0).getFlowRate('kg/hr') +
offshore_asset_well_feed_model.getUnit('south east manifold').getSplitStream(1).getFlowRate('kg/hr') +
offshore_asset_well_feed_model.getUnit('Offshore Area C Feed').getFlowRate('kg/hr') +
offshore_asset_well_feed_model.getUnit('ThirdPartyFeed Feed').getFlowRate('kg/hr') +
offshore_asset_well_feed_model.getUnit('reference_stream_c calibration_stream manifold').getOutletStream().getFlowRate('kg/hr') +
offshore_asset_well_feed_model.getUnit('LP manifold A').getOutletStream().getFlowRate('kg/hr') +
offshore_asset_well_feed_model.getUnit('LP manifold B').getOutletStream().getFlowRate('kg/hr') +
offshore_asset_well_feed_model.getUnit('test manifold').getOutletStream().getFlowRate('kg/hr') +
offshore_asset_well_feed_model.getUnit('HP manifold A').getOutletStream().getFlowRate('kg/hr') +
offshore_asset_well_feed_model.getUnit('HP manifold B').getOutletStream().getFlowRate('kg/hr')
)
print(f"Exit flow: {exitflow:.2f} kg/hr")
print('mass balance check: ', (feedflow - exitflow) / feedflow * 100, '%')
Code cell 12: Offshore Area Well Feed Model Function
Notebook cell 22. This code belongs to the Offshore Area Well Feed Model Function section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid-characterization files, and case-specific result folders.
%%script false --no-raise-error uncomment if model should be saved to file
print('total first stage HP separator A ', offshore_asset_well_feed_model.getUnit(
"HP manifold A").getOutletStream().getFluid().getFlowRate("MSm3/day"), 'MSm3/day')
print('total first stage HP separator B ', offshore_asset_well_feed_model.getUnit(
"HP manifold B").getOutletStream().getFluid().getFlowRate("MSm3/day"), 'MSm3/day')
print('total first stage LP separator A ', offshore_asset_well_feed_model.getUnit(
"LP manifold A").getOutStream().getFluid().getFlowRate("MSm3/day"), 'MSm3/day')
print('total first stage LP separator B ', offshore_asset_well_feed_model.getUnit(
"LP manifold B").getOutStream().getFluid().getFlowRate("MSm3/day"), 'MSm3/day')
print('total first stage reference_stream_c-calibration_stream sep ', offshore_asset_well_feed_model.getUnit(
"reference_stream_c calibration_stream manifold").getOutStream().getFluid().getFlowRate("MSm3/day"), 'MSm3/day')
print('test manifold ', offshore_asset_well_feed_model.getUnit(
"test manifold").getOutStream().getFluid().getFlowRate("MSm3/day"), 'MSm3/day')
print('offshore_asset east ', offshore_asset_well_feed_model.getUnit(
"Offshore Area East Feed").getFluid().getFlowRate("idSm3/hr")*24, 'm3/day')
print('offshore_asset south ', offshore_asset_well_feed_model.getUnit(
"Offshore Area South Feed").getFluid().getFlowRate("idSm3/hr")*24, 'm3/day')
Code cell 13: ReferenceStreamC CalibrationStream Separation Process
Notebook cell 26. This code belongs to the ReferenceStreamC CalibrationStream Separation Process section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid-characterization files, and case-specific result folders.
def reference_stream_c_calibration_stream_separation_process(inp: ProcessInput, feed_stream):
separation_process = neqsim.process.processmodel.ProcessSystem()
liquidresyclestream = create_stream('refluxstream_gas_secondstage_1')
liquidresyclestream.setFlowRate(10.0, 'kg/hr')
liquidresyclestream.setPressure(inp.reference_stream_c_calibration_stream_separator_pressure, 'bara')
liquidresyclestream.run()
separation_process.add(liquidresyclestream)
recyclegasstream = create_stream('recycle 1st stage CALIB_STAGE')
recyclegasstream.setPressure(inp.reference_stream_c_calibration_stream_separator_pressure, 'bara')
recyclegasstream.setTemperature(25.0, 'C')
recyclegasstream.setFlowRate(10.0, "kg/hr")
recyclegasstream.run()
separation_process.add(recyclegasstream)
recycle_gas_cooler = neqsim.process.equipment.heatexchanger.Cooler(
'recycle gas cooler', recyclegasstream)
recycle_gas_cooler.setOutTemperature(35.0, 'C')
recycle_gas_cooler.run()
separation_process.add(recycle_gas_cooler)
inletmanifold = neqsim.process.equipment.mixer.Mixer('inlet manifold')
inletmanifold.addStream(feed_stream)
inletmanifold.addStream(liquidresyclestream)
inletmanifold.addStream(recycle_gas_cooler.getOutletStream())
inletmanifold.run()
separation_process.add(inletmanifold)
reference_stream_cSeparator = neqsim.process.equipment.separator.Separator(
"reference_stream_c separator", inletmanifold.getOutStream())
reference_stream_cSeparator.run()
separation_process.add(reference_stream_cSeparator)
intermediate_pressure_compressor = math.sqrt(inp.reference_stream_c_calibration_stream_separator_pressure*inp.reference_stream_c_CALIB_STAGE_compressor_pressure)
calib_stage_compressor_1st_stage = neqsim.process.equipment.compressor.Compressor(
'CALIB_STAGE compressor 1st stage', reference_stream_cSeparator.getGasOutStream())
calib_stage_compressor_1st_stage.setUsePolytropicCalc(True)
calib_stage_compressor_1st_stage.setPolytropicEfficiency(0.8)
calib_stage_compressor_1st_stage.setCompressorChartType("interpolate and extrapolate")
calib_stage_compressor_1st_stage.setOutletPressure(intermediate_pressure_compressor, 'bara')
calib_stage_compressor_1st_stage.getCompressorChart().setHeadUnit(inp.headunit)
calib_stage_compressor_1st_stage.getCompressorChart().getSurgeCurve().setCurve(inp.chartConditions, inp.surgeflow_CALIB_STAGE__compressor_1st_stage, inp.surgehead_CALIB_STAGE_compressor_1st_stage)
try:
calib_stage_compressor_1st_stage.run()
except:
print('error in calib_stage_compressor 1st stage')
separation_process.add(calib_stage_compressor_1st_stage)
gassplitter = neqsim.process.equipment.splitter.Splitter(
'1st stage anti surge splitter CALIB_STAGE')
gassplitter.setInletStream(calib_stage_compressor_1st_stage.getOutletStream())
gassplitter.setFlowRates([-1, 1.0], "kg/hr")
try:
gassplitter.run()
except:
print('error in gassplitter')
separation_process.add(gassplitter)
antisurgeCalculator = neqsim.process.equipment.util.Calculator("anti surge calculator_1")
antisurgeCalculator.addInputVariable(calib_stage_compressor_1st_stage)
antisurgeCalculator.setOutputVariable(gassplitter)
try:
antisurgeCalculator.run()
except:
print('error in antisurgeCalculator')
separation_process.add(antisurgeCalculator)
antisurgevalve = neqsim.process.equipment.valve.ThrottlingValve('aniti surge valve', gassplitter.getSplitStream(1))
antisurgevalve.setOutletPressure(inp.reference_stream_c_calibration_stream_separator_pressure, "bara")
antisurgevalve.run()
separation_process.add(antisurgevalve)
recycl = neqsim.process.equipment.util.Recycle("recycle anti surge 1st stage compressor")
recycl.addStream(antisurgevalve.getOutletStream())
recycl.setOutletStream(recyclegasstream)
recycl.setTolerance(1e-2)
recycl.run()
separation_process.add(recycl)
gasmanifold = neqsim.process.equipment.manifold.Manifold('gas manifold')
gasmanifold.addStream(gassplitter.getSplitStream(0))
reference_stream_c_gas_fraction_to_CALIB_STAGE = inp.reference_stream_c_gas_fraction_to_CALIB_STAGE
reference_stream_c_gas_fraction_to_LPseparator = (
1.0 - reference_stream_c_gas_fraction_to_CALIB_STAGE)/2.0 # split in A/B train
gasmanifold.setSplitFactors(
[reference_stream_c_gas_fraction_to_CALIB_STAGE, reference_stream_c_gas_fraction_to_LPseparator, reference_stream_c_gas_fraction_to_LPseparator])
gasmanifold.run()
separation_process.add(gasmanifold)
recyclegasstream2 = create_stream('recycle CALIB_STAGE 2nd stage')
recyclegasstream2.setPressure(intermediate_pressure_compressor, 'bara')
recyclegasstream2.setTemperature(25.0, 'C')
recyclegasstream2.setFlowRate(1.0, "kg/hr")
recyclegasstream2.run()
separation_process.add(recyclegasstream2)
gas_mixer_CALIB_STAGE_2 = neqsim.process.equipment.mixer.Mixer(
"gas mixer CALIB_STAGE_2")
gas_mixer_CALIB_STAGE_2.addStream(gasmanifold.getSplitStream(0))
gas_mixer_CALIB_STAGE_2.addStream(recyclegasstream2)
try:
gas_mixer_CALIB_STAGE_2.run()
except:
print('error in gas_mixer_CALIB_STAGE_2')
separation_process.add(gas_mixer_CALIB_STAGE_2)
gascoolerreference_stream_c = neqsim.process.equipment.heatexchanger.Cooler(
'reference_stream_c gas cooler', gas_mixer_CALIB_STAGE_2.getOutletStream())
gascoolerreference_stream_c.setOutTemperature(inp.reference_stream_c_CALIB_STAGE_compressor_cooler_temperature, 'C') # for high case needed to increas it from 25 to 35 from 2033
try:
gascoolerreference_stream_c.run()
except:
print('error in gascoolerreference_stream_c')
separation_process.add(gascoolerreference_stream_c)
reference_stream_cScrubber = neqsim.process.equipment.separator.Separator(
"reference_stream_c gas scrubber", gascoolerreference_stream_c.getOutletStream())
try:
reference_stream_cScrubber.run()
except:
print('error in reference_stream_cScrubber')
separation_process.add(reference_stream_cScrubber)
liquidresycle = neqsim.process.equipment.util.Recycle('reference_stream_c liquid resycle')
liquidresycle.addStream(reference_stream_cScrubber.getLiquidOutStream())
liquidresycle.setOutletStream(liquidresyclestream)
liquidresycle.setTolerance(1e-1)
try:
liquidresycle.run()
except:
print('error in liquidresycle')
separation_process.add(liquidresycle)
oilmanifold = neqsim.process.equipment.manifold.Manifold('manifold')
oilmanifold.addStream(reference_stream_cSeparator.getLiquidOutStream())
oilmanifold.setSplitFactors([0.5, 0.5])
try:
oilmanifold.run()
except:
print('error in oilmanifold')
separation_process.add(oilmanifold)
calib_stage_compressor_2nd_stage = neqsim.process.equipment.compressor.Compressor(
'CALIB_STAGE compressor 2nd stage', reference_stream_cScrubber.getGasOutStream())
calib_stage_compressor_2nd_stage.setOutletPressure(inp.reference_stream_c_CALIB_STAGE_compressor_pressure, 'bara')
calib_stage_compressor_2nd_stage.setUsePolytropicCalc(True)
calib_stage_compressor_2nd_stage.setCompressorChartType("interpolate and extrapolate")
calib_stage_compressor_2nd_stage.setPolytropicEfficiency(0.8)
calib_stage_compressor_2nd_stage.getCompressorChart().setHeadUnit(inp.headunit)
calib_stage_compressor_2nd_stage.getCompressorChart().getSurgeCurve().setCurve(inp.chartConditions, inp.surgeflow_CALIB_STAGE__compressor_2nd_stage, inp.surgehead_CALIB_STAGE_compressor_2nd_stage)
try:
calib_stage_compressor_2nd_stage.run()
except:
print('error in CALIB_STAGE compressor')
separation_process.add(calib_stage_compressor_2nd_stage)
gassplitter2 = neqsim.process.equipment.splitter.Splitter(
'2nd stage anti surge splitter')
gassplitter2.setInletStream(calib_stage_compressor_2nd_stage.getOutletStream())
gassplitter2.setFlowRates([-1, 1.0], "kg/hr")
try:
gassplitter2.run()
except:
print('error in gassplitter2')
separation_process.add(gassplitter2)
antisurgeCalculator2 = neqsim.process.equipment.util.Calculator("anti surge calculator_2")
antisurgeCalculator2.addInputVariable(calib_stage_compressor_2nd_stage)
antisurgeCalculator2.setOutputVariable(gassplitter2)
antisurgeCalculator2.run()
separation_process.add(antisurgeCalculator2)
antisurgevalve2 = neqsim.process.equipment.valve.ThrottlingValve('aniti surge valve 2', gassplitter2.getSplitStream(1))
antisurgevalve2.setOutletPressure(intermediate_pressure_compressor, "bara")
antisurgevalve2.run()
separation_process.add(antisurgevalve2)
recycl2 = neqsim.process.equipment.util.Recycle("recycle anti surge 2nd stage compressor")
recycl2.addStream(antisurgevalve2.getOutletStream())
recycl2.setOutletStream(recyclegasstream2)
recycl2.setTolerance(1e-2)
recycl2.run()
separation_process.add(recycl2)
calib_stage_injection_stream = neqsim.process.equipment.stream.Stream(
'CALIB_STAGE gas_injection stream', gassplitter2.getSplitStream(0))
calib_stage_injection_stream.run()
separation_process.add(calib_stage_injection_stream)
return separation_process
def test_separation_process(inp: ProcessInput, feed_stream):
separation_process = neqsim.process.processmodel.ProcessSystem()
testSeparator = neqsim.process.equipment.separator.Separator(
"test separator", feed_stream)
testSeparator.run()
separation_process.add(testSeparator)
return separation_process
Code cell 14: ReferenceStreamC CalibrationStream Separation Process
Notebook cell 28. This code belongs to the ReferenceStreamC CalibrationStream Separation Process section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid-characterization files, and case-specific result folders.
%%script false --no-raise-error uncomment if model should be saved to file
reference_stream_c_sep_process = reference_stream_c_calibration_stream_separation_process(
input_parameters, offshore_asset_well_feed_model.getUnit("reference_stream_c calibration_stream manifold").getOutStream())
reference_stream_c_sep_process.run()
#reference_stream_c_sep_process.run()
# Corrected feedflow calculation
feedflow = (
offshore_asset_well_feed_model.getUnit("reference_stream_c calibration_stream manifold").getOutStream().getFlowRate('kg/hr')
)
print(f"Total feed flow: {feedflow:.2f} kg/hr")
# Corrected feedflow calculation
exitflow = (
reference_stream_c_sep_process.getUnit("CALIB_STAGE gas_injection stream").getFlowRate('kg/hr') +
reference_stream_c_sep_process.getUnit("manifold").getSplitStream(0).getFlowRate('kg/hr') +
reference_stream_c_sep_process.getUnit("manifold").getSplitStream(1).getFlowRate('kg/hr') +
reference_stream_c_sep_process.getUnit("gas manifold").getSplitStream(1).getFlowRate('kg/hr') +
reference_stream_c_sep_process.getUnit("gas manifold").getSplitStream(2).getFlowRate('kg/hr')
)
print(f"Exit flow: {exitflow:.2f} kg/hr")
print('mass balance check: ', (feedflow - exitflow) / feedflow * 100, '%')
Code cell 15: Oil Separation Process Function
Notebook cell 31. This code belongs to the Oil Separation Process Function section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid- characterization files, and case-specific result folders.
def create_oil_separation_process(inp: ProcessInput, first_stage_feed, second_stage_feed, lp_gas_split_factor, reference_stream_ccalibration_streamoil=None, south_east_feed=None, test_separator=None, reference_stream_c_calibration_stream_gas=None, other_train_lp_gas_feed=None, other_train_lp2_gas_feed=None, other_train_mp_gas_feed=None, mpsplit = None):
separation_process = neqsim.process.processmodel.ProcessSystem()
inlet_TP_setter = neqsim.process.equipment.heatexchanger.Heater(
"inlet TP setter", first_stage_feed)
inlet_TP_setter.setdT(0.0)
inlet_TP_setter.setOutPressure(inp.first_stage_pressure, 'bara')
inlet_TP_setter.run()
separation_process.add(inlet_TP_setter)
firstStageSeparator = neqsim.process.equipment.separator.ThreePhaseSeparator(
"1st stage separator", inlet_TP_setter.getOutStream())
firstStageSeparator.setEntrainment(
0.5, "feed", "volume", "aqueous", "oil")
try:
firstStageSeparator.run()
except:
print('error in first stage separator')
separation_process.add(firstStageSeparator)
oil_1st_stage_mixer = neqsim.process.equipment.mixer.Mixer('oilmix')
oil_1st_stage_mixer.addStream(firstStageSeparator.getOilOutStream())
oil_1st_stage_mixer.run()
separation_process.add(oil_1st_stage_mixer)
oilvalve1 = neqsim.process.equipment.valve.ThrottlingValve(
"oil depres valve", oil_1st_stage_mixer.getOutStream())
oilvalve1.setOutletPressure(inp.second_stage_pressure, 'bara')
try:
oilvalve1.run()
except:
print('error in oil valve 1')
separation_process.add(oilvalve1)
oilfromfirststage = oilvalve1.getOutStream()
oilHeaterFromFirstStage = neqsim.process.equipment.heatexchanger.Heater(
"oil heater second stage", oilvalve1.getOutStream())
if inp.Year >= inp.heater_start_year:
oilHeaterFromFirstStage.setOutTemperature(
inp.first_stage_heater_temperature, 'C')
else:
oilHeaterFromFirstStage.setdT(0.0)
oilHeaterFromFirstStage.run()
separation_process.add(oilHeaterFromFirstStage)
oilFirstStage = create_stream("frist stage oil reflux")
oilFirstStage.setFlowRate(100.0, 'kg/hr')
oilFirstStage.setPressure(inp.second_stage_pressure, 'bara')
oilFirstStage.setTemperature(30.0, 'C')
oilFirstStage.run()
separation_process.add(oilFirstStage)
oilFirstStageMixer = neqsim.process.equipment.mixer.Mixer(
"first stage oil mixer")
oilFirstStageMixer.addStream(oilHeaterFromFirstStage.getOutletStream())
oilFirstStageMixer.addStream(oilFirstStage)
oilFirstStageMixer.addStream(second_stage_feed)
oilFirstStageMixer.addStream(
south_east_feed)
if reference_stream_ccalibration_streamoil:
oilFirstStageMixer.addStream(reference_stream_ccalibration_streamoil)
if reference_stream_c_calibration_stream_gas:
oilFirstStageMixer.addStream(reference_stream_c_calibration_stream_gas)
if test_separator:
oilFirstStageMixer.addStream(test_separator.getLiquidOutStream())
try:
oilFirstStageMixer.run()
except:
print('error in first stage oil mixer')
separation_process.add(oilFirstStageMixer)
secondStageSeparator = neqsim.process.equipment.separator.ThreePhaseSeparator(
"2nd stage separator", oilFirstStageMixer.getOutStream())
secondStageSeparator.setEntrainment(
0.2, "feed", "volume", "aqueous", "oil")
try:
secondStageSeparator.run()
except:
print('error in 2nd stage separator')
separation_process.add(secondStageSeparator)
oil_2nd_stage_mixer = neqsim.process.equipment.mixer.Mixer('oilmix 2')
oil_2nd_stage_mixer.addStream(secondStageSeparator.getOilOutStream())
oil_2nd_stage_mixer.run()
separation_process.add(oil_2nd_stage_mixer)
oilSeccondStage = create_stream("seccond stage oil reflux")
oilSeccondStage.setFlowRate(100.0, 'kg/hr')
oilSeccondStage.setPressure(inp.third_stage_pressure, 'bara')
oilSeccondStage.setTemperature(40.0, 'C')
oilSeccondStage.run()
separation_process.add(oilSeccondStage)
valve_oil_from_seccond_stage = neqsim.process.equipment.valve.ThrottlingValve(
"valve oil from seccond stage", oil_2nd_stage_mixer.getOutStream())
valve_oil_from_seccond_stage.setOutletPressure(
inp.third_stage_pressure, 'bara')
try:
valve_oil_from_seccond_stage.run()
except:
print('error in valve oil from seccond stage')
separation_process.add(valve_oil_from_seccond_stage)
oilSeccondStageMixer = neqsim.process.equipment.mixer.Mixer(
"seccond stage oil mixer")
oilSeccondStageMixer.addStream(
valve_oil_from_seccond_stage.getOutletStream())
oilSeccondStageMixer.addStream(oilSeccondStage)
oilSeccondStageMixer.run()
separation_process.add(oilSeccondStageMixer)
thirdStageSeparator = neqsim.process.equipment.separator.Separator(
"3rd stage separator", oilSeccondStageMixer.getOutStream())
try:
thirdStageSeparator.run()
except:
print('error in third stage separator')
separation_process.add(thirdStageSeparator)
valve_oil_from_third_stage = neqsim.process.equipment.valve.ThrottlingValve(
"valve oil from third stage", thirdStageSeparator.getLiquidOutStream())
valve_oil_from_third_stage.setOutletPressure(
inp.fourth_stage_pressure, 'bara')
valve_oil_from_third_stage.run()
separation_process.add(valve_oil_from_third_stage)
oilThirdStage = create_stream("third stage oil reflux")
oilThirdStage.setFlowRate(100.0, 'kg/hr')
oilThirdStage.setPressure(inp.fourth_stage_pressure, 'bara')
oilThirdStage.setTemperature(50.0, 'C')
oilThirdStage.run()
separation_process.add(oilThirdStage)
oilThirdStageMixer = neqsim.process.equipment.mixer.Mixer(
"third stage oil mixer")
oilThirdStageMixer.addStream(valve_oil_from_third_stage.getOutletStream())
oilThirdStageMixer.addStream(oilThirdStage)
oilThirdStageMixer.run()
separation_process.add(oilThirdStageMixer)
fourthStageSeparator = neqsim.process.equipment.separator.ThreePhaseSeparator(
"4th stage separator", oilThirdStageMixer.getOutStream())
# fourthStageSeparator.setEntrainment(0.001, "product", "volume", "aqueous", "oil")
try:
fourthStageSeparator.run()
except:
print('error in fourth stage separator')
separation_process.add(fourthStageSeparator)
fourth_stage_pressure_control_valve = neqsim.process.equipment.valve.ThrottlingValve(
"20PV153B", fourthStageSeparator.getGasOutStream())
fourth_stage_pressure_control_valve.setOutletPressure(
inp.flare_gas_recovery_pressure, 'bara')
fourth_stage_pressure_control_valve.run()
separation_process.add(fourth_stage_pressure_control_valve)
flareGas = create_flare_gas_stream("flare gas")
flareGas.setFlowRate(500.0, 'Sm3/hr')
flareGas.setPressure(inp.flare_gas_recovery_pressure, 'bara')
flareGas.setTemperature(15.0, 'C')
flareGas.run()
separation_process.add(flareGas)
mixerFlareGas = neqsim.process.equipment.mixer.Mixer("flare gas mixer")
mixerFlareGas.addStream(fourth_stage_pressure_control_valve.getOutStream())
mixerFlareGas.addStream(flareGas)
mixerFlareGas.run()
separation_process.add(mixerFlareGas)
pipe_from_fourth_stage = neqsim.process.equipment.pipeline.PipeBeggsAndBrills(
'pipe from fourth stage', mixerFlareGas.getOutStream())
pipe_from_fourth_stage.setDiameter(0.5)
pipe_from_fourth_stage.setPipeWallRoughness(50.0e-6)
pipe_from_fourth_stage.setLength(100)
pipe_from_fourth_stage.setElevation(0)
pipe_from_fourth_stage.run()
separation_process.add(pipe_from_fourth_stage)
gas_splitter_lp_gas = neqsim.process.equipment.splitter.Splitter(
"gas splitter LP gas", pipe_from_fourth_stage.getOutStream())
gas_splitter_lp_gas.setSplitFactors([lp_gas_split_factor, 1.0-lp_gas_split_factor])
gas_splitter_lp_gas.run()
separation_process.add(gas_splitter_lp_gas)
recyclegasstream = create_stream('recycle 1st stage')
recyclegasstream.setPressure(inp.fourth_stage_pressure, 'bara')
recyclegasstream.setTemperature(25.0, 'C')
recyclegasstream.setFlowRate(1.0, "kg/hr")
recyclegasstream.run()
separation_process.add(recyclegasstream)
gas_mixer_lp_gas = neqsim.process.equipment.mixer.Mixer(
"gas mixer LP gas")
gas_mixer_lp_gas.addStream(gas_splitter_lp_gas.getSplitStream(0))
gas_mixer_lp_gas.addStream(recyclegasstream)
if other_train_lp_gas_feed:
gas_mixer_lp_gas.addStream(other_train_lp_gas_feed)
gas_mixer_lp_gas.run()
separation_process.add(gas_mixer_lp_gas)
firstStageCooler = neqsim.process.equipment.heatexchanger.Cooler(
"1st stage cooler", gas_mixer_lp_gas.getOutletStream())
firstStageCooler.setOutTemperature(
inp.first_stage_scrubber_temperature, 'C')
firstStageCooler.run()
separation_process.add(firstStageCooler)
firstStageScrubber = neqsim.process.equipment.separator.Separator(
"1st stage scrubber", firstStageCooler.getOutStream())
try:
firstStageScrubber.run()
except:
print('error in third stage separator')
separation_process.add(firstStageScrubber)
firststagescrubberpump = neqsim.process.equipment.pump.Pump(
"1st stage scrubber pump", firstStageScrubber.getLiquidOutStream())
firststagescrubberpump.setOutletPressure(inp.second_stage_pressure, 'bara')
firststagescrubberpump.calculateAsCompressor(False)
try:
firststagescrubberpump.run()
except:
print('error in third stage firststagescrubberpump')
separation_process.add(firststagescrubberpump)
recycleliqpumpstream = create_stream('recycle pump stream')
recycleliqpumpstream.setPressure(inp.third_stage_pressure, 'bara')
recycleliqpumpstream.setTemperature(25.0, 'C')
recycleliqpumpstream.setFlowRate(1.0, "kg/hr")
recycleliqpumpstream.run()
separation_process.add(recycleliqpumpstream)
pumpresycle = neqsim.process.equipment.util.Recycle("recycle liquid 1st stage scrubber")
pumpresycle.addStream(firststagescrubberpump.getOutletStream())
pumpresycle.setOutletStream(recycleliqpumpstream)
pumpresycle.setTolerance(1e-1)
pumpresycle.run()
separation_process.add(pumpresycle)
firstStageCompressor = neqsim.process.equipment.compressor.Compressor(
"1st stage compressor", firstStageScrubber.getGasOutStream())
firstStageCompressor.setCompressorChartType("interpolate and extrapolate")
firstStageCompressor.setUsePolytropicCalc(True)
firstStageCompressor.setPolytropicEfficiency(0.7) #based on performance test 2024 (changed from 0.6 11.08.2025)
firstStageCompressor.setOutletPressure(inp.third_stage_pressure, 'bara')
firstStageCompressor.setSpeed(10250)
firstStageCompressor.getCompressorChart().setHeadUnit(inp.headunit)
firstStageCompressor.getCompressorChart().getSurgeCurve().setCurve(inp.chartConditions, inp.surgeflow_first_stage_compressor, inp.surgehead_first_stage_compressor)
firstStageCompressor.run()
separation_process.add(firstStageCompressor)
gassplitter = neqsim.process.equipment.splitter.Splitter(
'1st stage anti surge splitter')
gassplitter.setInletStream(firstStageCompressor.getOutletStream())
gassplitter.setFlowRates([-1, 1.0], "kg/hr")
try:
gassplitter.run()
except:
print('error in gassplitter')
separation_process.add(gassplitter)
antisurgeCalculator = neqsim.process.equipment.util.Calculator("anti surge calculator_1")
antisurgeCalculator.addInputVariable(firstStageCompressor)
antisurgeCalculator.setOutputVariable(gassplitter)
if lp_gas_split_factor > 0.1:
antisurgeCalculator.run()
separation_process.add(antisurgeCalculator)
antisurgevalve = neqsim.process.equipment.valve.ThrottlingValve('aniti surge valve', gassplitter.getSplitStream(1))
antisurgevalve.setOutletPressure(inp.fourth_stage_pressure, "bara")
antisurgevalve.run()
separation_process.add(antisurgevalve)
recycl = neqsim.process.equipment.util.Recycle("recycle anti surge 1st stage compressor")
recycl.addStream(antisurgevalve.getOutletStream())
recycl.setOutletStream(recyclegasstream)
recycl.setTolerance(1)
if lp_gas_split_factor > 0.1:
recycl.run()
separation_process.add(recycl)
firstStageCompressorAfterValve = neqsim.process.equipment.valve.ThrottlingValve(
"1st stage compressor after valve", gassplitter.getSplitStream(0))
firstStageCompressorAfterValve.setCalibrationStreamPressure(0.5, 'bara')
firstStageCompressorAfterValve.run()
separation_process.add(firstStageCompressorAfterValve)
gas_splitter_lp2_gas = neqsim.process.equipment.splitter.Splitter(
"gas splitter LP2 gas", thirdStageSeparator.getGasOutStream())
gas_splitter_lp2_gas.setSplitFactors([lp_gas_split_factor, 1.0-lp_gas_split_factor])
gas_splitter_lp2_gas.run()
separation_process.add(gas_splitter_lp2_gas)
recyclegasstream2 = create_stream('recycle 2nd stage')
recyclegasstream2.setPressure(inp.third_stage_pressure, 'bara')
recyclegasstream2.setTemperature(25.0, 'C')
recyclegasstream2.setFlowRate(1.0, "kg/hr")
recyclegasstream2.run()
separation_process.add(recyclegasstream2)
firststagegasmixer = neqsim.process.equipment.mixer.Mixer(
"first stage mixer")
firststagegasmixer.addStream(firstStageCompressorAfterValve.getOutStream())
firststagegasmixer.addStream(gas_splitter_lp2_gas.getSplitStream(0))
firststagegasmixer.addStream(recyclegasstream2)
if other_train_lp2_gas_feed:
firststagegasmixer.addStream(other_train_lp2_gas_feed) # ← CORRECT - adds to first stage gas mixer
firststagegasmixer.run()
separation_process.add(firststagegasmixer)
firstStageCooler2 = neqsim.process.equipment.heatexchanger.Cooler(
"1st stage cooler2", firststagegasmixer.getOutStream())
firstStageCooler2.setOutTemperature(
inp.second_stage_suction_cooler_temperature, 'C')
firstStageCooler2.run()
separation_process.add(firstStageCooler2)
firstStageScrubber2 = neqsim.process.equipment.separator.Separator(
"1st stage scrubber2", firstStageCooler2.getOutStream())
try:
firstStageScrubber2.run()
except:
print('error in 1st stage scrubber2')
separation_process.add(firstStageScrubber2)
firstStageCompressor2 = neqsim.process.equipment.compressor.Compressor(
"2nd stage compressor", firstStageScrubber2.getGasOutStream())
firstStageCompressor2.setCompressorChartType("interpolate and extrapolate")
firstStageCompressor2.setUsePolytropicCalc(True)
firstStageCompressor2.setPolytropicEfficiency(0.8)
firstStageCompressor2.setOutletPressure(inp.second_stage_pressure, 'bara')
firstStageCompressor2.setSpeed(10250)
firstStageCompressor2.getCompressorChart().setHeadUnit(inp.headunit)
firstStageCompressor2.getCompressorChart().getSurgeCurve().setCurve(inp.chartConditions, inp.surgeflow_second_stage_compressor, inp.surgehead_second_stage_compressor)
firstStageCompressor2.run()
separation_process.add(firstStageCompressor2)
gassplitter2 = neqsim.process.equipment.splitter.Splitter(
'2nd stage anti surge splitter')
gassplitter2.setInletStream(firstStageCompressor2.getOutletStream())
gassplitter2.setFlowRates([-1, 1.0], "kg/hr")
try:
gassplitter2.run()
except:
print('error in gassplitter2')
separation_process.add(gassplitter2)
antisurgeCalculator2 = neqsim.process.equipment.util.Calculator("anti surge calculator_2")
antisurgeCalculator2.addInputVariable(firstStageCompressor2)
antisurgeCalculator2.setOutputVariable(gassplitter2)
if lp_gas_split_factor> 0.1:
try:
antisurgeCalculator2.run()
except:
print('error in antisurge calculator 2')
separation_process.add(antisurgeCalculator2)
antisurgevalve2 = neqsim.process.equipment.valve.ThrottlingValve('aniti surge valve 2', gassplitter2.getSplitStream(1))
antisurgevalve2.setOutletPressure(inp.third_stage_pressure, "bara")
antisurgevalve2.run()
separation_process.add(antisurgevalve2)
recycl2 = neqsim.process.equipment.util.Recycle("recycle anti surge 2nd stage compressor")
recycl2.addStream(antisurgevalve2.getOutletStream())
recycl2.setOutletStream(recyclegasstream2)
recycl2.setTolerance(1)
if lp_gas_split_factor > 0.1:
recycl2.run()
separation_process.add(recycl2)
gas_splitter_mp_gas = neqsim.process.equipment.splitter.Splitter(
"gas splitter MP gas", secondStageSeparator.getGasOutStream())
gas_splitter_mp_gas.setSplitFactors([mpsplit, 1.0-mpsplit])
#if(inp.use_both_gas_recompression_trains_3_stage >= 0.5):
# gas_splitter_mp_gas.setSplitFactors([0.5, 0.5])
gas_splitter_mp_gas.run()
separation_process.add(gas_splitter_mp_gas)
recyclegasstream3 = create_stream('recycle 3rd stage')
recyclegasstream3.setPressure(inp.second_stage_pressure, 'bara')
recyclegasstream3.setTemperature(25.0, 'C')
recyclegasstream3.setFlowRate(1.0, "kg/hr")
recyclegasstream3.run()
separation_process.add(recyclegasstream3)
secondstagegasmixer = neqsim.process.equipment.mixer.Mixer(
"second Stage mixer")
secondstagegasmixer.addStream(gassplitter2.getSplitStream(0))
secondstagegasmixer.addStream(gas_splitter_mp_gas.getSplitStream(0))
secondstagegasmixer.addStream(recyclegasstream3)
if test_separator:
secondstagegasmixer.addStream(test_separator.getGasOutStream())
if other_train_mp_gas_feed:
secondstagegasmixer.addStream(other_train_mp_gas_feed)
secondstagegasmixer.run()
separation_process.add(secondstagegasmixer)
secondStageCooler = neqsim.process.equipment.heatexchanger.Cooler(
"2nd stage cooler", secondstagegasmixer.getOutStream())
secondStageCooler.setOutTemperature(
inp.third_stage_suction_cooler_temperature, 'C')
secondStageCooler.run()
separation_process.add(secondStageCooler)
secondStageScrubber = neqsim.process.equipment.separator.Separator(
"2nd stage scrubber", secondStageCooler.getOutStream())
try:
secondStageScrubber.run()
except:
print('error in second stage scrubber')
separation_process.add(secondStageScrubber)
secondStageCompressor = neqsim.process.equipment.compressor.Compressor(
"3rd stage compressor", secondStageScrubber.getGasOutStream())
secondStageCompressor.setUsePolytropicCalc(True)
secondStageCompressor.setPolytropicEfficiency(0.71)
secondStageCompressor.setSpeed(9000)
secondStageCompressor.getCompressorChart().setHeadUnit(inp.headunit)
if (inp.Year >= inp.year_pre_compression):
secondStageCompressor.getCompressorChart().getSurgeCurve().setCurve(inp.chartConditions, inp.surgeflow_third_stage_compressor_flow_new_boundle, inp.surgeflow_third_stage_compressor_head_new_boundle)
else:
secondStageCompressor.getCompressorChart().getSurgeCurve().setCurve(inp.chartConditions, inp.surgeflow_third_stage_compressor, inp.surgehead_third_stage_compressor)
if (inp.Year >= inp.year_pre_compression):
secondStageCompressor.setOutletPressure(
inp.new_compressor_pressure, 'bara')
else:
compressorPresSetter = neqsim.process.equipment.util.SetPoint('compressor pres setter')
compressorPresSetter.setSourceVariable(firstStageSeparator.getGasOutStream(), "pressure")
compressorPresSetter.setTargetVariable(secondStageCompressor)
compressorPresSetter.run()
separation_process.add(compressorPresSetter)
try:
secondStageCompressor.run()
except:
print('error in second stage compressor')
separation_process.add(secondStageCompressor)
gassplitter3 = neqsim.process.equipment.splitter.Splitter(
'3rd stage anti surge splitter')
gassplitter3.setInletStream(secondStageCompressor.getOutletStream())
gassplitter3.setFlowRates([-1, 1.0], "kg/hr")
gassplitter3.run()
separation_process.add(gassplitter3)
antisurgeCalculator3 = neqsim.process.equipment.util.Calculator("anti surge calculator_3")
antisurgeCalculator3.addInputVariable(secondStageCompressor)
antisurgeCalculator3.setOutputVariable(gassplitter3)
if mpsplit > 0.1:
try:
antisurgeCalculator3.run()
except:
print('error in antisurge calculator 3')
separation_process.add(antisurgeCalculator3)
antisurgevalve3 = neqsim.process.equipment.valve.ThrottlingValve('aniti surge valve 3', gassplitter3.getSplitStream(1))
antisurgevalve3.setOutletPressure(inp.second_stage_pressure, "bara")
antisurgevalve3.run()
separation_process.add(antisurgevalve3)
recycl3 = neqsim.process.equipment.util.Recycle("recycle anti surge 3rd stage compressor")
recycl3.addStream(antisurgevalve3.getOutletStream())
recycl3.setOutletStream(recyclegasstream3)
recycl3.setTolerance(1)
if mpsplit > 0.1:
recycl3.run()
separation_process.add(recycl3)
outgas = firstStageSeparator.getGasOutStream()
if (inp.Year >= inp.year_pre_compression):
recyclegasstreamNew = create_stream('recycle stage')
recyclegasstreamNew.setPressure(inp.first_stage_pressure, 'bara')
recyclegasstreamNew.setTemperature(25.0, 'C')
recyclegasstreamNew.setFlowRate(1.0, "kg/hr")
recyclegasstreamNew.run()
separation_process.add(recyclegasstreamNew)
newgasmixer = neqsim.process.equipment.mixer.Mixer(
"mixer new")
newgasmixer.addStream(outgas)
newgasmixer.addStream(recyclegasstreamNew)
newgasmixer.run()
separation_process.add(newgasmixer)
newcompressor_cooler = neqsim.process.equipment.heatexchanger.Cooler(
"new compressor cooler", newgasmixer.getOutStream())
newcompressor_cooler.setOutTemperature(
inp.new_compressor_cooler_out_temperature, 'C')
newcompressor_cooler.run()
separation_process.add(newcompressor_cooler)
scrubber_newcompressor = neqsim.process.equipment.separator.Separator(
"new compressor scrubber", newcompressor_cooler.getOutStream())
try:
scrubber_newcompressor.run()
except:
print('error in new compressor scrubber')
separation_process.add(scrubber_newcompressor)
oilFirstStageMixer.addStream(scrubber_newcompressor.getLiquidOutStream())
newcompressor = neqsim.process.equipment.compressor.Compressor(
"new compressor", scrubber_newcompressor.getGasOutStream())
newcompressor.setUsePolytropicCalc(True)
newcompressor.setPolytropicEfficiency(0.8)
newcompressor.setOutletPressure(inp.new_compressor_pressure, 'bara')
newcompressor.getCompressorChart().setHeadUnit(inp.headunit)
newcompressor.getCompressorChart().getSurgeCurve().setCurve(inp.chartConditions, inp.surgeflow_new_stage_compressor, inp.surgehead_new_stage_compressor)
newcompressor.run()
separation_process.add(newcompressor)
gassplitterNew = neqsim.process.equipment.splitter.Splitter(
'new stage anti surge splitter')
gassplitterNew.setInletStream(newcompressor.getOutletStream())
gassplitterNew.setFlowRates([-1, 1.0], "kg/hr")
gassplitterNew.run()
separation_process.add(gassplitterNew)
antisurgeCalculatorNew = neqsim.process.equipment.util.Calculator("anti surge calculator new")
antisurgeCalculatorNew.addInputVariable(newcompressor)
antisurgeCalculatorNew.setOutputVariable(gassplitterNew)
antisurgeCalculatorNew.run()
separation_process.add(antisurgeCalculatorNew)
antisurgevalveNew = neqsim.process.equipment.valve.ThrottlingValve('aniti surge valve new', gassplitterNew.getSplitStream(1))
antisurgevalveNew.setOutletPressure(inp.first_stage_pressure, "bara")
antisurgevalveNew.run()
separation_process.add(antisurgevalveNew)
recyclNew = neqsim.process.equipment.util.Recycle("recycle anti surge new")
recyclNew.addStream(antisurgevalveNew.getOutletStream())
recyclNew.setOutletStream(recyclegasstreamNew)
recyclNew.setTolerance(1)
recyclNew.run()
separation_process.add(recyclNew)
pipe_from_comp = neqsim.process.equipment.pipeline.PipeBeggsAndBrills(
'pipe from new compressor', gassplitterNew.getSplitStream(0))
pipe_from_comp.setDiameter(0.4826)
pipe_from_comp.setPipeWallRoughness(50.0e-6)
pipe_from_comp.setLength(161)
pipe_from_comp.setElevation(8.0)
pipe_from_comp.setNumberOfIncrements(10)
pipe_from_comp.setRunIsothermal(True)
pipe_from_comp.run()
separation_process.add(pipe_from_comp)
outgas = pipe_from_comp.getOutStream()
richGasMixer = neqsim.process.equipment.mixer.Mixer("fourth Stage mixer")
richGasMixer.addStream( gassplitter3.getSplitStream(0))
richGasMixer.addStream(outgas)
richGasMixer.run()
separation_process.add(richGasMixer)
mpLiqmixer = neqsim.process.equipment.mixer.Mixer("MP liq gas mixer")
mpLiqmixer.addStream(secondStageScrubber.getLiquidOutStream())
try:
mpLiqmixer.run()
except:
print('error in MP liq mixer')
separation_process.add(mpLiqmixer)
lpLiqmixer = neqsim.process.equipment.mixer.Mixer("LP liq gas mixer")
lpLiqmixer.addStream(firstStageScrubber2.getLiquidOutStream())
lpLiqmixer.addStream(recycleliqpumpstream)
try:
lpLiqmixer.run()
except:
print('error in LP liq mixer')
separation_process.add(lpLiqmixer)
mpResycle = neqsim.process.equipment.util.Recycle("MP liq resycle")
mpResycle.addStream(mpLiqmixer.getOutStream())
mpResycle.setOutletStream(oilSeccondStage)
mpResycle.setTolerance(1e-2)
try:
mpResycle.run()
except:
print('error in MP liq resycle')
separation_process.add(mpResycle)
lpResycle = neqsim.process.equipment.util.Recycle("LP liq resycle")
lpResycle.addStream(lpLiqmixer.getOutStream())
lpResycle.setOutletStream(oilThirdStage)
lpResycle.setTolerance(1e-2)
try:
lpResycle.run()
except:
print('error in LP liq resycle')
separation_process.add(lpResycle)
oilcooler = neqsim.process.equipment.heatexchanger.Cooler(
"4th stage oil cooler", fourthStageSeparator.getOilOutStream())
oilcooler.setOutTemperature(20.0, 'C')
oilcooler.run()
separation_process.add(oilcooler)
oilpump = neqsim.process.equipment.pump.Pump("4th stage oil pump", oilcooler.getOutletStream())
oilpump.setOutletPressure(30.0, 'bara')
oilpump.run()
separation_process.add(oilpump)
NGLiqmixer = neqsim.process.equipment.mixer.Mixer("NGL mixer")
NGLiqmixer.addStream(oilpump.getOutStream())
try:
NGLiqmixer.run()
except:
print('error in NGL mixer')
separation_process.add(NGLiqmixer)
return separation_process
Code cell 16: Oil Separation Process Function
Notebook cell 33. This code belongs to the Oil Separation Process Function section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid- characterization files, and case-specific result folders.
%%script false --no-raise-error uncomment if model should be saved to file
test_sep_process = test_separation_process(
input_parameters, offshore_asset_well_feed_model.getUnit("test manifold").getOutStream())
test_sep_process.run()
if input_parameters.use_both_gas_recompression_trains_1_2_stage < 0.1:
lp_split = 0.0001
else:
lp_split = 0.9999
if input_parameters.use_both_gas_recompression_trains_3_stage < 0.1:
mp_split = 0.0001
else:
mp_split = 0.9999
separation_process_train_A = create_oil_separation_process(input_parameters, offshore_asset_well_feed_model.getUnit("HP manifold A").getOutStream(), offshore_asset_well_feed_model.getUnit(
"LP manifold A").getOutStream(), lp_split, reference_stream_c_sep_process.getUnit("manifold").getSplitStream(0), offshore_asset_well_feed_model.getUnit("south east manifold").getSplitStream(0), test_separator=None, reference_stream_c_calibration_stream_gas=reference_stream_c_sep_process.getUnit("gas manifold").getSplitStream(1), mpsplit = mp_split)
separation_process_train_A.run()
separation_process_train_B = create_oil_separation_process(input_parameters, offshore_asset_well_feed_model.getUnit("HP manifold B").getOutletStream(), offshore_asset_well_feed_model.getUnit(
"LP manifold B").getOutletStream(), 0.9999, reference_stream_c_sep_process.getUnit("manifold").getSplitStream(1), offshore_asset_well_feed_model.getUnit("south east manifold").getSplitStream(1), test_separator=None, reference_stream_c_calibration_stream_gas=reference_stream_c_sep_process.getUnit("gas manifold").getSplitStream(2), other_train_lp_gas_feed=separation_process_train_A.getUnit("gas splitter LP gas").getSplitStream(1),
other_train_lp2_gas_feed=separation_process_train_A.getUnit("gas splitter LP2 gas").getSplitStream(1), other_train_mp_gas_feed=separation_process_train_A.getUnit("gas splitter MP gas").getSplitStream(1), mpsplit=0.9999)
separation_process_train_B.run()
# Corrected feedflow calculation
feedflow = (
offshore_asset_well_feed_model.getUnit("HP manifold A").getOutStream().getFlowRate('kg/hr') +
offshore_asset_well_feed_model.getUnit("LP manifold A").getOutStream().getFlowRate('kg/hr') +
reference_stream_c_sep_process.getUnit("manifold").getSplitStream(0).getFlowRate('kg/hr') +
reference_stream_c_sep_process.getUnit("gas manifold").getSplitStream(1).getFlowRate('kg/hr') +
offshore_asset_well_feed_model.getUnit("south east manifold").getSplitStream(0).getFlowRate('kg/hr') +
separation_process_train_A.getUnit("flare gas").getFlowRate('kg/hr')
)
print(f"Total feed flow: {feedflow:.2f} kg/hr")
# Corrected feedflow calculation
exitflow = (
separation_process_train_A.getUnit("4th stage oil pump").getOutletStream().getFlowRate('kg/hr') +
separation_process_train_A.getUnit("fourth Stage mixer").getOutletStream().getFlowRate('kg/hr') +
separation_process_train_A.getUnit("4th stage separator").getWaterOutStream().getFlowRate('kg/hr') +
separation_process_train_A.getUnit("2nd stage separator").getWaterOutStream().getFlowRate('kg/hr') +
separation_process_train_A.getUnit("1st stage separator").getWaterOutStream().getFlowRate('kg/hr') +
# to be chacked
separation_process_train_A.getUnit("gas splitter LP gas").getSplitStream(1).getFlowRate('kg/hr') +
separation_process_train_A.getUnit("gas splitter MP gas").getSplitStream(1).getFlowRate('kg/hr')
)
print(f"Exit flow: {exitflow:.2f} kg/hr")
print('mass balance check A : ', (feedflow - exitflow) / feedflow * 100, '%')
# Corrected feedflow calculation
feedflow = (
offshore_asset_well_feed_model.getUnit("HP manifold B").getOutStream().getFlowRate('kg/hr') +
offshore_asset_well_feed_model.getUnit("LP manifold B").getOutStream().getFlowRate('kg/hr') +
reference_stream_c_sep_process.getUnit("manifold").getSplitStream(1).getFlowRate('kg/hr') +
reference_stream_c_sep_process.getUnit("gas manifold").getSplitStream(2).getFlowRate('kg/hr') +
offshore_asset_well_feed_model.getUnit("south east manifold").getSplitStream(1).getFlowRate('kg/hr') +
separation_process_train_B.getUnit("flare gas").getFlowRate('kg/hr') +
separation_process_train_A.getUnit("gas splitter MP gas").getSplitStream(1).getFlowRate('kg/hr') +
separation_process_train_A.getUnit("gas splitter LP2 gas").getSplitStream(1).getFlowRate('kg/hr')
)
print(f"Total feed flow: {feedflow:.2f} kg/hr")
# Corrected feedflow calculation
exitflow = (
separation_process_train_B.getUnit("4th stage oil pump").getOutletStream().getFlowRate('kg/hr') +
separation_process_train_B.getUnit("fourth Stage mixer").getOutletStream().getFlowRate('kg/hr') +
separation_process_train_B.getUnit("4th stage separator").getWaterOutStream().getFlowRate('kg/hr') +
separation_process_train_B.getUnit("2nd stage separator").getWaterOutStream().getFlowRate('kg/hr') +
separation_process_train_B.getUnit("1st stage separator").getWaterOutStream().getFlowRate('kg/hr') +
# to be checked
separation_process_train_B.getUnit("gas splitter LP gas").getSplitStream(1).getFlowRate('kg/hr') +
separation_process_train_B.getUnit("gas splitter MP gas").getSplitStream(1).getFlowRate('kg/hr')
)
print(f"Exit flow: {exitflow:.2f} kg/hr")
print('mass balance check B : ', (feedflow - exitflow) / feedflow * 100, '%')
Code cell 17: Manifold and Pipe Model: LIQUID_TRAIN_A to GAS_TRAIN_B
Notebook cell 35. This code belongs to the Manifold and Pipe Model: LIQUID_TRAIN_A to GAS_TRAIN_B section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid-characterization files, and case-specific result folders.
def manifold_and_pipe_model_liquid_train_a_to_gas_train_b(inp: ProcessInput, feedstream1, feedstream2):
manifold_process = neqsim.process.processmodel.ProcessSystem()
gas_mixer = neqsim.process.equipment.mixer.Mixer('gas mixer')
gas_mixer.addStream(feedstream1)
gas_mixer.addStream(feedstream2)
gas_mixer.run()
manifold_process.add(gas_mixer)
pipe = neqsim.process.equipment.pipeline.PipeBeggsAndBrills(
'pipeline area_trainA to area_trainB', gas_mixer.getOutStream())
pipe.setDiameter(0.575)
pipe.setPipeWallRoughness(50.0e-6)
pipe.setLength(300)
pipe.setElevation(0.0)
pipe.setNumberOfIncrements(10)
pipe.setRunIsothermal(True)
pipe.run()
manifold_process.add(pipe)
manifold = neqsim.process.equipment.splitter.Splitter(
'manifold', pipe.getOutStream())
manifold.setSplitFactors([0.5, 0.5])
manifold.run()
manifold_process.add(manifold)
return manifold_process
Code cell 18: Water treatment
Notebook cell 37. This code belongs to the Water treatment section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid- characterization files, and case-specific result folders.
%%script false --no-raise-error uncomment if model should be saved to file
# to use this model - you will have to use CPA-EoS to get accurate results
def water_treatment_process(input_parameters, separation_process_train_A, separation_process_train_B):
water_treatment_model = neqsim.process.processmodel.ProcessSystem()
water_manifold = neqsim.process.equipment.mixer.Mixer('water treatment manifold')
water_manifold.addStream(separation_process_train_A.getUnit('1st stage separator').getWaterOutStream())
water_manifold.addStream(separation_process_train_A.getUnit('2nd stage separator').getWaterOutStream())
water_manifold.addStream(separation_process_train_B.getUnit('1st stage separator').getWaterOutStream())
water_manifold.addStream(separation_process_train_B.getUnit('2nd stage separator').getWaterOutStream())
water_manifold.run()
water_treatment_model.add(water_manifold)
valve_water = neqsim.process.equipment.valve.ThrottlingValve('valve 1', water_manifold.getOutStream())
valve_water.setOutletPressure(1.01325, 'bara')
valve_water.run()
water_treatment_model.add(valve_water)
water_separator = neqsim.process.equipment.separator.ThreePhaseSeparator('water separator', valve_water.getOutletStream())
water_separator.run()
water_treatment_model.add(water_separator)
emissionEstimator = neqsim.process.measurementdevice.CombustionEmissionsCalculator(
'gas emissions', water_separator.getGasOutStream())
water_treatment_model.add(emissionEstimator)
print(emissionEstimator.getMeasuredValue('kg/hr')/1000.0*360*24, ' tons/year CO2 emissions')
return water_treatment_model
water_prosess = water_treatment_process(input_parameters, separation_process_train_A, separation_process_train_B)
water_prosess.run()
#from neqsim.thermo import printFrame
#printFrame(water_prosess.getUnit('water separator').getFluid())
print('gas emissions ', water_prosess.getUnit('water separator').getGasOutStream().getFlowRate('kg/hr')/1000*24*360, ' tons year')
print('CO2 emission ', water_prosess.getUnit('water separator').getGasOutStream().getFlowRate('kg/hr')/1000*24*360*2.75, ' tons CO2 per year (emission facor 2.75 kg CO2/kg gas)')
print('produced water rate ', water_prosess.getUnit('water separator').getWaterOutStream().getFlowRate('kg/hr')/1000*24, ' m3/day')
raise SystemExit("Stop right there!")
Code cell 19: 6.1 DPCU process
Notebook cell 40. This code belongs to the 6.1 DPCU process section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid- characterization files, and case-specific result folders.
def tex_process(inp: ProcessInput, feedgas, sep_process, jt_mode=False):
tex_process = neqsim.process.processmodel.ProcessSystem()
coolingstream1 = create_gas_stream('refluxstream_gas_secondstage_1')
coolingstream1.setFlowRate(800000.1, 'kg/hr')
tempguess = -40.0
if inp.Year >= inp.tex_start_year:
tempguess = -10.0
coolingstream1.setTemperature(tempguess, 'C')
coolingstream1.setPressure(inp.expander_out_pressure, 'bara')
coolingstream1.run()
tex_process.add(coolingstream1)
cooling_gas_splitter = neqsim.process.equipment.splitter.Splitter(
"cooling gas splitter", coolingstream1)
cooling_gas_splitter.setSplitFactors([1.0-inp.cold_gas_bypass_multidunk, inp.cold_gas_bypass_multidunk])
cooling_gas_splitter.run()
tex_process.add(cooling_gas_splitter)
coolingstream2 = create_stream('refluxstream_gas_secondstage_2')
coolingstream2.setFlowRate(50000.1, 'kg/hr')
coolingstream2.setTemperature(tempguess, 'C')
coolingstream2.setPressure(inp.expander_out_pressure, 'bara')
coolingstream2.run()
tex_process.add(coolingstream2)
dewPointControlCooler = neqsim.process.equipment.heatexchanger.Cooler(
"dew point cooler", feedgas)
dewPointControlCooler.setOutTemperature(
inp.dew_point_scrubber_temperature, 'C')
dewPointControlCooler.run()
tex_process.add(dewPointControlCooler)
dewPointScrubber = neqsim.process.equipment.separator.Separator(
"dew point scrubber", dewPointControlCooler.getOutStream())
dewPointScrubber.run()
tex_process.add(dewPointScrubber)
water_dehydration = neqsim.process.equipment.splitter.ComponentSplitter(
"dehyd", dewPointScrubber.getGasOutStream())
complen = dewPointScrubber.getGasOutStream().getFluid().getNumberOfComponents()
water_dehydration.setSplitFactors([1.0] * (complen - 1) + [0.0])
water_dehydration.run()
tex_process.add(water_dehydration)
presuredrop_dehyd = neqsim.process.equipment.valve.ThrottlingValve(
"dp dehydration", water_dehydration.getSplitStream(0))
presuredrop_dehyd.setCalibrationStreamPressure(2.3, 'bara')
presuredrop_dehyd.run()
tex_process.add(presuredrop_dehyd)
fuel_gas_splitter = neqsim.process.equipment.splitter.Splitter(
"fuel gas splitter", presuredrop_dehyd.getOutStream())
fuel_gas_splitter.setFlowRates([-1, inp.fuel_gas_rate/2.0], 'MSm3/day')
try:
fuel_gas_splitter.run()
except:
print('error in fuel gas splitter')
tex_process.add(fuel_gas_splitter)
fuel_gas_stream = neqsim.process.equipment.stream.Stream(
"fuel gas stream", fuel_gas_splitter.getSplitStream(1))
fuel_gas_stream.run()
tex_process.add(fuel_gas_stream)
rich_gas_splitter = neqsim.process.equipment.splitter.Splitter(
"rich gas splitter", fuel_gas_splitter.getSplitStream(0))
if inp.rich_gas_bypass:
rich_gas_splitter.setFlowRates([-1, 10], 'kg/hr')
else:
rich_gas_splitter.setFlowRates([10, -1], 'kg/hr')
try:
rich_gas_splitter.run()
except:
print('error in rich gas splitter')
tex_process.add(rich_gas_splitter)
hpLiqmixer = neqsim.process.equipment.mixer.Mixer("HP liq gas mixer")
hpLiqmixer.addStream(dewPointScrubber.getLiquidOutStream())
hpLiqmixer.run()
tex_process.add(hpLiqmixer)
hpResycle = neqsim.process.equipment.util.Recycle("HP liq resycle")
hpResycle.addStream(hpLiqmixer.getOutStream())
hpResycle.setOutletStream(sep_process.getUnit("frist stage oil reflux"))
hpResycle.setTolerance(1)
hpResycle.run()
tex_process.add(hpResycle)
heatEx = neqsim.process.equipment.heatexchanger.MultiStreamHeatExchanger(
"HX_MULTI_STREAM")
heatEx.addInStream(rich_gas_splitter.getSplitStream(1))
heatEx.addInStream(cooling_gas_splitter.getSplitStream(0))
heatEx.addInStream(coolingstream2)
tempapproach = 5.5
heatEx.setTemperatureApproach(tempapproach)
heatEx.run()
tex_process.add(heatEx)
gasdewseparator2 = neqsim.process.equipment.separator.Separator(
'25VG101', heatEx.getOutStream(0))
gasdewseparator2.run()
tex_process.add(gasdewseparator2)
expander_energy_stream = neqsim.process.equipment.stream.EnergyStream(
"expander energy")
jt_tex_splitter = neqsim.process.equipment.splitter.Splitter(
"jt_tex_splitter", gasdewseparator2.getGasOutStream())
if jt_mode:
jt_tex_splitter.setSplitFactors([0.001, 0.999])
else:
jt_tex_splitter.setSplitFactors([0.999, 0.001])
jt_tex_splitter.run()
tex_process.add(jt_tex_splitter)
feed_stream_turbo_Expander = neqsim.process.equipment.stream.Stream(
"feed stream turbo expander", jt_tex_splitter.getSplitStream(0))
feed_stream_turbo_Expander.run()
tex_process.add(feed_stream_turbo_Expander)
turboexpander = neqsim.process.equipment.expander.Expander(
"TEX", jt_tex_splitter.getSplitStream(0))
turboexpander.setPolytropicEfficiency(inp.expander_efficiency/100.0)
turboexpander.setUsePolytropicCalc(True)
turboexpander.setOutletPressure(inp.expander_out_pressure, 'bara')
turboexpander.setEnergyStream(expander_energy_stream)
try:
turboexpander.run()
except:
print('error in turboexpander')
tex_process.add(turboexpander)
turboExpanderComp = neqsim.process.equipment.expander.TurboExpanderCompressor("TurboExpanderCompressor", jt_tex_splitter.getSplitStream(0))
turboExpanderComp.setUCcurve(
[0.9964751359624449, 0.7590835113213541, 0.984295619176559, 0.8827799803397821,
0.9552460269880922, 1.0],
[0.984090909090909, 0.796590909090909, 0.9931818181818183, 0.9363636363636364,
0.9943181818181818, 1.0])
turboExpanderComp.setQNEfficiencycurve([0.5, 0.7, 0.85, 1.0, 1.2, 1.4, 1.6],
[0.88, 0.91, 0.95, 1.0, 0.97, 0.85, 0.6])
turboExpanderComp.setQNHeadcurve([0.5, 0.8, 1.0, 1.2, 1.4, 1.6],
[1.1, 1.05, 1.0, 0.9, 0.7, 0.4])
turboExpanderComp.setImpellerDiameter(0.424)
turboExpanderComp.setDesignSpeed(6850.0)
turboExpanderComp.setExpanderDesignIsentropicEfficiency(0.88)
turboExpanderComp.setDesignUC(0.7)
turboExpanderComp.setDesignQn(0.03328)
turboExpanderComp.setExpanderOutPressure(inp.expander_out_pressure)
turboExpanderComp.setCompressorDesignPolytropicEfficiency(0.81)
turboExpanderComp.setCompressorDesignPolytropicHead(20.47)
turboExpanderComp.setMaximumIGVArea(1.637e4)
turboExpanderComp.run()
tex_process.add(turboExpanderComp)
jt_valve = neqsim.process.equipment.valve.ThrottlingValve(
"jt_valve", jt_tex_splitter.getSplitStream(1))
jt_valve.setOutletPressure(inp.expander_out_pressure, 'bara')
try:
jt_valve.run()
except:
print('error in jt valve')
tex_process.add(jt_valve)
tex_jt_mixer = neqsim.process.equipment.mixer.Mixer("tex_jt mixer")
#tex_jt_mixer.addStream(turboexpander.getOutStream())
tex_jt_mixer.addStream(turboExpanderComp.getExpanderOutletStream())
tex_jt_mixer.addStream(jt_valve.getOutStream())
tex_jt_mixer.run()
tex_process.add(tex_jt_mixer)
DPCUScrubber = neqsim.process.equipment.separator.Separator(
"TEX LT scrubber", tex_jt_mixer.getOutStream())
DPCUScrubber.run()
tex_process.add(DPCUScrubber)
NGLpremixer = neqsim.process.equipment.mixer.Mixer("NGL pre mixer")
NGLpremixer.addStream(DPCUScrubber.getLiquidOutStream())
NGLpremixer.addStream(gasdewseparator2.getLiquidOutStream())
NGLpremixer.run()
tex_process.add(NGLpremixer)
gas_expander_resycle = neqsim.process.equipment.util.Recycle("gas recycl")
gas_expander_resycle.addStream(DPCUScrubber.getGasOutStream())
gas_expander_resycle.setOutletStream(coolingstream1)
gas_expander_resycle.setTolerance(1.0)
gas_expander_resycle.run()
tex_process.add(gas_expander_resycle)
liq_expander_resycle = neqsim.process.equipment.util.Recycle("liq recycl")
liq_expander_resycle.addStream(NGLpremixer.getOutStream())
liq_expander_resycle.setOutletStream(coolingstream2)
liq_expander_resycle.setTolerance(1.0)
liq_expander_resycle.run()
tex_process.add(liq_expander_resycle)
compressor_feed_mixer = neqsim.process.equipment.mixer.Mixer(
"compressor feed mixer")
compressor_feed_mixer.addStream(heatEx.getOutStream(1))
compressor_feed_mixer.addStream(cooling_gas_splitter.getSplitStream(1))
compressor_feed_mixer.run()
tex_process.add(compressor_feed_mixer)
presuredrop_tex_pre_compressor = neqsim.process.equipment.valve.ThrottlingValve(
"dp tex pre comp", compressor_feed_mixer.getOutStream())
presuredrop_tex_pre_compressor.setCalibrationStreamPressure(1.0, 'bara')
presuredrop_tex_pre_compressor.run()
tex_process.add(presuredrop_tex_pre_compressor)
compressor_export_booster = neqsim.process.equipment.compressor.Compressor(
"comp_export_booster_b", presuredrop_tex_pre_compressor.getOutStream())
compressor_export_booster.setUsePolytropicCalc(True)
compressor_export_booster.setPolytropicEfficiency(0.75)
compressor_export_booster.setEnergyStream(expander_energy_stream)
compressor_export_booster.setCalcPressureOut(True)
compressor_export_booster.run()
tex_process.add(compressor_export_booster)
turboExpanderComp.setCompressorFeedStream(presuredrop_tex_pre_compressor.getOutStream())
turboExpanderComp.run()
#tex_gas_splitter = neqsim.process.equipment.splitter.Splitter(
# "tex_gas_splitter", compressor_export_booster.getOutStream())
tex_gas_splitter = neqsim.process.equipment.splitter.Splitter(
"tex_gas_splitter", turboExpanderComp.getCompressorOutletStream())
tex_gas_splitter.setFlowRates([-1.0, inp.injection_gas_rate_ht+0.000001], 'MSm3/day')
try:
tex_gas_splitter.run()
except:
print('error tex_splitter')
tex_process.add(tex_gas_splitter)
gas_to_DX_compressors = neqsim.process.equipment.stream.Stream('gas to dx compressor', tex_gas_splitter.getSplitStream(0))
if inp.rich_gas_bypass:
gas_to_DX_compressors = neqsim.process.equipment.stream.Stream('gas to dx compressor', rich_gas_splitter.getSplitStream(0))
gas_to_DX_compressors.run()
tex_process.add(gas_to_DX_compressors)
gas_to_HT_injection_compressors = neqsim.process.equipment.stream.Stream('gas to ht compressor', tex_gas_splitter.getSplitStream(1))
gas_to_HT_injection_compressors.run()
tex_process.add(gas_to_HT_injection_compressors)
return tex_process
Code cell 20: 6.2Testing the well, separation and DPCU process models
Notebook cell 42. This code belongs to the 6.2Testing the well, separation and DPCU process models section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid-characterization files, and case-specific result folders.
%%script false --no-raise-error uncomment if model should be saved to file
offshore_asset_well_feed_model = create_offshore_asset_well_feed_model(input_parameters)
offshore_asset_well_feed_model.run()
reference_stream_c_sep_process = reference_stream_c_calibration_stream_separation_process(
input_parameters, offshore_asset_well_feed_model.getUnit("reference_stream_c calibration_stream manifold").getOutStream())
reference_stream_c_sep_process.run()
test_sep_process = test_separation_process(
input_parameters, offshore_asset_well_feed_model.getUnit("test manifold").getOutStream())
test_sep_process.run()
if input_parameters.use_both_gas_recompression_trains_1_2_stage < 0.1:
lp_split = 0.0001
else:
lp_split = 0.9999
if input_parameters.use_both_gas_recompression_trains_3_stage < 0.1:
mp_split = 0.0001
else:
mp_split = 0.9999
separation_process_train_A = create_oil_separation_process(input_parameters, offshore_asset_well_feed_model.getUnit("HP manifold A").getOutStream(), offshore_asset_well_feed_model.getUnit(
"LP manifold A").getOutStream(), lp_split, reference_stream_c_sep_process.getUnit("manifold").getSplitStream(0), offshore_asset_well_feed_model.getUnit("south east manifold").getSplitStream(0), test_separator=None, reference_stream_c_calibration_stream_gas=reference_stream_c_sep_process.getUnit("gas manifold").getSplitStream(1), mpsplit = mp_split)
separation_process_train_A.run()
separation_process_train_B = create_oil_separation_process(input_parameters, offshore_asset_well_feed_model.getUnit("HP manifold B").getOutletStream(), offshore_asset_well_feed_model.getUnit(
"LP manifold B").getOutletStream(), 0.9999, reference_stream_c_sep_process.getUnit("manifold").getSplitStream(1), offshore_asset_well_feed_model.getUnit("south east manifold").getSplitStream(1), test_separator=None, reference_stream_c_calibration_stream_gas=reference_stream_c_sep_process.getUnit("gas manifold").getSplitStream(2), other_train_lp_gas_feed=separation_process_train_A.getUnit("gas splitter LP gas").getSplitStream(1),
other_train_lp2_gas_feed=separation_process_train_A.getUnit("gas splitter LP2 gas").getSplitStream(1), other_train_mp_gas_feed=separation_process_train_A.getUnit("gas splitter MP gas").getSplitStream(1), mpsplit=0.9999)
separation_process_train_B.run()
manifold_upstream_TEX = manifold_and_pipe_model_liquid_train_a_to_gas_train_b(input_parameters, separation_process_train_A.getUnit(
"fourth Stage mixer").getOutStream(), separation_process_train_B.getUnit("fourth Stage mixer").getOutStream())
manifold_upstream_TEX.run()
tex_process_1 = tex_process(input_parameters, manifold_upstream_TEX.getUnit(
"manifold").getSplitStream(0), separation_process_train_A, jt_mode=False)
tex_process_1.run()
tex_process_2 = tex_process(input_parameters, manifold_upstream_TEX.getUnit(
"manifold").getSplitStream(1), separation_process_train_B, jt_mode=False)
tex_process_2.run()
clear_output(wait=True)
print("Calculation finished")
Code cell 21: 6.2Testing the well, separation and DPCU process models
Notebook cell 43. This code belongs to the 6.2Testing the well, separation and DPCU process models section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid-characterization files, and case-specific result folders.
%%script false --no-raise-error uncomment if model should be saved to file
print('tex in ', tex_process_1.getUnit(
'TEX').getInStream().getFluid().getTemperature('C'))
print('tex out ', tex_process_1.getUnit(
'TEX').getOutStream().getFluid().getTemperature('C'))
print('jt in ', tex_process_1.getUnit(
'jt_valve').getInStream().getFluid().getTemperature('C'))
print('jt out ', tex_process_1.getUnit(
'jt_valve').getOutStream().getFluid().getTemperature('C'))
Code cell 22: 7. Offshore Area D NGL column model
Notebook cell 45. This code belongs to the 7. Offshore Area D NGL column model section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid-characterization files, and case-specific result folders.
def ngl_column_model(inp: ProcessInput, feed_stream, separation_process_train_A, separation_process_train_B, lp_split=0.5):
ngl_column_model = neqsim.process.processmodel.ProcessSystem()
NGLpreflash_valve = neqsim.process.equipment.valve.ThrottlingValve(
"NGL pre flash valve", feed_stream)
NGLpreflash_valve.setOutletPressure(inp.pre_flash_drum_pressure, 'bara')
try:
NGLpreflash_valve.run()
except:
print('error in NGL pre flash valve')
ngl_column_model.add(NGLpreflash_valve)
NGLpreflashsseparator = neqsim.process.equipment.separator.Separator(
"NGL pre flash separator", NGLpreflash_valve.getOutStream())
NGLpreflashsseparator.run()
ngl_column_model.add(NGLpreflashsseparator)
NGLgassplitter = neqsim.process.equipment.splitter.Splitter(
"NGL gas splitter", NGLpreflashsseparator.getGasOutStream())
NGLgassplitter.setSplitFactors([lp_split, 1.0-lp_split])
NGLgassplitter.run()
ngl_column_model.add(NGLgassplitter)
separation_process_train_A.getUnit('second Stage mixer').addStream(
NGLgassplitter.getSplitStream(0))
separation_process_train_B.getUnit('second Stage mixer').addStream(
NGLgassplitter.getSplitStream(1))
ngl_column_feed_valve = neqsim.process.equipment.valve.ThrottlingValve(
"ngl valve to column", NGLpreflashsseparator.getLiquidOutStream())
ngl_column_feed_valve.setOutletPressure(inp.ngl_column_pressure, 'bara')
try:
ngl_column_feed_valve.run()
except:
print('error in ngl column feed valve')
ngl_column_model.add(ngl_column_feed_valve)
NGLcolumn = neqsim.process.equipment.distillation.DistillationColumn(
'NGL column', 10, True, False)
NGLcolumn.addFeedStream(ngl_column_feed_valve.getOutStream(), 10)
NGLcolumn.getReboiler().setOutTemperature(
273.15 + inp.ngl_column_reboiler_temperature)
NGLcolumn.setTopPressure(inp.ngl_column_pressure)
NGLcolumn.setBottomPressure(inp.ngl_column_pressure+0.5)
NGLcolumn.setTemperatureTolerance(5.0e-1)
NGLcolumn.setMassBalanceTolerance(5.0e-1)
NGLcolumn.setEnthalpyBalanceTolerance(5.0e-1)
NGLcolumn.setMaxNumberOfIterations(10)
try:
NGLcolumn.run()
except:
print('error in NGL column')
ngl_column_model.add(NGLcolumn)
NGLcoumngassplitter = neqsim.process.equipment.splitter.Splitter(
"NGL column gas splitter", NGLcolumn.getGasOutStream())
NGLcoumngassplitter.setSplitFactors([lp_split, 1.0-lp_split])
NGLcoumngassplitter.run()
ngl_column_model.add(NGLcoumngassplitter)
separation_process_train_A.getUnit("first stage mixer").addStream(
NGLcoumngassplitter.getSplitStream(0))
separation_process_train_B.getUnit("first stage mixer").addStream(
NGLcoumngassplitter.getSplitStream(1))
NGLinjectionsplitter = neqsim.process.equipment.splitter.Splitter(
"NGL injection splitter", NGLcolumn.getLiquidOutStream())
#NGLinjectionsplitter.setFlowRates(
# [-1, inp.lpg_injection_flow], 'kg/hr')
NGLinjectionsplitter.setSplitFactors(
[1.0-inp.lpg_injection_fraction, inp.lpg_injection_fraction])
NGLinjectionsplitter.run()
ngl_column_model.add(NGLinjectionsplitter)
NGLsplitter = neqsim.process.equipment.splitter.Splitter(
"NGL splitter", NGLinjectionsplitter.getSplitStream(0))
NGLsplitter.setSplitFactors(
[inp.ngl_routing_to_oil, 1.0-inp.ngl_routing_to_oil])
NGLsplitter.run()
ngl_column_model.add(NGLsplitter)
lpg_injection_stream = neqsim.process.equipment.stream.Stream(
"lpg injection stream", NGLinjectionsplitter.getSplitStream(1))
lpg_injection_stream.run()
ngl_column_model.add(lpg_injection_stream)
lpg_production_stream = neqsim.process.equipment.stream.Stream(
"lpg production stream", NGLinjectionsplitter.getSplitStream(0))
lpg_production_stream.run()
ngl_column_model.add(lpg_production_stream)
NGLcoumnliquidsplittertoNGLmixer = neqsim.process.equipment.splitter.Splitter(
"NGL column liquid splitter", NGLsplitter.getSplitStream(0))
NGLcoumnliquidsplittertoNGLmixer.setSplitFactors([0.5, 0.5])
NGLcoumnliquidsplittertoNGLmixer.run()
ngl_column_model.add(NGLcoumnliquidsplittertoNGLmixer)
NGLcoumnliquidsplittertoMPliqmixer = neqsim.process.equipment.splitter.Splitter(
"NGL column liquid splitter2", NGLsplitter.getSplitStream(1))
NGLcoumnliquidsplittertoMPliqmixer.setSplitFactors([0.5, 0.5])
NGLcoumnliquidsplittertoMPliqmixer.run()
ngl_column_model.add(NGLcoumnliquidsplittertoMPliqmixer)
separation_process_train_A.getUnit("NGL mixer").addStream(
NGLcoumnliquidsplittertoNGLmixer.getSplitStream(0))
separation_process_train_B.getUnit("NGL mixer").addStream(
NGLcoumnliquidsplittertoNGLmixer.getSplitStream(1))
separation_process_train_A.getUnit("MP liq gas mixer").addStream(
NGLcoumnliquidsplittertoMPliqmixer.getSplitStream(0))
separation_process_train_B.getUnit("MP liq gas mixer").addStream(
NGLcoumnliquidsplittertoMPliqmixer.getSplitStream(1))
return ngl_column_model
Code cell 23: 8. Export compressor model
Notebook cell 47. This code belongs to the 8. Export compressor model section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid- characterization files, and case-specific result folders.
import math
def export_compressor_model(inp: ProcessInput, feedstream):
#print('feed flow ', feedstream.getFlowRate("kg/hr"), 'kg/hr')
compressor_model = neqsim.process.processmodel.ProcessSystem()
intermediatePressure = feedstream.getPressure(
) * math.sqrt(inp.export_compressor_pressure/feedstream.getPressure())+8
recyclegasstream = create_stream('recycle dx')
recyclegasstream.setPressure(feedstream.getPressure()+5.0, 'bara')
recyclegasstream.setTemperature(25.0, 'C')
recyclegasstream.setFlowRate(100.0, "kg/hr")
recyclegasstream.run()
compressor_model.add(recyclegasstream)
gas_mixer_hp_gas = neqsim.process.equipment.mixer.Mixer(
"gas mixer")
gas_mixer_hp_gas.addStream(feedstream)
gas_mixer_hp_gas.addStream(recyclegasstream)
gas_mixer_hp_gas.run()
compressor_model.add(gas_mixer_hp_gas)
compressor_EXPORT_COMPRESSOR = neqsim.process.equipment.compressor.Compressor(
"EXPORT_COMPRESSOR", gas_mixer_hp_gas.getOutStream())
compressor_EXPORT_COMPRESSOR.setUsePolytropicCalc(True)
compressor_EXPORT_COMPRESSOR.setPolytropicEfficiency(0.75)
compressor_EXPORT_COMPRESSOR.setOutletPressure(intermediatePressure, 'bara')
compressor_EXPORT_COMPRESSOR.setSpeed(9000)
compressor_EXPORT_COMPRESSOR.getCompressorChart().setHeadUnit(inp.headunit)
compressor_EXPORT_COMPRESSOR.getCompressorChart().getSurgeCurve().setCurve(inp.chartConditions, inp.surgeflow_DX1_stage_compressor, inp.surgehead_DX1_stage_compressor)
try:
compressor_EXPORT_COMPRESSOR.run()
except:
print('error in EXPORT_COMPRESSOR')
compressor_model.add(compressor_EXPORT_COMPRESSOR)
cooler_export_stage_1 = neqsim.process.equipment.heatexchanger.Cooler(
"EXPORT_COOLER_STAGE_1", compressor_EXPORT_COMPRESSOR.getOutStream())
cooler_export_stage_1.setOutTemperature(30.0, 'C')
cooler_export_stage_1.setPressureDrop(0.35)
cooler_export_stage_1.run()
compressor_model.add(cooler_export_stage_1)
compressor_EXPORT_COMPRESSOR = neqsim.process.equipment.compressor.Compressor(
"EXPORT_COMPRESSOR", cooler_export_stage_1.getOutStream())
compressor_EXPORT_COMPRESSOR.setUsePolytropicCalc(True)
compressor_EXPORT_COMPRESSOR.setPolytropicEfficiency(0.75)
compressor_EXPORT_COMPRESSOR.setOutletPressure(
inp.export_compressor_pressure, 'bara')
compressor_EXPORT_COMPRESSOR.setSpeed(9000)
compressor_EXPORT_COMPRESSOR.getCompressorChart().setHeadUnit(inp.headunit)
compressor_EXPORT_COMPRESSOR.getCompressorChart().getSurgeCurve().setCurve(inp.chartConditions, inp.surgeflow_DX1_stage_compressor, inp.surgehead_DX1_stage_compressor)
try:
compressor_EXPORT_COMPRESSOR.run()
except:
print('error in EXPORT_COMPRESSOR')
compressor_model.add(compressor_EXPORT_COMPRESSOR)
gassplitter = neqsim.process.equipment.splitter.Splitter(
'anti surge splitter')
gassplitter.setInletStream(compressor_EXPORT_COMPRESSOR.getOutletStream())
gassplitter.setFlowRates([-1, 1.0], "kg/hr")
try:
gassplitter.run()
except:
print('error in gassplitter')
compressor_model.add(gassplitter)
antisurgeCalculator = neqsim.process.equipment.util.Calculator("anti surge calculator_1")
antisurgeCalculator.addInputVariable(compressor_EXPORT_COMPRESSOR)
antisurgeCalculator.setOutputVariable(gassplitter)
antisurgeCalculator.run()
compressor_model.add(antisurgeCalculator)
antisurgevalve = neqsim.process.equipment.valve.ThrottlingValve('aniti surge valve', gassplitter.getSplitStream(1))
antisurgevalve.setOutletPressure(feedstream.getPressure()+5.0, "bara")
antisurgevalve.run()
compressor_model.add(antisurgevalve)
recycl = neqsim.process.equipment.util.Recycle("recycle anti surge 1st stage compressor")
recycl.addStream(antisurgevalve.getOutletStream())
recycl.setOutletStream(recyclegasstream)
recycl.setTolerance(1e-2)
recycl.run()
compressor_model.add(recycl)
gassplitter.setFlowRates([-1.0, 1.0], "kg/hr")
gassplitter.run()
compressor_out_stream = neqsim.process.equipment.stream.Stream(
"compressor EXPORT_COMPRESSOR out stream", gassplitter.getSplitStream(0))
compressor_out_stream.run()
compressor_model.add(compressor_out_stream)
#print('out flow ', compressor_out_stream.getFlowRate("kg/hr"), 'kg/hr')
return compressor_model
def stabilize_export_compressor_outlet(compressor_process, recycle_flow_kg_hr=1.0):
splitter = compressor_process.getUnit("anti surge splitter")
splitter.setFlowRates([-1.0, recycle_flow_kg_hr], "kg/hr")
splitter.run()
compressor_process.getUnit("compressor EXPORT_COMPRESSOR out stream").run()
Code cell 24: 8. Export compressor model
Notebook cell 48. This code belongs to the 8. Export compressor model section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid- characterization files, and case-specific result folders.
import math
def create_zero_ht_injection_process(inp: ProcessInput, reference_stream):
ht_process = neqsim.process.processmodel.ProcessSystem()
gas_export_ht = neqsim.process.equipment.stream.Stream('gas export ht', reference_stream)
gas_export_ht.setFlowRate(0.0, 'kg/hr')
gas_export_ht.setPressure(inp.export_compressor_pressure, 'bara')
gas_export_ht.run()
ht_process.add(gas_export_ht)
gas_injection_ht = neqsim.process.equipment.stream.Stream('gas injection ht', reference_stream)
gas_injection_ht.setFlowRate(0.0, 'kg/hr')
gas_injection_ht.setPressure(inp.injection_compressor_pressure, 'bara')
gas_injection_ht.run()
ht_process.add(gas_injection_ht)
return ht_process
def ht_injection_compressor_model(inp: ProcessInput, ht_feed_gas):
compressor_model = neqsim.process.processmodel.ProcessSystem()
gasmixer = neqsim.process.equipment.mixer.Mixer('ht gas mixer')
gasmixer.addStream(ht_feed_gas)
gasmixer.run()
compressor_model.add(gasmixer)
ht_intermediate_pressure = math.sqrt(ht_feed_gas.getPressure()*inp.injection_compressor_pressure)
recyclegasstream_ht1 = create_stream('recycle ht 1st stage')
recyclegasstream_ht1.setPressure(ht_intermediate_pressure, 'bara')
recyclegasstream_ht1.setTemperature(25.0, 'C')
recyclegasstream_ht1.setFlowRate(1.0, "kg/hr")
recyclegasstream_ht1.run()
compressor_model.add(recyclegasstream_ht1)
gas_mixer_lp_gas = neqsim.process.equipment.mixer.Mixer(
"gas mixer ht 1 gas")
gas_mixer_lp_gas.addStream(gasmixer.getOutletStream())
gas_mixer_lp_gas.addStream(recyclegasstream_ht1)
gas_mixer_lp_gas.run()
compressor_model.add(gas_mixer_lp_gas)
cooler_ht1 = neqsim.process.equipment.heatexchanger.Cooler('ht 1st stage cooler', gas_mixer_lp_gas.getOutStream())
cooler_ht1.setOutTemperature(inp.fourth_stage_suction_cooler_temperature, 'C')
cooler_ht1.run()
compressor_model.add(cooler_ht1)
separator_gas = neqsim.process.equipment.separator.Separator('gas scrubber for HT compressor stage 1', cooler_ht1.getOutStream())
separator_gas.run()
compressor_model.add(separator_gas)
ht1_compressor = neqsim.process.equipment.compressor.Compressor('ht 1st stage compressor', separator_gas.getGasOutStream())
ht1_compressor.setUsePolytropicCalc(True)
ht1_compressor.setPolytropicEfficiency(0.8)
ht1_compressor.setOutletPressure(ht_intermediate_pressure, 'bara')
ht1_compressor.getCompressorChart().setHeadUnit(inp.headunit)
ht1_compressor.getCompressorChart().getSurgeCurve().setCurve(inp.chartConditions, inp.surgeflow_htA_stage_compressor, inp.surgehead_htA_stage_compressor)
ht1_compressor.run()
compressor_model.add(ht1_compressor)
gassplitter_ht1 = neqsim.process.equipment.splitter.Splitter(
'ht1 anti surge splitter')
gassplitter_ht1.setInletStream(ht1_compressor.getOutletStream())
gassplitter_ht1.setFlowRates([-1, 0.01], "kg/hr")
try:
gassplitter_ht1.run()
except:
print('error in gassplitter ht1')
compressor_model.add(gassplitter_ht1)
antisurgeCalculator = neqsim.process.equipment.util.Calculator("anti surge calculator_1")
antisurgeCalculator.addInputVariable(ht1_compressor)
antisurgeCalculator.setOutputVariable(gassplitter_ht1)
antisurgeCalculator.run()
compressor_model.add(antisurgeCalculator)
antisurgevalve_ht = neqsim.process.equipment.valve.ThrottlingValve('aniti surge valve', gassplitter_ht1.getSplitStream(1))
antisurgevalve_ht.setOutletPressure(ht_feed_gas.getPressure()+10.0, "bara")
antisurgevalve_ht.run()
compressor_model.add(antisurgevalve_ht)
recycl = neqsim.process.equipment.util.Recycle("recycle anti surge ht 1 compressor")
recycl.addStream(antisurgevalve_ht.getOutletStream())
recycl.setOutletStream(recyclegasstream_ht1)
recycl.setTolerance(1e-2)
recycl.run()
compressor_model.add(recycl)
recyclegasstream_ht2 = create_stream('recycle ht 2nd stage')
recyclegasstream_ht2.setPressure(ht_intermediate_pressure, 'bara')
recyclegasstream_ht2.setTemperature(25.0, 'C')
recyclegasstream_ht2.setFlowRate(10.0, "kg/hr")
recyclegasstream_ht2.run()
compressor_model.add(recyclegasstream_ht2)
gas_mixer_ht2_gas = neqsim.process.equipment.mixer.Mixer(
"gas mixer ht 2 gas")
gas_mixer_ht2_gas.addStream(gassplitter_ht1.getSplitStream(0))
gas_mixer_ht2_gas.addStream(recyclegasstream_ht2)
gas_mixer_ht2_gas.run()
compressor_model.add(gas_mixer_ht2_gas)
cooler_ht2 = neqsim.process.equipment.heatexchanger.Cooler('ht 2nd stage cooler', gas_mixer_ht2_gas.getOutStream())
cooler_ht2.setOutTemperature(inp.fifth_stage_suction_cooler_temperature, 'C')
cooler_ht2.run()
compressor_model.add(cooler_ht2)
separator_ht_2= neqsim.process.equipment.separator.Separator('gas scrubber for HT compressor stage 2',cooler_ht2.getOutStream())
separator_ht_2.run()
compressor_model.add(separator_ht_2)
ht2_compressor = neqsim.process.equipment.compressor.Compressor('ht 2nd stage compressor', separator_ht_2.getGasOutStream())
ht2_compressor.setUsePolytropicCalc(True)
ht2_compressor.setPolytropicEfficiency(0.8)
ht2_compressor.setOutletPressure(inp.injection_compressor_pressure, 'bara')
ht2_compressor.getCompressorChart().setHeadUnit(inp.headunit)
ht2_compressor.getCompressorChart().getSurgeCurve().setCurve(inp.chartConditions, inp.surgeflow_htB_stage_compressor, inp.surgehead_htB_stage_compressor)
ht2_compressor.run()
compressor_model.add(ht2_compressor)
gassplitter_ht2 = neqsim.process.equipment.splitter.Splitter(
'ht2 anti surge splitter')
gassplitter_ht2.setInletStream(ht2_compressor.getOutletStream())
gassplitter_ht2.setFlowRates([-1, 1.0], "kg/hr")
try:
gassplitter_ht2.run()
except:
print('error in gassplitter ht2')
compressor_model.add(gassplitter_ht2)
antisurgeCalculator2 = neqsim.process.equipment.util.Calculator("anti surge calculator_2")
antisurgeCalculator2.addInputVariable(ht2_compressor)
antisurgeCalculator2.setOutputVariable(gassplitter_ht2)
antisurgeCalculator2.run()
compressor_model.add(antisurgeCalculator2)
antisurgevalve_ht2 = neqsim.process.equipment.valve.ThrottlingValve('aniti surge valve ht2', gassplitter_ht2.getSplitStream(1))
antisurgevalve_ht2.setOutletPressure(ht_intermediate_pressure, "bara")
antisurgevalve_ht2.run()
compressor_model.add(antisurgevalve_ht2)
recycl_2 = neqsim.process.equipment.util.Recycle("recycle anti surge ht 2 compressor")
recycl_2.addStream(antisurgevalve_ht2.getOutletStream())
recycl_2.setOutletStream(recyclegasstream_ht2)
recycl_2.setTolerance(1e-2)
recycl_2.run()
compressor_model.add(recycl_2)
injection_export_splitter = neqsim.process.equipment.splitter.Splitter('export gas splitter', gassplitter_ht2.getSplitStream(0))
injection_export_splitter.setSplitFactors([0.99999, 0.00001])
injection_export_splitter.run()
compressor_model.add(injection_export_splitter)
injection_gas_ht = neqsim.process.equipment.stream.Stream('gas injection ht', injection_export_splitter.getSplitStream(0))
injection_gas_ht.run()
compressor_model.add(injection_gas_ht)
export_gas_ht = neqsim.process.equipment.stream.Stream('gas export ht', injection_export_splitter.getSplitStream(1))
export_gas_ht.run()
compressor_model.add(export_gas_ht)
return compressor_model
Code cell 25: 8. Export compressor model
Notebook cell 49. This code belongs to the 8. Export compressor model section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid- characterization files, and case-specific result folders.
def exportgasprocess(inp: ProcessInput, export_compressor_train_A, export_compressor_train_B, offshore_asset_well_feed_model, ht_process_A, ht_process_B):
export_gas_process = neqsim.process.processmodel.ProcessSystem()
#print('feed train A ', export_compressor_train_A.getUnit(
# "compressor EXPORT_COMPRESSOR out stream").getOutletStream().getFlowRate('kg/hr'))
#print('feed train B ', export_compressor_train_B.getUnit(
# "compressor EXPORT_COMPRESSOR out stream").getOutletStream().getFlowRate('kg/hr'))
#print('ht train A ', ht_process_A.getOutletStream().getFlowRate('kg/hr'))
#print('ht train B ', ht_process_B.getOutletStream().getFlowRate('kg/hr'))
trainA_gas_splitter = neqsim.process.equipment.splitter.Splitter(
'train A gas splitter', export_compressor_train_A.getUnit(
"compressor EXPORT_COMPRESSOR out stream"))
trainA_gas_splitter.setFlowRates([0.0001, -1], 'MSm3/day')
try:
trainA_gas_splitter.run()
except:
print('error in train A gas splitter')
export_gas_process.add(trainA_gas_splitter)
#print('rate exp A', trainA_gas_splitter.getSplitStream(1).getFlowRate('MSm3/day'))
trainB_gas_splitter = neqsim.process.equipment.splitter.Splitter(
'train B gas splitter', export_compressor_train_B.getUnit(
"compressor EXPORT_COMPRESSOR out stream"))
#print('gas lift rate train B', inp.gas_lift_rate)
#print('injection gas rate train B', inp.injection_gas_rate_dx)
trainB_gas_splitter.setFlowRates(
[inp.gas_lift_rate + inp.injection_gas_rate_dx+0.0001, -1], 'MSm3/day')
try:
trainB_gas_splitter.run()
except:
print('error in train B gas splitter')
export_gas_process.add(trainB_gas_splitter)
#print('rate exp B', trainB_gas_splitter.getSplitStream(1).getFlowRate('MSm3/day'))
#print('rate exp B', trainB_gas_splitter.getSplitStream(0).getFlowRate('MSm3/day'))
gas_mixer = neqsim.process.equipment.mixer.Mixer('gas mixer')
gas_mixer.addStream(trainA_gas_splitter.getSplitStream(1))
gas_mixer.addStream(trainB_gas_splitter.getSplitStream(1))
gas_mixer.addStream(ht_process_A.getUnit('gas export ht'))
gas_mixer.addStream(ht_process_B.getUnit('gas export ht'))
gas_mixer.run()
export_gas_process.add(gas_mixer)
#print('ht feed A', ht_process_A.getUnit('gas export ht').getFlowRate('kg/hr'))
#print('ht feed B', ht_process_B.getUnit('gas export ht').getFlowRate('kg/hr'))
#print('mixer out ', gas_mixer.getOutStream().getFlowRate('kg/hr'))
export_gas_stream = neqsim.process.equipment.stream.Stream(
'export gas stream', gas_mixer.getOutStream())
export_gas_stream.run()
export_gas_process.add(export_gas_stream)
print('export gas stream', export_gas_stream.getFlowRate('kg/hr'))
injection_compressor_manifold = neqsim.process.equipment.manifold.Manifold(
'injection compressor manifold')
injection_compressor_manifold.addStream(
trainA_gas_splitter.getSplitStream(0))
injection_compressor_manifold.addStream(
trainB_gas_splitter.getSplitStream(0))
injection_compressor_manifold.setSplitFactors([0.9999, 0.0001])
injection_compressor_manifold.run()
export_gas_process.add(injection_compressor_manifold)
#print('manifold gas stream', injection_compressor_manifold.getSplitStream(0).getFlowRate('MSm3/day'))
#print('manifold gas stream2 ', injection_compressor_manifold.getSplitStream(1).getFlowRate('MSm3/day'))
#print('injection 1 ', injection_compressor_manifold.getSplitStream(0).getFlowRate('kg/hr'))
#print('injection 2 ', injection_compressor_manifold.getSplitStream(1).getFlowRate('kg/hr'))
recyclegasstream = create_stream('recycle dx stage')
recyclegasstream.setPressure(inp.export_compressor_pressure, 'bara')
recyclegasstream.setTemperature(25.0, 'C')
recyclegasstream.setFlowRate(100.0, "kg/hr")
recyclegasstream.run()
export_gas_process.add(recyclegasstream)
gas_mixer_dx_gas = neqsim.process.equipment.mixer.Mixer(
"gas mixer dx")
gas_mixer_dx_gas.addStream(injection_compressor_manifold.getSplitStream(0))
gas_mixer_dx_gas.addStream(recyclegasstream)
gas_mixer_dx_gas.run()
export_gas_process.add(gas_mixer_dx_gas)
cooler_dx_gas = neqsim.process.equipment.heatexchanger.Cooler(
"DX gas cooler", gas_mixer_dx_gas.getOutStream())
cooler_dx_gas.setOutTemperature(30.0, 'C')
cooler_dx_gas.run()
export_gas_process.add(cooler_dx_gas)
injection_compressor1 = neqsim.process.equipment.compressor.Compressor(
"injection compressor DXA", cooler_dx_gas.getOutStream())
injection_compressor1.setUsePolytropicCalc(True)
injection_compressor1.setPolytropicEfficiency(0.75)
injection_compressor1.setOutletPressure(
inp.injection_compressor_pressure, 'bara')
injection_compressor1.getCompressorChart().setHeadUnit(inp.headunit)
injection_compressor1.getCompressorChart().getSurgeCurve().setCurve(inp.chartConditions, inp.surgeflow_DX3_stage_compressor, inp.surgehead_DX3_stage_compressor)
try:
injection_compressor1.run()
except:
print('error in injection compressor 1')
export_gas_process.add(injection_compressor1)
gassplitter = neqsim.process.equipment.splitter.Splitter(
'anti surge splitter')
gassplitter.setInletStream(injection_compressor1.getOutletStream())
gassplitter.setFlowRates([-1, 1.0], "kg/hr")
try:
gassplitter.run()
except:
print('error in gassplitter')
export_gas_process.add(gassplitter)
antisurgeCalculator = neqsim.process.equipment.util.Calculator("anti surge calculator 1")
antisurgeCalculator.addInputVariable(injection_compressor1)
antisurgeCalculator.setOutputVariable(gassplitter)
antisurgeCalculator.run()
export_gas_process.add(antisurgeCalculator)
antisurgevalve = neqsim.process.equipment.valve.ThrottlingValve('aniti surge valve', gassplitter.getSplitStream(1))
antisurgevalve.setOutletPressure(inp.export_compressor_pressure, "bara")
antisurgevalve.run()
export_gas_process.add(antisurgevalve)
recycl = neqsim.process.equipment.util.Recycle("recycle anti surge")
recycl.addStream(antisurgevalve.getOutletStream())
recycl.setOutletStream(recyclegasstream)
recycl.setTolerance(1e-2)
recycl.run()
export_gas_process.add(recycl)
recyclegasstream2 = create_stream('recycle dx B')
recyclegasstream2.setPressure(inp.export_compressor_pressure, 'bara')
recyclegasstream2.setTemperature(25.0, 'C')
recyclegasstream2.setFlowRate(1.0, "kg/hr")
recyclegasstream2.run()
export_gas_process.add(recyclegasstream2)
gas_mixer_dx_gas2 = neqsim.process.equipment.mixer.Mixer(
"gas mixer 2")
gas_mixer_dx_gas2.addStream(injection_compressor_manifold.getSplitStream(1))
gas_mixer_dx_gas2.addStream(recyclegasstream2)
gas_mixer_dx_gas2.run()
export_gas_process.add(gas_mixer_dx_gas2)
cooler_dx_gas2 = neqsim.process.equipment.heatexchanger.Cooler(
"DX gas cooler 2", gas_mixer_dx_gas2.getOutStream())
cooler_dx_gas2.setOutTemperature(30.0, 'C')
cooler_dx_gas2.run()
export_gas_process.add(cooler_dx_gas2)
injection_compressor2 = neqsim.process.equipment.compressor.Compressor(
"injection compressor INJECTION_HUB", cooler_dx_gas2.getOutStream())
injection_compressor2.setUsePolytropicCalc(True)
injection_compressor2.setPolytropicEfficiency(0.75)
injection_compressor2.setOutletPressure(
inp.injection_compressor_pressure, 'bara')
injection_compressor2.getCompressorChart().setHeadUnit(inp.headunit)
injection_compressor2.getCompressorChart().getSurgeCurve().setCurve(inp.chartConditions, inp.surgeflow_DX3_stage_compressor, inp.surgehead_DX3_stage_compressor)
try:
injection_compressor2.run()
except:
print('error in injection compressor 2')
export_gas_process.add(injection_compressor2)
gassplitter2 = neqsim.process.equipment.splitter.Splitter(
'anti surge splitter2')
gassplitter2.setInletStream(injection_compressor2.getOutletStream())
gassplitter2.setFlowRates([-1, 1.0, ], "kg/hr")
try:
gassplitter2.run()
except:
print('error in gassplitter 2')
export_gas_process.add(gassplitter2)
antisurgeCalculator2 = neqsim.process.equipment.util.Calculator("anti surge calculator 2")
antisurgeCalculator2.addInputVariable(injection_compressor2)
antisurgeCalculator2.setOutputVariable(gassplitter2)
try:
antisurgeCalculator2.run()
except:
print('error in antisurge calculator 2')
export_gas_process.add(antisurgeCalculator2)
antisurgevalve2 = neqsim.process.equipment.valve.ThrottlingValve('aniti surge valve 2', gassplitter2.getSplitStream(1))
antisurgevalve2.setOutletPressure(inp.export_compressor_pressure, "bara")
antisurgevalve2.run()
export_gas_process.add(antisurgevalve2)
recyc2 = neqsim.process.equipment.util.Recycle("recycle anti surge 2")
recyc2.addStream(antisurgevalve2.getOutletStream())
recyc2.setOutletStream(recyclegasstream2)
recyc2.setTolerance(1e-2)
recyc2.run()
export_gas_process.add(recyc2)
gasinjection_manifold = neqsim.process.equipment.manifold.Manifold(
'gas injection manifold')
gasinjection_manifold.addStream(gassplitter.getSplitStream(0))
gasinjection_manifold.addStream(gassplitter2.getSplitStream(0))
gasinjection_manifold.addStream(ht_process_A.getUnit('gas injection ht'))
gasinjection_manifold.addStream(ht_process_B.getUnit('gas injection ht'))
gasinjection_manifold.setSplitFactors([0.9999, 0.0001])
try:
gasinjection_manifold.run()
except:
print('error in gas injection manifold')
export_gas_process.add(gasinjection_manifold)
gas_lift_injection_splitter = neqsim.process.equipment.splitter.Splitter(
'injection and lift gas splitter', gasinjection_manifold.getSplitStream(0))
gas_lift_injection_splitter.setFlowRates(
[inp.gas_lift_rate, -1], 'MSm3/day')
try:
gas_lift_injection_splitter.run()
except:
print('error in train B gas splitter')
export_gas_process.add(gas_lift_injection_splitter)
gas_lift_stream = neqsim.process.equipment.stream.Stream(
'gas lift stream', gas_lift_injection_splitter.getSplitStream(0))
gas_lift_stream.run()
export_gas_process.add(gas_lift_stream)
print('gas lift stream', gas_lift_stream.getFlowRate('kg/hr'))
gas_injection_stream = neqsim.process.equipment.stream.Stream(
'gas injection stream', gas_lift_injection_splitter.getSplitStream(1))
gas_injection_stream.run()
export_gas_process.add(gas_injection_stream)
#print('gas injection stream', gas_injection_stream.getFlowRate('kg/hr'))
gas_lift_resycle = neqsim.process.equipment.util.Recycle('gas lift resycle')
gas_lift_resycle.addStream(gas_lift_stream)
gas_lift_resycle.setOutletStream(offshore_asset_well_feed_model.getUnit("gas lift feed"))
gas_lift_resycle.setTolerance(1e-2)
gas_lift_resycle.run()
export_gas_process.add(gas_lift_resycle)
return export_gas_process
Code cell 26: 8. Export compressor model
Notebook cell 51. This code belongs to the 8. Export compressor model section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid- characterization files, and case-specific result folders.
offshore_asset_well_feed_model = create_offshore_asset_well_feed_model(input_parameters)
print('start well flow model.....')
offshore_asset_well_feed_model.run()
reference_stream_c_sep_process = reference_stream_c_calibration_stream_separation_process(
input_parameters, offshore_asset_well_feed_model.getUnit("reference_stream_c calibration_stream manifold").getOutStream())
print('start reference_stream_c model.....')
reference_stream_c_sep_process.run()
test_sep_process = test_separation_process(
input_parameters, offshore_asset_well_feed_model.getUnit("test manifold").getOutStream())
print('start test manifold.....')
test_sep_process.run()
if input_parameters.use_both_gas_recompression_trains_1_2_stage < 0.1:
lp_split = 0.0001
else:
lp_split = 0.9999
if input_parameters.use_both_gas_recompression_trains_3_stage < 0.1:
mp_split = 0.0001
else:
mp_split = 0.9999
separation_process_train_A = create_oil_separation_process(input_parameters, offshore_asset_well_feed_model.getUnit("HP manifold A").getOutStream(), offshore_asset_well_feed_model.getUnit(
"LP manifold A").getOutStream(), lp_split, reference_stream_c_sep_process.getUnit("manifold").getSplitStream(0), offshore_asset_well_feed_model.getUnit("south east manifold").getSplitStream(0), test_separator=None, reference_stream_c_calibration_stream_gas=reference_stream_c_sep_process.getUnit("gas manifold").getSplitStream(1), mpsplit = mp_split)
print('start sep A process.....')
separation_process_train_A.run()
separation_process_train_B = create_oil_separation_process(input_parameters, offshore_asset_well_feed_model.getUnit("HP manifold B").getOutletStream(), offshore_asset_well_feed_model.getUnit(
"LP manifold B").getOutletStream(), 0.9999, reference_stream_c_sep_process.getUnit("manifold").getSplitStream(1), offshore_asset_well_feed_model.getUnit("south east manifold").getSplitStream(1), test_separator=None, reference_stream_c_calibration_stream_gas=reference_stream_c_sep_process.getUnit("gas manifold").getSplitStream(2), other_train_lp_gas_feed=separation_process_train_A.getUnit("gas splitter LP gas").getSplitStream(1),
other_train_lp2_gas_feed=separation_process_train_A.getUnit("gas splitter LP2 gas").getSplitStream(1), other_train_mp_gas_feed=separation_process_train_A.getUnit("gas splitter MP gas").getSplitStream(1), mpsplit=0.9999)
print('start sep B process.....')
separation_process_train_B.run()
manifold_upstream_TEX = manifold_and_pipe_model_liquid_train_a_to_gas_train_b(input_parameters, separation_process_train_A.getUnit(
"fourth Stage mixer").getOutStream(), separation_process_train_B.getUnit("fourth Stage mixer").getOutStream())
print('start manifold_upstream_TEX process.....')
manifold_upstream_TEX.run()
tex_process_1 = tex_process(input_parameters, manifold_upstream_TEX.getUnit(
"manifold").getSplitStream(0), separation_process_train_A)
print('start tex A process.....')
tex_process_1.run()
tex_process_2 = tex_process(input_parameters, manifold_upstream_TEX.getUnit(
"manifold").getSplitStream(1), separation_process_train_B)
print('start tex B process.....')
tex_process_2.run()
manifold_upstream_column = manifold_model(
input_parameters, tex_process_1.getUnit("HX_MULTI_STREAM").getOutStream(2))
manifold_upstream_column.getUnit("manifold").addStream(
tex_process_2.getUnit("HX_MULTI_STREAM").getOutStream(2))
manifold_upstream_column.getUnit("manifold").setSplitFactors([1.0])
print('start manifold_upstream_column process.....')
manifold_upstream_column.run()
column_model = ngl_column_model(input_parameters, manifold_upstream_column.getUnit(
"manifold").getSplitStream(0), separation_process_train_A, separation_process_train_B, lp_split)
print('start column_model process.....')
column_model.run()
manifold_upstream_ht_injection_compressors = manifold_model(
input_parameters, tex_process_1.getUnit("gas to ht compressor"))
manifold_upstream_ht_injection_compressors.getUnit("manifold").addStream(
tex_process_2.getUnit("gas to ht compressor"))
manifold_upstream_ht_injection_compressors.getUnit("manifold").setSplitFactors([input_parameters.injection_gas_rate_ht_split_to_train_A, -1])
print('start manifold_upstream_ht_injection_compressors process.....')
manifold_upstream_ht_injection_compressors.run()
if input_parameters.injection_gas_rate_ht <= 1.0e-12:
print('HT injection rate is zero; bypassing HT injection compressor trains for this case.')
ht_injection_process_A = create_zero_ht_injection_process(
input_parameters, manifold_upstream_ht_injection_compressors.getUnit("manifold").getSplitStream(0))
ht_injection_process_A.run()
ht_injection_process_B = create_zero_ht_injection_process(
input_parameters, manifold_upstream_ht_injection_compressors.getUnit("manifold").getSplitStream(1))
ht_injection_process_B.run()
else:
ht_injection_process_A = ht_injection_compressor_model(input_parameters, manifold_upstream_ht_injection_compressors.getUnit("manifold").getSplitStream(0))
print('start ht_injection_process_A process.....')
ht_injection_process_A.run()
ht_injection_process_B = ht_injection_compressor_model(input_parameters, manifold_upstream_ht_injection_compressors.getUnit("manifold").getSplitStream(1))
print('start ht_injection_process_B process.....')
ht_injection_process_B.run()
manifold_upstream_compressors = manifold_model(
input_parameters, tex_process_1.getUnit("gas to dx compressor"))
manifold_upstream_compressors.getUnit("manifold").addStream(
tex_process_2.getUnit("gas to dx compressor"))
manifold_upstream_compressors.getUnit("manifold").setSplitFactors([0.5, 0.5])
print('start manifold_upstream_compressors process.....')
manifold_upstream_compressors.run()
exp_compressor_1 = export_compressor_model(
input_parameters, manifold_upstream_compressors.getUnit("manifold").getSplitStream(0))
print('start exp_compressor_1 process.....')
exp_compressor_1.run()
stabilize_export_compressor_outlet(exp_compressor_1)
exp_compressor_2 = export_compressor_model(
input_parameters, manifold_upstream_compressors.getUnit("manifold").getSplitStream(1))
print('start exp_compressor_2 process.....')
exp_compressor_2.run()
stabilize_export_compressor_outlet(exp_compressor_2)
exp_gas_process = exportgasprocess(input_parameters, exp_compressor_1,exp_compressor_2, offshore_asset_well_feed_model, ht_injection_process_A, ht_injection_process_B)
print('start exp_gas_process process.....')
exp_gas_process.run()
#clear_output(wait=True)
print("Calculation finished")
Code cell 27: 8. Export compressor model
Notebook cell 52. This code belongs to the 8. Export compressor model section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid- characterization files, and case-specific result folders.
try:
print('year ', input_parameters.Year)
print('tex in ', tex_process_1.getUnit(
"TEX").getInletStream().getTemperature('C'), ' C')
print('tex out ', tex_process_1.getUnit(
"TEX").getOutletStream().getTemperature('C'), ' C')
print('tex in ', tex_process_1.getUnit(
"TEX").getInletStream().getPressure('bara'), ' bara')
print('feed gas to DPC A+B ', tex_process_1.getUnit("dew point cooler").getInletStream().getFlowRate('MSm3/day') +
tex_process_2.getUnit("dew point cooler").getInletStream().getFlowRate('MSm3/day'), ' MSm3/day')
except:
print('tex in ', tex_process_1.getUnit(
"TEX").getInletStream().getTemperature('C'), ' C')
Code cell 28: 9. Oil mixing with oil from FISCAL_AREA, Condensate Area and ThirdPartyFeed
Notebook cell 54. This code belongs to the 9. Oil mixing with oil from FISCAL_AREA, Condensate Area and ThirdPartyFeed section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid-characterization files, and case- specific result folders.
def export_oil_model(inp: ProcessInput, feed_A_oil_stab, feed_B_oil_stab, condensate_area_feed, third_party_feed_feed):
oil_export_model = neqsim.process.processmodel.ProcessSystem()
mixer_oil = neqsim.process.equipment.mixer.Mixer("inlet oil mixer")
mixer_oil.addStream(feed_A_oil_stab)
mixer_oil.addStream(feed_B_oil_stab)
try:
mixer_oil.run()
except:
print('error in inlet oil mixer')
oil_export_model.add(mixer_oil)
fiscal_area_oil = neqsim.process.equipment.stream.Stream("fiscal_area oil export", mixer_oil.getOutStream())
fiscal_area_oil.run()
oil_export_model.add(fiscal_area_oil)
stable_oil_cooler = neqsim.process.equipment.heatexchanger.Cooler(
"stable oil cooler", fiscal_area_oil)
stable_oil_cooler.setOutTemperature(30.0, 'C')
stable_oil_cooler.run()
oil_export_model.add(stable_oil_cooler)
oil_mixer = neqsim.process.equipment.mixer.Mixer("oil mixer")
oil_mixer.addStream(stable_oil_cooler.getOutStream())
oil_mixer.addStream(condensate_area_feed)
oil_mixer.addStream(third_party_feed_feed)
oil_mixer.run()
oil_export_model.add(oil_mixer)
oil_pump = neqsim.process.equipment.pump.Pump(
"oil pump", oil_mixer.getOutStream())
oil_pump.setOutletPressure(40.0, 'bara')
try:
oil_pump.run()
except:
print('error in oil pump')
oil_export_model.add(oil_pump)
return oil_export_model
Code cell 29: 9. Oil mixing with oil from FISCAL_AREA, Condensate Area and ThirdPartyFeed
Notebook cell 55. This code belongs to the 9. Oil mixing with oil from FISCAL_AREA, Condensate Area and ThirdPartyFeed section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid-characterization files, and case- specific result folders.
export_model = export_oil_model(input_parameters, separation_process_train_A.getUnit("NGL mixer").getOutStream(), separation_process_train_B.getUnit(
"NGL mixer").getOutStream(), offshore_asset_well_feed_model.getUnit("Offshore Area C Feed"), offshore_asset_well_feed_model.getUnit("ThirdPartyFeed Feed"))
#export_model.run()
clear_output(wait=True)
print('total oil flow ', export_model.getUnit('oil pump').getOutletStream().getFlowRate('m3/hr'), ' m3/hr')
print("Calculation finished")
Code cell 30: 10. Offshore Area combined full process model
Notebook cell 57. This code belongs to the 10. Offshore Area combined full process model section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid-characterization files, and case-specific result folders.
offshore_asset_process = neqsim.process.processmodel.ProcessModel()
offshore_asset_process.add("well process", offshore_asset_well_feed_model)
offshore_asset_process.add("reference_stream_c calibration_stream process",reference_stream_c_sep_process)
offshore_asset_process.add("test separator process",test_sep_process)
offshore_asset_process.add("sep train A", separation_process_train_A)
offshore_asset_process.add("sep train B", separation_process_train_B)
offshore_asset_process.add("manifold TEX",manifold_upstream_TEX)
offshore_asset_process.add("TEX process A", tex_process_1)
offshore_asset_process.add("TEX process B", tex_process_2)
offshore_asset_process.add("column manifold",manifold_upstream_column)
offshore_asset_process.add("NGL column", column_model)
offshore_asset_process.add("ht injection compressor manifold", manifold_upstream_ht_injection_compressors)
offshore_asset_process.add("HT injection process A", ht_injection_process_A)
offshore_asset_process.add("HT injection process B", ht_injection_process_B)
offshore_asset_process.add("export compressor manifold", manifold_upstream_compressors)
offshore_asset_process.add("Export train A", exp_compressor_1)
offshore_asset_process.add("Export train B", exp_compressor_2)
offshore_asset_process.add("export gas", exp_gas_process)
offshore_asset_process.add("export oil", export_model)
#prothread = offshore_asset_process.runAsThread()
#prothread.join(5*60*1000)
offshore_asset_process.setRunStep(True)
#Run a number of times (run in steps)
for i in range(1, 10):
print('run ', i)
offshore_asset_process.run()
clear_output(wait=True)
print("Calculation finished")
Code cell 31: 11.1 General reporting
Notebook cell 60. This code belongs to the 11.1 General reporting section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid- characterization files, and case-specific result folders.
try:
print('year ', input_parameters.Year)
print('tex in ', tex_process_1.getUnit(
"TEX").getInletStream().getTemperature('C'), ' C')
print('tex out ', tex_process_1.getUnit(
"TEX").getOutletStream().getTemperature('C'), ' C')
print('tex in ', tex_process_1.getUnit(
"TEX").getInletStream().getPressure('bara'), ' bara')
print('feed gas to DPC A+B ', tex_process_1.getUnit("dew point cooler").getInletStream().getFlowRate('MSm3/day') +
tex_process_2.getUnit("dew point cooler").getInletStream().getFlowRate('MSm3/day'), ' MSm3/day')
print('tex out ', tex_process_1.getUnit(
"TEX").getOutletStream().getPressure('bara'), ' bara')
print('expander energy ', tex_process_1.getUnit("TEX").getPower('MW'), ' MW')
print('expander compressor energy ', tex_process_1.getUnit(
"comp_export_booster_b").getPower('MW'), ' MW')
print('tex in hot stream ', tex_process_1.getUnit(
"HX_MULTI_STREAM").getInStream(0).getTemperature('C'), ' C')
print('tex in cold stream ', tex_process_1.getUnit(
"HX_MULTI_STREAM").getInStream(1).getTemperature('C'), ' C')
print('tex in cold stream ', tex_process_1.getUnit(
"HX_MULTI_STREAM").getInStream(2).getTemperature('C'), ' C')
print('tex out hot stram out ', tex_process_1.getUnit(
"HX_MULTI_STREAM").getOutStream(0).getTemperature('C'), ' C')
print('tex out cold stream 2 ', tex_process_1.getUnit(
"HX_MULTI_STREAM").getOutStream(1).getTemperature('C'), ' C')
print('tex out cold stream 3 ', tex_process_1.getUnit(
"HX_MULTI_STREAM").getOutStream(2).getTemperature('C'), ' C')
print('gas export pressure ', exp_compressor_1.getUnit(
"compressor EXPORT_COMPRESSOR out stream").getPressure("bara"), ' bara')
print('gas export 1 ', exp_compressor_1.getUnit(
"compressor EXPORT_COMPRESSOR out stream").getFlowRate("MSm3/day"), ' MSm3/day')
print('gas export 2 ', exp_compressor_2.getUnit(
"compressor EXPORT_COMPRESSOR out stream").getFlowRate("MSm3/day"), ' MSm3/day')
print('oil temperature 1st stage train A ', separation_process_train_B.getUnit(
"1st stage separator").getLiquidOutStream().getTemperature("C"), ' C')
print('oil temperature 1st stage train B ', separation_process_train_B.getUnit(
"1st stage separator").getLiquidOutStream().getTemperature("C"), ' C')
print('oil temperature 2nd stage train A ', separation_process_train_B.getUnit(
"2nd stage separator").getLiquidOutStream().getTemperature("C"), ' C')
print('oil temperature 2nd stage train B ', separation_process_train_B.getUnit(
"2nd stage separator").getLiquidOutStream().getTemperature("C"), ' C')
print('oil temperature 3rd stage train A ', separation_process_train_B.getUnit(
"3rd stage separator").getLiquidOutStream().getTemperature("C"), ' C')
print('oil temperature 3rd stage train B ', separation_process_train_B.getUnit(
"3rd stage separator").getLiquidOutStream().getTemperature("C"), ' C')
print('oil temperature 4th stage train A ', separation_process_train_B.getUnit(
"4th stage separator").getOilOutStream().getTemperature("C"), ' C')
print('oil temperature 4th stage train B ', separation_process_train_B.getUnit(
"4th stage separator").getOilOutStream().getTemperature("C"), ' C')
print('oil rate 4th and NGL Train A ', separation_process_train_A.getUnit(
"NGL mixer").getOutletStream().getFlowRate("idSm3/hr"), ' Sm3/hr')
print('oil rate 4th and NGL Train B ', separation_process_train_B.getUnit(
"NGL mixer").getOutletStream().getFlowRate("idSm3/hr"), ' Sm3/hr')
print('oil temperature 1st stage ', separation_process_train_B.getUnit(
"1st stage separator").getOilOutStream().getTemperature("C"), ' C')
print('oil temperature 2nd stage ', separation_process_train_B.getUnit(
"2nd stage separator").getLiquidOutStream().getTemperature("C"), ' C')
print('water from 2nd stage ', separation_process_train_B.getUnit(
"2nd stage separator").getWaterOutStream().getFlowRate("Sm3/day"), ' Sm3/day')
print('water from 4th stage ', separation_process_train_B.getUnit(
"4th stage separator").getWaterOutStream().getFlowRate("Sm3/day"), ' Sm3/day')
print('oil from 4th stage ', separation_process_train_B.getUnit(
"4th stage separator").getOilOutStream().getFlowRate("idSm3/hr"), ' Sm3/hr')
print('WI ', exp_compressor_1.getUnit("compressor EXPORT_COMPRESSOR out stream").getWI(
"volume", 15.0, 25.0)/1e6, ' MJ/m3')
print('compressor power ', exp_compressor_1.getUnit(
'EXPORT_COMPRESSOR').getPower()/1e6, ' MW')
#print('compressor power ', exp_compressor_1.getUnit(
# 'EXPORT_COMPRESSOR').getPower()/1e6, ' MW')
print('ethane ', separation_process_train_A.getUnit("NGL mixer").getOutletStream().getFluid().getComponent('ethane').getNumberOfMolesInPhase(
)*3600.0*separation_process_train_A.getUnit("NGL mixer").getOutletStream().getFluid().getComponent('ethane').getMolarMass(), ' kg/hr')
print('ethane ', export_model.getUnit("oil pump").getOutletStream().getFluid().getComponent('ethane').getNumberOfMolesInPhase() *
3600.0*separation_process_train_A.getUnit("NGL mixer").getOutletStream().getFluid().getComponent('ethane').getMolarMass(), ' kg/hr')
print('Condensate Area TVP 30C ', offshore_asset_well_feed_model.getUnit("Offshore Area C Feed").getTVP(30.0, "C", "bara"), 'bara')
print('Condensate Area TVP 9C ', offshore_asset_well_feed_model.getUnit("Offshore Area C Feed").getTVP(9.0, "C", "bara"), 'bara')
print('Condensate Area RVP 37.8C ', offshore_asset_well_feed_model.getUnit("Offshore Area C Feed").getRVP(37.8, "C", "bara"), 'bara')
print('Condensate Area RVP 37.8C (RVP_ASTM_D6377)', offshore_asset_well_feed_model.getUnit("Offshore Area C Feed").getRVP(37.8, "C", "bara", "RVP_ASTM_D6377"), 'bara')
print('Condensate Area RVP 37.8C (RVP_ASTM_D323_82) ', offshore_asset_well_feed_model.getUnit("Offshore Area C Feed").getRVP(37.8, "C", "bara", "RVP_ASTM_D323_82"), 'bara')
print('Condensate Area RVP 37.8C (RVP_ASTM_D323_82) ', offshore_asset_well_feed_model.getUnit("Offshore Area C Feed").getRVP(37.8, "C", "psia", "RVP_ASTM_D323_82"), 'psia')
print('ThirdPartyFeed TVP 30C ', offshore_asset_well_feed_model.getUnit("ThirdPartyFeed Feed").getTVP(30.0, "C", "bara"), 'bara')
print('ThirdPartyFeed TVP 9C ', offshore_asset_well_feed_model.getUnit("ThirdPartyFeed Feed").getTVP(9.0, "C", "bara"), 'bara')
print('ThirdPartyFeed RVP 37.8C (VPCR4) ', offshore_asset_well_feed_model.getUnit("ThirdPartyFeed Feed").getRVP(37.8, "C", "bara", "VPCR4"), 'bara')
print('ThirdPartyFeed RVP 37.8C (VPCR4_no_water) ', offshore_asset_well_feed_model.getUnit("ThirdPartyFeed Feed").getRVP(37.8, "C", "bara", "VPCR4_no_water"), 'bara')
print('ThirdPartyFeed RVP 37.8C (RVP_ASTM_D6377)', offshore_asset_well_feed_model.getUnit("ThirdPartyFeed Feed").getRVP(37.8, "C", "bara", "RVP_ASTM_D6377"), 'bara')
print('ThirdPartyFeed RVP 37.8C (RVP_ASTM_D323_82) ', offshore_asset_well_feed_model.getUnit("ThirdPartyFeed Feed").getRVP(37.8, "C", "bara", "RVP_ASTM_D323_82"), 'bara')
print('ThirdPartyFeed RVP 37.8C (RVP_ASTM_D323_82) ', offshore_asset_well_feed_model.getUnit("ThirdPartyFeed Feed").getRVP(37.8, "C", "psia", "RVP_ASTM_D323_82"), 'psia')
print('ThirdPartyFeed rate ', offshore_asset_well_feed_model.getUnit("ThirdPartyFeed Feed").getFlowRate("idSm3/hr"), ' Sm3/hr')
print('Condensate Area rate ', offshore_asset_well_feed_model.getUnit("Offshore Area C Feed").getFlowRate("idSm3/hr"), ' Sm3/hr')
print('flow FISCAL_AREA ', separation_process_train_A.getUnit(
"NGL mixer").getOutletStream().getFlowRate("idSm3/hr"), ' Sm3/hr')
print('density oil FISCAL_AREA ', separation_process_train_A.getUnit(
"NGL mixer").getOutletStream().getFluid().getDensity("kg/m3"), ' kg/m3')
print('temperature oil FISCAL_AREA ', separation_process_train_A.getUnit(
"NGL mixer").getOutletStream().getTemperature('C'), ' C')
print('oil rate from 4th stage train B ', separation_process_train_B.getUnit(
"4th stage separator").getOilOutStream().getFlowRate("idSm3/hr"), ' Sm3/hr')
print('TVP FISCAL_AREA oil (30C) ', separation_process_train_A.getUnit(
"NGL mixer").getOutletStream().TVP(30.0, "C"), ' bara')
print('TVP FISCAL_AREA oil (9C) ', separation_process_train_A.getUnit(
"NGL mixer").getOutletStream().TVP(9.0, "C"), ' bara')
print('RVP FISCAL_AREA oil (37.8C) ', separation_process_train_A.getUnit(
"NGL mixer").getOutletStream().getRVP(37.8, "C", "bara"), ' bara')
print('RVP FISCAL_AREA oil (37.8C) (RVP_ASTM_D6377)', separation_process_train_A.getUnit(
"NGL mixer").getOutletStream().getRVP(37.8, "C", "bara", "RVP_ASTM_D6377"), 'bara')
print('TVP NGL column oil (30C) ', column_model.getUnit(
"NGL column").getLiquidOutStream().TVP(30.0, "C"), ' bara')
print('TVP NGL column (9C) ', column_model.getUnit(
"NGL column").getLiquidOutStream().TVP(9.0, "C"), ' bara')
print('RVP NGL column (37.8C) ', column_model.getUnit(
"NGL column").getLiquidOutStream().getRVP(37.8, "C", "bara"), ' bara')
print('RVP NGL column (37.8C) (RVP_ASTM_D6377)', column_model.getUnit(
"NGL column").getLiquidOutStream().getRVP(37.8, "C", "bara", "RVP_ASTM_D6377"), 'bara')
print('NGL flow ', column_model.getUnit(
"NGL column").getLiquidOutStream().getFlowRate("idSm3/hr"), ' Sm3/hr')
print('NGL density ', column_model.getUnit(
"NGL column").getLiquidOutStream().getFluid().getDensity("kg/m3"), ' kg/m3')
print('oil rate from 4th stage train A ', separation_process_train_A.getUnit(
"4th stage separator").getOilOutStream().getFlowRate("idSm3/hr"), ' Sm3/hr')
print('oil rate from 4th stage train B ', separation_process_train_B.getUnit(
"4th stage separator").getOilOutStream().getFlowRate("idSm3/hr"), ' Sm3/hr')
print('TVP 4th stage (30C) ', separation_process_train_A.getUnit(
"4th stage separator").getOilOutStream().TVP(30.0, "C"), ' bara')
print('TVP 4th stage (9C) ', separation_process_train_A.getUnit(
"4th stage separator").getOilOutStream().TVP(9.0, "C"), ' bara')
print('RVP 4th stage (37.8C) ', separation_process_train_A.getUnit(
"4th stage separator").getOilOutStream().getRVP(37.8, "C", "bara"), ' bara')
print('RVP 4th stage (37.8C) (RVP_ASTM_D6377)', separation_process_train_A.getUnit(
"4th stage separator").getOilOutStream().getRVP(37.8, "C", "bara", "RVP_ASTM_D6377"), 'bara')
print('oill density 4th stage ', separation_process_train_A.getUnit(
"4th stage separator").getOilOutStream().getFluid().getDensity('kg/m3'), ' kg/m3')
print('TVP FISCAL_AREA export oil (9C) ', export_model.getUnit(
"stable oil cooler").getOutletStream().TVP(9.0, "C"), ' bara')
print('TVP FISCAL_AREA export oil (30C) ', export_model.getUnit(
"stable oil cooler").getOutletStream().TVP(30.0, "C"), ' bara')
print('RVP FISCAL_AREA export oil (37.8C) (RVP_ASTM_D6377)', export_model.getUnit(
"stable oil cooler").getOutletStream().getRVP(37.8, "C", "bara", "RVP_ASTM_D6377"), 'bara')
print('TVP export oil (30C) ', export_model.getUnit(
"oil pump").getOutletStream().TVP(30.0, "C"), ' bara')
print('TVP export oil (9C) ', export_model.getUnit(
"oil pump").getOutletStream().TVP(9.0, "C"), ' bara')
print('RVP export oil (37.8C) ', export_model.getUnit(
"oil pump").getOutletStream().getRVP(37.8, "C", "bara"), ' bara')
print('RVP export oil (37.8C) (RVP_ASTM_D6377)', export_model.getUnit(
"oil pump").getOutletStream().getRVP(37.8, "C", "bara", "RVP_ASTM_D6377"), 'bara')
print('total oil flow ', export_model.getUnit(
"oil pump").getOutletStream().getFluid().getFlowRate("m3/hr"), ' m3/hr')
print('total oil flow ', export_model.getUnit(
"oil pump").getOutletStream().getFluid().getFlowRate("idSm3/hr"), ' idSm3/hr')
print('wt frac ethane in oil mix to TerminalAsset ', export_model.getUnit(
"oil pump").getOutletStream().getFluid().getPhase('oil').getWtFrac('ethane'))
print('wt frac ethane in FISCAL_AREA oil ', export_model.getUnit(
"stable oil cooler").getOutletStream().getFluid().getPhase('oil').getWtFrac('ethane'))
print('wt frac propane in oil mix to TerminalAsset ', export_model.getUnit(
"oil pump").getOutletStream().getFluid().getPhase('oil').getWtFrac('propane'))
print('wt frac propane in FISCAL_AREA oil ', export_model.getUnit(
"stable oil cooler").getOutletStream().getFluid().getPhase('oil').getWtFrac('propane'))
print()
except:
print('error in printing results')
Code cell 32: 11.2 Heaters and coolers
Notebook cell 62. This code belongs to the 11.2 Heaters and coolers section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid- characterization files, and case-specific result folders.
print('oil heater from first stage train A ', separation_process_train_A.getUnit(
"oil heater second stage").getDuty()/1e6, ' MW')
print('oil heater from first stage train B ', separation_process_train_B.getUnit(
"oil heater second stage").getDuty()/1e6, ' MW')
print('satellite heater ', offshore_asset_well_feed_model.getUnit(
"satellite heater").getDuty()/1e6, ' MW')
print('total oil flow ', export_model.getUnit(
"oil pump").getOutletStream().getFluid().getFlowRate("m3/hr"))
print('TVP export oil (30C) ', export_model.getUnit(
"oil pump").getOutletStream().TVP(30.0, "C"), ' bara')
print('RVP export oil (37.8C) ', export_model.getUnit(
"oil pump").getOutletStream().getRVP(37.8, "C", "bara"), ' bara')
print('TVP export oil (9C) ', export_model.getUnit(
"oil pump").getOutletStream().TVP(9.0, "C"), ' bara')
# exp_compressor_1.getUnit("EXPORT_COMPRESSOR").getOutletStream().getFluid().prettyPrint()
print('ethane ', export_model.getUnit("oil pump").getOutletStream().getFluid().getComponent('ethane').getNumberOfMolesInPhase() *
3600.0*separation_process_train_A.getUnit("NGL mixer").getOutletStream().getFluid().getComponent('ethane').getMolarMass(), ' kg/hr')
# print('oil heater 1 ', separation_process_train_B.getUnit("oil heater second stage").getOutletStream().getFlowRate("idSm3/hr"), ' Sm3/hr')
Code cell 33: 11.3 Compressors
Notebook cell 64. This code belongs to the 11.3 Compressors section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid- characterization files, and case-specific result folders.
print('year ', input_parameters.Year)
# 1st stage
separation_process_train_A.getUnit("1st stage compressor").run()
print('1st stage compressor ', separation_process_train_A.getUnit(
"1st stage compressor").getInletStream().getPressure('bara'), ' bar')
print('1st stage compressor ', separation_process_train_A.getUnit(
"1st stage compressor").getOutletStream().getPressure('bara'), ' bar')
print('1st stage compressor ', separation_process_train_A.getUnit(
"1st stage compressor").getPower()/1e6, ' MW')
print('1st stage compressor ', separation_process_train_A.getUnit(
"1st stage compressor").getInletStream().getFlowRate("m3/hr"), ' m3/hr')
print('1st stage compressor ', separation_process_train_A.getUnit(
"1st stage compressor").getPolytropicFluidHead(), ' kJ/kg')
print('B train')
separation_process_train_B.getUnit("1st stage compressor").run()
print('1st stage compressor ', separation_process_train_B.getUnit(
"1st stage compressor").getInletStream().getPressure('bara'), ' bar')
print('1st stage compressor ', separation_process_train_B.getUnit(
"1st stage compressor").getOutletStream().getPressure('bara'), ' bar')
print('1st stage compressor ', separation_process_train_B.getUnit(
"1st stage compressor").getPower()/1e6, ' MW')
print('1st stage compressor ', separation_process_train_B.getUnit(
"1st stage compressor").getInletStream().getFlowRate("m3/hr"), ' m3/hr')
print('1st stage compressor ', separation_process_train_B.getUnit(
"1st stage compressor").getPolytropicFluidHead(), ' kJ/kg')
# 2nd stage
print('2nd stage compressor ', separation_process_train_A.getUnit(
"2nd stage compressor").getInletStream().getPressure('bara'), ' bar')
print('2nd stage compressor ', separation_process_train_A.getUnit(
"2nd stage compressor").getOutletStream().getPressure('bara'), ' bar')
print('2nd stage compressor ', separation_process_train_A.getUnit(
"2nd stage compressor").getPower()/1e6, ' MW')
print('2nd stage compressor ', separation_process_train_A.getUnit(
"2nd stage compressor").getInletStream().getFlowRate("m3/hr"), ' m3/hr')
print('2nd stage compressor ', separation_process_train_A.getUnit(
"2nd stage compressor").getPolytropicFluidHead(), ' kJ/kg')
# 3rd stage
print('3rd stage compressor ', separation_process_train_A.getUnit(
"3rd stage compressor").getInletStream().getPressure('bara'), ' bar')
print('3rd stage compressor ', separation_process_train_A.getUnit(
"3rd stage compressor").getOutletStream().getPressure('bara'), ' bar')
print('3rd stage compressor ', separation_process_train_A.getUnit(
"3rd stage compressor").getPower()/1e6, ' MW')
print('3rd stage compressor ', separation_process_train_A.getUnit(
"3rd stage compressor").getInletStream().getFlowRate("m3/hr"), ' m3/hr')
print('3rd stage compressor ', separation_process_train_A.getUnit(
"3rd stage compressor").getInletStream().getFlowRate("MSm3/day"), ' MSm3/day')
print('3rd stage compressor ', separation_process_train_A.getUnit(
"3rd stage compressor").getPolytropicFluidHead(), ' kJ/kg')
# new comp stage
if (input_parameters.Year >= input_parameters.year_pre_compression):
print('new pre comp stage compressor ', separation_process_train_A.getUnit(
"new compressor").getPower()/1e6, ' MW')
print('new pre comp compressor ', separation_process_train_A.getUnit(
"new compressor").getInletStream().getFlowRate("m3/hr"), ' m3/hr')
print('new pre comp compressor ', separation_process_train_A.getUnit(
"new compressor").getInletStream().getFlowRate("MSm3/day"), ' MSm3/day')
print('new pre comp compressor ', separation_process_train_A.getUnit(
"new compressor").getPolytropicFluidHead(), ' kJ/kg')
print('EXPORT_BOOSTER_B ', tex_process_1.getUnit(
"comp_export_booster_b").getInletStream().getPressure("bara"), ' bara')
print('EXPORT_BOOSTER_B ', tex_process_1.getUnit(
"comp_export_booster_b").getOutletStream().getPressure("bara"), ' bara')
print('EXPORT_BOOSTER_B ', tex_process_1.getUnit(
"comp_export_booster_b").getPower('MW'), ' MW')
print('EXPORT_BOOSTER_B ', tex_process_1.getUnit(
"comp_export_booster_b").getOutletStream().getFlowRate("MSm3/day"), ' MSm3/day')
print('EXPORT_BOOSTER_B ', tex_process_1.getUnit(
"comp_export_booster_b").getOutletStream().getFlowRate("m3/hr"), ' m3/hr')
print('EXPORT_BOOSTER_B ', tex_process_1.getUnit(
"comp_export_booster_b").getPolytropicFluidHead(), ' kJ/kg')
print('EXPORT_BOOSTER_B ', tex_process_1.getUnit("comp_export_booster_b").getSpeed(), ' rpm')
print('EXPORT_COMPRESSOR ', exp_compressor_1.getUnit(
"EXPORT_COMPRESSOR").getInletStream().getPressure("bara"), ' bara')
print('EXPORT_COMPRESSOR ', exp_compressor_1.getUnit(
"EXPORT_COMPRESSOR").getOutletStream().getPressure("bara"), ' bara')
print('EXPORT_COMPRESSOR ', exp_compressor_1.getUnit("EXPORT_COMPRESSOR").getPower('MW'), ' MW')
print('EXPORT_COMPRESSOR ', exp_compressor_1.getUnit(
"EXPORT_COMPRESSOR").getOutletStream().getFlowRate("MSm3/day"), ' MSm3/day')
print('EXPORT_COMPRESSOR ', exp_compressor_1.getUnit(
"EXPORT_COMPRESSOR").getOutletStream().getFlowRate("m3/hr"), ' m3/hr')
print('EXPORT_COMPRESSOR ', exp_compressor_1.getUnit(
"EXPORT_COMPRESSOR").getPolytropicFluidHead(), ' kJ/kg')
print('EXPORT_COMPRESSOR ', exp_compressor_1.getUnit("EXPORT_COMPRESSOR").getSpeed(), ' rpm')
print('EXPORT_COMPRESSOR ', exp_compressor_1.getUnit(
"EXPORT_COMPRESSOR").getInletStream().getPressure("bara"), ' bara')
print('EXPORT_COMPRESSOR ', exp_compressor_1.getUnit(
"EXPORT_COMPRESSOR").getOutletStream().getPressure("bara"), ' bara')
print('EXPORT_COMPRESSOR ', exp_compressor_1.getUnit("EXPORT_COMPRESSOR").getPower('MW'), ' MW')
print('EXPORT_COMPRESSOR ', exp_compressor_1.getUnit(
"EXPORT_COMPRESSOR").getOutletStream().getFlowRate("MSm3/day"), ' MSm3/day')
print('EXPORT_COMPRESSOR ', exp_compressor_1.getUnit(
"EXPORT_COMPRESSOR").getOutletStream().getFlowRate("m3/hr"), ' m3/hr')
print('EXPORT_COMPRESSOR ', exp_compressor_1.getUnit(
"EXPORT_COMPRESSOR").getPolytropicFluidHead(), ' kJ/kg')
print('EXPORT_COMPRESSOR ', exp_compressor_1.getUnit("EXPORT_COMPRESSOR").getSpeed(), ' rpm')
print('injection compressor INJECTION_HUB ', exp_gas_process.getUnit(
"injection compressor INJECTION_HUB").getInletStream().getPressure("bara"), ' bara')
print('injection compressor INJECTION_HUB ', exp_gas_process.getUnit(
"injection compressor INJECTION_HUB").getOutletStream().getPressure("bara"), ' bara')
print('injection compressor INJECTION_HUB ', exp_gas_process.getUnit("injection compressor INJECTION_HUB").getPower('MW'), ' MW')
print('injection compressor INJECTION_HUB ', exp_gas_process.getUnit(
"injection compressor INJECTION_HUB").getOutletStream().getFlowRate("MSm3/day"), ' MSm3/day')
print('injection compressor INJECTION_HUB ', exp_gas_process.getUnit(
"injection compressor INJECTION_HUB").getOutletStream().getFlowRate("m3/hr"), ' m3/hr')
print('injection compressor INJECTION_HUB ', exp_gas_process.getUnit(
"injection compressor INJECTION_HUB").getPolytropicFluidHead(), ' kJ/kg')
print('injection compressor INJECTION_HUB ', exp_gas_process.getUnit("injection compressor INJECTION_HUB").getSpeed(), ' rpm')
print('HT injection process A ', ht_injection_process_A.getUnit(
"ht 1st stage compressor").getInletStream().getPressure("bara"), ' bara')
print('HT injection process A ', ht_injection_process_A.getUnit(
"ht 1st stage compressor").getOutletStream().getPressure("bara"), ' bara')
print('HT injection process A ', ht_injection_process_A.getUnit("ht 1st stage compressor").getPower('MW'), ' MW')
print('HT injection process A ', ht_injection_process_A.getUnit(
"ht 1st stage compressor").getOutletStream().getFlowRate("MSm3/day"), ' MSm3/day')
print('HT injection process A ', ht_injection_process_A.getUnit(
"ht 1st stage compressor").getOutletStream().getFlowRate("m3/hr"), ' m3/hr')
print('HT injection process A', ht_injection_process_A.getUnit(
"ht 1st stage compressor").getPolytropicFluidHead(), ' kJ/kg')
print('HT injection process A ', ht_injection_process_A.getUnit("ht 1st stage compressor").getSpeed(), ' rpm')
print('HT injection process B ', ht_injection_process_B.getUnit(
"ht 1st stage compressor").getInletStream().getPressure("bara"), ' bara')
print('HT injection process B ', ht_injection_process_B.getUnit(
"ht 1st stage compressor").getOutletStream().getPressure("bara"), ' bara')
print('HT injection process B ', ht_injection_process_B.getUnit("ht 1st stage compressor").getPower('MW'), ' MW')
print('HT injection process B ', ht_injection_process_B.getUnit(
"ht 1st stage compressor").getOutletStream().getFlowRate("MSm3/day"), ' MSm3/day')
print('HT injection process B ', ht_injection_process_B.getUnit(
"ht 1st stage compressor").getOutletStream().getFlowRate("m3/hr"), ' m3/hr')
print('HT injection process B', ht_injection_process_B.getUnit(
"ht 1st stage compressor").getPolytropicFluidHead(), ' kJ/kg')
print('HT injection process B ', ht_injection_process_B.getUnit("ht 1st stage compressor").getSpeed(), ' rpm')
Code cell 34: Mass balance check
Notebook cell 66. This code belongs to the Mass balance check section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid- characterization files, and case-specific result folders.
# Corrected feedflow calculation
feedflow = (
offshore_asset_well_feed_model.getUnit('gas lift feed').getFlowRate('kg/hr') +
offshore_asset_well_feed_model.getUnit('main reference_stream_b hc feed').getFlowRate('kg/hr') +
offshore_asset_well_feed_model.getUnit('cluster_b hc feed').getFlowRate('kg/hr') +
offshore_asset_well_feed_model.getUnit('cluster_a hc feed').getFlowRate('kg/hr') +
offshore_asset_well_feed_model.getUnit('Offshore Area East Feed').getFlowRate('kg/hr') +
offshore_asset_well_feed_model.getUnit('calibration_stream hc feed').getFlowRate('kg/hr') +
offshore_asset_well_feed_model.getUnit('Offshore Area C Feed').getFlowRate('kg/hr') +
offshore_asset_well_feed_model.getUnit('reference_stream_c reference_stream_b hc flow').getFlowRate('kg/hr') +
offshore_asset_well_feed_model.getUnit('ThirdPartyFeed Feed').getFlowRate('kg/hr') +
offshore_asset_well_feed_model.getUnit('Offshore Area South Feed').getFlowRate('kg/hr') +
separation_process_train_A.getUnit('flare gas').getFlowRate('kg/hr') +
separation_process_train_B.getUnit('flare gas').getFlowRate('kg/hr')
)
if input_parameters.simulation_case == 'high' or input_parameters.Year <= 100:
feedflow += (
offshore_asset_well_feed_model.getUnit('cluster_d hc flow').getFlowRate('kg/hr') +
offshore_asset_well_feed_model.getUnit('cluster_c hc flow').getFlowRate('kg/hr') +
offshore_asset_well_feed_model.getUnit('reference_stream_creference_stream_a hc flow').getFlowRate('kg/hr')
)
print('Feed flow:', feedflow)
# Corrected outletflow calculation
outletflow = (
exp_gas_process.getUnit("export gas stream").getFlowRate("kg/hr") +
exp_gas_process.getUnit("gas lift stream").getFlowRate("kg/hr") +
exp_gas_process.getUnit("gas injection stream").getFlowRate("kg/hr") +
tex_process_1.getUnit("fuel gas splitter").getSplitStream(1).getFlowRate("kg/hr") +
tex_process_2.getUnit("fuel gas splitter").getSplitStream(1).getFlowRate("kg/hr") +
reference_stream_c_sep_process.getUnit("CALIB_STAGE gas_injection stream").getFlowRate("kg/hr") +
export_model.getUnit('oil pump').getOutletStream().getFlowRate('kg/hr') +
column_model.getUnit('lpg injection stream').getFlowRate('kg/hr')
)
print('Outlet flow:', outletflow)
# Mass balance calculation
mass_balance = (feedflow - outletflow) / feedflow * 100
print('Mass balance error:', mass_balance, '%')
Code cell 35: 11.4 Write results to file
Notebook cell 69. This code belongs to the 11.4 Write results to file section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid- characterization files, and case-specific result folders.
def write_simulation_results(input_parameters,
tex_process_1,
exp_compressor_1,
exp_compressor_2,
separation_process_train_A,
separation_process_train_B, exp_gas_process):
"""
Writes all simulation results to a file named "results_<year>.txt".
"""
case = input_parameters.simulation_case
file_name = f"{input_parameters.results_path}/results_{input_parameters.Year}_{case}.txt"
with open(file_name, "w") as f:
print('year ', input_parameters.Year, file=f)
print('mass_balance ', mass_balance, ' %', file=f)
print('feed gas to DPC A+B ', tex_process_1.getUnit("dew point cooler").getInletStream().getFlowRate('MSm3/day') +
tex_process_2.getUnit("dew point cooler").getInletStream().getFlowRate('MSm3/day'), ' MSm3/day', file=f)
print('export gas ', exp_gas_process.getUnit("export gas stream").getFlowRate('MSm3/day') , ' MSm3/day', file=f)
print('INJECTION_HUB injection gas ', exp_gas_process.getUnit("gas injection stream").getFlowRate('MSm3/day') , ' MSm3/day', file=f)
print('INJECTION_HUB lift gas ', exp_gas_process.getUnit("gas lift stream").getFlowRate('MSm3/day') , ' MSm3/day', file=f)
print('tex in ', tex_process_1.getUnit(
"TEX").getInletStream().getTemperature('C'), ' C', file=f)
print('tex out ', tex_process_1.getUnit(
"TEX").getOutletStream().getTemperature('C'), ' C', file=f)
print('tex in ', tex_process_1.getUnit(
"TEX").getInletStream().getPressure('bara'), ' bara', file=f)
print('tex out ', tex_process_1.getUnit(
"TEX").getOutletStream().getPressure('bara'), ' bara', file=f)
print('expander energy ', tex_process_1.getUnit(
"TEX").getPower('MW'), ' MW', file=f)
print('expanderpolytropic efficiency ',
input_parameters.expander_efficiency, file=f)
print('expander compressor energy ', tex_process_1.getUnit(
"comp_export_booster_b").getPower('MW'), ' MW', file=f)
print('tex in hot stream ', tex_process_1.getUnit(
"HX_MULTI_STREAM").getInStream(0).getTemperature('C'), ' C', file=f)
print('tex in cold stream ', tex_process_1.getUnit(
"HX_MULTI_STREAM").getInStream(1).getTemperature('C'), ' C', file=f)
print('tex in cold stream ', tex_process_1.getUnit(
"HX_MULTI_STREAM").getInStream(2).getTemperature('C'), ' C', file=f)
print('tex out hot stram out ', tex_process_1.getUnit(
"HX_MULTI_STREAM").getOutStream(0).getTemperature('C'), ' C', file=f)
print('tex out cold stream 2 ', tex_process_1.getUnit(
"HX_MULTI_STREAM").getOutStream(1).getTemperature('C'), ' C', file=f)
print('tex out cold stream 3 ', tex_process_1.getUnit(
"HX_MULTI_STREAM").getOutStream(2).getTemperature('C'), ' C', file=f)
print('gas export pressure ', exp_compressor_1.getUnit(
"EXPORT_COMPRESSOR").getOutletStream().getPressure("bara"), ' bara', file=f)
print('gas export 1 ', exp_compressor_1.getUnit(
"compressor EXPORT_COMPRESSOR out stream").getFlowRate("MSm3/day"), ' MSm3/day', file=f)
print('gas export 2 ', exp_compressor_2.getUnit(
"compressor EXPORT_COMPRESSOR out stream").getFlowRate("MSm3/day"), ' MSm3/day', file=f)
print('oil temperature 4th stage train A ', separation_process_train_B.getUnit(
"4th stage separator").getOilOutStream().getTemperature("C"), ' C', file=f)
print('oil temperature 4th stage train B ', separation_process_train_B.getUnit(
"4th stage separator").getOilOutStream().getTemperature("C"), ' C', file=f)
print('oil rate 4th and NGL Train A ', separation_process_train_A.getUnit(
"NGL mixer").getOutletStream().getFlowRate("idSm3/hr"), ' Sm3/hr', file=f)
print('oil rate 4th and NGL Train B ', separation_process_train_B.getUnit(
"NGL mixer").getOutletStream().getFlowRate("idSm3/hr"), ' Sm3/hr', file=f)
print('oil temperature 1st stage ', separation_process_train_B.getUnit(
"1st stage separator").getOilOutStream().getTemperature("C"), ' C', file=f)
print('oil temperature 2nd stage ', separation_process_train_B.getUnit(
"2nd stage separator").getLiquidOutStream().getTemperature("C"), ' C', file=f)
print('water from 2nd stage ', separation_process_train_B.getUnit(
"2nd stage separator").getWaterOutStream().getFlowRate("Sm3/day"), ' Sm3/day', file=f)
print('water from 4th stage ', separation_process_train_B.getUnit(
"4th stage separator").getWaterOutStream().getFlowRate("Sm3/day"), ' Sm3/day', file=f)
print('oil from 4th stage ', separation_process_train_B.getUnit(
"4th stage separator").getOilOutStream().getFlowRate("idSm3/hr"), ' Sm3/hr', file=f)
print('WI ', exp_compressor_1.getUnit("compressor EXPORT_COMPRESSOR out stream").getWI(
"volume", 15.0, 25.0)/1e6, ' MJ/m3', file=f)
print('TVP (30C) ', separation_process_train_A.getUnit(
"NGL mixer").getOutletStream().TVP(30.0, "C"), ' bara', file=f)
print('RVP (37.8C) ', separation_process_train_A.getUnit(
"NGL mixer").getOutletStream().getRVP(37.8, "C", "bara"), ' bara', file=f)
print('TVP (9C) ', separation_process_train_A.getUnit(
"NGL mixer").getOutletStream().TVP(9.0, "C"), ' bara', file=f)
print('compressor power ', exp_compressor_1.getUnit(
'EXPORT_COMPRESSOR').getPower()/1e6, ' MW', file=f)
print('compressor power ', exp_compressor_1.getUnit(
'EXPORT_COMPRESSOR').getPower()/1e6, ' MW', file=f)
print('ethane ', separation_process_train_A.getUnit("NGL mixer").getOutletStream().getFluid().getComponent('ethane').getNumberOfMolesInPhase(
)*3600.0*separation_process_train_A.getUnit("NGL mixer").getOutletStream().getFluid().getComponent('ethane').getMolarMass(), ' kg/hr', file=f)
print('oil heater from first stage train A ', separation_process_train_A.getUnit(
"oil heater second stage").getDuty()/1e6, ' MW', file=f)
print('oil heater from first stage train B ', separation_process_train_B.getUnit(
"oil heater second stage").getDuty()/1e6, ' MW', file=f)
print('satellite heater ', offshore_asset_well_feed_model.getUnit(
"satellite heater").getDuty()/1e6, ' MW', file=f)
print('year ', input_parameters.Year, file=f)
# 1st stage
separation_process_train_A.getUnit("1st stage compressor").run()
print('1st stage compressor ', separation_process_train_A.getUnit(
"1st stage compressor").getInletStream().getPressure('bara'), ' bar', file=f)
print('1st stage compressor ', separation_process_train_A.getUnit(
"1st stage compressor").getOutletStream().getPressure('bara'), ' bar', file=f)
print('1st stage compressor ', separation_process_train_A.getUnit(
"1st stage compressor").getPower()/1e6, ' MW', file=f)
print('1st stage compressor ', separation_process_train_A.getUnit(
"1st stage compressor").getInletStream().getFlowRate("m3/hr"), ' m3/hr', file=f)
print('1st stage compressor ', separation_process_train_A.getUnit(
"1st stage compressor").getPolytropicFluidHead(), ' kJ/kg', file=f)
# 2nd stage
print('2nd stage compressor ', separation_process_train_A.getUnit(
"2nd stage compressor").getInletStream().getPressure('bara'), ' bar', file=f)
print('2nd stage compressor ', separation_process_train_A.getUnit(
"2nd stage compressor").getOutletStream().getPressure('bara'), ' bar', file=f)
print('2nd stage compressor ', separation_process_train_A.getUnit(
"2nd stage compressor").getPower()/1e6, ' MW', file=f)
print('2nd stage compressor ', separation_process_train_A.getUnit(
"2nd stage compressor").getInletStream().getFlowRate("m3/hr"), ' m3/hr', file=f)
print('2nd stage compressor ', separation_process_train_A.getUnit(
"2nd stage compressor").getPolytropicFluidHead(), ' kJ/kg', file=f)
# 3rd stage
print('3rd stage compressor ', separation_process_train_A.getUnit(
"3rd stage compressor").getInletStream().getPressure('bara'), ' bar', file=f)
print('3rd stage compressor ', separation_process_train_A.getUnit(
"3rd stage compressor").getOutletStream().getPressure('bara'), ' bar', file=f)
print('3rd stage compressor ', separation_process_train_A.getUnit(
"3rd stage compressor").getPower()/1e6, ' MW', file=f)
print('3rd stage compressor ', separation_process_train_A.getUnit(
"3rd stage compressor").getInletStream().getFlowRate("m3/hr"), ' m3/hr', file=f)
print('3rd stage compressor ', separation_process_train_A.getUnit(
"3rd stage compressor").getInletStream().getFlowRate("MSm3/day"), ' MSm3/day', file=f)
print('3rd stage compressor ', separation_process_train_A.getUnit(
"3rd stage compressor").getPolytropicFluidHead(), ' kJ/kg', file=f)
# new comp stage
if input_parameters.Year >= input_parameters.year_pre_compression:
print('new pre comp stage compressor ', separation_process_train_A.getUnit(
"new compressor").getPower()/1e6, ' MW', file=f)
print('new pre comp compressor ', separation_process_train_A.getUnit(
"new compressor").getInletStream().getFlowRate("m3/hr"), ' m3/hr', file=f)
print('new pre comp compressor ', separation_process_train_A.getUnit(
"new compressor").getInletStream().getFlowRate("MSm3/day"), ' MSm3/day', file=f)
print('new pre comp compressor ', separation_process_train_A.getUnit(
"new compressor").getPolytropicFluidHead(), ' kJ/kg', file=f)
print('EXPORT_BOOSTER_B ', tex_process_1.getUnit(
"comp_export_booster_b").getInletStream().getPressure("bara"), ' bara', file=f)
print('EXPORT_BOOSTER_B ', tex_process_1.getUnit(
"comp_export_booster_b").getOutletStream().getPressure("bara"), ' bara', file=f)
print('EXPORT_BOOSTER_B ', tex_process_1.getUnit(
"comp_export_booster_b").getPower('MW'), ' MW', file=f)
print('EXPORT_BOOSTER_B ', tex_process_1.getUnit(
"comp_export_booster_b").getOutletStream().getFlowRate("MSm3/day"), ' MSm3/day', file=f)
print('EXPORT_BOOSTER_B ', tex_process_1.getUnit(
"comp_export_booster_b").getOutletStream().getFlowRate("m3/hr"), ' m3/hr', file=f)
print('EXPORT_BOOSTER_B ', tex_process_1.getUnit(
"comp_export_booster_b").getPolytropicFluidHead(), ' kJ/kg', file=f)
print('EXPORT_BOOSTER_B ', tex_process_1.getUnit(
"comp_export_booster_b").getSpeed(), ' rpm', file=f)
print('EXPORT_COMPRESSOR ', exp_compressor_1.getUnit(
"EXPORT_COMPRESSOR").getInletStream().getPressure("bara"), ' bara', file=f)
print('EXPORT_COMPRESSOR ', exp_compressor_1.getUnit(
"EXPORT_COMPRESSOR").getOutletStream().getPressure("bara"), ' bara', file=f)
print('EXPORT_COMPRESSOR ', exp_compressor_1.getUnit(
"EXPORT_COMPRESSOR").getPower('MW'), ' MW', file=f)
print('EXPORT_COMPRESSOR ', exp_compressor_1.getUnit(
"EXPORT_COMPRESSOR").getOutletStream().getFlowRate("MSm3/day"), ' MSm3/day', file=f)
print('EXPORT_COMPRESSOR ', exp_compressor_1.getUnit(
"EXPORT_COMPRESSOR").getOutletStream().getFlowRate("m3/hr"), ' m3/hr', file=f)
print('EXPORT_COMPRESSOR ', exp_compressor_1.getUnit(
"EXPORT_COMPRESSOR").getPolytropicFluidHead(), ' kJ/kg', file=f)
print('EXPORT_COMPRESSOR ', exp_compressor_1.getUnit(
"EXPORT_COMPRESSOR").getSpeed(), ' rpm', file=f)
print('EXPORT_COMPRESSOR ', exp_compressor_1.getUnit(
"EXPORT_COMPRESSOR").getInletStream().getPressure("bara"), ' bara', file=f)
print('EXPORT_COMPRESSOR ', exp_compressor_1.getUnit(
"EXPORT_COMPRESSOR").getOutletStream().getPressure("bara"), ' bara', file=f)
print('EXPORT_COMPRESSOR ', exp_compressor_1.getUnit(
"EXPORT_COMPRESSOR").getPower('MW'), ' MW', file=f)
print('EXPORT_COMPRESSOR ', exp_compressor_1.getUnit(
"EXPORT_COMPRESSOR").getOutletStream().getFlowRate("MSm3/day"), ' MSm3/day', file=f)
print('EXPORT_COMPRESSOR ', exp_compressor_1.getUnit(
"EXPORT_COMPRESSOR").getOutletStream().getFlowRate("m3/hr"), ' m3/hr', file=f)
print('EXPORT_COMPRESSOR ', exp_compressor_1.getUnit(
"EXPORT_COMPRESSOR").getPolytropicFluidHead(), ' kJ/kg', file=f)
print('EXPORT_COMPRESSOR ', exp_compressor_1.getUnit(
"EXPORT_COMPRESSOR").getSpeed(), ' rpm', file=f)
# Include component names and molar compositions
print('export gas composition ', file=f)
component_names = exp_compressor_1.getUnit(
"EXPORT_COMPRESSOR").getOutletStream().getFluid().getComponentNames()
molar_compositions = exp_compressor_1.getUnit(
"EXPORT_COMPRESSOR").getOutletStream().getFluid().getMolarComposition()
print('Component Names: ', component_names, file=f)
print('Molar Compositions: ', molar_compositions, file=f)
print('oil rate from 4th stage train A ', separation_process_train_A.getUnit(
"4th stage separator").getOilOutStream().getFlowRate("idSm3/hr"), ' Sm3/hr', file=f)
print('oil rate from 4th stage train B ', separation_process_train_B.getUnit(
"4th stage separator").getOilOutStream().getFlowRate("idSm3/hr"), ' Sm3/hr', file=f)
component_names = separation_process_train_A.getUnit(
"4th stage separator").getOilOutStream().getFluid().getComponentNames()
molar_compositions = separation_process_train_A.getUnit(
"4th stage separator").getOilOutStream().getFluid().getMolarComposition()
print('Oil 4th stage component Names: ', component_names, file=f)
print('Oil 4th stage Molar Compositions: ', molar_compositions, file=f)
print('ngl column liquid flow ', column_model.getUnit(
"NGL column").getLiquidOutStream().getFlowRate("m3/hr"), ' m3/hr', file=f)
component_names = column_model.getUnit(
"NGL column").getLiquidOutStream().getFluid().getComponentNames()
molar_compositions = column_model.getUnit(
"NGL column").getLiquidOutStream().getFluid().getMolarComposition()
print('Oil Component Names: ', component_names, file=f)
print('NGL column liquid Molar Compositions: ', molar_compositions, file=f)
component_names = separation_process_train_B.getUnit(
"NGL mixer").getOutletStream().getFluid().getComponentNames()
molar_compositions = separation_process_train_B.getUnit(
"NGL mixer").getOutletStream().getFluid().getMolarComposition()
print('Oil Component Names: ', component_names, file=f)
print('Oil Molar Compositions: ', molar_compositions, file=f)
component_names = tex_process_1.getUnit(
"fuel gas splitter").getSplitStream(1).getFluid().getComponentNames()
molar_compositions = tex_process_1.getUnit(
"fuel gas splitter").getSplitStream(1).getFluid().getMolarComposition()
print('Fuel Gas Component Names: ', component_names, file=f)
print('Fuel Gas Molar Compositions: ', molar_compositions, file=f)
print('NGL column oil flow ', column_model.getUnit(
"NGL column").getLiquidOutStream().getFluid().getFlowRate("m3/hr"), file=f)
print('NGL column TVP (30C) ', column_model.getUnit(
"NGL column").getLiquidOutStream().TVP(30.0, "C"), ' bara', file=f)
print('NGL column RVP (37.8C) ', column_model.getUnit("NGL column").getLiquidOutStream().getRVP(37.8, "C", "bara"), ' bara', file=f)
print('NGL column TVP (9C) ', column_model.getUnit( "NGL column").getLiquidOutStream().TVP(9.0, "C"), ' bara', file=f)
print('NGL column oil RVP 37.8C (RVP_ASTM_D6377)', column_model.getUnit( "NGL column").getLiquidOutStream().getRVP(37.8, "C", "bara", "RVP_ASTM_D6377"), 'bara', file=f)
print('NGL column oil RVP 37.8C (RVP_ASTM_D323_82) ', column_model.getUnit( "NGL column").getLiquidOutStream().getRVP(37.8, "C", "bara", "RVP_ASTM_D323_82"), 'bara', file=f)
print('NGL column oil RVP 37.8C (RVP_ASTM_D323_82) ', column_model.getUnit( "NGL column").getLiquidOutStream().getRVP(37.8, "C", "psia", "RVP_ASTM_D323_82"), 'psia', file=f)
print('total oil flow ', export_model.getUnit(
"oil pump").getOutletStream().getFluid().getFlowRate("m3/hr"), file=f)
print('TVP (30C) ', export_model.getUnit(
"oil pump").getOutletStream().TVP(30.0, "C"), ' bara', file=f)
print('RVP (37.8C) ', export_model.getUnit("oil pump").getOutletStream().getRVP(37.8, "C", "bara"), ' bara', file=f)
print('TVP (9C) ', export_model.getUnit( "oil pump").getOutletStream().TVP(9.0, "C"), ' bara', file=f)
print('export oil RVP 37.8C (RVP_ASTM_D6377)', export_model.getUnit( "oil pump").getOutletStream().getRVP(37.8, "C", "bara", "RVP_ASTM_D6377"), 'bara', file=f)
print('export oil RVP 37.8C (RVP_ASTM_D323_82) ', export_model.getUnit( "oil pump").getOutletStream().getRVP(37.8, "C", "bara", "RVP_ASTM_D323_82"), 'bara', file=f)
print('export oil RVP 37.8C (RVP_ASTM_D323_82) ', export_model.getUnit( "oil pump").getOutletStream().getRVP(37.8, "C", "psia", "RVP_ASTM_D323_82"), 'psia', file=f)
print('ethane ', export_model.getUnit("oil pump").getOutletStream().getFluid().getComponent('ethane').getNumberOfMolesInPhase() *
3600.0*separation_process_train_A.getUnit("NGL mixer").getOutletStream().getFluid().getComponent('ethane').getMolarMass(), ' kg/hr', file=f)
print('Condensate Area TVP 30C ', offshore_asset_well_feed_model.getUnit("Offshore Area C Feed").getTVP(30.0, "C", "bara"), 'bara', file=f)
print('Condensate Area TVP 9C ', offshore_asset_well_feed_model.getUnit("Offshore Area C Feed").getTVP(9.0, "C", "bara"), 'bara', file=f)
print('Condensate Area RVP 37.8C ', offshore_asset_well_feed_model.getUnit("Offshore Area C Feed").getRVP(37.8, "C", "bara"), 'bara', file=f)
print('Condensate Area RVP 37.8C (RVP_ASTM_D6377)', offshore_asset_well_feed_model.getUnit("Offshore Area C Feed").getRVP(37.8, "C", "bara", "RVP_ASTM_D6377"), 'bara', file=f)
print('Condensate Area RVP 37.8C (RVP_ASTM_D323_82) ', offshore_asset_well_feed_model.getUnit("Offshore Area C Feed").getRVP(37.8, "C", "bara", "RVP_ASTM_D323_82"), 'bara', file=f)
print('Condensate Area RVP 37.8C (RVP_ASTM_D323_82) ', offshore_asset_well_feed_model.getUnit("Offshore Area C Feed").getRVP(37.8, "C", "psia", "RVP_ASTM_D323_82"), 'psia', file=f)
print('ThirdPartyFeed TVP 30C ', offshore_asset_well_feed_model.getUnit("ThirdPartyFeed Feed").getTVP(30.0, "C", "bara"), 'bara', file=f)
print('ThirdPartyFeed TVP 9C ', offshore_asset_well_feed_model.getUnit("ThirdPartyFeed Feed").getTVP(9.0, "C", "bara"), 'bara', file=f)
print('ThirdPartyFeed RVP 37.8C (VPCR4) ', offshore_asset_well_feed_model.getUnit("ThirdPartyFeed Feed").getRVP(37.8, "C", "bara", "VPCR4"), 'bara', file=f)
print('ThirdPartyFeed RVP 37.8C (VPCR4_no_water) ', offshore_asset_well_feed_model.getUnit("ThirdPartyFeed Feed").getRVP(37.8, "C", "bara", "VPCR4_no_water"), 'bara', file=f)
print('ThirdPartyFeed RVP 37.8C (RVP_ASTM_D6377)', offshore_asset_well_feed_model.getUnit("ThirdPartyFeed Feed").getRVP(37.8, "C", "bara", "RVP_ASTM_D6377"), 'bara', file=f)
print('ThirdPartyFeed RVP 37.8C (RVP_ASTM_D323_82) ', offshore_asset_well_feed_model.getUnit("ThirdPartyFeed Feed").getRVP(37.8, "C", "bara", "RVP_ASTM_D323_82"), 'bara', file=f)
print('ThirdPartyFeed RVP 37.8C (RVP_ASTM_D323_82) ', offshore_asset_well_feed_model.getUnit("ThirdPartyFeed Feed").getRVP(37.8, "C", "psia", "RVP_ASTM_D323_82"), 'psia', file=f)
print(f"Results have been written to {file_name}.")
# Example usage
# Assuming the objects input_parameters, tex_process_1, etc. exist:
write_simulation_results(
input_parameters, tex_process_1, exp_compressor_1, exp_compressor_2,
separation_process_train_A, separation_process_train_B, exp_gas_process
)
Code cell 36: 11.4.2 Print to results/result_case.txt
Notebook cell 71. This code belongs to the 11.4.2 Print to results/result_case.txt section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid-characterization files, and case-specific result folders.
import os
def write_simulation_results(input_parameters,
tex_process_1,
tex_process_2,
exp_compressor_1,
exp_compressor_2,
separation_process_train_A,
separation_process_train_B, exp_gas_process):
"""
Writes simulation results to a case-specific file, overwriting entries by year.
"""
year = input_parameters.Year
case = input_parameters.simulation_case
file_path = f"{input_parameters.results_path}/results_{case}.txt"
# Ensure results directory exists
os.makedirs(os.path.dirname(file_path), exist_ok=True)
# Read existing results
results = {}
header = "year oil_export oil_export_4th_stage gas_export lpg_export lpg_injected TVP9_OFC TVP30_OFC RVP37s_OFC rvp37terminal_asset tvp30terminal_asset tvp9terminal_asset WI N2 CO2 C1 C2 C3 iC4 nC4 iC5 nC5 CCB massbalance rich_gas_bypass rich_gas_bypass_pressure rich_gas_bypass_temperature"
if os.path.exists(file_path):
with open(file_path, "r", encoding="utf-8", errors="replace") as f:
lines = f.readlines()
if lines:
header = lines[0].strip()
for line in lines[1:]:
parts = line.strip().split()
if len(parts) >= 2:
try:
result_year = int(parts[0])
results[result_year] = line.strip()
except ValueError:
continue # Skip malformed lines
# Compute new results
oil_export = (
separation_process_train_A.getUnit("NGL mixer").getOutStream().getFlowRate('idSm3/hr') +
separation_process_train_B.getUnit(
"NGL mixer").getOutStream().getFlowRate('idSm3/hr')
)
# Compute new results
oil_export_4th_stage = (
separation_process_train_A.getUnit("4th stage oil pump").getOutStream().getFlowRate('idSm3/hr') +
separation_process_train_B.getUnit("4th stage oil pump").getOutStream().getFlowRate('idSm3/hr')
)
lpg_export = (
column_model.getUnit(
"lpg production stream").getFlowRate('idSm3/hr')
)
lpg_injected = (
column_model.getUnit(
"lpg injection stream").getFlowRate('idSm3/hr')
)
rich_gas_bypass = tex_process_1.getUnit('gas to dx compressor').getFlowRate('MSm3/day') + tex_process_2.getUnit('gas to dx compressor').getFlowRate('MSm3/day')
gas_export = exp_gas_process.getUnit("export gas stream").getFlowRate('MSm3/day')
rich_gas_bypass_pressure = tex_process_1.getUnit("rich gas splitter").getSplitStream(1).getPressure('bara')
rich_gas_bypass_temperature = tex_process_1.getUnit("rich gas splitter").getSplitStream(1).getTemperature('C')
tvp9 = (
separation_process_train_A.getUnit(
"NGL mixer").getOutletStream().TVP(9.0, "C")
)
tvp30 = (
separation_process_train_A.getUnit(
"NGL mixer").getOutletStream().TVP(30.0, "C")
)
rvp37 = (
separation_process_train_A.getUnit(
"NGL mixer").getOutletStream().getRVP(37.8, "C", "bara", "RVP_ASTM_D6377")
)
rvp37oilmix = export_model.getUnit( "oil pump").getOutletStream().getRVP(37.8, "C", "bara", "RVP_ASTM_D6377")
tvp30oilmix = export_model.getUnit( "oil pump").getOutletStream().TVP(30.0, "C")
tvp9oilmix = export_model.getUnit( "oil pump").getOutletStream().TVP(9.0, "C")
wi = (
exp_gas_process.getUnit("export gas stream").getWI(
"volume", 15.0, 25.0)/1e6
)
component_names = exp_compressor_1.getUnit(
"EXPORT_COMPRESSOR").getOutletStream().getFluid().getComponentNames()
molar_compositions = exp_compressor_1.getUnit(
"EXPORT_COMPRESSOR").getOutletStream().getFluid().getMolarComposition()
ccb_value = exp_gas_process.getUnit("export gas stream").CCB('bara')
new_result_line = f"{year} {oil_export:.2f} {oil_export_4th_stage:.2f} {lpg_export:.2f} {lpg_injected:.2f} {gas_export:.2f} {tvp9:.2f} {tvp30:.2f} {rvp37:.2f} {rvp37oilmix:.2f} {tvp30oilmix:.2f} {tvp9oilmix:.2f} {wi:.4f} {molar_compositions[0]:.4f} {molar_compositions[1]:.4f} {molar_compositions[2]:.4f} {molar_compositions[3]:.4f} {molar_compositions[4]:.4f} {molar_compositions[5]:.4f} {molar_compositions[6]:.4f} {molar_compositions[7]:.4f} {molar_compositions[8]:.4f} {ccb_value:.4f} {mass_balance:.4f} {rich_gas_bypass:.2f} {rich_gas_bypass_pressure:.2f} {rich_gas_bypass_temperature:.2f}"
results[year] = new_result_line
# Write updated results sorted by year
with open(file_path, "w", encoding="utf-8") as f:
f.write(header + "\n")
for result_year in sorted(results):
f.write(results[result_year] + "\n")
print(
f"Results have been successfully written to {file_path} for year {year}.")
write_simulation_results(
input_parameters, tex_process_1, tex_process_2, exp_compressor_1, exp_compressor_2,
separation_process_train_A, separation_process_train_B, exp_gas_process)
Code cell 37: 12. Oil pipeline to TerminalAsset
Notebook cell 73. This code belongs to the 12. Oil pipeline to TerminalAsset section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid-characterization files, and case-specific result folders.
def pipeline(inp: ProcessInput, feed_stream):
pipeline_process = neqsim.process.processmodel.ProcessSystem()
pipe = neqsim.process.equipment.pipeline.PipeBeggsAndBrills(
'OS transport', feed_stream)
pipe.setDiameter(inp.offshore_asset_transport_diameter)
pipe.setPipeWallRoughness(50.0e-6)
pipe.setLength(inp.offshore_asset_transport_length)
pipe.setElevation(0.0)
#pipe.setConstantSurfaceTemperature(9.0, "C")
#pipe.setHeatTransferCoefficient(10.0)
#pipe.setNumberOfIncrements(10)
#pipe.setRunIsothermal(False)
try:
pipe.run()
except:
print("Error in pipeline calculation")
pipeline_process.add(pipe)
return pipeline_process
Code cell 38: 12.1 Test the OST pipeline model
Notebook cell 75. This code belongs to the 12.1 Test the OST pipeline model section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid-characterization files, and case-specific result folders.
#%%script false --no-raise-error uncomment if model should be saved to file
export_pipe = pipeline(input_parameters, export_model.getUnit(
"oil pump").getOutletStream())
try:
export_pipe.run()
export_pipe.getUnit("OS transport").getOutStream(
).getFluid().getFlowRate("m3/hr")
print('TerminalAsset arrival pressure ', export_pipe.getUnit(
"OS transport").getOutStream().getPressure("bara"), ' bara')
print('TerminalAsset arrival temperature ', export_pipe.getUnit(
"OS transport").getOutStream().getTemperature("C"), ' C')
print('oil velocity ', export_pipe.getUnit(
"OS transport").getOutletSuperficialVelocity(), ' m/sec')
export_pipe.getUnit("OS transport").getOutStream().getFluid().prettyPrint()
except:
print('error in pipeline')
#print('transport time ', input_parameters.offshore_asset_transport_length/export_pipe.getUnit(
# "OS transport").getOutletSuperficialVelocity()/3600/24, 'days')
print('reynolds number ', export_pipe.getUnit(
"OS transport").getMixtureReynoldsNumber()[-1], ' - ')
print('viscosity ', export_pipe.getUnit(
"OS transport").getOutletStream().getFluid().getViscosity('kg/msec'), ' - ')
print('density ', export_pipe.getUnit(
"OS transport").getOutletStream().getFluid().getDensity('kg/m3'), ' - ')
print('flow ', export_pipe.getUnit(
"OS transport").getOutletStream().getFlowRate('m3/hr'), ' m3/hr')
print('flow ', export_pipe.getUnit(
"OS transport").getOutletStream().getFlowRate('kg/hr'), ' kg/hr')
#re = export_pipe.getUnit(
# "OS transport").getOutletSuperficialVelocity()*input_parameters.offshore_asset_transport_diameter*export_pipe.getUnit(
# "OS transport").getOutletStream().getFluid().getDensity('kg/m3')/export_pipe.getUnit(
# "OS transport").getOutletStream().getFluid().getViscosity('kg/msec')
#print('reynolds number ', re, ' - ')
#print('diameter ', input_parameters.offshore_asset_transport_diameter)
Code cell 39: 13. TerminalAsset process model
Notebook cell 77. This code belongs to the 13. TerminalAsset process model section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid- characterization files, and case-specific result folders.
from dataclasses import dataclass, field
from typing import List
@dataclass
class TerminalAssetProcessInput:
year: int = 2025 # Year of the simulation
cavern_temperature: float = 9.0 # Temperature in the cavern (°C)
water_wash_rate: float = 1.0 # Water wash rate (m3/hr)
cavern_pressure: float = 3.0 # Pressure in the cavern (bara)
heated_oil_separator_pressure: float = 3.0 # Heated oil separator pressure (bara)
heated_oil_separator_temperature: float = 100.0 # Heated oil separator temperature (°C)
compressor_pressure: float = 13.0 # Compressor pressure (bara)
condenser_temperature: float = 35.0 # Condenser temperature (°C)
de_ethanizer_pressure: float = 14.8 # De-ethanizer pressure (bara)
de_ethanizer_reboiler_temperature: float = 70.0 # De-ethanizer reboiler temperature (°C)
de_ethanizer_gas_cooler_temperature: float = 35.0 # De-ethanizer gas cooler temperature (°C)
de_butanizer_pressure: float = 8.4 # De-butanizer pressure (bara)
de_butanizer_reboiler_temperature: float = 130.0 # De-butanizer reboiler temperature (°C)
de_butanizer_reflux_ratio: float = 0.5 # De-butanizer reflux ratio
naphta_rate_to_de_ethanizer_reflux: float = 100.0 # Naphta rate to de-ethanizer reflux (kg/hr)
naphta_product_split: float = 0.5 # Naphta product split ratio
pump_unstable_oil_pressure: float = 9.5 # Pump unstable oil pressure (bara)
lpg_to_naphta_split: float = 0.5 # LPG to Naphta split ratio
input_terminal_asset = TerminalAssetProcessInput()
Code cell 40: 13. TerminalAsset process model
Notebook cell 78. This code belongs to the 13. TerminalAsset process model section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid- characterization files, and case-specific result folders.
#%%script false --no-raise-error uncomment if model should be saved to file
import pandas as pd
def update_terminal_asset_input_from_excel(file_path: str, year: int, input_terminal_asset: TerminalAssetProcessInput):
"""
Update the input_terminal_asset object with data from a given year in an Excel sheet.
Parameters:
- file_path (str): Path to the Excel file.
- year (int): The year for which data should be extracted.
- input_terminal_asset (TerminalAssetProcessInput): The object to update.
"""
try:
# Read the Excel file
df = pd.read_excel(file_path, sheet_name=0, header=0)
# Filter the data for the given year
row = df.loc[df['Year'] == year]
if row.empty:
raise ValueError(f"No data found for year {year} in {file_path}.")
# Update the input_terminal_asset object with values from the row
row = row.iloc[0] # Get the first matching row
input_terminal_asset.cavern_temperature = row['cavern_temperature']
input_terminal_asset.cavern_pressure = row['cavern_pressure']
input_terminal_asset.pump_unstable_oil_pressure = row['pump_unstable_oil_pressure']
input_terminal_asset.water_wash_rate = row['water_wash_rate']
input_terminal_asset.heated_oil_separator_temperature = row['heated_oil_separator_temperature']
input_terminal_asset.heated_oil_separator_temperature = row['heated_oil_separator_temperature']
input_terminal_asset.compressor_pressure = row['compressor_pressure']
input_terminal_asset.condenser_temperature = row['condenser_temperature']
input_terminal_asset.de_ethanizer_pressure = row['de_ethanizer_pressure']
input_terminal_asset.de_ethanizer_reboiler_temperature = row['de_ethanizer_reboiler_temperature']
input_terminal_asset.de_ethanizer_gas_cooler_temperature = row['de_ethanizer_gas_cooler_temperature']
input_terminal_asset.de_butanizer_pressure = row['de_butanizer_pressure']
input_terminal_asset.de_butanizer_reboiler_temperature = row['de_butanizer_reboiler_temperature']
input_terminal_asset.de_butanizer_reflux_ratio = row['de_butanizer_reflux_ratio']
input_terminal_asset.naphta_rate_to_de_ethanizer_reflux = row['naphta_rate_to_de_ethanizer_reflux']
input_terminal_asset.naphta_product_split = row['naphta_product_split']
input_terminal_asset.lpg_to_naphta_split = row['lpg_to_naphta_split']
print(f"TerminalAsset input updated for year {year}.")
except Exception as e:
print(f"Error updating TerminalAsset input: {e}")
file_path = "./parameters_terminal_asset.xlsx"
update_terminal_asset_input_from_excel(file_path, input_parameters.Year, input_terminal_asset)
Code cell 41: 13. TerminalAsset process model
Notebook cell 79. This code belongs to the 13. TerminalAsset process model section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid- characterization files, and case-specific result folders.
def get_terminal_asset_process(inp: TerminalAssetProcessInput, terminal_asset_oil_feed):
terminal_assetprocess = neqsim.process.processmodel.ProcessSystem()
"""
The method creates the TerminalAsset SCUP process model using neqsim
"""
cavernfeed = neqsim.process.equipment.heatexchanger.Heater(
"oil cavern feed", terminal_asset_oil_feed)
cavernfeed.setOutTemperature(inp.cavern_temperature, 'C')
cavernfeed.setOutPressure(inp.cavern_pressure, 'bara')
cavernfeed.run()
terminal_assetprocess.add(cavernfeed)
oil_cavern = neqsim.process.equipment.separator.ThreePhaseSeparator(
"oil cavern", cavernfeed.getOutStream())
oil_cavern.run()
terminal_assetprocess.add(oil_cavern)
oil_pump = neqsim.process.equipment.pump.Pump(
"oil pump", oil_cavern.getOilOutStream())
oil_pump.setOutletPressure(inp.pump_unstable_oil_pressure, 'bara')
oil_pump.run()
terminal_assetprocess.add(oil_pump)
water_wash_stream = create_water_stream('water wash stream')
water_wash_stream.setFlowRate(inp.water_wash_rate*1000.0, 'kg/hr')
water_wash_stream.setTemperature(inp.cavern_temperature, 'C')
water_wash_stream.setPressure(inp.pump_unstable_oil_pressure, 'bara')
water_wash_stream.run()
terminal_assetprocess.add(water_wash_stream)
oil_water_mixer = neqsim.process.equipment.mixer.Mixer(
"oil water mixer")
oil_water_mixer.addStream(oil_pump.getOutStream())
oil_water_mixer.addStream(water_wash_stream)
oil_water_mixer.run()
terminal_assetprocess.add(oil_water_mixer)
gasrecyl = create_stream('res 0')
gasrecyl.setFlowRate(20000.0, 'kg/hr')
gasrecyl.setTemperature(inp.heated_oil_separator_temperature, 'C')
gasrecyl.setPressure(inp.heated_oil_separator_pressure, 'bara')
gasrecyl.run()
terminal_assetprocess.add(gasrecyl)
inlet_TP_setter = neqsim.process.equipment.heatexchanger.Heater(
"gas valve dp and cooling", gasrecyl.getOutletStream())
inlet_TP_setter.setOutTemperature(inp.heated_oil_separator_temperature-5.0, 'C')
inlet_TP_setter.run()
terminal_assetprocess.add(inlet_TP_setter)
hx1 = neqsim.process.equipment.heatexchanger.HeatExchanger(
"1st oil heater", oil_water_mixer.getOutletStream())
hx1.setFeedStream(1, inlet_TP_setter.getOutletStream())
hx1.setGuessOutTemperature(273.15 + 35.0)
hx1.setUAvalue(300000.2)
try:
hx1.run()
except:
print('error in 1st oil heater')
terminal_assetprocess.add(hx1)
oilrecyl = create_stream('res 1')
oilrecyl.setFlowRate(terminal_asset_oil_feed.getFlowRate('kg/hr')*0.9, 'kg/hr')
oilrecyl.setTemperature(inp.heated_oil_separator_temperature, 'C')
oilrecyl.setPressure(inp.heated_oil_separator_pressure, 'bara')
oilrecyl.run()
terminal_assetprocess.add(oilrecyl)
hx2 = neqsim.process.equipment.heatexchanger.HeatExchanger(
"2nd oil heater", hx1.getOutStream(0))
hx2.setFeedStream(1, oilrecyl.getOutletStream())
hx2.setGuessOutTemperature(273.15 + 75.0)
hx2.setUAvalue(1100000.0)
try:
hx2.run()
except:
print('error in 2nd oil heater')
terminal_assetprocess.add(hx2)
oilvalve = neqsim.process.equipment.valve.ThrottlingValve(
'oil valve', hx2.getOutStream(0))
oilvalve.setOutletPressure(inp.heated_oil_separator_pressure, 'bara')
oilvalve.run()
terminal_assetprocess.add(oilvalve)
hotoil_heater = neqsim.process.equipment.heatexchanger.Heater(
"hot oil heater", oilvalve.getOutStream())
hotoil_heater.setOutTemperature(inp.heated_oil_separator_temperature, 'C')
hotoil_heater.run()
terminal_assetprocess.add(hotoil_heater)
stabilized_oil_separator = neqsim.process.equipment.separator.ThreePhaseSeparator(
"heated oil separator", hotoil_heater.getOutStream())
try:
stabilized_oil_separator.run()
except:
print('error in heated oil separator')
terminal_assetprocess.add(stabilized_oil_separator)
gas_resycle = neqsim.process.equipment.util.Recycle("gas resycle")
gas_resycle.addStream(stabilized_oil_separator.getGasOutStream())
gas_resycle.setOutletStream(gasrecyl)
gas_resycle.setTolerance(1e-2)
gas_resycle.run()
terminal_assetprocess.add(gas_resycle)
oil_resycle = neqsim.process.equipment.util.Recycle("oil resycle")
oil_resycle.addStream(stabilized_oil_separator.getOilOutStream())
oil_resycle.setOutletStream(oilrecyl)
oil_resycle.setTolerance(1e-2)
oil_resycle.run()
terminal_assetprocess.add(oil_resycle)
separato1 = neqsim.process.equipment.separator.ThreePhaseSeparator(
"sep1 cavernsep", hx1.getOutStream(1))
separato1.run()
terminal_assetprocess.add(separato1)
pump1 = neqsim.process.equipment.pump.Pump(
"pump1", separato1.getOilOutStream())
pump1.setOutletPressure(inp.compressor_pressure, 'bara')
try:
pump1.run()
except:
print('error in pump1 run')
terminal_assetprocess.add(pump1)
compressor_gas_to_gas_colummn = neqsim.process.equipment.compressor.Compressor(
"compressor gas to gas column", separato1.getGasOutStream())
compressor_gas_to_gas_colummn.setUsePolytropicCalc(True)
compressor_gas_to_gas_colummn.setPolytropicEfficiency(0.8)
compressor_gas_to_gas_colummn.setOutletPressure(inp.compressor_pressure, 'bara')
try:
compressor_gas_to_gas_colummn.run()
except:
print('error in compressor gas to gas column')
terminal_assetprocess.add(compressor_gas_to_gas_colummn)
mixer = neqsim.process.equipment.mixer.Mixer(
'mixer gas to gas column')
mixer.addStream(compressor_gas_to_gas_colummn.getOutStream())
mixer.addStream(pump1.getOutStream())
mixer.run()
terminal_assetprocess.add(mixer)
feed_to_fuel_gas_column_bublepoint = neqsim.process.equipment.stream.Stream('gas to fuel condesation stream', mixer.getOutStream())
feed_to_fuel_gas_column_bublepoint.setSpecification("bubP");
feed_to_fuel_gas_column_bublepoint.run()
terminal_assetprocess.add(feed_to_fuel_gas_column_bublepoint)
cooler_1 = neqsim.process.equipment.heatexchanger.Cooler('cooler 1', mixer.getOutStream())
cooler_1.setOutTemperature(inp.condenser_temperature, 'C')
cooler_1.run()
terminal_assetprocess.add(cooler_1)
feed_to_fuel_gas_column = neqsim.process.equipment.stream.Stream('stream to fuel gas column', feed_to_fuel_gas_column_bublepoint)
#feed_to_fuel_gas_column = neqsim.process.equipment.stream.Stream('stream to fuel gas column', cooler_1.getOutStream())
feed_to_fuel_gas_column.run()
terminal_assetprocess.add(feed_to_fuel_gas_column)
separato2 = neqsim.process.equipment.separator.ThreePhaseSeparator(
"sep1 gas fule", feed_to_fuel_gas_column)
separato2.run()
terminal_assetprocess.add(separato2)
extra_gas_to_fuel = neqsim.process.equipment.stream.Stream(
'extra gas to fuel', separato2.getGasOutStream())
extra_gas_to_fuel.run()
terminal_assetprocess.add(extra_gas_to_fuel)
pump2 = neqsim.process.equipment.pump.Pump(
"feed pump 2", separato2.getOilOutStream())
pump2.setOutletPressure(inp.de_ethanizer_pressure, 'bara')
pump2.run()
terminal_assetprocess.add(pump2)
water_dehydration = neqsim.process.equipment.splitter.ComponentSplitter(
"molsieve dehydration", pump2.getOutStream())
complen = pump2.getOutStream().getFluid().getNumberOfComponents()
water_dehydration.setSplitFactors([1.0] * (complen - 1) + [0.0])
try:
water_dehydration.run()
except:
print('error in water dehydration')
terminal_assetprocess.add(water_dehydration)
feedHeater = neqsim.process.equipment.heatexchanger.Heater("deethanizer feed heater", water_dehydration.getSplitStream(0))
feedHeater.setdT(0.0)
feedHeater.run()
terminal_assetprocess.add(feedHeater)
tempfluid = feedHeater.getOutStream().getFluid().clone()
tempfluid.setMolarComposition([1.1269232486923688e-06, 0.002642638817095049, 0.015237623814512286,
0.09479920295855006, 0.2664367133684378, 0.072638890369372, 0.18433074090670493,
0.05893425834431258, 0.07525119431180687, 0.07925100506320992, 0.0814576540555948,
0.04665775189490658, 0.01708982141273816, 0.004685723238493833, 5.666335741724731e-06,
2.608641697723448e-12, 0.0])
lqiuidrefluc = neqsim.process.equipment.stream.Stream("recycle", tempfluid.clone())
lqiuidrefluc.setFlowRate(20000.0, "kg/hr")
lqiuidrefluc.setTemperature(30.0, "C")
lqiuidrefluc.setPressure(inp.de_ethanizer_pressure, "bara")
lqiuidrefluc.run()
terminal_assetprocess.add(lqiuidrefluc)
deethanizer = neqsim.process.equipment.distillation.DistillationColumn(
'de ethanizer column', 7, True, False)
deethanizer.addFeedStream(feedHeater.getOutletStream(), 3)
deethanizer.addFeedStream(lqiuidrefluc, 7)
deethanizer.getReboiler().setOutTemperature(273.15 + float(inp.de_ethanizer_reboiler_temperature))
deethanizer.setTopPressure(inp.de_ethanizer_pressure)
deethanizer.setBottomPressure(inp.de_ethanizer_pressure)
deethanizer.run()
terminal_assetprocess.add(deethanizer)
naphta_recyl = neqsim.process.equipment.stream.Stream('recycle napht',tempfluid.clone())
naphta_recyl.setFlowRate(8000.0, 'kg/hr')
naphta_recyl.setTemperature(30.0, 'C')
naphta_recyl.setPressure(inp.de_ethanizer_pressure, 'bara')
naphta_recyl.run()
terminal_assetprocess.add(naphta_recyl)
gasfrom_deethanizer_mixer = neqsim.process.equipment.mixer.Mixer(
'gas from deethanizer mixer')
gasfrom_deethanizer_mixer.addStream(deethanizer.getGasOutStream())
gasfrom_deethanizer_mixer.addStream(naphta_recyl)
gasfrom_deethanizer_mixer.run()
terminal_assetprocess.add(gasfrom_deethanizer_mixer)
gasfrom_deethanizer_cooler = neqsim.process.equipment.heatexchanger.Cooler(
'gas from deethanizer cooler', gasfrom_deethanizer_mixer.getOutStream())
gasfrom_deethanizer_cooler.setOutTemperature(inp.de_ethanizer_gas_cooler_temperature, 'C')
gasfrom_deethanizer_cooler.run()
terminal_assetprocess.add(gasfrom_deethanizer_cooler)
deethanizer_separator = neqsim.process.equipment.separator.Separator(
'deethanizer gas separator', gasfrom_deethanizer_cooler.getOutStream())
deethanizer_separator.run()
terminal_assetprocess.add(deethanizer_separator)
gasfrom_deethanizer_separator = neqsim.process.equipment.stream.Stream(
'gas from deethanizer separator', deethanizer_separator.getGasOutStream())
gasfrom_deethanizer_separator.run()
terminal_assetprocess.add(gasfrom_deethanizer_separator)
liquid_from_deethanizer_separator = neqsim.process.equipment.stream.Stream(
'liquid from deethanizer separator', deethanizer_separator.getLiquidOutStream())
liquid_from_deethanizer_separator.run()
terminal_assetprocess.add(liquid_from_deethanizer_separator)
liquid_from_deethanizer = neqsim.process.equipment.stream.Stream(
'liquid from deethanizer', deethanizer.getLiquidOutStream())
liquid_from_deethanizer.run()
terminal_assetprocess.add(liquid_from_deethanizer)
deethanizerRecycle = neqsim.process.equipment.util.Recycle("ethane liquid to deethanizer recycle")
deethanizerRecycle.addStream(liquid_from_deethanizer_separator)
deethanizerRecycle.setOutletStream(lqiuidrefluc)
deethanizerRecycle.setTolerance(1e-2)
deethanizerRecycle.run()
terminal_assetprocess.add(deethanizerRecycle)
valve_debutanizer = neqsim.process.equipment.valve.ThrottlingValve(
'debutanizer valve', liquid_from_deethanizer)
valve_debutanizer.setOutletPressure(inp.de_butanizer_pressure, 'bara')
valve_debutanizer.run()
terminal_assetprocess.add(valve_debutanizer)
#heater_debutanizer = neqsim.process.equipment.heatexchanger.Heater('heater debutanizer', valve_debutanizer.getOutStream())
#heater_debutanizer.setOutTemperature(73.0, 'C')
#heater_debutanizer.run()
#terminal_assetprocess.add(heater_debutanizer)
oilrecyhx = create_stream('res oil resycl')
oilrecyhx.setFlowRate(200.0, 'kg/hr')
oilrecyhx.setTemperature(125.0, 'C')
oilrecyhx.setPressure(inp.de_butanizer_pressure, 'bara')
oilrecyhx.run()
terminal_assetprocess.add(oilrecyhx)
hx22 = neqsim.process.equipment.heatexchanger.HeatExchanger(
"2nd oil heater debut", valve_debutanizer.getOutStream())
hx22.setFeedStream(1, oilrecyhx.getOutletStream())
hx22.setGuessOutTemperature(273.15 + 50.0)
hx22.setUAvalue(1100000.0)
try:
hx22.run()
except:
print('error in 2nd oil heater')
terminal_assetprocess.add(hx22)
debutanizer = neqsim.process.equipment.distillation.DistillationColumn(
'de butanizer column', 4, True, True)
debutanizer.addFeedStream(hx22.getOutStream(0), 1)
debutanizer.getReboiler().setOutTemperature(273.15 + inp.de_butanizer_reboiler_temperature)
debutanizer.getCondenser().setRefluxRatio(inp.de_butanizer_reflux_ratio)
debutanizer.getCondenser().setTotalCondenser(True)
debutanizer.setTopPressure(inp.de_butanizer_pressure)
debutanizer.setBottomPressure(inp.de_butanizer_pressure)
debutanizer.setSolverType(neqsim.process.equipment.distillation.DistillationColumn.SolverType.DAMPED_SUBSTITUTION)
try:
debutanizer.run()
except:
print('error in debutanizer')
terminal_assetprocess.add(debutanizer)
liquid_from_debutanizer = neqsim.process.equipment.stream.Stream(
'liquid from butinizer', debutanizer.getLiquidOutStream())
liquid_from_debutanizer.run()
terminal_assetprocess.add(liquid_from_debutanizer)
liquid_from_debutanizer_recycle = neqsim.process.equipment.util.Recycle(
'liquid_from_debutanizer recycle')
liquid_from_debutanizer_recycle.addStream(liquid_from_debutanizer)
liquid_from_debutanizer_recycle.setOutletStream(oilrecyhx)
liquid_from_debutanizer_recycle.setTolerance(1e-2)
liquid_from_debutanizer_recycle.run()
terminal_assetprocess.add(liquid_from_debutanizer_recycle)
naptha_liquid_to_deethanizer_pump = neqsim.process.equipment.pump.Pump(
'naphta liquid to deethanizer pump', hx22.getOutStream(1))
naptha_liquid_to_deethanizer_pump.setOutletPressure(inp.de_ethanizer_pressure, 'bara')
naptha_liquid_to_deethanizer_pump.run()
terminal_assetprocess.add(naptha_liquid_to_deethanizer_pump)
naptha_liquid_to_deethanizer_cooler = neqsim.process.equipment.heatexchanger.Cooler(
'naphta liquid to deethanizer cooler', naptha_liquid_to_deethanizer_pump.getOutStream())
naptha_liquid_to_deethanizer_cooler.setOutTemperature(273.15 + 40.0)
naptha_liquid_to_deethanizer_cooler.run()
terminal_assetprocess.add(naptha_liquid_to_deethanizer_cooler)
naphta_liquid_splitter = neqsim.process.equipment.splitter.Splitter(
'butane liquid splitter', naptha_liquid_to_deethanizer_cooler.getOutStream())
naphta_liquid_splitter.setFlowRates([-1, inp.naphta_rate_to_de_ethanizer_reflux], 'kg/hr')
#naphta_liquid_splitter.setFlowRates([-1, pump2.getOutStream().getFlowRate('kg/hr')*0.1470636], 'kg/hr')
#print('naphta rate to deethanizer reflux ', feedHeater.getOutStream().getFlowRate('kg/hr')*0.1470636, ' kg/hr')
#naphta_liquid_splitter.setSplitF([0.8, 0.2])
try:
naphta_liquid_splitter.run()
except:
print('error in butane liquid splitter')
terminal_assetprocess.add(naphta_liquid_splitter)
naphta_liquid_product = neqsim.process.equipment.stream.Stream(
'naphta liquid product', naphta_liquid_splitter.getSplitStream(0))
naphta_liquid_product.run()
terminal_assetprocess.add(naphta_liquid_product)
naptha_liquid_to_deethanizer = neqsim.process.equipment.stream.Stream(
'naphta liquid to deethanizer', naphta_liquid_splitter.getSplitStream(1))
naptha_liquid_to_deethanizer.run()
terminal_assetprocess.add(naptha_liquid_to_deethanizer)
naptha_liquid_to_deethanizer_recycle = neqsim.process.equipment.util.Recycle(
'naphta liquid to deethanizer recycle')
naptha_liquid_to_deethanizer_recycle.addStream(naptha_liquid_to_deethanizer)
naptha_liquid_to_deethanizer_recycle.setOutletStream(naphta_recyl)
naptha_liquid_to_deethanizer_recycle.setTolerance(1e-2)
naptha_liquid_to_deethanizer_recycle.run()
terminal_assetprocess.add(naptha_liquid_to_deethanizer_recycle)
gas_for_fuel_mixer = neqsim.process.equipment.mixer.Mixer(
'mixer gas for fuel gas column')
gas_for_fuel_mixer.addStream(gasfrom_deethanizer_separator)
#gas_for_fuel_mixer.addStream(extra_gas_to_fuel)
gas_for_fuel_mixer.run()
terminal_assetprocess.add(gas_for_fuel_mixer)
gas_for_fuel = neqsim.process.equipment.stream.Stream(
'gas for fuel', gas_for_fuel_mixer.getOutStream())
gas_for_fuel.run()
terminal_assetprocess.add(gas_for_fuel)
#air = neqsim.thermo.system.SystemSrkEos(298.15, 20.0)
#air.addComponent("nitrogen", 79.0)
#air.addComponent("oxygen", 21.0)
#airstream = neqsim.process.equipment.stream.Stream("air stream", air)
#airstream.setTemperature(15.0, "C")
#airstream.setPressure(1.0, "bara")
#airstream.setFlowRate(5000.0, "kg/hr")
#airstream.run()
#terminal_assetprocess.add(airstream)
#flare_burner = neqsim.process.equipment.reactor.FurnaceBurner(
# "terminal_asset burner")
#flare_burner.setFuelInlet(gas_for_fuel)
#flare_burner.setAirInlet(airstream)
#flare_burner.setExcessAirFraction(0.4)
#flare_burner.run()
#terminal_assetprocess.add(flare_burner)
lpg_stream = neqsim.process.equipment.stream.Stream(
'lpg stream', debutanizer.getGasOutStream())
lpg_stream.run()
terminal_assetprocess.add(lpg_stream)
lpg_product_splitter = neqsim.process.equipment.splitter.Splitter(
'lpg product splitter', lpg_stream)
lpg_product_splitter.setSplitFactors([inp.lpg_to_naphta_split, 1.0-inp.lpg_to_naphta_split])
try:
lpg_product_splitter.run()
except:
print('error in lpg_product_splitter')
terminal_assetprocess.add(lpg_product_splitter)
lpg_stream_storage = neqsim.process.equipment.stream.Stream(
'lpg stream to lpg storage', lpg_product_splitter.getSplitStream(1))
lpg_stream_storage.run()
terminal_assetprocess.add(lpg_stream_storage)
naphta_product_splitter = neqsim.process.equipment.splitter.Splitter(
'butane product splitter', naphta_liquid_product)
naphta_product_splitter.setSplitFactors([inp.naphta_product_split,1-inp.naphta_product_split])
try:
naphta_product_splitter.run()
except:
print('error in naphta_product_splitter')
terminal_assetprocess.add(naphta_product_splitter)
naptha_vestProcess_mixer = neqsim.process.equipment.mixer.Mixer(
'naphta to vestprocess mixer')
naptha_vestProcess_mixer.addStream(lpg_product_splitter.getSplitStream(0))
naptha_vestProcess_mixer.addStream(naphta_product_splitter.getSplitStream(0))
naptha_vestProcess_mixer.run()
terminal_assetprocess.add(naptha_vestProcess_mixer)
naphta_product_vest_process = neqsim.process.equipment.stream.Stream(
'naphta product to vestprocess', naptha_vestProcess_mixer.getOutStream())
naphta_product_vest_process.run()
terminal_assetprocess.add(naphta_product_vest_process)
naphta_product_to_oil = neqsim.process.equipment.stream.Stream(
'naphta product to oil', naphta_product_splitter.getSplitStream(1))
naphta_product_to_oil.run()
terminal_assetprocess.add(naphta_product_to_oil)
oil_to_cavern_mixer = neqsim.process.equipment.mixer.Mixer(
'oil to cavern mixer')
oil_to_cavern_mixer.addStream(hx2.getOutStream(1))
oil_to_cavern_mixer.addStream(naphta_product_to_oil)
oil_to_cavern_mixer.run()
terminal_assetprocess.add(oil_to_cavern_mixer)
water_removal_oil_to_cavern = neqsim.process.equipment.splitter.ComponentSplitter(
"dehydration oil to cavern", oil_to_cavern_mixer.getOutStream())
complen = oil_to_cavern_mixer.getOutStream().getFluid().getNumberOfComponents()
water_removal_oil_to_cavern.setSplitFactors([1.0] * (complen - 1) + [0.0])
try:
water_removal_oil_to_cavern.run()
except:
print('error in water dehydration of oil')
terminal_assetprocess.add(water_removal_oil_to_cavern)
oil_to_cavern_stream = neqsim.process.equipment.stream.Stream(
'oil to cavern stream', water_removal_oil_to_cavern.getSplitStream(0))
oil_to_cavern_stream.run()
terminal_assetprocess.add(oil_to_cavern_stream)
return terminal_assetprocess
Code cell 42: 13. TerminalAsset process model
Notebook cell 81. This code belongs to the 13. TerminalAsset process model section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid- characterization files, and case-specific result folders.
#%%script false --no-raise-error uncomment if model should be saved to file
input_terminal_asset.de_ethanizer_pressure = 13.8
input_terminal_asset.naphta_rate_to_de_ethanizer_reflux = 9000.0
input_terminal_asset.de_ethanizer_gas_cooler_temperature = 30
input_terminal_asset.de_butanizer_reboiler_temperature = 125
input_terminal_asset.de_butanizer_reflux_ratio = 0.2
input_terminal_asset.de_butanizer_pressure = 8.4
input_terminal_asset.heated_oil_separator_temperature = 98
terminal_asset = get_terminal_asset_process(
input_terminal_asset, export_pipe.getUnit("OS transport").getOutStream())
try:
for i in range(10):
terminal_asset.run_step()
except:
print('error in terminal_asset run')
clear_output(wait=True)
print("Calculation finished")
totalPower = terminal_asset.getUnit("hot oil heater").getDuty()+ terminal_asset.getUnit("de ethanizer column").getReboiler().getDuty() + terminal_asset.getUnit("de butanizer column").getReboiler().getDuty()
totalFuelPower = terminal_asset.getUnit("gas for fuel").LCV()*terminal_asset.getUnit("gas for fuel").getFlowRate("Sm3/sec")*0.8
error = totalFuelPower - totalPower
print('hot oil heater duty (W): ', terminal_asset.getUnit("hot oil heater").getDuty()/1e6)
print('deethanizer reboiler duty (W): ', terminal_asset.getUnit("de ethanizer column").getReboiler().getDuty()/1e6)
print('debutanizer reboiler duty (W): ', terminal_asset.getUnit("de butanizer column").getReboiler().getDuty()/1e6)
print('Total power needed (W): ', totalPower/1e6)
print('Total fuel power (W): ', totalFuelPower/1e6)
print('Energy balance (W): ', error/1e6)
Code cell 43: 14. Report TerminalAsset process results
Notebook cell 83. This code belongs to the 14. Report TerminalAsset process results section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid-characterization files, and case-specific result folders.
#emissions = terminal_asset.getUnit("terminal_asset burner").getEmissionRatesKgPerHr()
#print('emissions terminal_asset stack ', emissions)
totalPower = terminal_asset.getUnit("hot oil heater").getDuty()+ terminal_asset.getUnit("de ethanizer column").getReboiler().getDuty() + terminal_asset.getUnit("de butanizer column").getReboiler().getDuty()
totalFuelPower = terminal_asset.getUnit("gas for fuel").LCV()*terminal_asset.getUnit("gas for fuel").getFlowRate("Sm3/sec")
temperaturegasforfuel = terminal_asset.getUnit("gas for fuel").getTemperature('C')
print('temperature gas for fuel ', temperaturegasforfuel, ' C')
print('total power consumption ', totalPower/1e6, ' MW')
print('total fuel power ', totalFuelPower/1e6, ' MW')
#print(terminal_asset.getUnit("feed pump 2").getFluid().getMolarComposition())
print(terminal_asset.getUnit("2nd oil heater").getOutStream(0).getFlowRate('m3/hr'))
print(terminal_asset.getUnit("feed pump 2").getOutStream().getFlowRate('kg/hr'))
print(terminal_asset.getUnit("hot oil heater").getDuty('MW'), ' MW')
print(terminal_asset.getUnit("de ethanizer column").getReboiler().getDuty()/1e6, ' MW')
print(terminal_asset.getUnit("de butanizer column").getReboiler().getDuty()/1e6, ' MW')
print('fuel ', terminal_asset.getUnit("gas for fuel").getFlowRate("Sm3/hr"), ' Sm3/hr')
print('fuel ', terminal_asset.getUnit("gas for fuel").LCV()*terminal_asset.getUnit("gas for fuel").getFlowRate("Sm3/sec")/1e6, ' MW')
print('fuel gas composition ', terminal_asset.getUnit("gas for fuel").getFluid().getMolarComposition())
print('fuel gas composition wt ', terminal_asset.getUnit("gas for fuel").getFluid().getPhase(0).getComposition('wtfraction'))
print('lpg composition wt ', terminal_asset.getUnit("lpg stream").getFluid().getPhase(0).getComposition('wtfraction'))
print('lpg density ', terminal_asset.getUnit("lpg stream").getFluid().getPhase(0).getDensity('kg/m3'))
print('naphta composition wt ', terminal_asset.getUnit("naphta product to vestprocess").getFluid().getPhase(0).getComposition('wtfraction'))
print('naphta density ', terminal_asset.getUnit("naphta liquid product").getFluid().getPhase(0).getDensity('kg/m3'))
print('naphta flow ', terminal_asset.getUnit("naphta liquid product").getFluid().getFlowRate('idSm3/hr') , ' Sm3/hr')
print('flow rate ', terminal_asset.getUnit("gas from deethanizer separator").getFlowRate('kg/hr'), ' kg/hr')
print('density vest process', terminal_asset.getUnit("naphta product to vestprocess").getFluid().getPhase(0).getDensity('kg/m3'))
print('flow vest process ', terminal_asset.getUnit("naphta product to vestprocess").getFluid().getFlowRate('idSm3/hr') , ' Sm3/hr')
print('naphta product to oil ', terminal_asset.getUnit("naphta product to oil").getFluid().getFlowRate('idSm3/hr') , ' Sm3/hr')
print('feed to fuel gas column ', terminal_asset.getUnit("deethanizer feed heater").getOutletStream().getFlowRate('idSm3/hr') , ' Sm3/hr')
#printFrame(terminal_asset.getUnit("molsieve dehydration").getSplitStream(0).getFluid())
print(terminal_asset.getUnit("molsieve dehydration").getSplitStream(0).getFluid().getMolarComposition())
terminal_asset.getUnit("molsieve dehydration").getSplitStream(0).setTemperature(60.0,'C')
terminal_asset.getUnit("molsieve dehydration").getSplitStream(0).run()
#printFrame(terminal_asset.getUnit("molsieve dehydration").getSplitStream(0).getFluid())
print('feed to fuel gas column ', terminal_asset.getUnit("deethanizer feed heater").getOutletStream().getFlowRate('kg/sec') , ' kg/sec')
print('naphta product to oil ', terminal_asset.getUnit("naphta product to oil").getFluid().getFlowRate('kg/sec') , ' kg/sec')
print('fuel gas ', terminal_asset.getUnit("gas for fuel").getFlowRate("kg/sec"), ' kg/sec')
print('flow vest process ', terminal_asset.getUnit("naphta product to vestprocess").getFluid().getFlowRate('kg/sec') , ' kg/sec')
print('flow lpg storage ', terminal_asset.getUnit("lpg stream to lpg storage").getFluid().getFlowRate('kg/sec') , ' kg/sec')
Code cell 44: 14. Report TerminalAsset process results
Notebook cell 84. This code belongs to the 14. Report TerminalAsset process results section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid-characterization files, and case-specific result folders.
#%%script false --no-raise-error uncomment if model should be saved to file
print('year ', input_parameters.Year)
print()
print('total oil to TerminalAsset ', export_model.getUnit(
"oil pump").getOutletStream().getFlowRate("m3/hr"), ' m3/hr')
print()
print('TerminalAsset results')
print('cavern pressure TVP 9) ', export_model.getUnit(
"oil pump").getOutStream().getTVP(9.0, 'C', 'bara'), ' bara')
print('tvp feed oil ', export_model.getUnit(
"oil pump").getOutStream().getTVP(30.0, 'C', 'bara'), ' bara')
print('rvp feed oil ', export_model.getUnit(
"oil pump").getOutStream().getRVP(37.8, "C", "bara", "RVP_ASTM_D323_82"), 'bara')
print('tvp stable oil (30C) ', terminal_asset.getUnit(
"2nd oil heater").getOutStream(1).getTVP(30.0, 'C', 'bara'), ' bara')
print('tvp stable oil (9C) ', terminal_asset.getUnit(
"2nd oil heater").getOutStream(1).getTVP(9.0, 'C', 'bara'), ' bara')
print('1st oil heater out temperature ', terminal_asset.getUnit(
"1st oil heater").getOutStream(0).getTemperature('C'), ' C')
print('1st oil heater gas out temperature ', terminal_asset.getUnit(
"1st oil heater").getOutStream(1).getTemperature('C'), ' C')
print('gas heat loss ', terminal_asset.getUnit(
"gas valve dp and cooling").getDuty()/1e6, ' MW')
print('heat balance heated/cooled error ', terminal_asset.getUnit(
"1st oil heater").getHotColdDutyBalance()-1, ' [-]')
print('2nd oil heater out temperature ', terminal_asset.getUnit(
"2nd oil heater").getOutStream(0).getTemperature('C'), ' C')
print('2nd oil heater out temperature cooled oil ', terminal_asset.getUnit(
"2nd oil heater").getOutStream(1).getTemperature('C'), ' C')
print('tvp stable oil ', terminal_asset.getUnit("2nd oil heater").getOutStream(
1).getTVP(30.0, 'C', 'bara'), ' bara')
print('gas from de-ethanizer ', terminal_asset.getUnit('de ethanizer column').getGasOutStream().getFlowRate('kg/hr'), ' kg/hr')
print('energy in gas from de-ethanizer ', terminal_asset.getUnit('gas for fuel').getFlowRate('MSm3/day') * terminal_asset.getUnit('gas for fuel').getGCV("volume", 15.0, 25.0)/3600.0/24, ' MW')
print('ethane in gas from de-ethanizer ', terminal_asset.getUnit('de ethanizer column').getGasOutStream().getFluid().getComponent('ethane').getz()*100, ' mol%')
print('de ethanizer top temperature ', terminal_asset.getUnit('de ethanizer column').getTray(2).getTemperature() - 273.15, 'C')
print('inlet hot oil heater temperature ', terminal_asset.getUnit('hot oil heater').getInletStream().getTemperature('C'), ' C')
print('hot oil heater duty ', terminal_asset.getUnit('hot oil heater').getDuty()/1e6, ' MW')
print('de ethanizer duty ', terminal_asset.getUnit('de ethanizer column').getReboiler().getDuty()/1e6, 'MW')
print('de butanizer duty ', terminal_asset.getUnit('de butanizer column').getReboiler().getDuty()/1e6, 'MW')
print('de butanizer top temperature ', terminal_asset.getUnit('de butanizer column').getCondenser().getGasOutStream().getTemperature('C'), 'C')
print('propane in gas from de-ethanizer ', terminal_asset.getUnit('de ethanizer column').getGasOutStream().getFluid().getComponent('propane').getz()*100, ' mol%')
print('gas from de-butanizer ', terminal_asset.getUnit('de butanizer column').getGasOutStream().getFlowRate('kg/hr'), ' kg/hr')
print('liquid from de-butanizer ', terminal_asset.getUnit('de butanizer column').getLiquidOutStream().getFlowRate('kg/hr'), ' kg/hr')
print('temperature in condenser ', terminal_asset.getUnit('stream to fuel gas column').getTemperature('C'), ' C')
print('gas for fuels ', terminal_asset.getUnit('gas for fuel').getFlowRate('kg/hr'), ' kg/hr')
print('lpg to vestprosess ', terminal_asset.getUnit('lpg stream').getFlowRate('kg/hr'), ' kg/hr')
print('naphtpa to vestprosess ', terminal_asset.getUnit('naphta product to vestprocess').getFlowRate('kg/hr'), ' kg/hr')
print('naphtpa to oil ', terminal_asset.getUnit('naphta product to oil').getFlowRate('kg/hr'), ' kg/hr')
print('oil to cavern ', terminal_asset.getUnit('oil to cavern stream').getFlowRate('kg/hr'), ' kg/hr')
print('total oil to TerminalAsset ', export_model.getUnit(
"oil pump").getOutletStream().getFlowRate("kg/hr"), ' kg/hr')
mass_balance = (export_model.getUnit(
"oil pump").getOutletStream().getFlowRate("kg/hr") - terminal_asset.getUnit('oil to cavern stream').getFlowRate('kg/hr') - terminal_asset.getUnit('naphta product to vestprocess').getFlowRate('kg/hr') - terminal_asset.getUnit('gas for fuel').getFlowRate('kg/hr')
- terminal_asset.getUnit('oil cavern').getWaterOutStream().getFlowRate('kg/hr')-terminal_asset.getUnit("lpg stream to lpg storage").getFluid().getFlowRate('kg/hr')
)/export_model.getUnit(
"oil pump").getOutletStream().getFlowRate("kg/hr")*100
print('mass balance ', mass_balance, ' %')
print('to ethane column ', terminal_asset.getUnit(
"feed pump 2").getOutletStream().getFlowRate("kg/hr"), ' kg/hr')
print('total oil to TerminalAsset ', terminal_asset.getUnit(
"feed pump 2").getOutletStream().getFlowRate("kg/hr"), ' kg/hr')
print('to ethane column ', terminal_asset.getUnit(
"liquid from butinizer").getFlowRate("kg/hr"), ' kg/hr')
print('condensing temperature ', terminal_asset.getUnit('gas to fuel condesation stream').getTemperature('C'), ' C')
from neqsim.thermo import printFrame
#printFrame(terminal_asset.getUnit(
# "heater debutanizer").getOutStream().getFluid())
# MSm3/day * MJ/Sm3 = MJ/day/3600/24 = MW
# evne tilå få kondensert blandingen (trykk og temperatur som kreves for å få kondensert blandingen)
# Varmebehov (20 MW - 10 5 MW)
# Lave trykk som kompressor kan)
# neste ikke fritt vann i olje
Code cell 45: 15. Save model to file
Notebook cell 86. This code belongs to the 15. Save model to file section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid- characterization files, and case-specific result folders.
#%%script false --no-raise-error uncomment if model should be saved to file
offshore_asset_terminal_asset_process = neqsim.process.processmodel.ProcessModel()
offshore_asset_terminal_asset_process.add("well process", offshore_asset_well_feed_model)
offshore_asset_terminal_asset_process.add("reference_stream_c calibration_stream process",reference_stream_c_sep_process)
offshore_asset_terminal_asset_process.add("test separator process",test_sep_process)
offshore_asset_terminal_asset_process.add("sep train A", separation_process_train_A)
offshore_asset_terminal_asset_process.add("sep train B", separation_process_train_B)
offshore_asset_terminal_asset_process.add("manifold TEX",manifold_upstream_TEX)
offshore_asset_terminal_asset_process.add("TEX process A", tex_process_1)
offshore_asset_terminal_asset_process.add("TEX process B", tex_process_2)
offshore_asset_terminal_asset_process.add("ht 3rd stage compressor manifold", manifold_upstream_ht_injection_compressors)
offshore_asset_terminal_asset_process.add("HT injection process A", ht_injection_process_A)
offshore_asset_terminal_asset_process.add("HT injection process B", ht_injection_process_B)
offshore_asset_terminal_asset_process.add("column manifold",manifold_upstream_column)
offshore_asset_terminal_asset_process.add("NGL column", column_model)
offshore_asset_terminal_asset_process.add("export compressor manifold", manifold_upstream_compressors)
offshore_asset_terminal_asset_process.add("Export train A", exp_compressor_1)
offshore_asset_terminal_asset_process.add("Export train B", exp_compressor_2)
offshore_asset_terminal_asset_process.add("export gas", exp_gas_process)
offshore_asset_terminal_asset_process.add("export oil", export_model)
offshore_asset_terminal_asset_process.add("export pipeline", export_pipe)
offshore_asset_terminal_asset_process.add("terminal_asset", terminal_asset)
offshore_asset_terminal_asset_process.setRunStep(True)
Code cell 46: 15. Save model to file
Notebook cell 87. This code belongs to the 15. Save model to file section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid- characterization files, and case-specific result folders.
#%%script false --no-raise-error uncomment if model should be saved to file
offshore_asset_terminal_asset_process.get("TEX process A").getUnit("feed stream turbo expander").run()
offshore_asset_terminal_asset_process.get("TEX process A").getUnit("feed stream turbo expander").getFluid().prettyPrint()
print('mass flow ', offshore_asset_terminal_asset_process.get("TEX process A").getUnit("feed stream turbo expander").getFlowRate('kg/hr'), ' kg/hr')
print('mass flow ', offshore_asset_terminal_asset_process.get("TEX process A").getUnit("TurboExpanderCompressor").getCompressorFeedStream().getFlowRate('kg/hr'), ' kg/hr')
print('expander out temperature ',offshore_asset_terminal_asset_process.get("TEX process A").getUnit("TurboExpanderCompressor").getExpanderOutletStream().getTemperature('C'), ' C')
print('compressor out pressure ',offshore_asset_terminal_asset_process.get("TEX process A").getUnit("TurboExpanderCompressor").getCompressorOutletStream().getPressure('bara'), ' bara')
print('expander power ',offshore_asset_terminal_asset_process.get("TEX process A").getUnit("TurboExpanderCompressor").getPowerExpander()/1e6, ' MW')
print('expander isentropic efficiency ',offshore_asset_terminal_asset_process.get("TEX process A").getUnit("TurboExpanderCompressor").getExpanderIsentropicEfficiency(), ' -')
print('compressor polytropic efficiency ',offshore_asset_terminal_asset_process.get("TEX process A").getUnit("TurboExpanderCompressor").getCompressorPolytropicEfficiency(), ' -')
Code cell 47: 15. Save model to file
Notebook cell 89. This code belongs to the 15. Save model to file section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid- characterization files, and case-specific result folders.
#%%script false --no-raise-error uncomment if model should be saved to file
offshore_asset_terminal_asset_process.get('sep train A').getUnit('1st stage separator').setTagName('V-GEN-101A')
offshore_asset_terminal_asset_process.get('sep train B').getUnit('1st stage separator').setTagName('V-GEN-101B')
offshore_asset_terminal_asset_process.get('sep train A').getUnit('2nd stage separator').setTagName('V-GEN-102A')
offshore_asset_terminal_asset_process.get('sep train B').getUnit('2nd stage separator').setTagName('V-GEN-102B')
offshore_asset_terminal_asset_process.get('sep train A').getUnit('3rd stage separator').setTagName('V-GEN-103A')
offshore_asset_terminal_asset_process.get('sep train B').getUnit('3rd stage separator').setTagName('V-GEN-103B')
offshore_asset_terminal_asset_process.get('sep train A').getUnit('4th stage separator').setTagName('V-GEN-104A')
offshore_asset_terminal_asset_process.get('sep train B').getUnit('3rd stage separator').setTagName('V-GEN-104B')
Code cell 48: 15. Save model to file
Notebook cell 90. This code belongs to the 15. Save model to file section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid- characterization files, and case-specific result folders.
#%%script false --no-raise-error uncomment if model should be saved to file
from neqsim import save_neqsim, save_xml
from neqsim.process import results_json
year = input_parameters.Year
case = input_parameters.simulation_case
file_path = f"{results_path}/neqsim_model_{case}_{year}.neqsim"
save_neqsim(offshore_asset_terminal_asset_process, file_path)
output_file_path = f"{input_parameters.results_path}/neqsim_model_{case}_{year}.json"
try:
output_offshore_asset_process = results_json(offshore_asset_terminal_asset_process, output_file_path)
except:
print('error in offshore_asset process json export')
print(f"\nOutput saved to {input_parameters.results_path}")
Code cell 49: Mass Balance Check for Unit Operations
Notebook cell 92. This code belongs to the Mass Balance Check for Unit Operations section and is included to preserve the full model implementation. It is intentionally not executed during book builds because the full model requires the project input workbook, fluid-characterization files, and case-specific result folders.
# Mass balance check for all init operations in offshore_asset_process
print("=" * 80)
print("MASS BALANCE CHECK FOR INIT OPERATIONS")
print("=" * 80)
# Check mass balance for each process module
# Returns: Map<String, Map<String, ProcessSystem.MassBalanceResult>>
# Structure: {process_name: {unit_name: MassBalanceResult}}
all_mass_balance_results = offshore_asset_terminal_asset_process.checkMassBalance("kg/hr")
# Display results for each process
total_units = 0
total_failed = 0
for process_name, unit_results in all_mass_balance_results.items():
print(f"\n{process_name}:")
print("-" * 80)
if not unit_results:
print(" No unit operations found")
continue
failed_count = 0
for unit_name, result in unit_results.items():
total_units += 1
abs_error = result.getAbsoluteError()
percent_error = result.getPercentError()
# Check if unit failed (>0.1% error)
if abs(percent_error) > 0.1:
failed_count += 1
total_failed += 1
status = "✗"
else:
status = "✓"
print(f" {status} {unit_name}: {abs_error:.6f} {result.getUnit()} ({percent_error:.4f}%)")
if failed_count > 0:
print(f" → {failed_count} unit(s) failed in this process")
# Check for failed units across all processes
print("\n" + "=" * 80)
print("FAILED MASS BALANCE UNITS (>0.1% error)")
print("=" * 80)
failed_results = offshore_asset_process.getFailedMassBalance("kg/hr", 0.1)
if not failed_results:
print("\n✓ All processes and units passed mass balance check!")
else:
print(f"\n✗ Found {total_failed} unit(s) with mass balance errors:\n")
for process_name, failed_units in failed_results.items():
print(f"\n Process: {process_name}")
for unit_name, result in failed_units.items():
print(f" - {unit_name}:")
print(f" Absolute Error: {result.getAbsoluteError():.6f} {result.getUnit()}")
print(f" Percent Error: {result.getPercentError():.4f}%")
print("\n" + "=" * 80)
print(f"SUMMARY: {total_units} total units checked, {total_failed} failed")
print("=" * 80)
Capstone Portfolio and Self-Assessment
The best way to finish this book is to package one reproducible study. It can be small: a separator train, compressor case, pipeline check, PVT report, dynamic example, or optimization study. What matters is that the work is complete enough for another engineer to rerun and review.
Minimum Portfolio
A complete portfolio contains eight artifacts:
- Problem statement. One page describing the engineering question, operating envelope, assumptions, and acceptance criteria.
- Fluid definition. Components, EOS, mixing rule, plus-fraction or electrolyte choices, and at least one reference or sanity check.
- Runnable notebook. A notebook that builds the case from inputs and runs without private files or hidden local state.
- Validation evidence. Mass balance, unit checks, convergence status, and a comparison against a benchmark, simple estimate, plant value, or published result.
- Figures and tables. At least two figures or tables with units and short interpretation paragraphs.
- Machine-readable output. A
results.jsonfile or equivalent dictionary containing key results, validation flags, assumptions, and figure captions. - Saved model state. A
.neqsimarchive, lifecycle-state JSON, or another reproducible state file. - Decision memo. A short conclusion that states what you would do next and what evidence would change the decision.
Quality Rubric
| Level | Model | Evidence | Engineering judgment |
|---|---|---|---|
| Apprentice | Runs a single base case | Shows main outputs and one basic check | Describes what happened |
| Practitioner | Runs a base case plus scenario variations | Includes mass balance, units, figures, and benchmark comparison | Explains why the result is plausible and identifies active constraints |
| Lead engineer | Packages a reusable model with validation and state export | Includes structured inputs/outputs, scenario table, figures, saved state, and reproducibility notes | Makes a defensible recommendation and states residual risks |
Aim for Practitioner on your first pass. Aim for Lead engineer when the study will be shared, reused, or used for a design or operating decision.
Capstone Project Ideas
- Gas export compressor screening. Vary inlet flow and discharge pressure, calculate power and discharge temperature, identify the feasible envelope, and export the selected case.
- Compressor map and anti-surge envelope. Attach generated or vendor curves, vary flow and speed, report surge margin, stonewall margin, driver margin, and the minimum recycle needed for turndown.
- Three-stage separation study. Vary separator pressures, track oil, gas, water, and compression power, and recommend a pressure set with mass-balance evidence.
- Distillation column diagnostic study. Build a deethanizer or debutanizer, compare solvers, log residuals, add a product specification, and produce a tray-temperature and product-composition report.
- PVT characterization report. Build a characterized fluid, run CME or CVD calculations, compare against reference data, and explain deviations.
- TEG dehydration sensitivity. Vary lean glycol circulation or contactor conditions, calculate water content or dew point, and produce a design table.
- Dynamic control example. Add a measurement device and controller to a simple process, run a transient disturbance, and interpret the response.
- Parallel scenario batch. Build independent process cases, run them using managed
runAsTask()execution, collect a scenario table, and discuss speed, failures, and resource limits. - API-ready model. Wrap a process model behind a structured input/output function, save a lifecycle state, and sketch the FastAPI endpoint contract.
Reviewer Checklist
Before calling the portfolio finished, ask:
- Can a reviewer rerun the notebook from a clean environment?
- Does the code-block or notebook verification report show zero failed runnable examples?
- Are all temperatures, pressures, flows, and powers labelled with units?
- Is every major result traceable to an input, a model option, and a code cell?
- Is the fluid basis ledger complete: component source, database mode, EOS, mixing rule, and validation point?
- Did the model converge, and is convergence checked explicitly?
- Is there at least one independent sanity check?
- Are figures discussed with observation, mechanism, implication, and recommendation?
- Are uncertainty, operating envelope, or residual risk stated honestly?
- Is the final recommendation specific enough to act on?
If the answer to any of these is no, the next improvement is clear.
References
- Solbraa, "Equilibrium and non-equilibrium thermodynamics of natural gas processing — Measurements and modelling of {CO2," PhD Thesis, NTNU, 2002.