-
Notifications
You must be signed in to change notification settings - Fork 19
option to import component properties from chemicals database #338
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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 * | ||||||||||||||||||
|
|
@@ -315,6 +317,270 @@ | |||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
| 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 | ||||||||||||||||||
|
||||||||||||||||||
| except Exception: # pragma: no cover - defensive fallback | |
| except Exception as e: # pragma: no cover - defensive fallback | |
| logger.warning( | |
| "Failed to get phase at index %d in system %r: %s", | |
| phase_index, system, e, | |
| exc_info=True | |
| ) |
Copilot
AI
Oct 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Catching bare Exception silently falls back to the original name. Consider catching specific exceptions (e.g., AttributeError, TypeError) or logging when alias resolution fails to help diagnose configuration issues.
| except Exception: # pragma: no cover - defensive alias resolution | |
| return name | |
| except (AttributeError, TypeError) as e: # pragma: no cover - defensive alias resolution | |
| logger.warning(f"Alias resolution failed for '{name}': {e}") | |
| return name | |
| except Exception as e: | |
| logger.error(f"Unexpected error during alias resolution for '{name}': {e}", exc_info=True) | |
| return name |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| import pytest | ||
|
|
||
|
|
||
| chemicals = pytest.importorskip("chemicals") | ||
|
|
||
| import chemicals.critical as critical_data # type: ignore # noqa: E402 | ||
| from chemicals.critical import Pc, Tc, Vc, Zc, omega # type: ignore # noqa: E402 | ||
| from chemicals.elements import molecular_weight # type: ignore # noqa: E402 | ||
| from chemicals.phase_change import Tb # type: ignore # noqa: E402 | ||
| from chemicals.identifiers import CAS_from_any # type: ignore # noqa: E402 | ||
EvenSol marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| from neqsim.thermo.thermoTools import addComponent, fluid | ||
|
|
||
|
|
||
| def test_use_extended_database_allows_missing_component(): | ||
| system = fluid("srk") | ||
|
|
||
| with pytest.raises(Exception): | ||
| system.addComponent("dimethylsulfoxide", 1.0) | ||
|
|
||
| system.useExtendedDatabase(True) | ||
| system.addComponent("dimethylsulfoxide", 1.0) | ||
|
|
||
| component = system.getPhase(0).getComponent("dimethylsulfoxide") | ||
| cas = CAS_from_any("dimethylsulfoxide") | ||
|
|
||
| assert pytest.approx(component.getTC(), rel=1e-6) == Tc(cas) | ||
| assert pytest.approx(component.getPC(), rel=1e-6) == Pc(cas) / 1.0e5 | ||
| assert pytest.approx(component.getAcentricFactor(), rel=1e-6) == omega(cas) | ||
|
|
||
| molar_mass = molecular_weight(CASRN=cas) | ||
| assert molar_mass is not None | ||
| assert pytest.approx(component.getMolarMass(), rel=1e-6) == molar_mass / 1000.0 | ||
|
|
||
| normal_boiling_point = Tb(cas) | ||
| if normal_boiling_point is not None: | ||
| assert ( | ||
| pytest.approx(component.getNormalBoilingPoint(), rel=1e-6) | ||
| == normal_boiling_point | ||
| ) | ||
|
|
||
| critical_volume = Vc(cas) | ||
| if critical_volume is not None: | ||
| assert ( | ||
| pytest.approx(component.getCriticalVolume(), rel=1e-6) | ||
| == critical_volume * 1.0e6 | ||
| ) | ||
|
|
||
| critical_compressibility = Zc(cas) | ||
| if critical_compressibility is not None: | ||
| assert ( | ||
| pytest.approx(component.getCriticalCompressibilityFactor(), rel=1e-6) | ||
| == critical_compressibility | ||
| ) | ||
|
|
||
| triple_point_func = getattr(critical_data, "Ttriple", None) or getattr( | ||
| critical_data, "Tt", None | ||
| ) | ||
EvenSol marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if triple_point_func is not None: | ||
| triple_point_temperature = triple_point_func(cas) | ||
| if triple_point_temperature is not None: | ||
| assert ( | ||
| pytest.approx(component.getTriplePointTemperature(), rel=1e-6) | ||
| == triple_point_temperature | ||
| ) | ||
|
|
||
|
|
||
| def test_module_add_component_uses_extended_database(): | ||
| system = fluid("srk") | ||
|
|
||
| with pytest.raises(Exception): | ||
| addComponent(system, "dimethylsulfoxide", 1.0) | ||
|
|
||
| system.useExtendedDatabase(True) | ||
| addComponent(system, "dimethylsulfoxide", 1.0) | ||
|
|
||
| assert system.getPhase(0).hasComponent("dimethylsulfoxide") | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Catching bare
Exceptionsilences all errors including unexpected ones. Consider catching specific exceptions that are expected during the function call, or at minimum log the error before returning None so debugging is possible.