Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 0 additions & 14 deletions .pre-commit-config.yaml

This file was deleted.

28 changes: 22 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,28 @@ See the [NeqSim Python Wiki](https://github.com/equinor/neqsimpython/wiki) for h
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/neqsimpython/network/dependencies).


## Contributing

Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests.


## Discussion forum
## Contributing

Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests.

### Development tooling

This repository uses [`pre-commit`](https://pre-commit.com/) to run automated formatting and linting before each commit. After installing the project dependencies (for example with `poetry install`), point Git to the versioned hook scripts so the checks run automatically on every commit:

```
git config core.hooksPath .githooks
```

The configured hook calls `pre-commit` via Poetry when available, so the same tooling is used regardless of how the command is invoked. You can still install the hooks with `poetry run pre-commit install` if you prefer the standard workflow.

Run all hooks against the codebase at any time with:

```
poetry run pre-commit run --all-files
```


## Discussion forum

Questions related to neqsim can be posted in the [github discussion pages](https://github.com/equinor/neqsim/discussions).

Expand Down
6 changes: 5 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ repository = "https://github.com/Equinor/neqsimpython"
classifiers = ["Operating System :: OS Independent"]

[tool.poetry.dependencies]
python = "^3.8"
python = ">=3.8.1,<4.0"
JPype1 = "^1.5.0"
numpy = [
{ version = "^1.24.4", markers = "python_version == '3.8'" },
Expand All @@ -19,6 +19,7 @@ pandas = [
{ version = "^1.3.5", markers = "python_version == '3.8'" },
{ version = "^2.0.3", markers = "python_version > '3.8'" },
]
chemicals = { version = "^1.1.5", optional = true }
matplotlib = { version = "^3.7.0", optional = true }
jupyter = { version = "^1.0.0", optional = true }
tabulate = { version = "^0.9.0", optional = true }
Expand All @@ -27,9 +28,12 @@ tabulate = { version = "^0.9.0", optional = true }
black = ">=23.12,<25.0"
pytest = "^7.4.3"
pre-commit = "^3.5.0" # Higher versions require python 3.9+
flake8 = "^7.1.1"
isort = "^5.13.2"

[tool.poetry.extras]
interactive = ["matplotlib", "jupyter", "tabulate"]
extended-database = ["chemicals"]

[build-system]
requires = ["poetry-core"]
Expand Down
277 changes: 276 additions & 1 deletion src/neqsim/thermo/thermoTools.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,8 +265,10 @@

"""

import importlib
import logging
from typing import List, Optional, Union
from dataclasses import dataclass
from typing import List, Optional, Tuple, Union
import jpype
import pandas
from jpype.types import *
Expand Down Expand Up @@ -315,6 +317,256 @@
}


class ExtendedDatabaseError(Exception):
"""Raised when a component cannot be resolved in the extended database."""


@dataclass
class _ChemicalComponentData:
name: str
CAS: str
tc: float
pc: float
omega: float
molar_mass: Optional[float] = None
normal_boiling_point: Optional[float] = None
triple_point_temperature: Optional[float] = None
critical_volume: Optional[float] = None
critical_compressibility: Optional[float] = None


def _create_extended_database_provider():
"""Create a chemicals database provider."""

return _ChemicalsDatabaseProvider()


class _ChemicalsDatabaseProvider:
"""Lookup component data from the `chemicals` package."""

def __init__(self):
try:
from chemicals.identifiers import CAS_from_any
except ImportError as exc: # pragma: no cover - import guard
raise ModuleNotFoundError(
"The 'chemicals' package is required to use the extended component database."
) from exc

self._cas_from_any = CAS_from_any
critical = importlib.import_module("chemicals.critical")
try:
phase_change = importlib.import_module("chemicals.phase_change")
except ImportError: # pragma: no cover - optional submodule
phase_change = None
try:
elements = importlib.import_module("chemicals.elements")
except ImportError: # pragma: no cover - optional submodule
elements = None

self._tc = getattr(critical, "Tc")
self._pc = getattr(critical, "Pc")
self._omega = getattr(critical, "omega")
self._vc = getattr(critical, "Vc", None)
self._zc = getattr(critical, "Zc", None)
triple_point_candidates = [
getattr(critical, "Ttriple", None),
getattr(critical, "Tt", None),
]
if phase_change is not None:
triple_point_candidates.append(getattr(phase_change, "Tt", None))
self._triple_point = next((func for func in triple_point_candidates if func), None)
self._tb = getattr(phase_change, "Tb", None) if phase_change is not None else None
self._molecular_weight = (
getattr(elements, "molecular_weight", None) if elements is not None else None
)

def get_component(self, name: str) -> _ChemicalComponentData:
cas = self._cas_from_any(name)
if not cas:
raise ExtendedDatabaseError(
f"Component '{name}' was not found in the chemicals database."
)

tc = self._tc(cas)
pc = self._pc(cas)
omega = self._omega(cas)

if None in (tc, pc, omega):
raise ExtendedDatabaseError(
f"Incomplete property data for '{name}' (CAS {cas})."
)

molar_mass = self._call_optional(self._molecular_weight, cas)
if molar_mass is not None:
molar_mass = float(molar_mass) / 1000.0

normal_boiling_point = self._call_optional(self._tb, cas)
triple_point_temperature = self._call_optional(self._triple_point, cas)

critical_volume = self._call_optional(self._vc, cas)
if critical_volume is not None:
critical_volume = float(critical_volume) * 1.0e6 # m^3/mol -> cm^3/mol

critical_compressibility = self._call_optional(self._zc, cas)

return _ChemicalComponentData(
name=name,
CAS=cas,
tc=float(tc),
pc=float(pc) / 1.0e5, # chemicals returns pressure in Pa
omega=float(omega),
molar_mass=molar_mass,
normal_boiling_point=
float(normal_boiling_point) if normal_boiling_point is not None else None,
triple_point_temperature=
float(triple_point_temperature)
if triple_point_temperature is not None
else None,
critical_volume=critical_volume,
critical_compressibility=
float(critical_compressibility)
if critical_compressibility is not None
else None,
)

@staticmethod
def _call_optional(func, cas):
if func is None:
return None
for call in (
lambda: func(cas),
lambda: func(CASRN=cas),
):
try:
value = call()
except TypeError:
continue
except Exception: # pragma: no cover - defensive fallback
return None
else:
return value
return None


def _get_extended_provider(system):
provider = getattr(system, "_extended_database_provider", None)
if provider is None:
provider = _create_extended_database_provider()
system._extended_database_provider = provider # type: ignore[attr-defined]
return provider


def _apply_extended_properties(
system, component_names: Tuple[str, ...], data: _ChemicalComponentData
):
setter_map = {
"CAS": "setCASnumber",
"molar_mass": "setMolarMass",
"normal_boiling_point": "setNormalBoilingPoint",
"triple_point_temperature": "setTriplePointTemperature",
"critical_volume": "setCriticalVolume",
"critical_compressibility": "setCriticalCompressibilityFactor",
}

for phase_index in range(system.getNumberOfPhases()):
try:
phase = system.getPhase(phase_index)
except Exception: # pragma: no cover - defensive fallback
continue
if not hasattr(phase, "hasComponent"):
continue
component = None
for name in component_names:
if phase.hasComponent(name):
component = phase.getComponent(name)
break
if component is None:
continue
for field, setter_name in setter_map.items():
value = getattr(data, field, None)
if value is None:
continue
setter = getattr(component, setter_name, None)
if setter is None:
continue
setter(value)

def _system_interface_class():
"""Return the JPype proxy for ``neqsim.thermo.system.SystemInterface``."""

if not hasattr(_system_interface_class, "_cached"):
_system_interface_class._cached = jpype.JClass( # type: ignore[attr-defined]
"neqsim.thermo.system.SystemInterface"
)
return _system_interface_class._cached # type: ignore[attr-defined]


def _resolve_alias(name: str) -> str:
try:
return jneqsim.thermo.component.Component.getComponentNameFromAlias(name)
except Exception: # pragma: no cover - defensive alias resolution
return name


def _has_component_in_database(name: str) -> bool:
database = jneqsim.util.database.NeqSimDataBase
return database.hasComponent(name) or database.hasTempComponent(name)


def _args_look_like_component_properties(args: Tuple[object, ...]) -> bool:
return len(args) == 3 and all(isinstance(value, (int, float)) for value in args)


@jpype.JImplementationFor("neqsim.thermo.system.SystemInterface")
class _SystemInterface:
def useExtendedDatabase(self, enable: bool = True):
"""Enable or disable usage of the chemicals based component database."""

if enable:
provider = _create_extended_database_provider()
self._use_extended_database = True # type: ignore[attr-defined]
self._extended_database_provider = provider # type: ignore[attr-defined]
else:
self._use_extended_database = False # type: ignore[attr-defined]
if hasattr(self, "_extended_database_provider"):
delattr(self, "_extended_database_provider")
return self

def addComponent(self, name, amount, *args): # noqa: N802 - Java signature
alias_name = _resolve_alias(name)
component_data = None

if getattr(self, "_use_extended_database", False) and not _has_component_in_database(
alias_name
):
try:
provider = _get_extended_provider(self)
component_data = provider.get_component(name)
except (ExtendedDatabaseError, ModuleNotFoundError):
component_data = None

if component_data is not None and not _args_look_like_component_properties(args):
if args:
raise NotImplementedError(
"Extended database currently supports components specified in moles (unit='no') "
"without explicit phase targeting or alternative units."
)
result = _system_interface_class().addComponent(
self,
name,
float(amount),
component_data.tc,
component_data.pc,
component_data.omega,
)

_apply_extended_properties(self, (alias_name, name), component_data)

return result

return _system_interface_class().addComponent(self, name, amount, *args)


def fluid(name="srk", temperature=298.15, pressure=1.01325):
"""
Create a thermodynamic fluid system.
Expand Down Expand Up @@ -1100,6 +1352,29 @@ def addComponent(thermoSystem, name, moles, unit="no", phase=-10):
Returns:
None
"""
alias_name = _resolve_alias(name)

if getattr(thermoSystem, "_use_extended_database", False) and not _has_component_in_database(alias_name):
try:
provider = _get_extended_provider(thermoSystem)
component_data = provider.get_component(name)
except (ExtendedDatabaseError, ModuleNotFoundError):
component_data = None
if component_data is not None:
if unit != "no" or phase != -10:
raise NotImplementedError(
"Extended database currently supports components specified in moles (unit='no') "
"without explicit phase targeting."
)
thermoSystem.addComponent(
name,
moles,
component_data.tc,
component_data.pc,
component_data.omega,
)
return

if phase == -10 and unit == "no":
thermoSystem.addComponent(name, moles)
elif phase == -10:
Expand Down
Loading
Loading