优化仿真求解性能并修复流量闭合问题(初版)
This commit is contained in:
1 parent
57b459bc72
commit
5332a788f3
55 files changed
+8973
-549
No files matched your search
@@ -0,0 +1,214 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from math import nextafter
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import textwrap
|
||||
from threading import Barrier
|
||||
import unittest
|
||||
|
||||
from app.simulation.components.amesim.media.mediums import (
|
||||
AmesimHeliumPengRobinsonMedium,
|
||||
)
|
||||
from app.simulation.property_cache import (
|
||||
cache_property_calculation,
|
||||
current_property_cache,
|
||||
property_cache_run,
|
||||
)
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
class SimulationPropertyCacheTests(unittest.TestCase):
|
||||
def test_reuses_only_exact_inputs(self) -> None:
|
||||
medium = AmesimHeliumPengRobinsonMedium()
|
||||
pressure = 15.3e6
|
||||
temperature = 293.15
|
||||
|
||||
with property_cache_run() as cache:
|
||||
assert cache is not None
|
||||
first = medium.density(pressure, temperature)
|
||||
repeated = medium.density(pressure, temperature)
|
||||
changed = medium.density(nextafter(pressure, float("inf")), temperature)
|
||||
info = cache.info()
|
||||
|
||||
self.assertEqual(repeated, first)
|
||||
self.assertNotEqual(changed, first)
|
||||
self.assertEqual(info.hits, 1)
|
||||
self.assertEqual(info.misses, 2)
|
||||
self.assertEqual(info.current_entries, 2)
|
||||
|
||||
def test_failed_calculations_are_not_cached(self) -> None:
|
||||
class FailingProperty:
|
||||
def __init__(self) -> None:
|
||||
self.calls = 0
|
||||
|
||||
@cache_property_calculation("failure")
|
||||
def calculate(self, value: float) -> float:
|
||||
self.calls += 1
|
||||
raise ValueError(f"invalid {value}")
|
||||
|
||||
owner = FailingProperty()
|
||||
with property_cache_run() as cache:
|
||||
assert cache is not None
|
||||
for _ in range(2):
|
||||
with self.assertRaisesRegex(ValueError, "invalid"):
|
||||
owner.calculate(1.0)
|
||||
info = cache.info()
|
||||
|
||||
self.assertEqual(owner.calls, 2)
|
||||
self.assertEqual(info.hits, 0)
|
||||
self.assertEqual(info.misses, 2)
|
||||
self.assertEqual(info.current_entries, 0)
|
||||
|
||||
def test_lru_capacity_is_bounded_and_oldest_entry_is_evicted(self) -> None:
|
||||
class CachedProperty:
|
||||
def __init__(self) -> None:
|
||||
self.calls = 0
|
||||
|
||||
@cache_property_calculation("bounded")
|
||||
def calculate(self, value: int) -> int:
|
||||
self.calls += 1
|
||||
return value * 10
|
||||
|
||||
owner = CachedProperty()
|
||||
with property_cache_run(max_entries=2) as cache:
|
||||
assert cache is not None
|
||||
owner.calculate(1)
|
||||
owner.calculate(2)
|
||||
owner.calculate(1)
|
||||
owner.calculate(3)
|
||||
second = owner.calculate(2)
|
||||
info = cache.info()
|
||||
|
||||
self.assertEqual(second, 20)
|
||||
self.assertEqual(owner.calls, 4)
|
||||
self.assertEqual(info.current_entries, 2)
|
||||
self.assertEqual(info.evictions, 2)
|
||||
|
||||
def test_each_run_gets_an_independent_cache_and_releases_context(self) -> None:
|
||||
medium = AmesimHeliumPengRobinsonMedium()
|
||||
with property_cache_run() as first_cache:
|
||||
assert first_cache is not None
|
||||
medium.density(100_000.0, 300.0)
|
||||
medium.density(100_000.0, 300.0)
|
||||
first_info = first_cache.info()
|
||||
|
||||
self.assertIsNone(current_property_cache())
|
||||
|
||||
with property_cache_run() as second_cache:
|
||||
assert second_cache is not None
|
||||
medium.density(100_000.0, 300.0)
|
||||
second_info = second_cache.info()
|
||||
|
||||
self.assertIsNot(first_cache, second_cache)
|
||||
self.assertEqual(first_info.hits, 1)
|
||||
self.assertEqual(second_info.hits, 0)
|
||||
self.assertEqual(second_info.misses, 1)
|
||||
self.assertIsNone(current_property_cache())
|
||||
|
||||
def test_async_tasks_do_not_share_run_local_caches(self) -> None:
|
||||
medium = AmesimHeliumPengRobinsonMedium()
|
||||
|
||||
async def exercise(pressure: float) -> tuple[int, int, int]:
|
||||
with property_cache_run() as cache:
|
||||
assert cache is not None
|
||||
await asyncio.sleep(0)
|
||||
medium.density(pressure, 300.0)
|
||||
medium.density(pressure, 300.0)
|
||||
info = cache.info()
|
||||
return id(cache), info.hits, info.misses
|
||||
|
||||
async def run_both() -> list[tuple[int, int, int]]:
|
||||
return list(
|
||||
await asyncio.gather(
|
||||
exercise(100_000.0),
|
||||
exercise(200_000.0),
|
||||
)
|
||||
)
|
||||
|
||||
results = asyncio.run(run_both())
|
||||
|
||||
self.assertNotEqual(results[0][0], results[1][0])
|
||||
self.assertEqual(results[0][1:], (1, 1))
|
||||
self.assertEqual(results[1][1:], (1, 1))
|
||||
|
||||
def test_worker_threads_do_not_share_run_local_caches(self) -> None:
|
||||
medium = AmesimHeliumPengRobinsonMedium()
|
||||
barrier = Barrier(2)
|
||||
|
||||
def exercise(pressure: float) -> tuple[int, int, int]:
|
||||
with property_cache_run() as cache:
|
||||
assert cache is not None
|
||||
barrier.wait(timeout=5.0)
|
||||
medium.density(pressure, 300.0)
|
||||
medium.density(pressure, 300.0)
|
||||
info = cache.info()
|
||||
return id(cache), info.hits, info.misses
|
||||
|
||||
with ThreadPoolExecutor(max_workers=2) as executor:
|
||||
results = list(
|
||||
executor.map(
|
||||
exercise,
|
||||
(100_000.0, 200_000.0),
|
||||
)
|
||||
)
|
||||
|
||||
self.assertNotEqual(results[0][0], results[1][0])
|
||||
self.assertEqual(results[0][1:], (1, 1))
|
||||
self.assertEqual(results[1][1:], (1, 1))
|
||||
|
||||
def test_cache_on_and_off_produce_identical_helium_results(self) -> None:
|
||||
script = textwrap.dedent(
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
from app.main import build_reactflow_system_xml, run_system_xml_simulation
|
||||
from tests.test_amesim_pnvo001_signal_xml import (
|
||||
high_pressure_helium_step_project,
|
||||
)
|
||||
|
||||
result = run_system_xml_simulation(
|
||||
build_reactflow_system_xml(high_pressure_helium_step_project())
|
||||
)
|
||||
assert result["success"], result["message"]
|
||||
payload = json.dumps(
|
||||
{
|
||||
"status": result["status"],
|
||||
"series": result["series"],
|
||||
"final": result["final"],
|
||||
},
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
).encode("utf-8")
|
||||
print(hashlib.sha256(payload).hexdigest())
|
||||
"""
|
||||
)
|
||||
hashes: list[str] = []
|
||||
for enabled in ("on", "off"):
|
||||
environment = os.environ.copy()
|
||||
environment["SIMULATIONAPP_PROFILE"] = "off"
|
||||
environment["SIMULATIONAPP_PROPERTY_CACHE"] = enabled
|
||||
completed = subprocess.run(
|
||||
[sys.executable, "-c", script],
|
||||
cwd=PROJECT_ROOT,
|
||||
env=environment,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
)
|
||||
hashes.append(completed.stdout.strip())
|
||||
|
||||
self.assertEqual(hashes[0], hashes[1])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user