Skip to content

Commit f231c1e

Browse files
committed
refactor: pure dataclass Fluid architecture with two-level caching
Architecture refactoring: - Replace ThermoSystem (JVM reference) with pure dataclass Fluid + FluidProperties - FluidStream is now a pure dataclass without JVM reference - Introduce FluidService with dependency injection for flash operations - Rename StreamMixer to SimplifiedStreamMixer - Extract remove_liquid to separate method Performance improvements: - Add two-level caching: reference fluid cache and flash operation cache - Add CacheService for centralized cache management Bug fixes: - Fix circular import issue in thermo.py - Fix cache stats printout for pytest Tests and documentation: - Add cache behavior tests for fluid service - Update snapshots for precision changes - Add reference state warning to flash_ph docstrings - Clarify standard density and cache key structure Chore: - Include anonymous volume in docker services to avoid venv issues
1 parent 0571c97 commit f231c1e

74 files changed

Lines changed: 7586 additions & 7023 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.dockerignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ venv/
33
docs/
44
examples/
55
.github/
6-
.pytest_cache/
6+
.pytest_cache/
7+
tests/.pytest_cache/

src/ecalc_neqsim_wrapper/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
from ecalc_neqsim_wrapper.cache_service import CacheService, LRUCache
2+
3+
# Import last to avoid circular import (fluid_service depends on thermo)
4+
from ecalc_neqsim_wrapper.fluid_service import NeqSimFluidService
15
from ecalc_neqsim_wrapper.java_service import NeqsimService
26
from ecalc_neqsim_wrapper.thermo import NeqsimFluid
37

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
"""Centralized cache management for NeqSim-related caches.
2+
3+
All caches registered here will be cleared when the JVM service shuts down,
4+
ensuring no dangling references to JVM objects.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
import threading
10+
from collections import OrderedDict
11+
from typing import Generic, TypeVar
12+
13+
K = TypeVar("K")
14+
V = TypeVar("V")
15+
16+
17+
class LRUCache(Generic[K, V]):
18+
"""Thread-safe LRU cache with statistics tracking."""
19+
20+
def __init__(self, max_size: int = 10000):
21+
self._cache: OrderedDict[K, V] = OrderedDict()
22+
self._max_size = max_size
23+
self._lock = threading.RLock()
24+
self._stats: dict[str, int] = {"hits": 0, "misses": 0, "evictions": 0}
25+
26+
def get(self, key: K) -> V | None:
27+
"""Get value from cache, returns None if not found."""
28+
with self._lock:
29+
if key in self._cache:
30+
self._cache.move_to_end(key)
31+
self._stats["hits"] += 1
32+
return self._cache[key]
33+
self._stats["misses"] += 1
34+
return None
35+
36+
def put(self, key: K, value: V) -> None:
37+
"""Add value to cache with LRU eviction."""
38+
with self._lock:
39+
if key in self._cache:
40+
self._cache.move_to_end(key)
41+
self._cache[key] = value
42+
43+
while len(self._cache) > self._max_size:
44+
self._cache.popitem(last=False)
45+
self._stats["evictions"] += 1
46+
47+
def clear(self) -> None:
48+
"""Clear all cached entries and reset statistics."""
49+
with self._lock:
50+
self._cache.clear()
51+
self._stats = {"hits": 0, "misses": 0, "evictions": 0}
52+
53+
def get_stats(self) -> dict[str, int | float]:
54+
"""Get cache statistics."""
55+
with self._lock:
56+
total = self._stats["hits"] + self._stats["misses"]
57+
hit_rate = (self._stats["hits"] / total * 100) if total > 0 else 0
58+
return {
59+
**self._stats,
60+
"size": len(self._cache),
61+
"max_size": self._max_size,
62+
"hit_rate_percent": round(hit_rate, 1),
63+
}
64+
65+
def __len__(self) -> int:
66+
with self._lock:
67+
return len(self._cache)
68+
69+
70+
class CacheService:
71+
"""Central registry for application caches.
72+
73+
Usage:
74+
# Create a cache
75+
my_cache = CacheService.create_cache("my_cache", max_size=1000)
76+
77+
# Use it
78+
my_cache.put(key, value)
79+
value = my_cache.get(key)
80+
81+
# All caches cleared on JVM shutdown automatically
82+
"""
83+
84+
_caches: dict[str, LRUCache] = {}
85+
_lock: threading.RLock = threading.RLock()
86+
87+
@classmethod
88+
def create_cache(cls, name: str, max_size: int = 10000) -> LRUCache:
89+
"""Create and register a named cache."""
90+
with cls._lock:
91+
if name in cls._caches:
92+
return cls._caches[name]
93+
cache: LRUCache = LRUCache(max_size)
94+
cls._caches[name] = cache
95+
return cache
96+
97+
@classmethod
98+
def get_cache(cls, name: str) -> LRUCache | None:
99+
"""Get a cache by name."""
100+
with cls._lock:
101+
return cls._caches.get(name)
102+
103+
@classmethod
104+
def clear_all(cls) -> None:
105+
"""Clear all registered caches. Called on JVM shutdown."""
106+
with cls._lock:
107+
for cache in cls._caches.values():
108+
cache.clear()
109+
110+
@classmethod
111+
def clear_cache(cls, name: str) -> None:
112+
"""Clear a specific cache by name."""
113+
with cls._lock:
114+
if name in cls._caches:
115+
cls._caches[name].clear()
116+
117+
@classmethod
118+
def get_all_stats(cls) -> dict[str, dict]:
119+
"""Get stats from all caches for monitoring."""
120+
with cls._lock:
121+
return {name: cache.get_stats() for name, cache in cls._caches.items()}

0 commit comments

Comments
 (0)