From 644ef504a7699795c105c7857102aa9344d8d196 Mon Sep 17 00:00:00 2001 From: Even Solbraa <41290109+EvenSol@users.noreply.github.com> Date: Sun, 14 Dec 2025 09:47:15 +0100 Subject: [PATCH 01/10] add examples --- README.md | 7 + docs/pvt_simulation.md | 80 +++ examples/process_api.py | 509 ++++++++++++++++++ examples/pvtsimulation/README.md | 20 + .../fluid_characterization_and_lumping.py | 65 +++ .../pvt_experiments_java_access.py | 142 +++++ examples/pvtsimulation/pvt_tuning_cme.py | 86 +++ examples/pvtsimulation/pvt_tuning_cvd.py | 83 +++ .../pvtsimulation/pvt_tuning_to_saturation.py | 95 ++++ .../pvtsimulation/pvt_tuning_viscosity.py | 78 +++ 10 files changed, 1165 insertions(+) create mode 100644 docs/pvt_simulation.md create mode 100644 examples/process_api.py create mode 100644 examples/pvtsimulation/README.md create mode 100644 examples/pvtsimulation/fluid_characterization_and_lumping.py create mode 100644 examples/pvtsimulation/pvt_experiments_java_access.py create mode 100644 examples/pvtsimulation/pvt_tuning_cme.py create mode 100644 examples/pvtsimulation/pvt_tuning_cvd.py create mode 100644 examples/pvtsimulation/pvt_tuning_to_saturation.py create mode 100644 examples/pvtsimulation/pvt_tuning_viscosity.py diff --git a/README.md b/README.md index c7cdf93..60f77e6 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,13 @@ print(f"Compressor power: {comp.getPower()/1e6:.2f} MW") See the [examples folder](https://github.com/equinor/neqsim-python/tree/master/examples) for more process simulation examples, including [processApproaches.py](https://github.com/equinor/neqsim-python/blob/master/examples/processApproaches.py) which demonstrates all four approaches. +## PVT Simulation (PVTsimulation) + +NeqSim also includes a `pvtsimulation` package for common PVT experiments (CCE/CME, CVD, differential liberation, separator tests, swelling, viscosity, etc.) and tuning workflows. + +- Documentation: `docs/pvt_simulation.md` +- Direct Java access examples: `examples/pvtsimulation/README.md` + ### Prerequisites Java version 8 or higher ([Java JDK](https://adoptium.net/)) needs to be installed. The Python package [JPype](https://github.com/jpype-project/jpype) is used to connect Python and Java. Read the [installation requirements for Jpype](https://jpype.readthedocs.io/en/latest/install.html). Be aware that mixing 64 bit Python with 32 bit Java and vice versa crashes on import of the jpype module. The needed Python packages are listed in the [NeqSim Python dependencies page](https://github.com/equinor/neqsim-python/network/dependencies). diff --git a/docs/pvt_simulation.md b/docs/pvt_simulation.md new file mode 100644 index 0000000..897df35 --- /dev/null +++ b/docs/pvt_simulation.md @@ -0,0 +1,80 @@ +# PVTsimulation (PVT experiments, characterization, tuning) + +NeqSim’s `pvtsimulation` package contains common PVT laboratory experiments (CCE/CME, CVD, DL, separator tests, swelling, viscosity, etc.) and utilities for tuning fluid characterization to match measured data. + +This repository supports two ways of running these experiments from Python: + +1. **Python wrappers** in `neqsim.thermo` / `neqsim.thermo.thermoTools` (quickest) +2. **Direct Java access** via `from neqsim import jneqsim` (full control, more outputs, tuning) + +## Experiment coverage + +| Experiment | Java class | Python wrapper | Example | +| --- | --- | --- | --- | +| Saturation pressure (bubble/dew point) | `jneqsim.pvtsimulation.simulation.SaturationPressure` | `neqsim.thermo.saturationpressure()` | `examples/pvtsimulation/pvt_experiments_java_access.py`, `examples/pvtsimulation/pvt_tuning_to_saturation.py` | +| Constant mass expansion (CCE/CME) | `...ConstantMassExpansion` | `neqsim.thermo.CME()` | `examples/pvtsimulation/pvt_experiments_java_access.py`, `examples/pvtsimulation/pvt_tuning_cme.py` | +| Constant volume depletion (CVD) | `...ConstantVolumeDepletion` | `neqsim.thermo.CVD()` | `examples/pvtsimulation/pvt_experiments_java_access.py`, `examples/pvtsimulation/pvt_tuning_cvd.py` | +| Differential liberation (DL) | `...DifferentialLiberation` | `neqsim.thermo.difflib()` | `examples/pvtsimulation/pvt_experiments_java_access.py` | +| Separator test | `...SeparatorTest` | `neqsim.thermo.separatortest()` | `examples/pvtsimulation/pvt_experiments_java_access.py` | +| Swelling test | `...SwellingTest` | `neqsim.thermo.swellingtest()` | `examples/pvtsimulation/pvt_experiments_java_access.py` | +| Viscosity | `...ViscositySim` | `neqsim.thermo.viscositysim()` | `examples/pvtsimulation/pvt_experiments_java_access.py`, `examples/pvtsimulation/pvt_tuning_viscosity.py` | +| GOR / Bo | `...GOR` | `neqsim.thermo.GOR()` | `examples/pvtsimulation/pvt_experiments_java_access.py` | + +Other available simulations (direct Java access): + +- `jneqsim.pvtsimulation.simulation.WaxFractionSim` (wax appearance / wax fraction vs T,P; requires wax model setup) +- `jneqsim.pvtsimulation.simulation.ViscosityWaxOilSim` (wax + viscosity) +- `jneqsim.pvtsimulation.simulation.DensitySim` +- `jneqsim.pvtsimulation.simulation.SlimTubeSim` (slim-tube style displacement simulation) + +## Direct Java access: key patterns + +### Passing arrays + +Many PVTsimulation methods expect Java `double[]`. With JPype you can pass: + +- A Python list (often auto-converted), or +- An explicit `double[]` using `jpype.types.JDouble[:]` + +Example: + +```python +from jpype.types import JDouble +from neqsim import jneqsim + +pressures = [400.0, 300.0, 200.0] +temperatures = [373.15, 373.15, 373.15] + +cme = jneqsim.pvtsimulation.simulation.ConstantMassExpansion(oil) +cme.setTemperaturesAndPressures(JDouble[:](temperatures), JDouble[:](pressures)) +cme.runCalc() +``` + +### Experimental data for tuning + +For several experiments, NeqSim expects `double[1][n]` experimental data, i.e. a single row with `n` values aligned with your pressure/temperature points: + +```python +cme.setExperimentalData([[0.98, 1.02, 1.10]]) # relative volume values +cme.runTuning() +``` + +## Fluid characterization + PVT lumping + +See `examples/pvtsimulation/fluid_characterization_and_lumping.py` for a typical black-oil characterization workflow using: + +- `addTBPfraction(...)` and `addPlusFraction(...)` +- `getCharacterization().setLumpingModel("PVTlumpingModel")` +- `getCharacterization().characterisePlusFraction()` + +## Run the examples + +```bash +python examples/pvtsimulation/pvt_experiments_java_access.py +python examples/pvtsimulation/pvt_tuning_to_saturation.py +python examples/pvtsimulation/pvt_tuning_cme.py +python examples/pvtsimulation/pvt_tuning_cvd.py +python examples/pvtsimulation/pvt_tuning_viscosity.py +``` + +Tuning scripts default to skipping the actual tuning step; set `run_tuning = True` inside the script when you are ready to tune against measured data. diff --git a/examples/process_api.py b/examples/process_api.py new file mode 100644 index 0000000..ddfeec6 --- /dev/null +++ b/examples/process_api.py @@ -0,0 +1,509 @@ +""" +NeqSim Process Simulation API + +A REST API for running process simulations from YAML/JSON configurations. + +Usage: + 1. Install dependencies: + pip install fastapi uvicorn pyyaml + + 2. Run the API server: + uvicorn process_api:app --reload --port 8000 + + 3. Open API docs: + http://localhost:8000/docs + + 4. Send POST request with process configuration: + curl -X POST http://localhost:8000/simulate \\ + -H "Content-Type: application/json" \\ + -d @process_config.json + +Example request body (JSON): +{ + "name": "Simple Compression", + "fluids": { + "feed": { + "model": "srk", + "temperature": 303.15, + "pressure": 10.0, + "components": [ + {"name": "methane", "moles": 0.9}, + {"name": "ethane", "moles": 0.1} + ] + } + }, + "equipment": [ + {"type": "stream", "name": "inlet", "fluid": "feed", + "flow_rate": 5.0, "flow_unit": "MSm3/day"}, + {"type": "compressor", "name": "comp1", "inlet": "inlet", + "pressure": 50.0} + ] +} +""" + +from fastapi import FastAPI, HTTPException, Body +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel, Field +from typing import Any, Dict, List, Optional, Union +import json +import traceback + +# Initialize NeqSim (must be done before importing process modules) +try: + from neqsim.process import create_process_from_config, create_fluid_from_config + from neqsim.thermo import TPflash, dataFrame + NEQSIM_AVAILABLE = True +except ImportError as e: + NEQSIM_AVAILABLE = False + NEQSIM_ERROR = str(e) + +# FastAPI app +app = FastAPI( + title="NeqSim Process Simulation API", + description="REST API for running process simulations from YAML/JSON configurations", + version="1.0.0", +) + +# Enable CORS for web clients +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +# ============================================================================= +# Pydantic Models for Request/Response +# ============================================================================= + + +class ComponentConfig(BaseModel): + """Component configuration for fluid creation.""" + name: str = Field(..., description="Component name (e.g., 'methane', 'CO2')") + moles: Optional[float] = Field(None, description="Molar amount") + rate: Optional[float] = Field(None, description="Flow rate") + unit: Optional[str] = Field("mol/sec", description="Flow rate unit") + mole_fraction: Optional[float] = Field(None, description="Mole fraction") + + +class FluidConfig(BaseModel): + """Fluid configuration.""" + type: Optional[str] = Field("custom", description="'predefined' or 'custom'") + name: Optional[str] = Field(None, description="Predefined fluid name") + model: Optional[str] = Field("srk", description="Equation of state") + temperature: Optional[float] = Field(298.15, description="Temperature in K") + pressure: Optional[float] = Field(1.01325, description="Pressure in bara") + components: Optional[List[ComponentConfig]] = Field(None, description="Component list") + mixing_rule: Optional[str] = Field(None, description="Mixing rule") + ge_model: Optional[str] = Field(None, description="GE model") + multiphase: Optional[bool] = Field(False, description="Enable multiphase") + solid_check: Optional[bool] = Field(False, description="Enable solid check") + + +class EquipmentConfig(BaseModel): + """Equipment configuration.""" + type: str = Field(..., description="Equipment type (e.g., 'stream', 'compressor')") + name: str = Field(..., description="Unique equipment name") + + class Config: + extra = "allow" # Allow additional fields for equipment-specific params + + +class ProcessConfig(BaseModel): + """Complete process configuration.""" + name: Optional[str] = Field("Process", description="Process name") + fluids: Optional[Dict[str, FluidConfig]] = Field(None, description="Fluid definitions") + equipment: List[EquipmentConfig] = Field(..., description="Equipment list") + + +class FlashRequest(BaseModel): + """Request for flash calculation.""" + fluid: FluidConfig = Field(..., description="Fluid configuration") + temperature: Optional[float] = Field(None, description="Flash temperature in K") + pressure: Optional[float] = Field(None, description="Flash pressure in bara") + + +class SimulationResult(BaseModel): + """Simulation result response.""" + success: bool + process_name: str + equipment_results: Dict[str, Any] + stream_data: Optional[List[Dict[str, Any]]] = None + error: Optional[str] = None + + +class FlashResult(BaseModel): + """Flash calculation result.""" + success: bool + temperature: float + pressure: float + phases: List[Dict[str, Any]] + properties: Dict[str, Any] + error: Optional[str] = None + + +# ============================================================================= +# API Endpoints +# ============================================================================= + + +@app.get("/") +async def root(): + """API health check and info.""" + return { + "service": "NeqSim Process Simulation API", + "version": "1.0.0", + "neqsim_available": NEQSIM_AVAILABLE, + "docs_url": "/docs", + "endpoints": { + "POST /simulate": "Run process simulation from config", + "POST /flash": "Run flash calculation", + "GET /equipment-types": "List available equipment types", + "GET /fluid-models": "List available equation of state models", + } + } + + +@app.get("/health") +async def health_check(): + """Health check endpoint.""" + if not NEQSIM_AVAILABLE: + raise HTTPException(status_code=503, detail=f"NeqSim not available: {NEQSIM_ERROR}") + return {"status": "healthy", "neqsim": "available"} + + +@app.get("/equipment-types") +async def get_equipment_types(): + """Get list of available equipment types for YAML/JSON configuration.""" + equipment_types = { + "streams": [ + "stream", "water_stream", "neq_stream", "energy_stream", + "well_stream", "virtual_stream", "stream_from_outlet" + ], + "separators": [ + "separator", "three_phase_separator", "gas_scrubber", + "gas_scrubber_with_options", "separator_with_dimensions" + ], + "pressure_changers": [ + "compressor", "pump", "expander", "valve", "valve_with_options", + "compressor_with_chart", "polytopic_compressor", "polytropic_compressor" + ], + "heat_transfer": [ + "heater", "cooler", "heat_exchanger" + ], + "mixing_splitting": [ + "mixer", "splitter", "manifold", "static_mixer", + "static_phase_mixer", "component_splitter", "splitter_with_flowrates" + ], + "pipelines": [ + "pipe", "beggs_brill_pipe", "two_phase_pipe" + ], + "columns": [ + "distillation_column", "teg_absorber", "water_stripper", "simple_absorber" + ], + "reactors": [ + "reactor", "gibbs_reactor" + ], + "utilities": [ + "saturator", "filter", "calculator", "setpoint", + "adjuster", "ejector", "flare", "tank" + ], + "measurement": [ + "pressure_transmitter", "level_transmitter", + "flow_transmitter", "temperature_transmitter" + ], + "control": [ + "pid_controller", "flow_setter", "flow_rate_adjuster" + ], + "recycle": [ + "recycle", "recycle_loop", "close_recycle" + ] + } + return equipment_types + + +@app.get("/fluid-models") +async def get_fluid_models(): + """Get list of available equation of state models.""" + return { + "cubic_eos": [ + {"id": "srk", "name": "Soave-Redlich-Kwong"}, + {"id": "pr", "name": "Peng-Robinson"}, + {"id": "rk", "name": "Redlich-Kwong"}, + ], + "advanced_eos": [ + {"id": "cpa", "name": "CPA (Cubic Plus Association)"}, + {"id": "cpa-srk", "name": "CPA-SRK"}, + {"id": "cpa-pr", "name": "CPA-PR"}, + {"id": "gerg-2008", "name": "GERG-2008"}, + ], + "electrolyte": [ + {"id": "electrolyte", "name": "Electrolyte EoS"}, + {"id": "cpa-el", "name": "Electrolyte CPA"}, + ], + "activity_coefficient": [ + {"id": "nrtl", "name": "NRTL"}, + {"id": "unifac", "name": "UNIFAC"}, + ], + "predefined_fluids": [ + "dry gas", "rich gas", "light oil", "black oil", + "water", "air", "combustion air" + ] + } + + +@app.post("/simulate", response_model=SimulationResult) +async def run_simulation( + config: ProcessConfig = Body(..., example={ + "name": "Simple Compression", + "fluids": { + "feed": { + "model": "srk", + "temperature": 303.15, + "pressure": 10.0, + "components": [ + {"name": "methane", "moles": 0.9}, + {"name": "ethane", "moles": 0.1} + ] + } + }, + "equipment": [ + {"type": "stream", "name": "inlet", "fluid": "feed", + "flow_rate": 5.0, "flow_unit": "MSm3/day"}, + {"type": "compressor", "name": "comp1", "inlet": "inlet", + "pressure": 50.0} + ] + }) +): + """ + Run a process simulation from JSON configuration. + + The configuration should include: + - name: Optional process name + - fluids: Dictionary of fluid configurations + - equipment: List of equipment in process order + + Returns simulation results including equipment data and stream properties. + """ + if not NEQSIM_AVAILABLE: + raise HTTPException(status_code=503, detail=f"NeqSim not available: {NEQSIM_ERROR}") + + try: + # Convert Pydantic model to dict + config_dict = config.model_dump(exclude_none=True) + + # Convert fluids config + if "fluids" in config_dict: + fluids_dict = {} + for name, fluid_cfg in config_dict["fluids"].items(): + fluids_dict[name] = fluid_cfg + config_dict["fluids"] = fluids_dict + + # Run simulation + process = create_process_from_config(config_dict, run=True) + + # Collect results + equipment_results = {} + for eq_name, eq_obj in process.equipment.items(): + eq_result = {"name": eq_name, "type": type(eq_obj).__name__} + + # Try to get common properties + try: + if hasattr(eq_obj, "getOutletStream"): + outlet = eq_obj.getOutletStream() + if outlet: + eq_result["outlet_temperature_K"] = outlet.getTemperature() + eq_result["outlet_pressure_bara"] = outlet.getPressure() + eq_result["outlet_flow_kg_hr"] = outlet.getFlowRate("kg/hr") + except: + pass + + try: + if hasattr(eq_obj, "getPower"): + eq_result["power_W"] = eq_obj.getPower() + eq_result["power_MW"] = eq_obj.getPower() / 1e6 + except: + pass + + try: + if hasattr(eq_obj, "getDuty"): + eq_result["duty_W"] = eq_obj.getDuty() + eq_result["duty_MW"] = eq_obj.getDuty() / 1e6 + except: + pass + + try: + if hasattr(eq_obj, "getPolytropicEfficiency"): + eq_result["polytropic_efficiency"] = eq_obj.getPolytropicEfficiency() + if hasattr(eq_obj, "getIsentropicEfficiency"): + eq_result["isentropic_efficiency"] = eq_obj.getIsentropicEfficiency() + except: + pass + + equipment_results[eq_name] = eq_result + + # Get full results as JSON + try: + full_results = process.results_json() + except: + full_results = None + + # Get stream data + stream_data = None + try: + df = process.results_dataframe() + stream_data = df.to_dict(orient="records") + except: + pass + + return SimulationResult( + success=True, + process_name=config.name or "Process", + equipment_results=equipment_results, + stream_data=stream_data, + ) + + except Exception as e: + return SimulationResult( + success=False, + process_name=config.name or "Process", + equipment_results={}, + error=f"{type(e).__name__}: {str(e)}\n{traceback.format_exc()}" + ) + + +@app.post("/flash", response_model=FlashResult) +async def run_flash( + request: FlashRequest = Body(..., example={ + "fluid": { + "model": "srk", + "temperature": 303.15, + "pressure": 50.0, + "components": [ + {"name": "methane", "moles": 0.85}, + {"name": "ethane", "moles": 0.10}, + {"name": "propane", "moles": 0.05} + ] + }, + "temperature": 280.0, + "pressure": 30.0 + }) +): + """ + Run a TP flash calculation on a fluid. + + Returns phase properties and compositions. + """ + if not NEQSIM_AVAILABLE: + raise HTTPException(status_code=503, detail=f"NeqSim not available: {NEQSIM_ERROR}") + + try: + # Create fluid + fluid_config = request.fluid.model_dump(exclude_none=True) + fluid = create_fluid_from_config(fluid_config) + + # Set conditions + if request.temperature: + fluid.setTemperature(request.temperature) + if request.pressure: + fluid.setPressure(request.pressure) + + # Run flash + TPflash(fluid) + + # Collect phase results + phases = [] + for i in range(fluid.getNumberOfPhases()): + phase = fluid.getPhase(i) + phase_data = { + "phase_number": i, + "phase_type": str(phase.getPhaseTypeName()), + "mole_fraction": fluid.getBeta(i), + "temperature_K": phase.getTemperature(), + "pressure_bara": phase.getPressure(), + "density_kg_m3": phase.getDensity("kg/m3"), + "molar_mass_kg_kmol": phase.getMolarMass() * 1000, + "Z_factor": phase.getZ(), + "viscosity_Pa_s": phase.getViscosity("kg/msec"), + "components": {} + } + + for j in range(phase.getNumberOfComponents()): + comp = phase.getComponent(j) + phase_data["components"][comp.getName()] = { + "mole_fraction": comp.getx(), + "fugacity_coefficient": comp.getFugacityCoefficient(), + } + + phases.append(phase_data) + + # Overall properties + properties = { + "number_of_phases": fluid.getNumberOfPhases(), + "enthalpy_J_mol": fluid.getEnthalpy() / fluid.getTotalNumberOfMoles(), + "entropy_J_mol_K": fluid.getEntropy() / fluid.getTotalNumberOfMoles(), + } + + return FlashResult( + success=True, + temperature=fluid.getTemperature(), + pressure=fluid.getPressure(), + phases=phases, + properties=properties, + ) + + except Exception as e: + return FlashResult( + success=False, + temperature=request.temperature or 0, + pressure=request.pressure or 0, + phases=[], + properties={}, + error=f"{type(e).__name__}: {str(e)}" + ) + + +@app.post("/simulate/yaml") +async def run_simulation_yaml(yaml_content: str = Body(..., media_type="text/plain")): + """ + Run a process simulation from YAML content. + + Send raw YAML as the request body with Content-Type: text/plain + """ + if not NEQSIM_AVAILABLE: + raise HTTPException(status_code=503, detail=f"NeqSim not available: {NEQSIM_ERROR}") + + try: + import yaml + config = yaml.safe_load(yaml_content) + + # Run simulation + process = create_process_from_config(config, run=True) + + # Return results as JSON + return { + "success": True, + "results": process.results_json() + } + + except ImportError: + raise HTTPException(status_code=500, detail="PyYAML not installed") + except Exception as e: + return { + "success": False, + "error": f"{type(e).__name__}: {str(e)}" + } + + +# ============================================================================= +# Main Entry Point +# ============================================================================= + + +if __name__ == "__main__": + import uvicorn + print("Starting NeqSim Process Simulation API...") + print("API documentation available at: http://localhost:8000/docs") + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/examples/pvtsimulation/README.md b/examples/pvtsimulation/README.md new file mode 100644 index 0000000..1a0ec7a --- /dev/null +++ b/examples/pvtsimulation/README.md @@ -0,0 +1,20 @@ +# PVTsimulation examples (direct Java access) + +These examples use **direct Java access** (`from neqsim import jneqsim`) for full control of NeqSim's `pvtsimulation` package. + +Files: + +- `pvt_experiments_java_access.py`: Run common PVT experiments (Psat, CME, CVD, DL, separator test, swelling, viscosity, GOR). +- `pvt_tuning_to_saturation.py`: Tune plus-fraction molar mass to match a measured saturation pressure (bubble/dew point). +- `pvt_tuning_cme.py`: Tune to match experimental CME relative-volume data. +- `pvt_tuning_cvd.py`: Tune to match experimental CVD relative-volume data. +- `pvt_tuning_viscosity.py`: Tune to match experimental viscosity data. +- `fluid_characterization_and_lumping.py`: Typical black-oil characterization workflow (TBP/plus fraction + PVT lumping). + +Run: + +```bash +python examples/pvtsimulation/pvt_experiments_java_access.py +``` + +Tuning scripts default to skipping the actual tuning step; set `run_tuning = True` inside the script when you are ready to tune against measured data. diff --git a/examples/pvtsimulation/fluid_characterization_and_lumping.py b/examples/pvtsimulation/fluid_characterization_and_lumping.py new file mode 100644 index 0000000..744b0a0 --- /dev/null +++ b/examples/pvtsimulation/fluid_characterization_and_lumping.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +""" +Fluid characterization + PVT lumping (black oil style) +===================================================== + +This example shows a typical characterization workflow when you have: + - Light ends as normal components (N2/CO2/C1.../C6) + - Heavy end as TBP cuts and a final plus fraction (e.g. C11+) + +Key steps: + 1) Add TBP fractions and a plus fraction with molar mass and density + 2) Select PVT lumping model (PVTlumpingModel) and number of pseudo components + 3) `characterisePlusFraction()` to split/lump the plus fraction into pseudo components + 4) Create database + set mixing rule + enable volume correction + +You can then run PVTsimulation experiments (CCE/CVD/DL/sep test) on the characterized fluid. +""" + +from __future__ import annotations + +from neqsim.thermo import TPflash, fluid, printFrame + + +def main() -> None: + oil = fluid("srk", 273.15 + 100.0, 250.0) + + oil.addComponent("nitrogen", 0.34) + oil.addComponent("CO2", 3.59) + oil.addComponent("methane", 67.42) + oil.addComponent("ethane", 9.02) + oil.addComponent("propane", 4.31) + oil.addComponent("i-butane", 0.93) + oil.addComponent("n-butane", 1.71) + oil.addComponent("i-pentane", 0.74) + oil.addComponent("n-pentane", 0.85) + oil.addComponent("n-hexane", 1.38) + + oil.addTBPfraction("C7", 1.50, 109.00 / 1000.0, 0.6912) + oil.addTBPfraction("C8", 1.69, 120.20 / 1000.0, 0.7255) + oil.addTBPfraction("C9", 1.14, 129.50 / 1000.0, 0.7454) + oil.addTBPfraction("C10", 0.80, 135.30 / 1000.0, 0.7864) + oil.addPlusFraction("C11", 4.58, 256.20 / 1000.0, 0.8398) + + oil.getCharacterization().setLumpingModel("PVTlumpingModel") + oil.getCharacterization().getLumpingModel().setNumberOfPseudoComponents(12) + oil.getCharacterization().characterisePlusFraction() + + oil.createDatabase(True) + oil.setMixingRule(2) + oil.setMultiPhaseCheck(True) + oil.useVolumeCorrection(True) + + TPflash(oil) + printFrame(oil) + + lumping = oil.getCharacterization().getLumpingModel() + n_lumped = int(lumping.getNumberOfLumpedComponents()) + print(f"\nNumber of lumped components: {n_lumped}") + for i in range(n_lumped): + print(f"{i:2d}: {lumping.getLumpedComponentName(i)}") + + +if __name__ == "__main__": + main() + diff --git a/examples/pvtsimulation/pvt_experiments_java_access.py b/examples/pvtsimulation/pvt_experiments_java_access.py new file mode 100644 index 0000000..8eecbab --- /dev/null +++ b/examples/pvtsimulation/pvt_experiments_java_access.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +""" +PVTsimulation experiments (direct Java access) +============================================ + +This script demonstrates NeqSim's PVTsimulation package using direct Java access: + + from neqsim import jneqsim + +Covered experiments (typical PVT lab / reservoir studies): + - Saturation pressure (bubble/dew point): SaturationPressure + - Constant mass expansion (CCE/CME): ConstantMassExpansion + - Constant volume depletion (CVD): ConstantVolumeDepletion + - Differential liberation (DL): DifferentialLiberation + - Separator test: SeparatorTest + - Swelling test: SwellingTest + - Viscosity study: ViscositySim + - GOR / Bo vs pressure: GOR + +Notes: + - NeqSim pressures are in bara (bar absolute) unless you explicitly manage units. + - Temperatures here are in Kelvin when passed to PVTsimulation classes. + - For simpler usage, see wrapper functions in `neqsim.thermo.thermoTools`. +""" + +from __future__ import annotations + +from jpype.types import JDouble + +from neqsim import jneqsim +from neqsim.thermo import TPflash, fluid + + +def _as_list(java_array) -> list[float]: + return [float(x) for x in java_array] + + +def _build_oil_for_pvt_experiments(): + oil = fluid("srk") + oil.addComponent("methane", 50.0) + oil.addComponent("ethane", 10.0) + oil.addComponent("propane", 5.0) + oil.addComponent("n-butane", 5.0) + oil.addComponent("n-hexane", 10.0) + oil.addPlusFraction("C20", 20.0, 381.0 / 1000.0, 0.88) + + oil.createDatabase(True) + oil.setMixingRule(2) # "classic" (kij from DB) + oil.setMultiPhaseCheck(True) + oil.useVolumeCorrection(True) + return oil + + +def main() -> None: + oil = _build_oil_for_pvt_experiments() + + reservoir_temperature_k = 273.15 + 100.0 + oil.setTemperature(reservoir_temperature_k) + oil.setPressure(250.0) + TPflash(oil) + + print("\n--- Saturation pressure ---") + sat = jneqsim.pvtsimulation.simulation.SaturationPressure(oil.clone()) + sat.setTemperature(reservoir_temperature_k, "K") + sat.run() + psat = float(sat.getSaturationPressure()) + print(f"Psat @ {reservoir_temperature_k:.2f} K: {psat:.3f} bara") + + pressures = [400.0, 350.0, 300.0, 250.0, 200.0, 150.0, 100.0, 50.0] + temperatures = [reservoir_temperature_k] * len(pressures) + + print("\n--- Constant Mass Expansion (CCE/CME) ---") + cme = jneqsim.pvtsimulation.simulation.ConstantMassExpansion(oil.clone()) + cme.setTemperaturesAndPressures(JDouble[:](temperatures), JDouble[:](pressures)) + cme.setTemperature(reservoir_temperature_k, "K") + cme.runCalc() + print("relativeVolume:", _as_list(cme.getRelativeVolume())) + print("liquidRelativeVolume:", _as_list(cme.getLiquidRelativeVolume())) + print("Yfactor:", _as_list(cme.getYfactor())) + print("isoThermalCompressibility [1/bar]:", _as_list(cme.getIsoThermalCompressibility())) + + print("\n--- Constant Volume Depletion (CVD) ---") + cvd = jneqsim.pvtsimulation.simulation.ConstantVolumeDepletion(oil.clone()) + cvd.setPressures(JDouble[:](pressures)) + cvd.setTemperature(reservoir_temperature_k, "K") + cvd.runCalc() + print("relativeVolume:", _as_list(cvd.getRelativeVolume())) + print("liquidRelativeVolume:", _as_list(cvd.getLiquidRelativeVolume())) + print("cummulativeMolePercDepleted [%]:", _as_list(cvd.getCummulativeMolePercDepleted())) + + print("\n--- Differential Liberation (DL) ---") + dl = jneqsim.pvtsimulation.simulation.DifferentialLiberation(oil.clone()) + dl.setPressures(JDouble[:](pressures + [1.01325])) + dl.setTemperature(reservoir_temperature_k, "K") + dl.runCalc() + print("Bo [m3/Sm3]:", _as_list(dl.getBo())) + print("Rs [Sm3/Sm3]:", _as_list(dl.getRs())) + print("oilDensity [kg/m3]:", _as_list(dl.getOilDensity())) + + print("\n--- Separator test ---") + sep_pressures = [50.0, 10.0, 1.01325] + sep_temperatures = [313.15, 303.15, 293.15] + sep = jneqsim.pvtsimulation.simulation.SeparatorTest(oil.clone()) + sep.setSeparatorConditions(JDouble[:](sep_temperatures), JDouble[:](sep_pressures)) + sep.runCalc() + print("separator GOR [Sm3/Sm3]:", _as_list(sep.getGOR())) + print("separator Bo [m3/Sm3]:", _as_list(sep.getBofactor())) + + print("\n--- Swelling test ---") + injection_gas = fluid("srk") + injection_gas.addComponent("CO2", 100.0) + injection_gas.createDatabase(True) + injection_gas.setMixingRule(2) + + mol_percent_injected = [0.0, 1.0, 5.0, 10.0, 20.0] + swell = jneqsim.pvtsimulation.simulation.SwellingTest(oil.clone()) + swell.setInjectionGas(injection_gas) + swell.setTemperature(reservoir_temperature_k, "K") + swell.setCummulativeMolePercentGasInjected(JDouble[:](mol_percent_injected)) + swell.runCalc() + print("swell pressures [bara]:", _as_list(swell.getPressures())) + print("relativeOilVolume [-]:", _as_list(swell.getRelativeOilVolume())) + + print("\n--- Viscosity study ---") + visc = jneqsim.pvtsimulation.simulation.ViscositySim(oil.clone()) + visc.setTemperaturesAndPressures(JDouble[:](temperatures), JDouble[:](pressures)) + visc.runCalc() + print("oilViscosity [Pa*s]:", _as_list(visc.getOilViscosity())) + print("gasViscosity [Pa*s]:", _as_list(visc.getGasViscosity())) + + print("\n--- GOR / Bo vs pressure ---") + gor = jneqsim.pvtsimulation.simulation.GOR(oil.clone()) + gor.setTemperaturesAndPressures(JDouble[:](temperatures), JDouble[:](pressures)) + gor.runCalc() + print("GOR [Sm3/Sm3]:", _as_list(gor.getGOR())) + print("Bo [m3/Sm3]:", _as_list(gor.getBofactor())) + + print("\nDone.") + + +if __name__ == "__main__": + main() diff --git a/examples/pvtsimulation/pvt_tuning_cme.py b/examples/pvtsimulation/pvt_tuning_cme.py new file mode 100644 index 0000000..870724e --- /dev/null +++ b/examples/pvtsimulation/pvt_tuning_cme.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +""" +Tune to CME (CCE) relative-volume data +===================================== + +This example uses `pvtsimulation.simulation.ConstantMassExpansion` with: + + - `setTemperaturesAndPressures(temps, pressures)` + - `setExperimentalData([[...relative volumes...]]])` + - `runTuning()` + +NeqSim's CME tuning currently uses the optimizer to adjust plus-fraction properties +(notably plus-fraction molar mass) to better match the experimental relative-volume curve. +""" + +from __future__ import annotations + +from jpype.types import JDouble + +from neqsim import jneqsim +from neqsim.thermo import fluid + + +def _build_oil_with_plus_fraction(): + oil = fluid("srk") + oil.addComponent("methane", 50.0) + oil.addComponent("ethane", 10.0) + oil.addComponent("propane", 5.0) + oil.addComponent("n-butane", 5.0) + oil.addComponent("n-hexane", 10.0) + oil.addPlusFraction("C20", 20.0, 381.0 / 1000.0, 0.88) + + oil.createDatabase(True) + oil.setMixingRule(2) + oil.setMultiPhaseCheck(True) + oil.useVolumeCorrection(True) + return oil + + +def _as_list(java_array) -> list[float]: + return [float(x) for x in java_array] + + +def main() -> None: + run_tuning = False # set True when tuning against measured CME data + + oil = _build_oil_with_plus_fraction() + + temperature_k = 273.15 + 80.0 + pressures = [400.0, 350.0, 300.0, 250.0, 200.0, 150.0] + temperatures = [temperature_k] * len(pressures) + + # Example data (replace with lab CCE/CME data aligned with the pressures above). + # Format expected by NeqSim: double[1][n], i.e. a single row of n data points. + exp_relative_volume = [0.98, 1.02, 1.08, 1.18, 1.38, 1.80] + + cme = jneqsim.pvtsimulation.simulation.ConstantMassExpansion(oil) + cme.setTemperature(temperature_k, "K") + cme.setTemperaturesAndPressures(JDouble[:](temperatures), JDouble[:](pressures)) + + cme.runCalc() + rv_before = _as_list(cme.getRelativeVolume()) + + print("CME relativeVolume before tuning:", rv_before) + + if not run_tuning: + print( + "Skipping tuning (set `run_tuning = True` to run `ConstantMassExpansion.runTuning`). " + "Note: tuning convergence depends on data/initial fluid and may fail for some cases." + ) + return + + cme.setExperimentalData([exp_relative_volume]) + cme.getOptimizer().setMaxNumberOfIterations(10) + try: + cme.runTuning() + except Exception as exc: + print(f"Tuning failed: {exc}") + return + + rv_after = _as_list(cme.getRelativeVolume()) + print("CME relativeVolume after tuning: ", rv_after) + + +if __name__ == "__main__": + main() diff --git a/examples/pvtsimulation/pvt_tuning_cvd.py b/examples/pvtsimulation/pvt_tuning_cvd.py new file mode 100644 index 0000000..20ccb37 --- /dev/null +++ b/examples/pvtsimulation/pvt_tuning_cvd.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +""" +Tune to CVD relative-volume data +=============================== + +This example uses `pvtsimulation.simulation.ConstantVolumeDepletion` with: + + - `setTemperaturesAndPressures(temps, pressures)` (for tuning) + - `setExperimentalData([[...relative volumes...]]])` + - `runTuning()` + +Replace the example data with your lab CVD relative volumes aligned with the pressures. +""" + +from __future__ import annotations + +from jpype.types import JDouble + +from neqsim import jneqsim +from neqsim.thermo import fluid + + +def _build_oil_with_plus_fraction(): + oil = fluid("srk") + oil.addComponent("methane", 50.0) + oil.addComponent("ethane", 10.0) + oil.addComponent("propane", 5.0) + oil.addComponent("n-butane", 5.0) + oil.addComponent("n-hexane", 10.0) + oil.addPlusFraction("C20", 20.0, 381.0 / 1000.0, 0.88) + + oil.createDatabase(True) + oil.setMixingRule(2) + oil.setMultiPhaseCheck(True) + oil.useVolumeCorrection(True) + return oil + + +def _as_list(java_array) -> list[float]: + return [float(x) for x in java_array] + + +def main() -> None: + run_tuning = False # set True when tuning against measured CVD data + + oil = _build_oil_with_plus_fraction() + + temperature_k = 273.15 + 80.0 + pressures = [400.0, 350.0, 300.0, 250.0, 200.0, 150.0, 100.0] + temperatures = [temperature_k] * len(pressures) + + exp_relative_volume = [0.96, 0.98, 1.00, 1.05, 1.15, 1.30, 1.60] + + cvd = jneqsim.pvtsimulation.simulation.ConstantVolumeDepletion(oil) + cvd.setTemperature(temperature_k, "K") + cvd.setTemperaturesAndPressures(JDouble[:](temperatures), JDouble[:](pressures)) + + cvd.runCalc() + rv_before = _as_list(cvd.getRelativeVolume()) + + print("CVD relativeVolume before tuning:", rv_before) + + if not run_tuning: + print( + "Skipping tuning (set `run_tuning = True` to run `ConstantVolumeDepletion.runTuning`). " + "Note: tuning convergence depends on data/initial fluid and may fail for some cases." + ) + return + + cvd.setExperimentalData([exp_relative_volume]) + cvd.getOptimizer().setMaxNumberOfIterations(20) + try: + cvd.runTuning() + except Exception as exc: + print(f"Tuning failed: {exc}") + return + + rv_after = _as_list(cvd.getRelativeVolume()) + print("CVD relativeVolume after tuning: ", rv_after) + + +if __name__ == "__main__": + main() diff --git a/examples/pvtsimulation/pvt_tuning_to_saturation.py b/examples/pvtsimulation/pvt_tuning_to_saturation.py new file mode 100644 index 0000000..06e2f1e --- /dev/null +++ b/examples/pvtsimulation/pvt_tuning_to_saturation.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +""" +Tune plus fraction to measured saturation pressure +================================================== + +This example uses: + - `pvtsimulation.simulation.SaturationPressure` to calculate saturation pressure + - `pvtsimulation.modeltuning.TuneToSaturation` to tune plus-fraction molar mass + +Typical use: + - You have a measured bubble-point (oil) or dew-point (gas condensate) pressure at a given T. + - You want the NeqSim characterized fluid to match that saturation point before tuning against + full PVT experiments (CCE/CVD/DL, etc). +""" + +from __future__ import annotations + +from neqsim import jneqsim +from neqsim.thermo import fluid + + +def _build_oil_with_plus_fraction(): + oil = fluid("srk") + oil.addComponent("methane", 50.0) + oil.addComponent("ethane", 10.0) + oil.addComponent("propane", 5.0) + oil.addComponent("n-butane", 5.0) + oil.addComponent("n-hexane", 10.0) + oil.addPlusFraction("C20", 20.0, 381.0 / 1000.0, 0.88) + + oil.createDatabase(True) + oil.setMixingRule(2) + oil.setMultiPhaseCheck(True) + oil.useVolumeCorrection(True) + return oil + + +def main() -> None: + run_tuning = False # set True when tuning against measured Psat + + oil = _build_oil_with_plus_fraction() + + measured_temperature_k = 273.15 + 80.0 + + sat_before = jneqsim.pvtsimulation.simulation.SaturationPressure(oil.clone()) + sat_before.setTemperature(measured_temperature_k, "K") + sat_before.run() + psat_before = float(sat_before.getSaturationPressure()) + + # Default target: saturation pressure of the *characterized* version of the same fluid. + # Replace with lab bubble/dew point (keep reasonably close to the initial model to avoid + # unrealistic tuned heavy-end properties). + oil_target = oil.clone() + oil_target.getCharacterization().characterisePlusFraction() + oil_target.createDatabase(True) + oil_target.setMixingRule(2) + oil_target.setMultiPhaseCheck(True) + + sat_target = jneqsim.pvtsimulation.simulation.SaturationPressure(oil_target) + sat_target.setTemperature(measured_temperature_k, "K") + sat_target.run() + measured_psat_bara = float(sat_target.getSaturationPressure()) + + print(f"Psat before tuning: {psat_before:.3f} bara") + print(f"Psat target: {measured_psat_bara:.3f} bara") + + if not run_tuning: + print( + "Skipping tuning (set `run_tuning = True` to run `TuneToSaturation`). " + "Note: this is an experimental heuristic and may not converge for all fluids." + ) + return + + sat_sim = jneqsim.pvtsimulation.simulation.SaturationPressure(oil) + sat_sim.setTemperature(measured_temperature_k, "K") + + tuning = jneqsim.pvtsimulation.modeltuning.TuneToSaturation(sat_sim) + tuning.setSaturationConditions(measured_temperature_k, measured_psat_bara) + tuning.setTunePlusMolarMass(True) + tuning.setTuneVolumeCorrection(False) + tuning.run() + + sat_after = jneqsim.pvtsimulation.simulation.SaturationPressure( + tuning.getSimulation().getThermoSystem() + ) + sat_after.setTemperature(measured_temperature_k, "K") + sat_after.run() + psat_after = float(sat_after.getSaturationPressure()) + + print(f"Psat after tuning: {psat_after:.3f} bara") + print("Tuned system available as: tuning.getSimulation().getThermoSystem()") + + +if __name__ == "__main__": + main() diff --git a/examples/pvtsimulation/pvt_tuning_viscosity.py b/examples/pvtsimulation/pvt_tuning_viscosity.py new file mode 100644 index 0000000..9c55d46 --- /dev/null +++ b/examples/pvtsimulation/pvt_tuning_viscosity.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +""" +Tune to viscosity data +===================== + +This example uses `pvtsimulation.simulation.ViscositySim` with: + + - `setTemperaturesAndPressures(temps, pressures)` + - `setExperimentalData([[...viscosity...]]])` + - `runTuning()` + +Replace the example data with your measured viscosities. +""" + +from __future__ import annotations + +from jpype.types import JDouble + +from neqsim import jneqsim +from neqsim.thermo import fluid + + +def _build_oil_for_viscosity(): + oil = fluid("srk") + oil.addComponent("n-heptane", 6.78) + oil.addPlusFraction("C20", 10.62, 381.0 / 1000.0, 0.88) + oil.getCharacterization().characterisePlusFraction() + oil.createDatabase(True) + oil.setMixingRule(2) + oil.setMultiPhaseCheck(True) + oil.useVolumeCorrection(True) + return oil + + +def _as_list(java_array) -> list[float]: + return [float(x) for x in java_array] + + +def main() -> None: + run_tuning = False # set True when tuning against measured viscosities + + oil = _build_oil_for_viscosity() + + temperatures_k = [300.15, 293.15, 283.15, 273.15] + pressures_bara = [5.0, 5.0, 5.0, 5.0] + + # Example viscosity data in Pa*s (replace with lab values). + exp_viscosity = [2.0e-4, 2.8e-4, 4.0e-4, 5.5e-4] + + visc = jneqsim.pvtsimulation.simulation.ViscositySim(oil) + visc.setTemperaturesAndPressures(JDouble[:](temperatures_k), JDouble[:](pressures_bara)) + visc.runCalc() + mu_before = _as_list(visc.getOilViscosity()) + + print("oil viscosity before tuning [Pa*s]:", mu_before) + + if not run_tuning: + print( + "Skipping tuning (set `run_tuning = True` to run `ViscositySim.runTuning`). " + "Note: tuning convergence depends on data/initial fluid and may fail for some cases." + ) + return + + visc.setExperimentalData([exp_viscosity]) + visc.getOptimizer().setMaxNumberOfIterations(20) + try: + visc.runTuning() + except Exception as exc: + print(f"Tuning failed: {exc}") + return + visc.runCalc() + mu_after = _as_list(visc.getOilViscosity()) + + print("oil viscosity after tuning [Pa*s]:", mu_after) + + +if __name__ == "__main__": + main() From 9dc680f82b1629cf840fa4ea4ced3fb142a28b67 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Sun, 14 Dec 2025 08:48:36 +0000 Subject: [PATCH 02/10] [pre-commit.ci lite] apply automatic fixes --- examples/process_api.py | 288 +++++++++++------- .../fluid_characterization_and_lumping.py | 1 - .../pvt_experiments_java_access.py | 10 +- .../pvtsimulation/pvt_tuning_viscosity.py | 4 +- 4 files changed, 185 insertions(+), 118 deletions(-) diff --git a/examples/process_api.py b/examples/process_api.py index ddfeec6..649c230 100644 --- a/examples/process_api.py +++ b/examples/process_api.py @@ -52,6 +52,7 @@ try: from neqsim.process import create_process_from_config, create_fluid_from_config from neqsim.thermo import TPflash, dataFrame + NEQSIM_AVAILABLE = True except ImportError as e: NEQSIM_AVAILABLE = False @@ -81,6 +82,7 @@ class ComponentConfig(BaseModel): """Component configuration for fluid creation.""" + name: str = Field(..., description="Component name (e.g., 'methane', 'CO2')") moles: Optional[float] = Field(None, description="Molar amount") rate: Optional[float] = Field(None, description="Flow rate") @@ -90,12 +92,15 @@ class ComponentConfig(BaseModel): class FluidConfig(BaseModel): """Fluid configuration.""" + type: Optional[str] = Field("custom", description="'predefined' or 'custom'") name: Optional[str] = Field(None, description="Predefined fluid name") model: Optional[str] = Field("srk", description="Equation of state") temperature: Optional[float] = Field(298.15, description="Temperature in K") pressure: Optional[float] = Field(1.01325, description="Pressure in bara") - components: Optional[List[ComponentConfig]] = Field(None, description="Component list") + components: Optional[List[ComponentConfig]] = Field( + None, description="Component list" + ) mixing_rule: Optional[str] = Field(None, description="Mixing rule") ge_model: Optional[str] = Field(None, description="GE model") multiphase: Optional[bool] = Field(False, description="Enable multiphase") @@ -104,22 +109,27 @@ class FluidConfig(BaseModel): class EquipmentConfig(BaseModel): """Equipment configuration.""" + type: str = Field(..., description="Equipment type (e.g., 'stream', 'compressor')") name: str = Field(..., description="Unique equipment name") - + class Config: extra = "allow" # Allow additional fields for equipment-specific params class ProcessConfig(BaseModel): """Complete process configuration.""" + name: Optional[str] = Field("Process", description="Process name") - fluids: Optional[Dict[str, FluidConfig]] = Field(None, description="Fluid definitions") + fluids: Optional[Dict[str, FluidConfig]] = Field( + None, description="Fluid definitions" + ) equipment: List[EquipmentConfig] = Field(..., description="Equipment list") class FlashRequest(BaseModel): """Request for flash calculation.""" + fluid: FluidConfig = Field(..., description="Fluid configuration") temperature: Optional[float] = Field(None, description="Flash temperature in K") pressure: Optional[float] = Field(None, description="Flash pressure in bara") @@ -127,6 +137,7 @@ class FlashRequest(BaseModel): class SimulationResult(BaseModel): """Simulation result response.""" + success: bool process_name: str equipment_results: Dict[str, Any] @@ -136,6 +147,7 @@ class SimulationResult(BaseModel): class FlashResult(BaseModel): """Flash calculation result.""" + success: bool temperature: float pressure: float @@ -162,7 +174,7 @@ async def root(): "POST /flash": "Run flash calculation", "GET /equipment-types": "List available equipment types", "GET /fluid-models": "List available equation of state models", - } + }, } @@ -170,7 +182,9 @@ async def root(): async def health_check(): """Health check endpoint.""" if not NEQSIM_AVAILABLE: - raise HTTPException(status_code=503, detail=f"NeqSim not available: {NEQSIM_ERROR}") + raise HTTPException( + status_code=503, detail=f"NeqSim not available: {NEQSIM_ERROR}" + ) return {"status": "healthy", "neqsim": "available"} @@ -179,47 +193,67 @@ async def get_equipment_types(): """Get list of available equipment types for YAML/JSON configuration.""" equipment_types = { "streams": [ - "stream", "water_stream", "neq_stream", "energy_stream", - "well_stream", "virtual_stream", "stream_from_outlet" + "stream", + "water_stream", + "neq_stream", + "energy_stream", + "well_stream", + "virtual_stream", + "stream_from_outlet", ], "separators": [ - "separator", "three_phase_separator", "gas_scrubber", - "gas_scrubber_with_options", "separator_with_dimensions" + "separator", + "three_phase_separator", + "gas_scrubber", + "gas_scrubber_with_options", + "separator_with_dimensions", ], "pressure_changers": [ - "compressor", "pump", "expander", "valve", "valve_with_options", - "compressor_with_chart", "polytopic_compressor", "polytropic_compressor" - ], - "heat_transfer": [ - "heater", "cooler", "heat_exchanger" + "compressor", + "pump", + "expander", + "valve", + "valve_with_options", + "compressor_with_chart", + "polytopic_compressor", + "polytropic_compressor", ], + "heat_transfer": ["heater", "cooler", "heat_exchanger"], "mixing_splitting": [ - "mixer", "splitter", "manifold", "static_mixer", - "static_phase_mixer", "component_splitter", "splitter_with_flowrates" - ], - "pipelines": [ - "pipe", "beggs_brill_pipe", "two_phase_pipe" + "mixer", + "splitter", + "manifold", + "static_mixer", + "static_phase_mixer", + "component_splitter", + "splitter_with_flowrates", ], + "pipelines": ["pipe", "beggs_brill_pipe", "two_phase_pipe"], "columns": [ - "distillation_column", "teg_absorber", "water_stripper", "simple_absorber" - ], - "reactors": [ - "reactor", "gibbs_reactor" + "distillation_column", + "teg_absorber", + "water_stripper", + "simple_absorber", ], + "reactors": ["reactor", "gibbs_reactor"], "utilities": [ - "saturator", "filter", "calculator", "setpoint", - "adjuster", "ejector", "flare", "tank" + "saturator", + "filter", + "calculator", + "setpoint", + "adjuster", + "ejector", + "flare", + "tank", ], "measurement": [ - "pressure_transmitter", "level_transmitter", - "flow_transmitter", "temperature_transmitter" + "pressure_transmitter", + "level_transmitter", + "flow_transmitter", + "temperature_transmitter", ], - "control": [ - "pid_controller", "flow_setter", "flow_rate_adjuster" - ], - "recycle": [ - "recycle", "recycle_loop", "close_recycle" - ] + "control": ["pid_controller", "flow_setter", "flow_rate_adjuster"], + "recycle": ["recycle", "recycle_loop", "close_recycle"], } return equipment_types @@ -248,67 +282,86 @@ async def get_fluid_models(): {"id": "unifac", "name": "UNIFAC"}, ], "predefined_fluids": [ - "dry gas", "rich gas", "light oil", "black oil", - "water", "air", "combustion air" - ] + "dry gas", + "rich gas", + "light oil", + "black oil", + "water", + "air", + "combustion air", + ], } @app.post("/simulate", response_model=SimulationResult) async def run_simulation( - config: ProcessConfig = Body(..., example={ - "name": "Simple Compression", - "fluids": { - "feed": { - "model": "srk", - "temperature": 303.15, - "pressure": 10.0, - "components": [ - {"name": "methane", "moles": 0.9}, - {"name": "ethane", "moles": 0.1} - ] - } + config: ProcessConfig = Body( + ..., + example={ + "name": "Simple Compression", + "fluids": { + "feed": { + "model": "srk", + "temperature": 303.15, + "pressure": 10.0, + "components": [ + {"name": "methane", "moles": 0.9}, + {"name": "ethane", "moles": 0.1}, + ], + } + }, + "equipment": [ + { + "type": "stream", + "name": "inlet", + "fluid": "feed", + "flow_rate": 5.0, + "flow_unit": "MSm3/day", + }, + { + "type": "compressor", + "name": "comp1", + "inlet": "inlet", + "pressure": 50.0, + }, + ], }, - "equipment": [ - {"type": "stream", "name": "inlet", "fluid": "feed", - "flow_rate": 5.0, "flow_unit": "MSm3/day"}, - {"type": "compressor", "name": "comp1", "inlet": "inlet", - "pressure": 50.0} - ] - }) + ) ): """ Run a process simulation from JSON configuration. - + The configuration should include: - name: Optional process name - fluids: Dictionary of fluid configurations - equipment: List of equipment in process order - + Returns simulation results including equipment data and stream properties. """ if not NEQSIM_AVAILABLE: - raise HTTPException(status_code=503, detail=f"NeqSim not available: {NEQSIM_ERROR}") - + raise HTTPException( + status_code=503, detail=f"NeqSim not available: {NEQSIM_ERROR}" + ) + try: # Convert Pydantic model to dict config_dict = config.model_dump(exclude_none=True) - + # Convert fluids config if "fluids" in config_dict: fluids_dict = {} for name, fluid_cfg in config_dict["fluids"].items(): fluids_dict[name] = fluid_cfg config_dict["fluids"] = fluids_dict - + # Run simulation process = create_process_from_config(config_dict, run=True) - + # Collect results equipment_results = {} for eq_name, eq_obj in process.equipment.items(): eq_result = {"name": eq_name, "type": type(eq_obj).__name__} - + # Try to get common properties try: if hasattr(eq_obj, "getOutletStream"): @@ -319,37 +372,41 @@ async def run_simulation( eq_result["outlet_flow_kg_hr"] = outlet.getFlowRate("kg/hr") except: pass - + try: if hasattr(eq_obj, "getPower"): eq_result["power_W"] = eq_obj.getPower() eq_result["power_MW"] = eq_obj.getPower() / 1e6 except: pass - + try: if hasattr(eq_obj, "getDuty"): eq_result["duty_W"] = eq_obj.getDuty() eq_result["duty_MW"] = eq_obj.getDuty() / 1e6 except: pass - + try: if hasattr(eq_obj, "getPolytropicEfficiency"): - eq_result["polytropic_efficiency"] = eq_obj.getPolytropicEfficiency() + eq_result["polytropic_efficiency"] = ( + eq_obj.getPolytropicEfficiency() + ) if hasattr(eq_obj, "getIsentropicEfficiency"): - eq_result["isentropic_efficiency"] = eq_obj.getIsentropicEfficiency() + eq_result["isentropic_efficiency"] = ( + eq_obj.getIsentropicEfficiency() + ) except: pass - + equipment_results[eq_name] = eq_result - + # Get full results as JSON try: full_results = process.results_json() except: full_results = None - + # Get stream data stream_data = None try: @@ -357,62 +414,67 @@ async def run_simulation( stream_data = df.to_dict(orient="records") except: pass - + return SimulationResult( success=True, process_name=config.name or "Process", equipment_results=equipment_results, stream_data=stream_data, ) - + except Exception as e: return SimulationResult( success=False, process_name=config.name or "Process", equipment_results={}, - error=f"{type(e).__name__}: {str(e)}\n{traceback.format_exc()}" + error=f"{type(e).__name__}: {str(e)}\n{traceback.format_exc()}", ) @app.post("/flash", response_model=FlashResult) async def run_flash( - request: FlashRequest = Body(..., example={ - "fluid": { - "model": "srk", - "temperature": 303.15, - "pressure": 50.0, - "components": [ - {"name": "methane", "moles": 0.85}, - {"name": "ethane", "moles": 0.10}, - {"name": "propane", "moles": 0.05} - ] + request: FlashRequest = Body( + ..., + example={ + "fluid": { + "model": "srk", + "temperature": 303.15, + "pressure": 50.0, + "components": [ + {"name": "methane", "moles": 0.85}, + {"name": "ethane", "moles": 0.10}, + {"name": "propane", "moles": 0.05}, + ], + }, + "temperature": 280.0, + "pressure": 30.0, }, - "temperature": 280.0, - "pressure": 30.0 - }) + ) ): """ Run a TP flash calculation on a fluid. - + Returns phase properties and compositions. """ if not NEQSIM_AVAILABLE: - raise HTTPException(status_code=503, detail=f"NeqSim not available: {NEQSIM_ERROR}") - + raise HTTPException( + status_code=503, detail=f"NeqSim not available: {NEQSIM_ERROR}" + ) + try: # Create fluid fluid_config = request.fluid.model_dump(exclude_none=True) fluid = create_fluid_from_config(fluid_config) - + # Set conditions if request.temperature: fluid.setTemperature(request.temperature) if request.pressure: fluid.setPressure(request.pressure) - + # Run flash TPflash(fluid) - + # Collect phase results phases = [] for i in range(fluid.getNumberOfPhases()): @@ -427,25 +489,25 @@ async def run_flash( "molar_mass_kg_kmol": phase.getMolarMass() * 1000, "Z_factor": phase.getZ(), "viscosity_Pa_s": phase.getViscosity("kg/msec"), - "components": {} + "components": {}, } - + for j in range(phase.getNumberOfComponents()): comp = phase.getComponent(j) phase_data["components"][comp.getName()] = { "mole_fraction": comp.getx(), "fugacity_coefficient": comp.getFugacityCoefficient(), } - + phases.append(phase_data) - + # Overall properties properties = { "number_of_phases": fluid.getNumberOfPhases(), "enthalpy_J_mol": fluid.getEnthalpy() / fluid.getTotalNumberOfMoles(), "entropy_J_mol_K": fluid.getEntropy() / fluid.getTotalNumberOfMoles(), } - + return FlashResult( success=True, temperature=fluid.getTemperature(), @@ -453,7 +515,7 @@ async def run_flash( phases=phases, properties=properties, ) - + except Exception as e: return FlashResult( success=False, @@ -461,7 +523,7 @@ async def run_flash( pressure=request.pressure or 0, phases=[], properties={}, - error=f"{type(e).__name__}: {str(e)}" + error=f"{type(e).__name__}: {str(e)}", ) @@ -469,32 +531,29 @@ async def run_flash( async def run_simulation_yaml(yaml_content: str = Body(..., media_type="text/plain")): """ Run a process simulation from YAML content. - + Send raw YAML as the request body with Content-Type: text/plain """ if not NEQSIM_AVAILABLE: - raise HTTPException(status_code=503, detail=f"NeqSim not available: {NEQSIM_ERROR}") - + raise HTTPException( + status_code=503, detail=f"NeqSim not available: {NEQSIM_ERROR}" + ) + try: import yaml + config = yaml.safe_load(yaml_content) - + # Run simulation process = create_process_from_config(config, run=True) - + # Return results as JSON - return { - "success": True, - "results": process.results_json() - } - + return {"success": True, "results": process.results_json()} + except ImportError: raise HTTPException(status_code=500, detail="PyYAML not installed") except Exception as e: - return { - "success": False, - "error": f"{type(e).__name__}: {str(e)}" - } + return {"success": False, "error": f"{type(e).__name__}: {str(e)}"} # ============================================================================= @@ -504,6 +563,7 @@ async def run_simulation_yaml(yaml_content: str = Body(..., media_type="text/pla if __name__ == "__main__": import uvicorn + print("Starting NeqSim Process Simulation API...") print("API documentation available at: http://localhost:8000/docs") uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/examples/pvtsimulation/fluid_characterization_and_lumping.py b/examples/pvtsimulation/fluid_characterization_and_lumping.py index 744b0a0..d4c3e2e 100644 --- a/examples/pvtsimulation/fluid_characterization_and_lumping.py +++ b/examples/pvtsimulation/fluid_characterization_and_lumping.py @@ -62,4 +62,3 @@ def main() -> None: if __name__ == "__main__": main() - diff --git a/examples/pvtsimulation/pvt_experiments_java_access.py b/examples/pvtsimulation/pvt_experiments_java_access.py index 8eecbab..8b4de73 100644 --- a/examples/pvtsimulation/pvt_experiments_java_access.py +++ b/examples/pvtsimulation/pvt_experiments_java_access.py @@ -77,7 +77,10 @@ def main() -> None: print("relativeVolume:", _as_list(cme.getRelativeVolume())) print("liquidRelativeVolume:", _as_list(cme.getLiquidRelativeVolume())) print("Yfactor:", _as_list(cme.getYfactor())) - print("isoThermalCompressibility [1/bar]:", _as_list(cme.getIsoThermalCompressibility())) + print( + "isoThermalCompressibility [1/bar]:", + _as_list(cme.getIsoThermalCompressibility()), + ) print("\n--- Constant Volume Depletion (CVD) ---") cvd = jneqsim.pvtsimulation.simulation.ConstantVolumeDepletion(oil.clone()) @@ -86,7 +89,10 @@ def main() -> None: cvd.runCalc() print("relativeVolume:", _as_list(cvd.getRelativeVolume())) print("liquidRelativeVolume:", _as_list(cvd.getLiquidRelativeVolume())) - print("cummulativeMolePercDepleted [%]:", _as_list(cvd.getCummulativeMolePercDepleted())) + print( + "cummulativeMolePercDepleted [%]:", + _as_list(cvd.getCummulativeMolePercDepleted()), + ) print("\n--- Differential Liberation (DL) ---") dl = jneqsim.pvtsimulation.simulation.DifferentialLiberation(oil.clone()) diff --git a/examples/pvtsimulation/pvt_tuning_viscosity.py b/examples/pvtsimulation/pvt_tuning_viscosity.py index 9c55d46..202956b 100644 --- a/examples/pvtsimulation/pvt_tuning_viscosity.py +++ b/examples/pvtsimulation/pvt_tuning_viscosity.py @@ -48,7 +48,9 @@ def main() -> None: exp_viscosity = [2.0e-4, 2.8e-4, 4.0e-4, 5.5e-4] visc = jneqsim.pvtsimulation.simulation.ViscositySim(oil) - visc.setTemperaturesAndPressures(JDouble[:](temperatures_k), JDouble[:](pressures_bara)) + visc.setTemperaturesAndPressures( + JDouble[:](temperatures_k), JDouble[:](pressures_bara) + ) visc.runCalc() mu_before = _as_list(visc.getOilViscosity()) From 4694cb6f87f6c31c4a7b329348d729dc3db6502e Mon Sep 17 00:00:00 2001 From: Even Solbraa <41290109+EvenSol@users.noreply.github.com> Date: Sun, 14 Dec 2025 09:52:48 +0100 Subject: [PATCH 03/10] Potential fix for code scanning alert no. 7: Information exposure through an exception Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- examples/process_api.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/examples/process_api.py b/examples/process_api.py index 649c230..51301e4 100644 --- a/examples/process_api.py +++ b/examples/process_api.py @@ -47,6 +47,11 @@ from typing import Any, Dict, List, Optional, Union import json import traceback +import logging + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) # Initialize NeqSim (must be done before importing process modules) try: @@ -553,7 +558,8 @@ async def run_simulation_yaml(yaml_content: str = Body(..., media_type="text/pla except ImportError: raise HTTPException(status_code=500, detail="PyYAML not installed") except Exception as e: - return {"success": False, "error": f"{type(e).__name__}: {str(e)}"} + logger.error("Exception in /simulate/yaml endpoint", exc_info=True) + return {"success": False, "error": "An internal error occurred"} # ============================================================================= From 70c75002415681ec2ea6b3b4dbbf01906d7ebe44 Mon Sep 17 00:00:00 2001 From: Even Solbraa <41290109+EvenSol@users.noreply.github.com> Date: Sun, 14 Dec 2025 10:30:06 +0100 Subject: [PATCH 04/10] update --- README.md | 7 ++ src/neqsim/process/processTools.py | 111 +++++++++++++++--- .../test_process_config_io_security.py | 31 +++++ 3 files changed, 134 insertions(+), 15 deletions(-) create mode 100644 tests/process/test_process_config_io_security.py diff --git a/README.md b/README.md index 60f77e6..7001a91 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,13 @@ NeqSim also includes a `pvtsimulation` package for common PVT experiments (CCE/C - Documentation: `docs/pvt_simulation.md` - Direct Java access examples: `examples/pvtsimulation/README.md` +## Transient Multiphase Flow (Two-Fluid Model) + +NeqSim includes a mechanistic **Two-Fluid Transient Multiphase Flow Model** for pipeline simulation. + +- Jupyter notebook demo (direct Java access): `examples/jupyter/two_fluid_model.ipynb` +- Upstream documentation (NeqSim Java): https://github.com/equinor/neqsim/blob/master/docs/wiki/two_fluid_model.md + ### Prerequisites Java version 8 or higher ([Java JDK](https://adoptium.net/)) needs to be installed. The Python package [JPype](https://github.com/jpype-project/jpype) is used to connect Python and Java. Read the [installation requirements for Jpype](https://jpype.readthedocs.io/en/latest/install.html). Be aware that mixing 64 bit Python with 32 bit Java and vice versa crashes on import of the jpype module. The needed Python packages are listed in the [NeqSim Python dependencies page](https://github.com/equinor/neqsim-python/network/dependencies). diff --git a/src/neqsim/process/processTools.py b/src/neqsim/process/processTools.py index 353e6d1..f0c44f7 100644 --- a/src/neqsim/process/processTools.py +++ b/src/neqsim/process/processTools.py @@ -157,6 +157,7 @@ from __future__ import annotations import json +from pathlib import Path from typing import Any, Optional, List, Dict, Union import pandas as pd @@ -169,6 +170,56 @@ _loop_mode: bool = False +_YAML_SUFFIXES = {".yaml", ".yml"} + + +def _resolve_path_in_cwd( + user_path: str, + *, + allowed_suffixes: Optional[set[str]] = None, + must_exist: bool = False, +) -> Path: + """ + Resolve a user-supplied path safely inside the current working directory. + + This is used for convenience helpers that read/write local config/result files. + To avoid path traversal / arbitrary file read/write, absolute paths and paths that + escape the current working directory are rejected. + """ + if not isinstance(user_path, str): + raise TypeError("path must be a string") + + path = Path(user_path) + + # Disallow absolute paths and drive-relative paths (Windows: 'C:foo'). + if path.is_absolute() or path.drive: + raise ValueError( + "Absolute or drive-relative paths are not allowed. " + "Use a relative path within the current working directory." + ) + + base_dir = Path.cwd().resolve() + resolved = (base_dir / path).resolve() + + try: + resolved.relative_to(base_dir) + except ValueError as exc: + raise ValueError( + "Path traversal outside the current working directory is not allowed." + ) from exc + + if allowed_suffixes is not None: + suffix = resolved.suffix.lower() + if suffix not in allowed_suffixes: + allowed = ", ".join(sorted(allowed_suffixes)) + raise ValueError(f"Invalid file extension '{suffix}'. Allowed: {allowed}.") + + if must_exist and not resolved.is_file(): + raise FileNotFoundError(f"File not found: {resolved}") + + return resolved + + class ProcessContext: """ Context manager for explicit process simulation management. @@ -3108,7 +3159,10 @@ def from_json( >>> process = ProcessBuilder.from_json('process_config.json', ... fluids={'feed': my_fluid}).run() """ - with open(json_path, "r") as f: + json_file = _resolve_path_in_cwd( + json_path, allowed_suffixes={".json"}, must_exist=True + ) + with json_file.open("r", encoding="utf-8") as f: config = json.load(f) return cls.from_dict(config, fluids) @@ -3149,7 +3203,10 @@ def from_yaml( "PyYAML is required for YAML support. Install with: pip install pyyaml" ) - with open(yaml_path, "r") as f: + yaml_file = _resolve_path_in_cwd( + yaml_path, allowed_suffixes=_YAML_SUFFIXES, must_exist=True + ) + with yaml_file.open("r", encoding="utf-8") as f: config = yaml.safe_load(f) return cls.from_dict(config, fluids) @@ -3929,18 +3986,28 @@ def save_results(self, filename: str, format: str = "json") -> "ProcessBuilder": >>> process.save_results('results.xlsx', format='excel') """ if format == "json": - with open(filename, "w") as f: + out_file = _resolve_path_in_cwd( + filename, allowed_suffixes={".json"} + ) + out_file.parent.mkdir(parents=True, exist_ok=True) + with out_file.open("w", encoding="utf-8") as f: json.dump(self.results_json(), f, indent=2) elif format == "csv": - self.results_dataframe().to_csv(filename, index=False) + out_file = _resolve_path_in_cwd(filename, allowed_suffixes={".csv"}) + out_file.parent.mkdir(parents=True, exist_ok=True) + self.results_dataframe().to_csv(str(out_file), index=False) elif format == "excel": - self.results_dataframe().to_excel(filename, index=False) + out_file = _resolve_path_in_cwd( + filename, allowed_suffixes={".xlsx", ".xls"} + ) + out_file.parent.mkdir(parents=True, exist_ok=True) + self.results_dataframe().to_excel(str(out_file), index=False) else: raise ValueError( f"Unknown format: {format}. Use 'json', 'csv', or 'excel'." ) - print(f"Results saved to {filename}") + print(f"Results saved to {out_file}") return self @@ -4130,7 +4197,8 @@ def glycoldehydrationlmodule(name, teststream): def openprocess(filename): - processoperations = jneqsim.process.processmodel.ProcessSystem.open(filename) + file_path = _resolve_path_in_cwd(filename, must_exist=True) + processoperations = jneqsim.process.processmodel.ProcessSystem.open(str(file_path)) return processoperations @@ -4808,9 +4876,13 @@ def results_json(process, filename=None): # Save to file if a filename is provided if filename: - with open(filename, "w") as json_file: + out_file = _resolve_path_in_cwd( + filename, allowed_suffixes={".json"} + ) + out_file.parent.mkdir(parents=True, exist_ok=True) + with out_file.open("w", encoding="utf-8") as json_file: json.dump(results, json_file, indent=4) - print(f"JSON report saved to {filename}") + print(f"JSON report saved to {out_file}") return results except Exception as e: @@ -5566,7 +5638,8 @@ def create_process_from_config( or use pre-created fluid objects. Args: - config: Either a path to a YAML file or a configuration dictionary. + config: Either a path to a YAML file (relative to the current working + directory) or a configuration dictionary. fluids: Optional dictionary mapping fluid names to fluid objects. If the config includes a 'fluids' section, fluids are created automatically and merged with this dictionary. @@ -5709,7 +5782,10 @@ def create_process_from_config( "PyYAML is required for YAML support. Install with: pip install pyyaml" ) - with open(config, "r") as f: + yaml_file = _resolve_path_in_cwd( + config, allowed_suffixes=_YAML_SUFFIXES, must_exist=True + ) + with yaml_file.open("r", encoding="utf-8") as f: config = yaml.safe_load(f) # Initialize fluids dictionary @@ -5738,7 +5814,7 @@ def load_process_config(yaml_path: str) -> Dict[str, Any]: Useful for inspecting or modifying configurations before building. Args: - yaml_path: Path to YAML configuration file. + yaml_path: Path to YAML configuration file (relative to the current working directory). Returns: Dictionary with the configuration. @@ -5757,7 +5833,10 @@ def load_process_config(yaml_path: str) -> Dict[str, Any]: "PyYAML is required for YAML support. Install with: pip install pyyaml" ) - with open(yaml_path, "r") as f: + yaml_file = _resolve_path_in_cwd( + yaml_path, allowed_suffixes=_YAML_SUFFIXES, must_exist=True + ) + with yaml_file.open("r", encoding="utf-8") as f: return yaml.safe_load(f) @@ -5767,7 +5846,7 @@ def save_process_config(config: Dict[str, Any], yaml_path: str) -> None: Args: config: Configuration dictionary. - yaml_path: Path to save the YAML file. + yaml_path: Path to save the YAML file (relative to the current working directory). Example: >>> config = { @@ -5783,5 +5862,7 @@ def save_process_config(config: Dict[str, Any], yaml_path: str) -> None: "PyYAML is required for YAML support. Install with: pip install pyyaml" ) - with open(yaml_path, "w") as f: + yaml_file = _resolve_path_in_cwd(yaml_path, allowed_suffixes=_YAML_SUFFIXES) + yaml_file.parent.mkdir(parents=True, exist_ok=True) + with yaml_file.open("w", encoding="utf-8") as f: yaml.dump(config, f, default_flow_style=False, sort_keys=False) diff --git a/tests/process/test_process_config_io_security.py b/tests/process/test_process_config_io_security.py new file mode 100644 index 0000000..9a29390 --- /dev/null +++ b/tests/process/test_process_config_io_security.py @@ -0,0 +1,31 @@ +import json + +import pytest + + +def test_resolve_path_rejects_absolute(monkeypatch, tmp_path): + monkeypatch.chdir(tmp_path) + from neqsim.process import processTools + + with pytest.raises(ValueError): + processTools._resolve_path_in_cwd(str(tmp_path / "x.yaml"), must_exist=False) + + +def test_resolve_path_rejects_traversal(monkeypatch, tmp_path): + monkeypatch.chdir(tmp_path) + from neqsim.process import processTools + + with pytest.raises(ValueError): + processTools._resolve_path_in_cwd("../secrets.yaml", must_exist=False) + + +def test_processbuilder_from_json_reads_local_file(monkeypatch, tmp_path): + monkeypatch.chdir(tmp_path) + from neqsim.process.processTools import ProcessBuilder + + cfg = {"name": "Test", "equipment": []} + (tmp_path / "process_config.json").write_text(json.dumps(cfg), encoding="utf-8") + + builder = ProcessBuilder.from_json("process_config.json") + assert builder.get_process() is not None + From 8c8fbd7ba6d31bd6a927f61b3e7cf7ffb1b294e3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Sun, 14 Dec 2025 09:30:59 +0000 Subject: [PATCH 05/10] [pre-commit.ci lite] apply automatic fixes --- src/neqsim/process/processTools.py | 8 ++------ tests/process/test_process_config_io_security.py | 1 - 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/neqsim/process/processTools.py b/src/neqsim/process/processTools.py index f0c44f7..4ced0c1 100644 --- a/src/neqsim/process/processTools.py +++ b/src/neqsim/process/processTools.py @@ -3986,9 +3986,7 @@ def save_results(self, filename: str, format: str = "json") -> "ProcessBuilder": >>> process.save_results('results.xlsx', format='excel') """ if format == "json": - out_file = _resolve_path_in_cwd( - filename, allowed_suffixes={".json"} - ) + out_file = _resolve_path_in_cwd(filename, allowed_suffixes={".json"}) out_file.parent.mkdir(parents=True, exist_ok=True) with out_file.open("w", encoding="utf-8") as f: json.dump(self.results_json(), f, indent=2) @@ -4876,9 +4874,7 @@ def results_json(process, filename=None): # Save to file if a filename is provided if filename: - out_file = _resolve_path_in_cwd( - filename, allowed_suffixes={".json"} - ) + out_file = _resolve_path_in_cwd(filename, allowed_suffixes={".json"}) out_file.parent.mkdir(parents=True, exist_ok=True) with out_file.open("w", encoding="utf-8") as json_file: json.dump(results, json_file, indent=4) diff --git a/tests/process/test_process_config_io_security.py b/tests/process/test_process_config_io_security.py index 9a29390..ccbfb5a 100644 --- a/tests/process/test_process_config_io_security.py +++ b/tests/process/test_process_config_io_security.py @@ -28,4 +28,3 @@ def test_processbuilder_from_json_reads_local_file(monkeypatch, tmp_path): builder = ProcessBuilder.from_json("process_config.json") assert builder.get_process() is not None - From a5d0a3ce6881eb32b161eb1478f7b49d1b03483f Mon Sep 17 00:00:00 2001 From: Even Solbraa <41290109+EvenSol@users.noreply.github.com> Date: Sun, 14 Dec 2025 10:38:13 +0100 Subject: [PATCH 06/10] update --- src/neqsim/process/processTools.py | 35 ++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/src/neqsim/process/processTools.py b/src/neqsim/process/processTools.py index f0c44f7..ad93d85 100644 --- a/src/neqsim/process/processTools.py +++ b/src/neqsim/process/processTools.py @@ -178,6 +178,7 @@ def _resolve_path_in_cwd( *, allowed_suffixes: Optional[set[str]] = None, must_exist: bool = False, + allow_subdirs: bool = False, ) -> Path: """ Resolve a user-supplied path safely inside the current working directory. @@ -191,12 +192,22 @@ def _resolve_path_in_cwd( path = Path(user_path) - # Disallow absolute paths and drive-relative paths (Windows: 'C:foo'). - if path.is_absolute() or path.drive: - raise ValueError( - "Absolute or drive-relative paths are not allowed. " - "Use a relative path within the current working directory." - ) + if not allow_subdirs: + # Only allow a simple file name. This removes any directory components + # and blocks path traversal through subdirectories. + if path.name != user_path: + raise ValueError( + "Only file names are allowed (no directory components). " + "Use a file in the current working directory." + ) + path = Path(path.name) + else: + # Disallow absolute paths and drive-relative paths (Windows: 'C:foo'). + if path.is_absolute() or path.drive: + raise ValueError( + "Absolute or drive-relative paths are not allowed. " + "Use a relative path within the current working directory." + ) base_dir = Path.cwd().resolve() resolved = (base_dir / path).resolve() @@ -3160,7 +3171,7 @@ def from_json( ... fluids={'feed': my_fluid}).run() """ json_file = _resolve_path_in_cwd( - json_path, allowed_suffixes={".json"}, must_exist=True + json_path, allowed_suffixes={".json"}, must_exist=True, allow_subdirs=False ) with json_file.open("r", encoding="utf-8") as f: config = json.load(f) @@ -3204,7 +3215,7 @@ def from_yaml( ) yaml_file = _resolve_path_in_cwd( - yaml_path, allowed_suffixes=_YAML_SUFFIXES, must_exist=True + yaml_path, allowed_suffixes=_YAML_SUFFIXES, must_exist=True, allow_subdirs=False ) with yaml_file.open("r", encoding="utf-8") as f: config = yaml.safe_load(f) @@ -5783,7 +5794,7 @@ def create_process_from_config( ) yaml_file = _resolve_path_in_cwd( - config, allowed_suffixes=_YAML_SUFFIXES, must_exist=True + config, allowed_suffixes=_YAML_SUFFIXES, must_exist=True, allow_subdirs=False ) with yaml_file.open("r", encoding="utf-8") as f: config = yaml.safe_load(f) @@ -5834,7 +5845,7 @@ def load_process_config(yaml_path: str) -> Dict[str, Any]: ) yaml_file = _resolve_path_in_cwd( - yaml_path, allowed_suffixes=_YAML_SUFFIXES, must_exist=True + yaml_path, allowed_suffixes=_YAML_SUFFIXES, must_exist=True, allow_subdirs=False ) with yaml_file.open("r", encoding="utf-8") as f: return yaml.safe_load(f) @@ -5862,7 +5873,9 @@ def save_process_config(config: Dict[str, Any], yaml_path: str) -> None: "PyYAML is required for YAML support. Install with: pip install pyyaml" ) - yaml_file = _resolve_path_in_cwd(yaml_path, allowed_suffixes=_YAML_SUFFIXES) + yaml_file = _resolve_path_in_cwd( + yaml_path, allowed_suffixes=_YAML_SUFFIXES, allow_subdirs=False + ) yaml_file.parent.mkdir(parents=True, exist_ok=True) with yaml_file.open("w", encoding="utf-8") as f: yaml.dump(config, f, default_flow_style=False, sort_keys=False) From 2c410c09d2a0932916f99caf40846d981e22d586 Mon Sep 17 00:00:00 2001 From: Even Solbraa <41290109+EvenSol@users.noreply.github.com> Date: Sun, 14 Dec 2025 10:46:53 +0100 Subject: [PATCH 07/10] update --- .gitignore | 5 ++ src/neqsim/process/processTools.py | 53 +++++++------------ .../test_process_config_io_security.py | 31 ----------- 3 files changed, 24 insertions(+), 65 deletions(-) delete mode 100644 tests/process/test_process_config_io_security.py diff --git a/.gitignore b/.gitignore index 7134020..8fff02b 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,8 @@ poetry.lock test_gasoilprocess.zip test_gasoilprocess.neqsim + +# Local CodeQL artifacts +codeql-db/ +codeql-*.sarif +tools/codeql/ diff --git a/src/neqsim/process/processTools.py b/src/neqsim/process/processTools.py index ad93d85..24a641f 100644 --- a/src/neqsim/process/processTools.py +++ b/src/neqsim/process/processTools.py @@ -157,6 +157,7 @@ from __future__ import annotations import json +import os from pathlib import Path from typing import Any, Optional, List, Dict, Union @@ -178,7 +179,6 @@ def _resolve_path_in_cwd( *, allowed_suffixes: Optional[set[str]] = None, must_exist: bool = False, - allow_subdirs: bool = False, ) -> Path: """ Resolve a user-supplied path safely inside the current working directory. @@ -190,34 +190,23 @@ def _resolve_path_in_cwd( if not isinstance(user_path, str): raise TypeError("path must be a string") - path = Path(user_path) + if "\x00" in user_path: + raise ValueError("Path contains NUL byte.") - if not allow_subdirs: - # Only allow a simple file name. This removes any directory components - # and blocks path traversal through subdirectories. - if path.name != user_path: - raise ValueError( - "Only file names are allowed (no directory components). " - "Use a file in the current working directory." - ) - path = Path(path.name) - else: - # Disallow absolute paths and drive-relative paths (Windows: 'C:foo'). - if path.is_absolute() or path.drive: - raise ValueError( - "Absolute or drive-relative paths are not allowed. " - "Use a relative path within the current working directory." - ) - - base_dir = Path.cwd().resolve() - resolved = (base_dir / path).resolve() + base_dir = os.path.abspath(os.getcwd()) + resolved_str = os.path.abspath(os.path.join(base_dir, user_path)) - try: - resolved.relative_to(base_dir) - except ValueError as exc: + # Ensure the normalized path is still within the base directory. + # Using `startswith` on normalized paths is recognized by CodeQL as a safe-access check. + base_prefix = base_dir + os.sep + if resolved_str.startswith(base_prefix): + pass + else: raise ValueError( "Path traversal outside the current working directory is not allowed." - ) from exc + ) + + resolved = Path(resolved_str) if allowed_suffixes is not None: suffix = resolved.suffix.lower() @@ -3170,9 +3159,7 @@ def from_json( >>> process = ProcessBuilder.from_json('process_config.json', ... fluids={'feed': my_fluid}).run() """ - json_file = _resolve_path_in_cwd( - json_path, allowed_suffixes={".json"}, must_exist=True, allow_subdirs=False - ) + json_file = _resolve_path_in_cwd(json_path, allowed_suffixes={".json"}, must_exist=True) with json_file.open("r", encoding="utf-8") as f: config = json.load(f) return cls.from_dict(config, fluids) @@ -3215,7 +3202,7 @@ def from_yaml( ) yaml_file = _resolve_path_in_cwd( - yaml_path, allowed_suffixes=_YAML_SUFFIXES, must_exist=True, allow_subdirs=False + yaml_path, allowed_suffixes=_YAML_SUFFIXES, must_exist=True ) with yaml_file.open("r", encoding="utf-8") as f: config = yaml.safe_load(f) @@ -5794,7 +5781,7 @@ def create_process_from_config( ) yaml_file = _resolve_path_in_cwd( - config, allowed_suffixes=_YAML_SUFFIXES, must_exist=True, allow_subdirs=False + config, allowed_suffixes=_YAML_SUFFIXES, must_exist=True ) with yaml_file.open("r", encoding="utf-8") as f: config = yaml.safe_load(f) @@ -5845,7 +5832,7 @@ def load_process_config(yaml_path: str) -> Dict[str, Any]: ) yaml_file = _resolve_path_in_cwd( - yaml_path, allowed_suffixes=_YAML_SUFFIXES, must_exist=True, allow_subdirs=False + yaml_path, allowed_suffixes=_YAML_SUFFIXES, must_exist=True ) with yaml_file.open("r", encoding="utf-8") as f: return yaml.safe_load(f) @@ -5873,9 +5860,7 @@ def save_process_config(config: Dict[str, Any], yaml_path: str) -> None: "PyYAML is required for YAML support. Install with: pip install pyyaml" ) - yaml_file = _resolve_path_in_cwd( - yaml_path, allowed_suffixes=_YAML_SUFFIXES, allow_subdirs=False - ) + yaml_file = _resolve_path_in_cwd(yaml_path, allowed_suffixes=_YAML_SUFFIXES) yaml_file.parent.mkdir(parents=True, exist_ok=True) with yaml_file.open("w", encoding="utf-8") as f: yaml.dump(config, f, default_flow_style=False, sort_keys=False) diff --git a/tests/process/test_process_config_io_security.py b/tests/process/test_process_config_io_security.py deleted file mode 100644 index 9a29390..0000000 --- a/tests/process/test_process_config_io_security.py +++ /dev/null @@ -1,31 +0,0 @@ -import json - -import pytest - - -def test_resolve_path_rejects_absolute(monkeypatch, tmp_path): - monkeypatch.chdir(tmp_path) - from neqsim.process import processTools - - with pytest.raises(ValueError): - processTools._resolve_path_in_cwd(str(tmp_path / "x.yaml"), must_exist=False) - - -def test_resolve_path_rejects_traversal(monkeypatch, tmp_path): - monkeypatch.chdir(tmp_path) - from neqsim.process import processTools - - with pytest.raises(ValueError): - processTools._resolve_path_in_cwd("../secrets.yaml", must_exist=False) - - -def test_processbuilder_from_json_reads_local_file(monkeypatch, tmp_path): - monkeypatch.chdir(tmp_path) - from neqsim.process.processTools import ProcessBuilder - - cfg = {"name": "Test", "equipment": []} - (tmp_path / "process_config.json").write_text(json.dumps(cfg), encoding="utf-8") - - builder = ProcessBuilder.from_json("process_config.json") - assert builder.get_process() is not None - From 3206756b874ec42569596a6e0e0e4c91a2799540 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Sun, 14 Dec 2025 09:48:39 +0000 Subject: [PATCH 08/10] [pre-commit.ci lite] apply automatic fixes --- src/neqsim/process/processTools.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/neqsim/process/processTools.py b/src/neqsim/process/processTools.py index 959925a..a7c9eee 100644 --- a/src/neqsim/process/processTools.py +++ b/src/neqsim/process/processTools.py @@ -3159,7 +3159,9 @@ def from_json( >>> process = ProcessBuilder.from_json('process_config.json', ... fluids={'feed': my_fluid}).run() """ - json_file = _resolve_path_in_cwd(json_path, allowed_suffixes={".json"}, must_exist=True) + json_file = _resolve_path_in_cwd( + json_path, allowed_suffixes={".json"}, must_exist=True + ) with json_file.open("r", encoding="utf-8") as f: config = json.load(f) return cls.from_dict(config, fluids) From 7e3cbee7c8ea63f0f31d8ab7d68cd4edd1282795 Mon Sep 17 00:00:00 2001 From: Even Solbraa <41290109+EvenSol@users.noreply.github.com> Date: Sun, 14 Dec 2025 10:49:59 +0100 Subject: [PATCH 09/10] update --- src/neqsim/process/processTools.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/neqsim/process/processTools.py b/src/neqsim/process/processTools.py index 959925a..a7c9eee 100644 --- a/src/neqsim/process/processTools.py +++ b/src/neqsim/process/processTools.py @@ -3159,7 +3159,9 @@ def from_json( >>> process = ProcessBuilder.from_json('process_config.json', ... fluids={'feed': my_fluid}).run() """ - json_file = _resolve_path_in_cwd(json_path, allowed_suffixes={".json"}, must_exist=True) + json_file = _resolve_path_in_cwd( + json_path, allowed_suffixes={".json"}, must_exist=True + ) with json_file.open("r", encoding="utf-8") as f: config = json.load(f) return cls.from_dict(config, fluids) From 3cac95cef1c10ba68a3ce74efffbd8c6d811f637 Mon Sep 17 00:00:00 2001 From: Even Solbraa <41290109+EvenSol@users.noreply.github.com> Date: Sun, 14 Dec 2025 11:04:38 +0100 Subject: [PATCH 10/10] update --- README.md | 9 +-- docs/pvt_simulation.md | 6 +- .../pvt_experiments_java_access.py | 16 +++-- examples/pvtsimulation/pvt_tuning_cme.py | 4 +- examples/pvtsimulation/pvt_tuning_cvd.py | 4 +- .../pvtsimulation/pvt_tuning_viscosity.py | 6 +- src/neqsim/process/processTools.py | 49 ++++++++++----- src/neqsim/thermo/thermoTools.py | 61 +++++++++++++------ tests/process/test_ProcessTools.py | 9 ++- 9 files changed, 93 insertions(+), 71 deletions(-) diff --git a/README.md b/README.md index 7001a91..00c75a8 100644 --- a/README.md +++ b/README.md @@ -144,14 +144,7 @@ NeqSim also includes a `pvtsimulation` package for common PVT experiments (CCE/C - Documentation: `docs/pvt_simulation.md` - Direct Java access examples: `examples/pvtsimulation/README.md` -## Transient Multiphase Flow (Two-Fluid Model) - -NeqSim includes a mechanistic **Two-Fluid Transient Multiphase Flow Model** for pipeline simulation. - -- Jupyter notebook demo (direct Java access): `examples/jupyter/two_fluid_model.ipynb` -- Upstream documentation (NeqSim Java): https://github.com/equinor/neqsim/blob/master/docs/wiki/two_fluid_model.md - -### Prerequisites +## Prerequisites Java version 8 or higher ([Java JDK](https://adoptium.net/)) needs to be installed. The Python package [JPype](https://github.com/jpype-project/jpype) is used to connect Python and Java. Read the [installation requirements for Jpype](https://jpype.readthedocs.io/en/latest/install.html). Be aware that mixing 64 bit Python with 32 bit Java and vice versa crashes on import of the jpype module. The needed Python packages are listed in the [NeqSim Python dependencies page](https://github.com/equinor/neqsim-python/network/dependencies). diff --git a/docs/pvt_simulation.md b/docs/pvt_simulation.md index 897df35..86dca79 100644 --- a/docs/pvt_simulation.md +++ b/docs/pvt_simulation.md @@ -33,20 +33,18 @@ Other available simulations (direct Java access): Many PVTsimulation methods expect Java `double[]`. With JPype you can pass: -- A Python list (often auto-converted), or -- An explicit `double[]` using `jpype.types.JDouble[:]` +- A Python list (auto-converted to `double[]` by JPype) Example: ```python -from jpype.types import JDouble from neqsim import jneqsim pressures = [400.0, 300.0, 200.0] temperatures = [373.15, 373.15, 373.15] cme = jneqsim.pvtsimulation.simulation.ConstantMassExpansion(oil) -cme.setTemperaturesAndPressures(JDouble[:](temperatures), JDouble[:](pressures)) +cme.setTemperaturesAndPressures(temperatures, pressures) cme.runCalc() ``` diff --git a/examples/pvtsimulation/pvt_experiments_java_access.py b/examples/pvtsimulation/pvt_experiments_java_access.py index 8b4de73..11f5056 100644 --- a/examples/pvtsimulation/pvt_experiments_java_access.py +++ b/examples/pvtsimulation/pvt_experiments_java_access.py @@ -25,8 +25,6 @@ from __future__ import annotations -from jpype.types import JDouble - from neqsim import jneqsim from neqsim.thermo import TPflash, fluid @@ -71,7 +69,7 @@ def main() -> None: print("\n--- Constant Mass Expansion (CCE/CME) ---") cme = jneqsim.pvtsimulation.simulation.ConstantMassExpansion(oil.clone()) - cme.setTemperaturesAndPressures(JDouble[:](temperatures), JDouble[:](pressures)) + cme.setTemperaturesAndPressures(temperatures, pressures) cme.setTemperature(reservoir_temperature_k, "K") cme.runCalc() print("relativeVolume:", _as_list(cme.getRelativeVolume())) @@ -84,7 +82,7 @@ def main() -> None: print("\n--- Constant Volume Depletion (CVD) ---") cvd = jneqsim.pvtsimulation.simulation.ConstantVolumeDepletion(oil.clone()) - cvd.setPressures(JDouble[:](pressures)) + cvd.setPressures(pressures) cvd.setTemperature(reservoir_temperature_k, "K") cvd.runCalc() print("relativeVolume:", _as_list(cvd.getRelativeVolume())) @@ -96,7 +94,7 @@ def main() -> None: print("\n--- Differential Liberation (DL) ---") dl = jneqsim.pvtsimulation.simulation.DifferentialLiberation(oil.clone()) - dl.setPressures(JDouble[:](pressures + [1.01325])) + dl.setPressures(pressures + [1.01325]) dl.setTemperature(reservoir_temperature_k, "K") dl.runCalc() print("Bo [m3/Sm3]:", _as_list(dl.getBo())) @@ -107,7 +105,7 @@ def main() -> None: sep_pressures = [50.0, 10.0, 1.01325] sep_temperatures = [313.15, 303.15, 293.15] sep = jneqsim.pvtsimulation.simulation.SeparatorTest(oil.clone()) - sep.setSeparatorConditions(JDouble[:](sep_temperatures), JDouble[:](sep_pressures)) + sep.setSeparatorConditions(sep_temperatures, sep_pressures) sep.runCalc() print("separator GOR [Sm3/Sm3]:", _as_list(sep.getGOR())) print("separator Bo [m3/Sm3]:", _as_list(sep.getBofactor())) @@ -122,21 +120,21 @@ def main() -> None: swell = jneqsim.pvtsimulation.simulation.SwellingTest(oil.clone()) swell.setInjectionGas(injection_gas) swell.setTemperature(reservoir_temperature_k, "K") - swell.setCummulativeMolePercentGasInjected(JDouble[:](mol_percent_injected)) + swell.setCummulativeMolePercentGasInjected(mol_percent_injected) swell.runCalc() print("swell pressures [bara]:", _as_list(swell.getPressures())) print("relativeOilVolume [-]:", _as_list(swell.getRelativeOilVolume())) print("\n--- Viscosity study ---") visc = jneqsim.pvtsimulation.simulation.ViscositySim(oil.clone()) - visc.setTemperaturesAndPressures(JDouble[:](temperatures), JDouble[:](pressures)) + visc.setTemperaturesAndPressures(temperatures, pressures) visc.runCalc() print("oilViscosity [Pa*s]:", _as_list(visc.getOilViscosity())) print("gasViscosity [Pa*s]:", _as_list(visc.getGasViscosity())) print("\n--- GOR / Bo vs pressure ---") gor = jneqsim.pvtsimulation.simulation.GOR(oil.clone()) - gor.setTemperaturesAndPressures(JDouble[:](temperatures), JDouble[:](pressures)) + gor.setTemperaturesAndPressures(temperatures, pressures) gor.runCalc() print("GOR [Sm3/Sm3]:", _as_list(gor.getGOR())) print("Bo [m3/Sm3]:", _as_list(gor.getBofactor())) diff --git a/examples/pvtsimulation/pvt_tuning_cme.py b/examples/pvtsimulation/pvt_tuning_cme.py index 870724e..7fe3ef6 100644 --- a/examples/pvtsimulation/pvt_tuning_cme.py +++ b/examples/pvtsimulation/pvt_tuning_cme.py @@ -15,8 +15,6 @@ from __future__ import annotations -from jpype.types import JDouble - from neqsim import jneqsim from neqsim.thermo import fluid @@ -56,7 +54,7 @@ def main() -> None: cme = jneqsim.pvtsimulation.simulation.ConstantMassExpansion(oil) cme.setTemperature(temperature_k, "K") - cme.setTemperaturesAndPressures(JDouble[:](temperatures), JDouble[:](pressures)) + cme.setTemperaturesAndPressures(temperatures, pressures) cme.runCalc() rv_before = _as_list(cme.getRelativeVolume()) diff --git a/examples/pvtsimulation/pvt_tuning_cvd.py b/examples/pvtsimulation/pvt_tuning_cvd.py index 20ccb37..d71828c 100644 --- a/examples/pvtsimulation/pvt_tuning_cvd.py +++ b/examples/pvtsimulation/pvt_tuning_cvd.py @@ -14,8 +14,6 @@ from __future__ import annotations -from jpype.types import JDouble - from neqsim import jneqsim from neqsim.thermo import fluid @@ -53,7 +51,7 @@ def main() -> None: cvd = jneqsim.pvtsimulation.simulation.ConstantVolumeDepletion(oil) cvd.setTemperature(temperature_k, "K") - cvd.setTemperaturesAndPressures(JDouble[:](temperatures), JDouble[:](pressures)) + cvd.setTemperaturesAndPressures(temperatures, pressures) cvd.runCalc() rv_before = _as_list(cvd.getRelativeVolume()) diff --git a/examples/pvtsimulation/pvt_tuning_viscosity.py b/examples/pvtsimulation/pvt_tuning_viscosity.py index 202956b..91ff2c3 100644 --- a/examples/pvtsimulation/pvt_tuning_viscosity.py +++ b/examples/pvtsimulation/pvt_tuning_viscosity.py @@ -14,8 +14,6 @@ from __future__ import annotations -from jpype.types import JDouble - from neqsim import jneqsim from neqsim.thermo import fluid @@ -48,9 +46,7 @@ def main() -> None: exp_viscosity = [2.0e-4, 2.8e-4, 4.0e-4, 5.5e-4] visc = jneqsim.pvtsimulation.simulation.ViscositySim(oil) - visc.setTemperaturesAndPressures( - JDouble[:](temperatures_k), JDouble[:](pressures_bara) - ) + visc.setTemperaturesAndPressures(temperatures_k, pressures_bara) visc.runCalc() mu_before = _as_list(visc.getOilViscosity()) diff --git a/src/neqsim/process/processTools.py b/src/neqsim/process/processTools.py index a7c9eee..c38f90c 100644 --- a/src/neqsim/process/processTools.py +++ b/src/neqsim/process/processTools.py @@ -162,7 +162,6 @@ from typing import Any, Optional, List, Dict, Union import pandas as pd -from jpype.types import JDouble from jpype.types import * from neqsim import jneqsim @@ -174,6 +173,22 @@ _YAML_SUFFIXES = {".yaml", ".yml"} +def _as_float_list(values) -> list[float]: + if values is None: + return [] + if hasattr(values, "tolist"): + values = values.tolist() + return [float(v) for v in list(values)] + + +def _as_float_matrix(values) -> list[list[float]]: + if values is None: + return [] + if hasattr(values, "tolist"): + values = values.tolist() + return [[float(v) for v in row] for row in list(values)] + + def _resolve_path_in_cwd( user_path: str, *, @@ -4372,35 +4387,37 @@ def compressor( def compressorChart(compressor, curveConditions, speed, flow, head, polyEff): compressor.getCompressorChart().setCurves( - JDouble[:](curveConditions), - JDouble[:](speed), - JDouble[:][:](flow), - JDouble[:][:](head), - JDouble[:][:](polyEff), + _as_float_list(curveConditions), + _as_float_list(speed), + _as_float_matrix(flow), + _as_float_matrix(head), + _as_float_matrix(polyEff), ) def pumpChart(pump, curveConditions, speed, flow, head, polyEff): pump.getPumpChart().setCurves( - JDouble[:](curveConditions), - JDouble[:](speed), - JDouble[:][:](flow), - JDouble[:][:](head), - JDouble[:][:](polyEff), + _as_float_list(curveConditions), + _as_float_list(speed), + _as_float_matrix(flow), + _as_float_matrix(head), + _as_float_matrix(polyEff), ) def compressorSurgeCurve(compressor, curveConditions, surgeflow, surgehead): compressor.getCompressorChart().getSurgeCurve().setCurve( - JDouble[:](curveConditions), JDouble[:](surgeflow), JDouble[:](surgehead) + _as_float_list(curveConditions), + _as_float_list(surgeflow), + _as_float_list(surgehead), ) def compressorStoneWallCurve(compressor, curveConditions, stoneWallflow, stoneWallHead): compressor.getCompressorChart().getStoneWallCurve().setCurve( - JDouble[:](curveConditions), - JDouble[:](stoneWallflow), - JDouble[:](stoneWallHead), + _as_float_list(curveConditions), + _as_float_list(stoneWallflow), + _as_float_list(stoneWallHead), ) @@ -4534,7 +4551,7 @@ def splitter( spl = jneqsim.process.equipment.splitter.Splitter(name, teststream) if splitfactors is not None and len(splitfactors) > 0: spl.setSplitNumber(len(splitfactors)) - spl.setSplitFactors(JDouble[:](splitfactors)) + spl.setSplitFactors(_as_float_list(splitfactors)) _add_to_process(spl, process) return spl diff --git a/src/neqsim/thermo/thermoTools.py b/src/neqsim/thermo/thermoTools.py index 7339ea0..08b2569 100644 --- a/src/neqsim/thermo/thermoTools.py +++ b/src/neqsim/thermo/thermoTools.py @@ -277,6 +277,23 @@ logger = logging.getLogger(__name__) + +def _as_float_list(values) -> list[float]: + if values is None: + return [] + if hasattr(values, "tolist"): + values = values.tolist() + return [float(v) for v in list(values)] + + +def _as_float_matrix(values) -> list[list[float]]: + if values is None: + return [] + if hasattr(values, "tolist"): + values = values.tolist() + return [[float(v) for v in row] for row in list(values)] + + if has_matplotlib(): import matplotlib.pyplot as plt @@ -487,8 +504,8 @@ def createfluid2(names, molefractions=None, unit="mol/sec"): Fluid: The created fluid object. """ if molefractions is None: - fluidcreator.create2(JString[:](names)) - return fluidcreator.create2(JString[:](names), JDouble[:](molefractions), unit) + return fluidcreator.create2(JString[:](names)) + return fluidcreator.create2(JString[:](names), _as_float_list(molefractions), unit) def addOilFractions( @@ -503,9 +520,9 @@ def addOilFractions( ): fluid.addOilFractions( JString[:](charNames), - JDouble[:](molefractions), - JDouble[:](molarMass), - JDouble[:](density), + _as_float_list(molefractions), + _as_float_list(molarMass), + _as_float_list(density), lastIsPlusFraction, lumpComponents, numberOfPseudoComponents, @@ -522,8 +539,10 @@ def tunewaxmodel(fluid, experimentaldata, maxiterations=5): expList = [[x * 100.0 for x in experimentaldata["experiment"]]] waxsim = jneqsim.pvtsimulation.simulation.WaxFractionSim(fluid) - waxsim.setTemperaturesAndPressures(JDouble[:](tempList), JDouble[:](presList)) - waxsim.setExperimentalData(JDouble[:, :](expList)) + waxsim.setTemperaturesAndPressures( + _as_float_list(tempList), _as_float_list(presList) + ) + waxsim.setExperimentalData(_as_float_matrix(expList)) waxsim.getOptimizer().setNumberOfTuningParameters(3) waxsim.getOptimizer().setMaxNumberOfIterations(maxiterations) waxsim.runTuning() @@ -575,8 +594,8 @@ def calcproperties(gascondensateFluid, inputDict): """ properties = jneqsim.util.generator.PropertyGenerator( gascondensateFluid, - JDouble[:](inputDict["temperature"]), - JDouble[:](inputDict["pressure"]), + _as_float_list(inputDict["temperature"]), + _as_float_list(inputDict["pressure"]), ) props = properties.calculate() calculatedProperties = {k: list(v) for k, v in props.items()} @@ -725,7 +744,7 @@ def separatortest(fluid, pressure, temperature, GOR=None, Bo=None, display=False length = len(pressure) sepSim = jneqsim.pvtsimulation.simulation.SeparatorTest(fluid) - sepSim.setSeparatorConditions(JDouble[:](temperature), JDouble[:](pressure)) + sepSim.setSeparatorConditions(_as_float_list(temperature), _as_float_list(pressure)) sepSim.runCalc() for i in range(0, length): GOR.append(sepSim.getGOR()[i]) @@ -774,7 +793,7 @@ def CVD( length = len(pressure) cvdSim = jneqsim.pvtsimulation.simulation.ConstantVolumeDepletion(fluid) - cvdSim.setPressures(JDouble[:](pressure)) + cvdSim.setPressures(_as_float_list(pressure)) cvdSim.setTemperature(temperature) cvdSim.runCalc() for i in range(0, length): @@ -818,7 +837,9 @@ def viscositysim( aqueousviscosity = [] length = len(pressure) cmeSim = jneqsim.pvtsimulation.simulation.ViscositySim(fluid) - cmeSim.setTemperaturesAndPressures(JDouble[:](temperature), JDouble[:](pressure)) + cmeSim.setTemperaturesAndPressures( + _as_float_list(temperature), _as_float_list(pressure) + ) cmeSim.runCalc() for i in range(0, length): gasviscosity.append(cmeSim.getGasViscosity()[i]) @@ -880,7 +901,9 @@ def CME( length = len(pressure) cvdSim = jneqsim.pvtsimulation.simulation.ConstantMassExpansion(fluid) - cvdSim.setTemperaturesAndPressures(JDouble[:](temperature), JDouble[:](pressure)) + cvdSim.setTemperaturesAndPressures( + _as_float_list(temperature), _as_float_list(pressure) + ) cvdSim.runCalc() saturationPressure = cvdSim.getSaturationPressure() for i in range(0, length): @@ -952,7 +975,7 @@ def difflib( length = len(pressure) cvdSim = jneqsim.pvtsimulation.simulation.DifferentialLiberation(fluid) - cvdSim.setPressures(JDouble[:](pressure)) + cvdSim.setPressures(_as_float_list(pressure)) cvdSim.setTemperature(temperature) cvdSim.runCalc() for i in range(0, length): @@ -991,7 +1014,9 @@ def GOR(fluid, pressure, temperature, GORdata=None, Bo=None, display=False): length = len(pressure) jGOR = jneqsim.pvtsimulation.simulation.GOR(fluid) - jGOR.setTemperaturesAndPressures(JDouble[:](temperature), JDouble[:](pressure)) + jGOR.setTemperaturesAndPressures( + _as_float_list(temperature), _as_float_list(pressure) + ) jGOR.runCalc() for i in range(0, length): GORdata.append(jGOR.getGOR()[i]) @@ -1034,7 +1059,7 @@ def swellingtest( cvdSim.setInjectionGas(fluid2) cvdSim.setTemperature(temperature) cvdSim.setCummulativeMolePercentGasInjected( - JDouble[:](cummulativeMolePercentGasInjected) + _as_float_list(cummulativeMolePercentGasInjected) ) cvdSim.runCalc() for i in range(0, length2): @@ -1856,7 +1881,7 @@ def fluidComposition(testSystem, composition): Returns: None """ - testSystem.setMolarComposition(JDouble[:](composition)) + testSystem.setMolarComposition(_as_float_list(composition)) testSystem.init(0) @@ -1871,7 +1896,7 @@ def fluidCompositionPlus(testSystem, composition): Returns: None """ - testSystem.setMolarCompositionPlus(JDouble[:](composition)) + testSystem.setMolarCompositionPlus(_as_float_list(composition)) testSystem.init(0) diff --git a/tests/process/test_ProcessTools.py b/tests/process/test_ProcessTools.py index 81e07e5..992949b 100644 --- a/tests/process/test_ProcessTools.py +++ b/tests/process/test_ProcessTools.py @@ -23,7 +23,6 @@ ) from neqsim.thermo import TPflash, fluid, printFrame, fluid_df from pytest import approx -from jpype.types import JDouble from neqsim import jneqsim import pandas as pd import neqsim.standards @@ -149,7 +148,7 @@ def test_flowSplitter(): stream2 = stream("stre333", compressor_1.getOutStream()) streamSplit = splitter("split1", stream2) - streamSplit.setFlowRates(JDouble[:]([5.0, 0.1]), "MSm3/day") + streamSplit.setFlowRates([5.0, 0.1], "MSm3/day") resycStream1 = streamSplit.getSplitStream(1) @@ -166,17 +165,17 @@ def test_flowSplitter(): assert exportStream.getFlowRate("MSm3/day") == approx(5.0) assert streamresycl.getFlowRate("MSm3/day") == approx(0.1) - streamSplit.setFlowRates(JDouble[:]([5, 0.5]), "MSm3/day") + streamSplit.setFlowRates([5, 0.5], "MSm3/day") runProcess() assert exportStream.getFlowRate("MSm3/day") == approx(5.0) assert streamresycl.getFlowRate("MSm3/day") == approx(0.5) - streamSplit.setFlowRates(JDouble[:]([-1, 2.5]), "MSm3/day") + streamSplit.setFlowRates([-1, 2.5], "MSm3/day") runProcess() assert exportStream.getFlowRate("MSm3/day") == approx(5.0) assert streamresycl.getFlowRate("MSm3/day") == approx(2.5) - streamSplit.setSplitFactors(JDouble[:]([1.0, 0.0])) + streamSplit.setSplitFactors([1.0, 0.0]) runProcess() assert exportStream.getFlowRate("MSm3/day") == approx(5.0) assert streamresycl.getFlowRate("MSm3/day") == approx(0.0)