|
265 | 265 |
|
266 | 266 | """ |
267 | 267 |
|
268 | | -import importlib |
269 | 268 | import logging |
270 | | -from dataclasses import dataclass |
271 | | -from typing import List, Optional, Tuple, Union |
| 269 | +from typing import List, Optional, Union |
272 | 270 | import jpype |
273 | 271 | import pandas |
274 | 272 | from jpype.types import * |
|
317 | 315 | } |
318 | 316 |
|
319 | 317 |
|
320 | | -class ExtendedDatabaseError(Exception): |
321 | | - """Raised when a component cannot be resolved in the extended database.""" |
322 | | - |
323 | | - |
324 | | -@dataclass |
325 | | -class _ChemicalComponentData: |
326 | | - name: str |
327 | | - CAS: str |
328 | | - tc: float |
329 | | - pc: float |
330 | | - omega: float |
331 | | - molar_mass: Optional[float] = None |
332 | | - normal_boiling_point: Optional[float] = None |
333 | | - triple_point_temperature: Optional[float] = None |
334 | | - critical_volume: Optional[float] = None |
335 | | - critical_compressibility: Optional[float] = None |
336 | | - |
337 | | - |
338 | | -def _create_extended_database_provider(): |
339 | | - """Create a chemicals database provider.""" |
340 | | - |
341 | | - return _ChemicalsDatabaseProvider() |
342 | | - |
343 | | - |
344 | | -class _ChemicalsDatabaseProvider: |
345 | | - """Lookup component data from the `chemicals` package.""" |
346 | | - |
347 | | - def __init__(self): |
348 | | - try: |
349 | | - from chemicals.identifiers import CAS_from_any |
350 | | - except ImportError as exc: # pragma: no cover - import guard |
351 | | - raise ModuleNotFoundError( |
352 | | - "The 'chemicals' package is required to use the extended component database." |
353 | | - ) from exc |
354 | | - |
355 | | - self._cas_from_any = CAS_from_any |
356 | | - critical = importlib.import_module("chemicals.critical") |
357 | | - try: |
358 | | - phase_change = importlib.import_module("chemicals.phase_change") |
359 | | - except ImportError: # pragma: no cover - optional submodule |
360 | | - phase_change = None |
361 | | - try: |
362 | | - elements = importlib.import_module("chemicals.elements") |
363 | | - except ImportError: # pragma: no cover - optional submodule |
364 | | - elements = None |
365 | | - |
366 | | - self._tc = getattr(critical, "Tc") |
367 | | - self._pc = getattr(critical, "Pc") |
368 | | - self._omega = getattr(critical, "omega") |
369 | | - self._vc = getattr(critical, "Vc", None) |
370 | | - self._zc = getattr(critical, "Zc", None) |
371 | | - triple_point_candidates = [ |
372 | | - getattr(critical, "Ttriple", None), |
373 | | - getattr(critical, "Tt", None), |
374 | | - ] |
375 | | - if phase_change is not None: |
376 | | - triple_point_candidates.append(getattr(phase_change, "Tt", None)) |
377 | | - self._triple_point = next( |
378 | | - (func for func in triple_point_candidates if func), None |
379 | | - ) |
380 | | - self._tb = ( |
381 | | - getattr(phase_change, "Tb", None) if phase_change is not None else None |
382 | | - ) |
383 | | - self._molecular_weight = ( |
384 | | - getattr(elements, "molecular_weight", None) |
385 | | - if elements is not None |
386 | | - else None |
387 | | - ) |
388 | | - |
389 | | - def get_component(self, name: str) -> _ChemicalComponentData: |
390 | | - cas = self._cas_from_any(name) |
391 | | - if not cas: |
392 | | - raise ExtendedDatabaseError( |
393 | | - f"Component '{name}' was not found in the chemicals database." |
394 | | - ) |
395 | | - |
396 | | - tc = self._tc(cas) |
397 | | - pc = self._pc(cas) |
398 | | - omega = self._omega(cas) |
399 | | - |
400 | | - if None in (tc, pc, omega): |
401 | | - raise ExtendedDatabaseError( |
402 | | - f"Incomplete property data for '{name}' (CAS {cas})." |
403 | | - ) |
404 | | - |
405 | | - molar_mass = self._call_optional(self._molecular_weight, cas) |
406 | | - if molar_mass is not None: |
407 | | - molar_mass = float(molar_mass) / 1000.0 |
408 | | - |
409 | | - normal_boiling_point = self._call_optional(self._tb, cas) |
410 | | - triple_point_temperature = self._call_optional(self._triple_point, cas) |
411 | | - |
412 | | - critical_volume = self._call_optional(self._vc, cas) |
413 | | - if critical_volume is not None: |
414 | | - critical_volume = float(critical_volume) * 1.0e6 # m^3/mol -> cm^3/mol |
415 | | - |
416 | | - critical_compressibility = self._call_optional(self._zc, cas) |
417 | | - |
418 | | - return _ChemicalComponentData( |
419 | | - name=name, |
420 | | - CAS=cas, |
421 | | - tc=float(tc), |
422 | | - pc=float(pc) / 1.0e5, # chemicals returns pressure in Pa |
423 | | - omega=float(omega), |
424 | | - molar_mass=molar_mass, |
425 | | - normal_boiling_point=( |
426 | | - float(normal_boiling_point) |
427 | | - if normal_boiling_point is not None |
428 | | - else None |
429 | | - ), |
430 | | - triple_point_temperature=( |
431 | | - float(triple_point_temperature) |
432 | | - if triple_point_temperature is not None |
433 | | - else None |
434 | | - ), |
435 | | - critical_volume=critical_volume, |
436 | | - critical_compressibility=( |
437 | | - float(critical_compressibility) |
438 | | - if critical_compressibility is not None |
439 | | - else None |
440 | | - ), |
441 | | - ) |
442 | | - |
443 | | - @staticmethod |
444 | | - def _call_optional(func, cas): |
445 | | - if func is None: |
446 | | - return None |
447 | | - for call in ( |
448 | | - lambda: func(cas), |
449 | | - lambda: func(CASRN=cas), |
450 | | - ): |
451 | | - try: |
452 | | - value = call() |
453 | | - except TypeError: |
454 | | - continue |
455 | | - except Exception: # pragma: no cover - defensive fallback |
456 | | - return None |
457 | | - else: |
458 | | - return value |
459 | | - return None |
460 | | - |
461 | | - |
462 | | -def _get_extended_provider(system): |
463 | | - provider = getattr(system, "_extended_database_provider", None) |
464 | | - if provider is None: |
465 | | - provider = _create_extended_database_provider() |
466 | | - system._extended_database_provider = provider # type: ignore[attr-defined] |
467 | | - return provider |
468 | | - |
469 | | - |
470 | | -def _apply_extended_properties( |
471 | | - system, component_names: Tuple[str, ...], data: _ChemicalComponentData |
472 | | -): |
473 | | - setter_map = { |
474 | | - "CAS": "setCASnumber", |
475 | | - "molar_mass": "setMolarMass", |
476 | | - "normal_boiling_point": "setNormalBoilingPoint", |
477 | | - "triple_point_temperature": "setTriplePointTemperature", |
478 | | - "critical_volume": "setCriticalVolume", |
479 | | - "critical_compressibility": "setCriticalCompressibilityFactor", |
480 | | - } |
481 | | - |
482 | | - for phase_index in range(system.getNumberOfPhases()): |
483 | | - try: |
484 | | - phase = system.getPhase(phase_index) |
485 | | - except Exception: # pragma: no cover - defensive fallback |
486 | | - continue |
487 | | - if not hasattr(phase, "hasComponent"): |
488 | | - continue |
489 | | - component = None |
490 | | - for name in component_names: |
491 | | - if phase.hasComponent(name): |
492 | | - component = phase.getComponent(name) |
493 | | - break |
494 | | - if component is None: |
495 | | - continue |
496 | | - for field, setter_name in setter_map.items(): |
497 | | - value = getattr(data, field, None) |
498 | | - if value is None: |
499 | | - continue |
500 | | - setter = getattr(component, setter_name, None) |
501 | | - if setter is None: |
502 | | - continue |
503 | | - setter(value) |
504 | | - |
505 | | - |
506 | | -def _system_interface_class(): |
507 | | - """Return the JPype proxy for ``neqsim.thermo.system.SystemInterface``.""" |
508 | | - |
509 | | - if not hasattr(_system_interface_class, "_cached"): |
510 | | - _system_interface_class._cached = jpype.JClass( # type: ignore[attr-defined] |
511 | | - "neqsim.thermo.system.SystemInterface" |
512 | | - ) |
513 | | - return _system_interface_class._cached # type: ignore[attr-defined] |
514 | | - |
515 | | - |
516 | | -def _resolve_alias(name: str) -> str: |
517 | | - try: |
518 | | - return jneqsim.thermo.component.Component.getComponentNameFromAlias(name) |
519 | | - except Exception: # pragma: no cover - defensive alias resolution |
520 | | - return name |
521 | | - |
522 | | - |
523 | | -def _has_component_in_database(name: str) -> bool: |
524 | | - database = jneqsim.util.database.NeqSimDataBase |
525 | | - return database.hasComponent(name) or database.hasTempComponent(name) |
526 | | - |
527 | | - |
528 | | -def _args_look_like_component_properties(args: Tuple[object, ...]) -> bool: |
529 | | - return len(args) == 3 and all(isinstance(value, (int, float)) for value in args) |
530 | | - |
531 | | - |
532 | | -@jpype.JImplementationFor("neqsim.thermo.system.SystemInterface") |
533 | | -class _SystemInterface: |
534 | | - def useExtendedDatabase(self, enable: bool = True): |
535 | | - """Enable or disable usage of the chemicals based component database.""" |
536 | | - |
537 | | - if enable: |
538 | | - provider = _create_extended_database_provider() |
539 | | - self._use_extended_database = True # type: ignore[attr-defined] |
540 | | - self._extended_database_provider = provider # type: ignore[attr-defined] |
541 | | - else: |
542 | | - self._use_extended_database = False # type: ignore[attr-defined] |
543 | | - if hasattr(self, "_extended_database_provider"): |
544 | | - delattr(self, "_extended_database_provider") |
545 | | - return self |
546 | | - |
547 | | - def addComponent(self, name, amount, *args): # noqa: N802 - Java signature |
548 | | - alias_name = _resolve_alias(name) |
549 | | - component_data = None |
550 | | - |
551 | | - if getattr( |
552 | | - self, "_use_extended_database", False |
553 | | - ) and not _has_component_in_database(alias_name): |
554 | | - try: |
555 | | - provider = _get_extended_provider(self) |
556 | | - component_data = provider.get_component(name) |
557 | | - except (ExtendedDatabaseError, ModuleNotFoundError): |
558 | | - component_data = None |
559 | | - |
560 | | - if component_data is not None and not _args_look_like_component_properties( |
561 | | - args |
562 | | - ): |
563 | | - if args: |
564 | | - raise NotImplementedError( |
565 | | - "Extended database currently supports components specified in moles (unit='no') " |
566 | | - "without explicit phase targeting or alternative units." |
567 | | - ) |
568 | | - result = _system_interface_class().addComponent( |
569 | | - self, |
570 | | - name, |
571 | | - float(amount), |
572 | | - component_data.tc, |
573 | | - component_data.pc, |
574 | | - component_data.omega, |
575 | | - ) |
576 | | - |
577 | | - _apply_extended_properties(self, (alias_name, name), component_data) |
578 | | - |
579 | | - return result |
580 | | - |
581 | | - return _system_interface_class().addComponent(self, name, amount, *args) |
582 | | - |
583 | | - |
584 | 318 | def fluid(name="srk", temperature=298.15, pressure=1.01325): |
585 | 319 | """ |
586 | 320 | Create a thermodynamic fluid system. |
@@ -1366,31 +1100,6 @@ def addComponent(thermoSystem, name, moles, unit="no", phase=-10): |
1366 | 1100 | Returns: |
1367 | 1101 | None |
1368 | 1102 | """ |
1369 | | - alias_name = _resolve_alias(name) |
1370 | | - |
1371 | | - if getattr( |
1372 | | - thermoSystem, "_use_extended_database", False |
1373 | | - ) and not _has_component_in_database(alias_name): |
1374 | | - try: |
1375 | | - provider = _get_extended_provider(thermoSystem) |
1376 | | - component_data = provider.get_component(name) |
1377 | | - except (ExtendedDatabaseError, ModuleNotFoundError): |
1378 | | - component_data = None |
1379 | | - if component_data is not None: |
1380 | | - if unit != "no" or phase != -10: |
1381 | | - raise NotImplementedError( |
1382 | | - "Extended database currently supports components specified in moles (unit='no') " |
1383 | | - "without explicit phase targeting." |
1384 | | - ) |
1385 | | - thermoSystem.addComponent( |
1386 | | - name, |
1387 | | - moles, |
1388 | | - component_data.tc, |
1389 | | - component_data.pc, |
1390 | | - component_data.omega, |
1391 | | - ) |
1392 | | - return |
1393 | | - |
1394 | 1103 | if phase == -10 and unit == "no": |
1395 | 1104 | thermoSystem.addComponent(name, moles) |
1396 | 1105 | elif phase == -10: |
|
0 commit comments