propax 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- propax/__init__.py +12 -0
- propax/build_tables.py +244 -0
- propax/core/__init__.py +3 -0
- propax/core/_state.py +122 -0
- propax/core/config.py +205 -0
- propax/core/domain.py +38 -0
- propax/core/flash/__init__.py +52 -0
- propax/core/flash/dispatch.py +176 -0
- propax/core/flash/results.py +93 -0
- propax/core/flash/single_phase.py +548 -0
- propax/core/flash/tables.py +52 -0
- propax/core/flash/two_phase.py +311 -0
- propax/core/interface.py +646 -0
- propax/core/interp.py +313 -0
- propax/core/saturation.py +179 -0
- propax/core/tolerances.py +170 -0
- propax/fluids/__init__.py +0 -0
- propax/fluids/_registry.py +84 -0
- propax/fluids/data/1-butene.json +961 -0
- propax/fluids/data/acetone.json +1356 -0
- propax/fluids/data/argon.json +1722 -0
- propax/fluids/data/benzene.json +866 -0
- propax/fluids/data/carbonmonoxide.json +956 -0
- propax/fluids/data/carbonylsulfide.json +703 -0
- propax/fluids/data/cis-2-butene.json +886 -0
- propax/fluids/data/cyclohexane.json +633 -0
- propax/fluids/data/cyclopentane.json +598 -0
- propax/fluids/data/cyclopropane.json +593 -0
- propax/fluids/data/d4.json +737 -0
- propax/fluids/data/d5.json +1948 -0
- propax/fluids/data/deuterium.json +440 -0
- propax/fluids/data/dichloroethane.json +1758 -0
- propax/fluids/data/diethylether.json +590 -0
- propax/fluids/data/dimethylcarbonate.json +1776 -0
- propax/fluids/data/dimethylether.json +836 -0
- propax/fluids/data/ethane.json +1734 -0
- propax/fluids/data/ethanol.json +1246 -0
- propax/fluids/data/ethylbenzene.json +918 -0
- propax/fluids/data/ethylene.json +1471 -0
- propax/fluids/data/ethyleneoxide.json +743 -0
- propax/fluids/data/heavywater.json +1889 -0
- propax/fluids/data/helium.json +912 -0
- propax/fluids/data/hfe143m.json +858 -0
- propax/fluids/data/hydrogen.json +592 -0
- propax/fluids/data/hydrogenchloride.json +793 -0
- propax/fluids/data/hydrogensulfide.json +560 -0
- propax/fluids/data/isobutane.json +1372 -0
- propax/fluids/data/isobutene.json +886 -0
- propax/fluids/data/isohexane.json +762 -0
- propax/fluids/data/isopentane.json +1110 -0
- propax/fluids/data/krypton.json +650 -0
- propax/fluids/data/m-xylene.json +1687 -0
- propax/fluids/data/md2m.json +795 -0
- propax/fluids/data/md3m.json +795 -0
- propax/fluids/data/md4m.json +737 -0
- propax/fluids/data/mdm.json +596 -0
- propax/fluids/data/methane.json +1709 -0
- propax/fluids/data/methyllinoleate.json +802 -0
- propax/fluids/data/methyllinolenate.json +702 -0
- propax/fluids/data/methyloleate.json +702 -0
- propax/fluids/data/methylpalmitate.json +701 -0
- propax/fluids/data/methylstearate.json +701 -0
- propax/fluids/data/mm.json +818 -0
- propax/fluids/data/n-butane.json +1221 -0
- propax/fluids/data/n-decane.json +735 -0
- propax/fluids/data/n-dodecane.json +1940 -0
- propax/fluids/data/n-hexane.json +797 -0
- propax/fluids/data/n-nonane.json +865 -0
- propax/fluids/data/n-octane.json +866 -0
- propax/fluids/data/n-pentane.json +799 -0
- propax/fluids/data/n-perfluorobutane.json +732 -0
- propax/fluids/data/n-perfluorohexane.json +592 -0
- propax/fluids/data/n-perfluoropentane.json +592 -0
- propax/fluids/data/n-propane.json +1462 -0
- propax/fluids/data/neon.json +597 -0
- propax/fluids/data/neopentane.json +782 -0
- propax/fluids/data/nitrogen.json +1430 -0
- propax/fluids/data/nitrousoxide.json +730 -0
- propax/fluids/data/novec649.json +1483 -0
- propax/fluids/data/o-xylene.json +1761 -0
- propax/fluids/data/orthodeuterium.json +440 -0
- propax/fluids/data/orthohydrogen.json +590 -0
- propax/fluids/data/oxygen.json +1422 -0
- propax/fluids/data/p-xylene.json +1903 -0
- propax/fluids/data/paradeuterium.json +438 -0
- propax/fluids/data/parahydrogen.json +596 -0
- propax/fluids/data/propylene.json +1427 -0
- propax/fluids/data/propyleneglycol.json +1212 -0
- propax/fluids/data/propyne.json +600 -0
- propax/fluids/data/r11.json +1024 -0
- propax/fluids/data/r1123.json +723 -0
- propax/fluids/data/r113.json +1133 -0
- propax/fluids/data/r1130(e).json +795 -0
- propax/fluids/data/r1132(e).json +1308 -0
- propax/fluids/data/r114.json +402 -0
- propax/fluids/data/r115.json +677 -0
- propax/fluids/data/r116.json +959 -0
- propax/fluids/data/r12.json +877 -0
- propax/fluids/data/r1224ydz.json +1993 -0
- propax/fluids/data/r123.json +1053 -0
- propax/fluids/data/r1233zd(e).json +1566 -0
- propax/fluids/data/r1234yf.json +933 -0
- propax/fluids/data/r1234ze(e).json +1836 -0
- propax/fluids/data/r1234ze(z).json +1397 -0
- propax/fluids/data/r124.json +914 -0
- propax/fluids/data/r1243zf.json +1690 -0
- propax/fluids/data/r13.json +401 -0
- propax/fluids/data/r1336mzz(e).json +877 -0
- propax/fluids/data/r1336mzz(z).json +1637 -0
- propax/fluids/data/r134a.json +868 -0
- propax/fluids/data/r13i1.json +832 -0
- propax/fluids/data/r14.json +742 -0
- propax/fluids/data/r141b.json +839 -0
- propax/fluids/data/r142b.json +962 -0
- propax/fluids/data/r143a.json +858 -0
- propax/fluids/data/r152a.json +855 -0
- propax/fluids/data/r161.json +1759 -0
- propax/fluids/data/r21.json +600 -0
- propax/fluids/data/r218.json +704 -0
- propax/fluids/data/r22.json +1887 -0
- propax/fluids/data/r227ea.json +1294 -0
- propax/fluids/data/r23.json +781 -0
- propax/fluids/data/r236ea.json +1549 -0
- propax/fluids/data/r236fa.json +1193 -0
- propax/fluids/data/r245ca.json +731 -0
- propax/fluids/data/r245fa.json +1682 -0
- propax/fluids/data/r32.json +1609 -0
- propax/fluids/data/r365mfc.json +1471 -0
- propax/fluids/data/r40.json +579 -0
- propax/fluids/data/r404a.json +1076 -0
- propax/fluids/data/r407c.json +1628 -0
- propax/fluids/data/r41.json +703 -0
- propax/fluids/data/r410a.json +793 -0
- propax/fluids/data/r507a.json +990 -0
- propax/fluids/data/rc318.json +598 -0
- propax/fluids/data/ses36.json +585 -0
- propax/fluids/data/sulfurdioxide.json +602 -0
- propax/fluids/data/sulfurhexafluoride.json +1023 -0
- propax/fluids/data/tetrahydrofuran.json +740 -0
- propax/fluids/data/toluene.json +842 -0
- propax/fluids/data/trans-2-butene.json +840 -0
- propax/fluids/data/vinylchloride.json +1762 -0
- propax/fluids/data/xenon.json +668 -0
- propax/fluids/generic/__init__.py +17 -0
- propax/fluids/generic/conductivity/__init__.py +2 -0
- propax/fluids/generic/conductivity/rational_polynomial.py +154 -0
- propax/fluids/generic/conductivity/schema_cond.py +185 -0
- propax/fluids/generic/conductivity/slots.py +266 -0
- propax/fluids/generic/eos/__init__.py +1 -0
- propax/fluids/generic/eos/helmholtz.py +345 -0
- propax/fluids/generic/eos/mixture_terms.py +4 -0
- propax/fluids/generic/eos/pure_terms.py +424 -0
- propax/fluids/generic/eos/schema_eos.py +167 -0
- propax/fluids/generic/transport_registry.py +138 -0
- propax/fluids/generic/viscosity/__init__.py +2 -0
- propax/fluids/generic/viscosity/rainwater_friend.py +95 -0
- propax/fluids/generic/viscosity/schema_visc.py +198 -0
- propax/fluids/generic/viscosity/slots.py +249 -0
- propax/fluids/schema.py +132 -0
- propax/make_fluid.py +110 -0
- propax/py.typed +0 -0
- propax/utils/__init__.py +0 -0
- propax/utils/build_utils/__init__.py +32 -0
- propax/utils/build_utils/assemble.py +608 -0
- propax/utils/build_utils/dome.py +166 -0
- propax/utils/build_utils/helpers.py +128 -0
- propax/utils/build_utils/single_phase.py +65 -0
- propax/utils/exact/__init__.py +31 -0
- propax/utils/exact/constants.py +69 -0
- propax/utils/exact/critical.py +261 -0
- propax/utils/exact/curve.py +116 -0
- propax/utils/exact/fit.py +214 -0
- propax/utils/exact/precision.py +57 -0
- propax/utils/exact/superancillary.py +297 -0
- propax/utils/make_utils/__init__.py +3 -0
- propax/utils/make_utils/build.py +321 -0
- propax/utils/make_utils/conductivity.py +138 -0
- propax/utils/make_utils/coverage.py +241 -0
- propax/utils/make_utils/eos.py +290 -0
- propax/utils/make_utils/source.py +99 -0
- propax/utils/make_utils/viscosity.py +120 -0
- propax/utils/numerics.py +26 -0
- propax/utils/solvers.py +165 -0
- propax/utils/types.py +7 -0
- propax-0.1.0.dist-info/METADATA +247 -0
- propax-0.1.0.dist-info/RECORD +189 -0
- propax-0.1.0.dist-info/WHEEL +5 -0
- propax-0.1.0.dist-info/licenses/LICENSE +21 -0
- propax-0.1.0.dist-info/top_level.txt +1 -0
propax/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from .core import Interface
|
|
2
|
+
from .fluids.generic import (
|
|
3
|
+
register_conductivity_residual,
|
|
4
|
+
register_viscosity_higher_order,
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
# restrain what can be imported if user enters `from propax import *`
|
|
8
|
+
__all__ = [
|
|
9
|
+
"Interface",
|
|
10
|
+
"register_viscosity_higher_order",
|
|
11
|
+
"register_conductivity_residual",
|
|
12
|
+
]
|
propax/build_tables.py
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import textwrap
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from .utils.build_utils import (
|
|
10
|
+
EQS_REGISTRY,
|
|
11
|
+
build_adaptive_table,
|
|
12
|
+
build_vectorized_table,
|
|
13
|
+
fluid_table_registry,
|
|
14
|
+
get_table_path,
|
|
15
|
+
logger,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def create_all_tables(
|
|
20
|
+
fluid_name: str,
|
|
21
|
+
target: float,
|
|
22
|
+
tables_base_path: Optional[Path] = None,
|
|
23
|
+
):
|
|
24
|
+
"""
|
|
25
|
+
Create all interpolation tables defined in TABLE_REGISTRY for a given
|
|
26
|
+
fluid.
|
|
27
|
+
|
|
28
|
+
`target` switches the axes from uniform to graded: they are then refined
|
|
29
|
+
until the measured interpolation error meets it, which spends nodes only
|
|
30
|
+
where a measurement asks for them.
|
|
31
|
+
|
|
32
|
+
Please note a table does not have to meet high precision requirements, by
|
|
33
|
+
design it is more suited for typical float32 compatible tolerances (~1e-5
|
|
34
|
+
to ~1e-7 rtols).
|
|
35
|
+
"""
|
|
36
|
+
if tables_base_path is None:
|
|
37
|
+
tables_base_path = get_table_path()
|
|
38
|
+
|
|
39
|
+
failures: list = []
|
|
40
|
+
for table_cfg in fluid_table_registry(fluid_name):
|
|
41
|
+
try:
|
|
42
|
+
if target is None:
|
|
43
|
+
build_vectorized_table(fluid_name, table_cfg, tables_base_path)
|
|
44
|
+
else:
|
|
45
|
+
build_adaptive_table(
|
|
46
|
+
fluid_name, table_cfg, tables_base_path, target=target
|
|
47
|
+
)
|
|
48
|
+
except Exception as e:
|
|
49
|
+
logger.error(f" ERROR building {table_cfg.name}: {e}", exc_info=True)
|
|
50
|
+
failures.append((table_cfg.name, e))
|
|
51
|
+
|
|
52
|
+
if failures:
|
|
53
|
+
names = ", ".join(name for name, _ in failures)
|
|
54
|
+
raise RuntimeError(
|
|
55
|
+
f"{len(failures)} table(s) of {fluid_name} failed to build: {names}. "
|
|
56
|
+
"The fluid is on disk but incomplete; see the logged tracebacks."
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def cached_fluids(tables_base_path: Optional[Path] = None) -> dict:
|
|
61
|
+
base = tables_base_path or get_table_path()
|
|
62
|
+
if not base.exists():
|
|
63
|
+
return {}
|
|
64
|
+
out = {}
|
|
65
|
+
for d in sorted(base.iterdir()):
|
|
66
|
+
if not d.is_dir():
|
|
67
|
+
continue
|
|
68
|
+
tables = sorted(t.name for t in d.iterdir() if t.is_dir())
|
|
69
|
+
size = sum(f.stat().st_size for f in d.rglob("*") if f.is_file())
|
|
70
|
+
out[d.name] = (tables, size)
|
|
71
|
+
return out
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def remove_fluid(fluid_name: str, tables_base_path: Optional[Path] = None) -> int:
|
|
75
|
+
base = tables_base_path or get_table_path()
|
|
76
|
+
target = base / fluid_name.lower().replace(" ", "")
|
|
77
|
+
if not target.is_dir():
|
|
78
|
+
raise FileNotFoundError(f"{target} is not in the cache")
|
|
79
|
+
size = sum(f.stat().st_size for f in target.rglob("*") if f.is_file())
|
|
80
|
+
shutil.rmtree(target)
|
|
81
|
+
return size
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _megabytes(n: int) -> str:
|
|
85
|
+
return f"{n / 1e6:.0f} MB"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def show_cache(base: Path) -> int:
|
|
89
|
+
cache = cached_fluids(base)
|
|
90
|
+
if not cache:
|
|
91
|
+
print(f"cache empty ({base})")
|
|
92
|
+
return 0
|
|
93
|
+
total = sum(size for _, size in cache.values())
|
|
94
|
+
print(f"{base} {len(cache)} fluid(s), {_megabytes(total)}\n")
|
|
95
|
+
for name, (tables, size) in cache.items():
|
|
96
|
+
print(f" {name:16s} {size / 1e6:8.1f} MB {', '.join(tables) or '(empty)'}")
|
|
97
|
+
return 0
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def clear_cache(base: Path, assume_yes: bool) -> int:
|
|
101
|
+
cache = cached_fluids(base)
|
|
102
|
+
if not cache:
|
|
103
|
+
print(f"cache already empty ({base})")
|
|
104
|
+
return 0
|
|
105
|
+
total = sum(size for _, size in cache.values())
|
|
106
|
+
question = f"delete {len(cache)} fluid(s), {_megabytes(total)}, from {base}? [y/N] "
|
|
107
|
+
if not assume_yes and not _confirmed(question):
|
|
108
|
+
print("aborted")
|
|
109
|
+
return 1
|
|
110
|
+
shutil.rmtree(base)
|
|
111
|
+
print(f"removed {_megabytes(total)}")
|
|
112
|
+
return 0
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _confirmed(question: str) -> bool:
|
|
116
|
+
try:
|
|
117
|
+
return input(question).strip().lower() == "y"
|
|
118
|
+
except EOFError: # not answered is a no
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def remove_fluids(names: list, base: Path) -> int:
|
|
123
|
+
status = 0
|
|
124
|
+
for name in names:
|
|
125
|
+
try:
|
|
126
|
+
freed = remove_fluid(name, base)
|
|
127
|
+
except FileNotFoundError as e:
|
|
128
|
+
print(f"{name}: {e}")
|
|
129
|
+
status = 1
|
|
130
|
+
continue
|
|
131
|
+
print(f"removed {name} ({_megabytes(freed)})")
|
|
132
|
+
return status
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def build_fluids(names: list, base: Path, target: float) -> int:
|
|
136
|
+
incomplete = []
|
|
137
|
+
for fluid in names:
|
|
138
|
+
try:
|
|
139
|
+
create_all_tables(fluid, tables_base_path=base, target=target)
|
|
140
|
+
except Exception as e:
|
|
141
|
+
logger.error(f"{fluid}: {e}")
|
|
142
|
+
incomplete.append(fluid)
|
|
143
|
+
|
|
144
|
+
if incomplete:
|
|
145
|
+
print(f"\nINCOMPLETE: {len(incomplete)} fluid(s) -> {', '.join(incomplete)}")
|
|
146
|
+
return 1
|
|
147
|
+
print(f"\nbuilt {len(names)} fluid(s) into {base}")
|
|
148
|
+
return 0
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _positive_int(raw: str) -> int:
|
|
152
|
+
value = int(raw)
|
|
153
|
+
if value < 1:
|
|
154
|
+
raise argparse.ArgumentTypeError("must be at least 1")
|
|
155
|
+
return value
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
159
|
+
parser = argparse.ArgumentParser(
|
|
160
|
+
prog="python -m propax.build_tables",
|
|
161
|
+
description=textwrap.dedent("""
|
|
162
|
+
Build the interpolation tables a fluid needs, into the propax cache
|
|
163
|
+
(PROPAX_TABLE_DIR, else ~/.cache/propax).
|
|
164
|
+
|
|
165
|
+
A table is a few tens to a few hundred MB, so build only the fluids
|
|
166
|
+
you actually use there is no reason to hold all covered fluids.
|
|
167
|
+
Grid corners outside the physical domain are filled from their nearest
|
|
168
|
+
valid neighbour; the warnings about them are only informative.
|
|
169
|
+
"""),
|
|
170
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
171
|
+
)
|
|
172
|
+
parser.add_argument("fluids", nargs="*", help="fluid names, e.g. hydrogen argon")
|
|
173
|
+
|
|
174
|
+
action = parser.add_mutually_exclusive_group()
|
|
175
|
+
action.add_argument("--all", action="store_true", help="build every known fluid")
|
|
176
|
+
action.add_argument("--list", action="store_true", help="show what the cache holds")
|
|
177
|
+
action.add_argument(
|
|
178
|
+
"--remove", metavar="FLUID", nargs="+", help="delete these from the cache"
|
|
179
|
+
)
|
|
180
|
+
action.add_argument("--clear", action="store_true", help="delete the whole cache")
|
|
181
|
+
|
|
182
|
+
parser.add_argument(
|
|
183
|
+
"--yes", action="store_true", help="answer yes to --clear's confirmation"
|
|
184
|
+
)
|
|
185
|
+
parser.add_argument(
|
|
186
|
+
"--table-dir", metavar="DIR", help="override the cache location"
|
|
187
|
+
)
|
|
188
|
+
parser.add_argument(
|
|
189
|
+
"--chunk-size",
|
|
190
|
+
type=_positive_int,
|
|
191
|
+
metavar="N",
|
|
192
|
+
help="map at most N nodes at a time instead of the whole grid; lowers "
|
|
193
|
+
"peak memory at some cost in speed (try 1024 if a build is swapping)",
|
|
194
|
+
)
|
|
195
|
+
parser.add_argument(
|
|
196
|
+
"--target",
|
|
197
|
+
type=float,
|
|
198
|
+
metavar="ERR",
|
|
199
|
+
help="grade the axes until the measured interpolation error meets ERR "
|
|
200
|
+
"(e.g. 1e-5) instead of using a uniform grid",
|
|
201
|
+
)
|
|
202
|
+
return parser
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _apply_settings(args) -> None:
|
|
206
|
+
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
|
|
207
|
+
if args.chunk_size is not None:
|
|
208
|
+
os.environ["PROPAX_CHUNK_SIZE"] = str(args.chunk_size)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _targets(args, parser: argparse.ArgumentParser) -> list:
|
|
212
|
+
if args.all:
|
|
213
|
+
return sorted(EQS_REGISTRY)
|
|
214
|
+
if not args.fluids:
|
|
215
|
+
parser.error(
|
|
216
|
+
"name at least one fluid, or pass --all / --list / --remove / --clear"
|
|
217
|
+
)
|
|
218
|
+
names = [f.lower().replace(" ", "") for f in args.fluids]
|
|
219
|
+
unknown = [f for f in names if f not in EQS_REGISTRY]
|
|
220
|
+
if unknown:
|
|
221
|
+
parser.error(
|
|
222
|
+
f"unknown fluid(s): {', '.join(unknown)}. "
|
|
223
|
+
f"Known: {', '.join(sorted(EQS_REGISTRY))}"
|
|
224
|
+
)
|
|
225
|
+
return names
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def main(argv=None) -> int:
|
|
229
|
+
parser = _build_parser()
|
|
230
|
+
args = parser.parse_args(argv)
|
|
231
|
+
_apply_settings(args)
|
|
232
|
+
base = Path(args.table_dir) if args.table_dir else get_table_path()
|
|
233
|
+
|
|
234
|
+
if args.list:
|
|
235
|
+
return show_cache(base)
|
|
236
|
+
if args.clear:
|
|
237
|
+
return clear_cache(base, args.yes)
|
|
238
|
+
if args.remove:
|
|
239
|
+
return remove_fluids(args.remove, base)
|
|
240
|
+
return build_fluids(_targets(args, parser), base, args.target)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
if __name__ == "__main__":
|
|
244
|
+
raise SystemExit(main())
|
propax/core/__init__.py
ADDED
propax/core/_state.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from typing import Iterator
|
|
3
|
+
|
|
4
|
+
import equinox as eqx
|
|
5
|
+
import jax
|
|
6
|
+
import jax.numpy as jnp
|
|
7
|
+
from jaxtyping import Array
|
|
8
|
+
|
|
9
|
+
from propax.utils.types import jaxBool, jaxFloat
|
|
10
|
+
|
|
11
|
+
from .config import ThermoVar
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PropertyMap(Mapping):
|
|
15
|
+
"""A property map that cannot be mutated in place.
|
|
16
|
+
|
|
17
|
+
Built from `ThermoVar` keys, read back by either those or the internal
|
|
18
|
+
name they carry. `replace` and `discard` return a new map, so a value
|
|
19
|
+
reaching a traced call is never rebound behind it.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
__slots__ = ("_entries",)
|
|
23
|
+
|
|
24
|
+
def __init__(self, entries=()):
|
|
25
|
+
stored = {}
|
|
26
|
+
for key, value in dict(entries).items():
|
|
27
|
+
if not isinstance(key, ThermoVar):
|
|
28
|
+
raise KeyError(
|
|
29
|
+
f"{key!r} cannot key a PropertyMap, only a ThermoVar can. "
|
|
30
|
+
"Use PropertyMap.of_internal to build one from internal names."
|
|
31
|
+
)
|
|
32
|
+
stored[key.internal_key] = value
|
|
33
|
+
self._entries = stored
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def of_internal(cls, entries) -> "PropertyMap":
|
|
37
|
+
built = cls.__new__(cls)
|
|
38
|
+
built._entries = dict(entries)
|
|
39
|
+
return built
|
|
40
|
+
|
|
41
|
+
def replace(self, entries) -> "PropertyMap":
|
|
42
|
+
merged = dict(self._entries)
|
|
43
|
+
for key, value in dict(entries).items():
|
|
44
|
+
merged[key.internal_key if isinstance(key, ThermoVar) else key] = value
|
|
45
|
+
return PropertyMap.of_internal(merged)
|
|
46
|
+
|
|
47
|
+
def discard(self, key) -> "PropertyMap":
|
|
48
|
+
name = key.internal_key if isinstance(key, ThermoVar) else key
|
|
49
|
+
if name not in self._entries:
|
|
50
|
+
raise KeyError(f"nothing stored under {key!r}")
|
|
51
|
+
return PropertyMap.of_internal(
|
|
52
|
+
{k: v for k, v in self._entries.items() if k != name}
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def __getitem__(self, key):
|
|
56
|
+
name = key.internal_key if isinstance(key, ThermoVar) else key
|
|
57
|
+
try:
|
|
58
|
+
return self._entries[name]
|
|
59
|
+
except KeyError:
|
|
60
|
+
held = ", ".join(sorted(self._entries)) or "nothing"
|
|
61
|
+
raise KeyError(f"{key!r} is not in this map, which holds {held}") from None
|
|
62
|
+
|
|
63
|
+
def __repr__(self) -> str:
|
|
64
|
+
return f"PropertyMap({', '.join(sorted(self._entries))})"
|
|
65
|
+
|
|
66
|
+
# functions to implement for mapping
|
|
67
|
+
def __iter__(self) -> Iterator[str]:
|
|
68
|
+
return iter(self._entries)
|
|
69
|
+
|
|
70
|
+
def __len__(self) -> int:
|
|
71
|
+
return len(self._entries)
|
|
72
|
+
|
|
73
|
+
def __reduce__(self):
|
|
74
|
+
names, values = _split(self)
|
|
75
|
+
return (_rebuild, (names, values))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _split(state: PropertyMap):
|
|
79
|
+
names = tuple(sorted(state))
|
|
80
|
+
return names, tuple(state[name] for name in names)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _rebuild(names, values) -> PropertyMap:
|
|
84
|
+
return PropertyMap.of_internal(zip(names, values))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _flatten(state: PropertyMap):
|
|
88
|
+
names, values = _split(state)
|
|
89
|
+
return values, names
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
jax.tree_util.register_pytree_node(PropertyMap, _flatten, _rebuild)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _to_array(v):
|
|
96
|
+
return jnp.asarray(v)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class TxResult(eqx.Module):
|
|
100
|
+
"""A two-phase state: T or P known, and the quality with it.
|
|
101
|
+
|
|
102
|
+
`mix` carries D, U, H, S of the mixture, `L` and `V` the same on each
|
|
103
|
+
saturated branch.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
mix: PropertyMap
|
|
107
|
+
L: PropertyMap
|
|
108
|
+
V: PropertyMap
|
|
109
|
+
x: jaxFloat
|
|
110
|
+
T: jaxFloat
|
|
111
|
+
P: jaxFloat
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class SaturationResult(eqx.Module):
|
|
115
|
+
"""A point of the saturation line: T and P, and D, U, H, S on both
|
|
116
|
+
branches, with no quality to place the state between them."""
|
|
117
|
+
|
|
118
|
+
L: PropertyMap
|
|
119
|
+
V: PropertyMap
|
|
120
|
+
T: Array = eqx.field(converter=_to_array)
|
|
121
|
+
P: Array = eqx.field(converter=_to_array)
|
|
122
|
+
is_valid: jaxBool = eqx.field(default=True, converter=_to_array)
|
propax/core/config.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from enum import IntEnum, StrEnum
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from types import MappingProxyType
|
|
6
|
+
from typing import List, Literal, Mapping, Optional
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, Field, model_validator
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_table_path() -> Path:
|
|
12
|
+
override = os.environ.get("PROPAX_TABLE_DIR")
|
|
13
|
+
if override:
|
|
14
|
+
return Path(override)
|
|
15
|
+
home = Path.home() / ".cache" / "propax"
|
|
16
|
+
home.mkdir(parents=True, exist_ok=True)
|
|
17
|
+
return home
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_chunk_size() -> Optional[int]:
|
|
21
|
+
"""How many nodes a mapped build call may hold at once, None for no cap
|
|
22
|
+
|
|
23
|
+
Use with :
|
|
24
|
+
|
|
25
|
+
`python -m propax.build_tables --chunk-size`
|
|
26
|
+
"""
|
|
27
|
+
raw = os.environ.get("PROPAX_CHUNK_SIZE")
|
|
28
|
+
if not raw:
|
|
29
|
+
return None
|
|
30
|
+
n = int(raw)
|
|
31
|
+
if n < 1:
|
|
32
|
+
raise ValueError(f"PROPAX_CHUNK_SIZE must be at least 1, got {n}")
|
|
33
|
+
return n
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True, slots=True)
|
|
37
|
+
class VarSpec:
|
|
38
|
+
"""What the flash needs to know about one property.
|
|
39
|
+
|
|
40
|
+
The CoolProp key is the enum member's own value, so it is not repeated here.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
internal_key: str
|
|
44
|
+
can_be_input: bool = True
|
|
45
|
+
linear_in_x: bool = False
|
|
46
|
+
invert_for_mixing: bool = False
|
|
47
|
+
sat_key_L: Optional[str] = None
|
|
48
|
+
sat_key_V: Optional[str] = None
|
|
49
|
+
sat_key_unique: Optional[str] = None
|
|
50
|
+
scale: float = 1.0
|
|
51
|
+
unit: str = ""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ThermoVar(StrEnum):
|
|
55
|
+
"""Canonical names for thermodynamic properties, for library internal use.
|
|
56
|
+
|
|
57
|
+
The member's value is the CoolProp key; everything else the flash needs
|
|
58
|
+
hangs off `spec`.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
P = "P"
|
|
62
|
+
T = "T"
|
|
63
|
+
H = "H"
|
|
64
|
+
S = "S"
|
|
65
|
+
U = "U"
|
|
66
|
+
D = "D"
|
|
67
|
+
Q = "Q"
|
|
68
|
+
CVMASS = "CVMASS"
|
|
69
|
+
CPMASS = "CPMASS"
|
|
70
|
+
VISCOSITY = "VISCOSITY"
|
|
71
|
+
CONDUCTIVITY = "CONDUCTIVITY"
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def spec(self) -> VarSpec:
|
|
75
|
+
return _SPECS[self]
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def internal_key(self) -> str:
|
|
79
|
+
return _SPECS[self].internal_key
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
_SPECS: Mapping[ThermoVar, VarSpec] = MappingProxyType(
|
|
83
|
+
{
|
|
84
|
+
ThermoVar.P: VarSpec("P", sat_key_unique="P_sat", scale=1e6, unit="Pa"),
|
|
85
|
+
ThermoVar.T: VarSpec("T", sat_key_unique="T_sat", scale=100.0, unit="K"),
|
|
86
|
+
ThermoVar.H: VarSpec(
|
|
87
|
+
"h",
|
|
88
|
+
sat_key_L="h_L",
|
|
89
|
+
sat_key_V="h_V",
|
|
90
|
+
linear_in_x=True,
|
|
91
|
+
scale=1e5,
|
|
92
|
+
unit="H",
|
|
93
|
+
),
|
|
94
|
+
ThermoVar.S: VarSpec(
|
|
95
|
+
"s",
|
|
96
|
+
sat_key_L="s_L",
|
|
97
|
+
sat_key_V="s_V",
|
|
98
|
+
linear_in_x=True,
|
|
99
|
+
scale=1e3,
|
|
100
|
+
unit="J/K",
|
|
101
|
+
),
|
|
102
|
+
ThermoVar.U: VarSpec(
|
|
103
|
+
"u",
|
|
104
|
+
sat_key_L="u_L",
|
|
105
|
+
sat_key_V="u_V",
|
|
106
|
+
linear_in_x=True,
|
|
107
|
+
scale=1e5,
|
|
108
|
+
unit="J",
|
|
109
|
+
),
|
|
110
|
+
ThermoVar.D: VarSpec(
|
|
111
|
+
"rho",
|
|
112
|
+
sat_key_L="rho_L",
|
|
113
|
+
sat_key_V="rho_V",
|
|
114
|
+
invert_for_mixing=True,
|
|
115
|
+
scale=10.0,
|
|
116
|
+
unit="kg/m3",
|
|
117
|
+
),
|
|
118
|
+
# An input, but only beside P or T `dispatch.SATURATED` says where.
|
|
119
|
+
# Any other partner would need a solve, and a quality is not a state
|
|
120
|
+
# variable of the EOS, so there is no bracket to lay one on.
|
|
121
|
+
ThermoVar.Q: VarSpec("x"),
|
|
122
|
+
ThermoVar.CVMASS: VarSpec("cv", can_be_input=False, unit="J/(K.kg)"),
|
|
123
|
+
ThermoVar.CPMASS: VarSpec("cp", can_be_input=False, unit="J/(K.kg)"),
|
|
124
|
+
ThermoVar.VISCOSITY: VarSpec("viscosity", can_be_input=False, unit="Pa/s"),
|
|
125
|
+
ThermoVar.CONDUCTIVITY: VarSpec("conductivity", can_be_input=False, unit="S/m"),
|
|
126
|
+
}
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class PhaseID(IntEnum):
|
|
131
|
+
UNKNOWN = -1
|
|
132
|
+
TWO_PHASE = 0
|
|
133
|
+
LIQUID = 1
|
|
134
|
+
GAS = 2
|
|
135
|
+
SUPERCRITICAL_GAS = 3
|
|
136
|
+
SUPERCRITICAL = 4
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class Axis(BaseModel):
|
|
140
|
+
variable: ThermoVar
|
|
141
|
+
min_val: float
|
|
142
|
+
max_val: float
|
|
143
|
+
spacing: Literal["linear", "log"] = "linear"
|
|
144
|
+
n_points: int = 1000
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def key(self) -> str:
|
|
148
|
+
return str(self.variable.value)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class TableSpec(BaseModel):
|
|
152
|
+
"""`outputs` is whichever of the natural variables (D, T) the axes do not
|
|
153
|
+
already give; a dome table appends the quality, since below the saturation
|
|
154
|
+
line (D, T) does not name the state on its own. Every other property is
|
|
155
|
+
recovered from the EOS at that (rho, T) when it is asked for, never stored:
|
|
156
|
+
a stored value is one more thing to keep consistent, and interpolating it
|
|
157
|
+
is strictly worse than evaluating the EOS the table just located.
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
x_axis: Axis
|
|
161
|
+
y_axis: Axis
|
|
162
|
+
outputs: List[ThermoVar] = Field(default_factory=list)
|
|
163
|
+
|
|
164
|
+
@model_validator(mode="after")
|
|
165
|
+
def _outputs_complete_the_flash(self):
|
|
166
|
+
axes = {self.x_axis.variable, self.y_axis.variable}
|
|
167
|
+
expected = {ThermoVar.D, ThermoVar.T} - axes
|
|
168
|
+
if set(self.outputs) != expected:
|
|
169
|
+
raise ValueError(
|
|
170
|
+
f"{self.name}: outputs must be exactly {sorted(v.value for v in expected)}, "
|
|
171
|
+
f"got {sorted(v.value for v in self.outputs)}. A table stores the flash "
|
|
172
|
+
"solution (D, T, and the quality under the dome); everything else comes "
|
|
173
|
+
"from the EOS."
|
|
174
|
+
)
|
|
175
|
+
return self
|
|
176
|
+
|
|
177
|
+
@property
|
|
178
|
+
def name(self) -> str:
|
|
179
|
+
return "_".join(("Table", self.x_axis.key, self.y_axis.key))
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _table(x: Axis, y: Axis) -> TableSpec:
|
|
183
|
+
axes = {x.variable, y.variable}
|
|
184
|
+
return TableSpec(
|
|
185
|
+
x_axis=x, y_axis=y, outputs=sorted({ThermoVar.D, ThermoVar.T} - axes)
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
_DENSITY = Axis(variable=ThermoVar.D, min_val=0.5, max_val=75.0, spacing="log")
|
|
190
|
+
_TEMPERATURE = Axis(variable=ThermoVar.T, min_val=15.0, max_val=150.0)
|
|
191
|
+
_PRESSURE = Axis(variable=ThermoVar.P, min_val=1e4, max_val=1.5e6, spacing="log")
|
|
192
|
+
_ENERGY = Axis(variable=ThermoVar.U, min_val=-5e4, max_val=1e6)
|
|
193
|
+
_ENTHALPY = Axis(variable=ThermoVar.H, min_val=-5e4, max_val=1.5e6)
|
|
194
|
+
_ENTROPY = Axis(variable=ThermoVar.S, min_val=-1e4, max_val=5e4)
|
|
195
|
+
|
|
196
|
+
TABLE_REGISTRY: List[TableSpec] = [
|
|
197
|
+
_table(_DENSITY, _ENERGY),
|
|
198
|
+
_table(_TEMPERATURE, _PRESSURE),
|
|
199
|
+
_table(_PRESSURE, _ENTHALPY),
|
|
200
|
+
_table(_PRESSURE, _ENTROPY),
|
|
201
|
+
_table(_PRESSURE, _ENERGY),
|
|
202
|
+
_table(_DENSITY, _ENTHALPY),
|
|
203
|
+
_table(_DENSITY, _ENTROPY),
|
|
204
|
+
_table(_TEMPERATURE, _ENTROPY),
|
|
205
|
+
]
|
propax/core/domain.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from typing import Tuple
|
|
2
|
+
|
|
3
|
+
import jax.numpy as jnp
|
|
4
|
+
|
|
5
|
+
from propax.fluids.generic import HelmholtzEOS
|
|
6
|
+
|
|
7
|
+
from .tolerances import TOL
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def rho_bounds(saturation) -> Tuple[float, float]:
|
|
11
|
+
"""Every density the correlation describes."""
|
|
12
|
+
return (
|
|
13
|
+
TOL.domain.rho_lo_frac * saturation.eos.rho_crit_mass,
|
|
14
|
+
saturation.rho_max,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def T_bounds(eos: HelmholtzEOS) -> Tuple[float, float]:
|
|
19
|
+
return eos.T_triple, eos.T_max
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def node_is_valid(saturation, rho, T):
|
|
23
|
+
T_lo, T_hi = T_bounds(saturation.eos)
|
|
24
|
+
return (rho > 0.0) & (rho <= saturation.rho_max) & (T > T_lo) & (T < T_hi)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def is_two_phase(quality, sat_is_valid, slack: float = 0.0):
|
|
28
|
+
"""
|
|
29
|
+
If the sat solve converged and the quality is withing [-slack, 1 + slack],
|
|
30
|
+
this is done to avoid misclassifying biphasic states.
|
|
31
|
+
"""
|
|
32
|
+
return sat_is_valid & (quality >= -slack) & (quality <= 1.0 + slack)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def clamp_quality(quality, slack: float = 0.0):
|
|
36
|
+
"""The slack lets a quality out of [0, 1]; the lever rule may not see more
|
|
37
|
+
than the caller asked for."""
|
|
38
|
+
return jnp.clip(quality, -slack, 1.0 + slack)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""The flash: given two properties, find the state.
|
|
2
|
+
|
|
3
|
+
`Interface` is the facade; the work is here, split by responsibility
|
|
4
|
+
rather than by size. Each module takes what it needs as arguments an EOS, a
|
|
5
|
+
saturation module, a table so a signature says which parts of the interface
|
|
6
|
+
a solver actually touches. Nothing here reaches back into the interface.
|
|
7
|
+
|
|
8
|
+
dispatch which strategy a pair takes, and nothing else
|
|
9
|
+
two_phase the lever rule, and the three ways T_sat is obtained
|
|
10
|
+
single_phase the bracketed 1D solve and the generic 2D one
|
|
11
|
+
results turning a solved state into the returned dictionary
|
|
12
|
+
tables the optional interpolation accelerator, isolated
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from .dispatch import (
|
|
16
|
+
INVALID_PAIRS,
|
|
17
|
+
both_natural_pair,
|
|
18
|
+
check_supported,
|
|
19
|
+
is_degenerate_pair,
|
|
20
|
+
is_nested_pair,
|
|
21
|
+
is_saturated_pair,
|
|
22
|
+
one_dim_known_var,
|
|
23
|
+
route_of,
|
|
24
|
+
supported_pairs,
|
|
25
|
+
)
|
|
26
|
+
from .results import (
|
|
27
|
+
add_transport,
|
|
28
|
+
fill_result_dict,
|
|
29
|
+
get_sat_bounds,
|
|
30
|
+
properties_Tx,
|
|
31
|
+
undefine_two_phase,
|
|
32
|
+
)
|
|
33
|
+
from .tables import call_interp, pass_pair_args_to_table
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"one_dim_known_var",
|
|
37
|
+
"is_nested_pair",
|
|
38
|
+
"is_saturated_pair",
|
|
39
|
+
"check_supported",
|
|
40
|
+
"INVALID_PAIRS",
|
|
41
|
+
"is_degenerate_pair",
|
|
42
|
+
"both_natural_pair",
|
|
43
|
+
"route_of",
|
|
44
|
+
"supported_pairs",
|
|
45
|
+
"add_transport",
|
|
46
|
+
"fill_result_dict",
|
|
47
|
+
"get_sat_bounds",
|
|
48
|
+
"properties_Tx",
|
|
49
|
+
"undefine_two_phase",
|
|
50
|
+
"call_interp",
|
|
51
|
+
"pass_pair_args_to_table",
|
|
52
|
+
]
|