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.
Files changed (189) hide show
  1. propax/__init__.py +12 -0
  2. propax/build_tables.py +244 -0
  3. propax/core/__init__.py +3 -0
  4. propax/core/_state.py +122 -0
  5. propax/core/config.py +205 -0
  6. propax/core/domain.py +38 -0
  7. propax/core/flash/__init__.py +52 -0
  8. propax/core/flash/dispatch.py +176 -0
  9. propax/core/flash/results.py +93 -0
  10. propax/core/flash/single_phase.py +548 -0
  11. propax/core/flash/tables.py +52 -0
  12. propax/core/flash/two_phase.py +311 -0
  13. propax/core/interface.py +646 -0
  14. propax/core/interp.py +313 -0
  15. propax/core/saturation.py +179 -0
  16. propax/core/tolerances.py +170 -0
  17. propax/fluids/__init__.py +0 -0
  18. propax/fluids/_registry.py +84 -0
  19. propax/fluids/data/1-butene.json +961 -0
  20. propax/fluids/data/acetone.json +1356 -0
  21. propax/fluids/data/argon.json +1722 -0
  22. propax/fluids/data/benzene.json +866 -0
  23. propax/fluids/data/carbonmonoxide.json +956 -0
  24. propax/fluids/data/carbonylsulfide.json +703 -0
  25. propax/fluids/data/cis-2-butene.json +886 -0
  26. propax/fluids/data/cyclohexane.json +633 -0
  27. propax/fluids/data/cyclopentane.json +598 -0
  28. propax/fluids/data/cyclopropane.json +593 -0
  29. propax/fluids/data/d4.json +737 -0
  30. propax/fluids/data/d5.json +1948 -0
  31. propax/fluids/data/deuterium.json +440 -0
  32. propax/fluids/data/dichloroethane.json +1758 -0
  33. propax/fluids/data/diethylether.json +590 -0
  34. propax/fluids/data/dimethylcarbonate.json +1776 -0
  35. propax/fluids/data/dimethylether.json +836 -0
  36. propax/fluids/data/ethane.json +1734 -0
  37. propax/fluids/data/ethanol.json +1246 -0
  38. propax/fluids/data/ethylbenzene.json +918 -0
  39. propax/fluids/data/ethylene.json +1471 -0
  40. propax/fluids/data/ethyleneoxide.json +743 -0
  41. propax/fluids/data/heavywater.json +1889 -0
  42. propax/fluids/data/helium.json +912 -0
  43. propax/fluids/data/hfe143m.json +858 -0
  44. propax/fluids/data/hydrogen.json +592 -0
  45. propax/fluids/data/hydrogenchloride.json +793 -0
  46. propax/fluids/data/hydrogensulfide.json +560 -0
  47. propax/fluids/data/isobutane.json +1372 -0
  48. propax/fluids/data/isobutene.json +886 -0
  49. propax/fluids/data/isohexane.json +762 -0
  50. propax/fluids/data/isopentane.json +1110 -0
  51. propax/fluids/data/krypton.json +650 -0
  52. propax/fluids/data/m-xylene.json +1687 -0
  53. propax/fluids/data/md2m.json +795 -0
  54. propax/fluids/data/md3m.json +795 -0
  55. propax/fluids/data/md4m.json +737 -0
  56. propax/fluids/data/mdm.json +596 -0
  57. propax/fluids/data/methane.json +1709 -0
  58. propax/fluids/data/methyllinoleate.json +802 -0
  59. propax/fluids/data/methyllinolenate.json +702 -0
  60. propax/fluids/data/methyloleate.json +702 -0
  61. propax/fluids/data/methylpalmitate.json +701 -0
  62. propax/fluids/data/methylstearate.json +701 -0
  63. propax/fluids/data/mm.json +818 -0
  64. propax/fluids/data/n-butane.json +1221 -0
  65. propax/fluids/data/n-decane.json +735 -0
  66. propax/fluids/data/n-dodecane.json +1940 -0
  67. propax/fluids/data/n-hexane.json +797 -0
  68. propax/fluids/data/n-nonane.json +865 -0
  69. propax/fluids/data/n-octane.json +866 -0
  70. propax/fluids/data/n-pentane.json +799 -0
  71. propax/fluids/data/n-perfluorobutane.json +732 -0
  72. propax/fluids/data/n-perfluorohexane.json +592 -0
  73. propax/fluids/data/n-perfluoropentane.json +592 -0
  74. propax/fluids/data/n-propane.json +1462 -0
  75. propax/fluids/data/neon.json +597 -0
  76. propax/fluids/data/neopentane.json +782 -0
  77. propax/fluids/data/nitrogen.json +1430 -0
  78. propax/fluids/data/nitrousoxide.json +730 -0
  79. propax/fluids/data/novec649.json +1483 -0
  80. propax/fluids/data/o-xylene.json +1761 -0
  81. propax/fluids/data/orthodeuterium.json +440 -0
  82. propax/fluids/data/orthohydrogen.json +590 -0
  83. propax/fluids/data/oxygen.json +1422 -0
  84. propax/fluids/data/p-xylene.json +1903 -0
  85. propax/fluids/data/paradeuterium.json +438 -0
  86. propax/fluids/data/parahydrogen.json +596 -0
  87. propax/fluids/data/propylene.json +1427 -0
  88. propax/fluids/data/propyleneglycol.json +1212 -0
  89. propax/fluids/data/propyne.json +600 -0
  90. propax/fluids/data/r11.json +1024 -0
  91. propax/fluids/data/r1123.json +723 -0
  92. propax/fluids/data/r113.json +1133 -0
  93. propax/fluids/data/r1130(e).json +795 -0
  94. propax/fluids/data/r1132(e).json +1308 -0
  95. propax/fluids/data/r114.json +402 -0
  96. propax/fluids/data/r115.json +677 -0
  97. propax/fluids/data/r116.json +959 -0
  98. propax/fluids/data/r12.json +877 -0
  99. propax/fluids/data/r1224ydz.json +1993 -0
  100. propax/fluids/data/r123.json +1053 -0
  101. propax/fluids/data/r1233zd(e).json +1566 -0
  102. propax/fluids/data/r1234yf.json +933 -0
  103. propax/fluids/data/r1234ze(e).json +1836 -0
  104. propax/fluids/data/r1234ze(z).json +1397 -0
  105. propax/fluids/data/r124.json +914 -0
  106. propax/fluids/data/r1243zf.json +1690 -0
  107. propax/fluids/data/r13.json +401 -0
  108. propax/fluids/data/r1336mzz(e).json +877 -0
  109. propax/fluids/data/r1336mzz(z).json +1637 -0
  110. propax/fluids/data/r134a.json +868 -0
  111. propax/fluids/data/r13i1.json +832 -0
  112. propax/fluids/data/r14.json +742 -0
  113. propax/fluids/data/r141b.json +839 -0
  114. propax/fluids/data/r142b.json +962 -0
  115. propax/fluids/data/r143a.json +858 -0
  116. propax/fluids/data/r152a.json +855 -0
  117. propax/fluids/data/r161.json +1759 -0
  118. propax/fluids/data/r21.json +600 -0
  119. propax/fluids/data/r218.json +704 -0
  120. propax/fluids/data/r22.json +1887 -0
  121. propax/fluids/data/r227ea.json +1294 -0
  122. propax/fluids/data/r23.json +781 -0
  123. propax/fluids/data/r236ea.json +1549 -0
  124. propax/fluids/data/r236fa.json +1193 -0
  125. propax/fluids/data/r245ca.json +731 -0
  126. propax/fluids/data/r245fa.json +1682 -0
  127. propax/fluids/data/r32.json +1609 -0
  128. propax/fluids/data/r365mfc.json +1471 -0
  129. propax/fluids/data/r40.json +579 -0
  130. propax/fluids/data/r404a.json +1076 -0
  131. propax/fluids/data/r407c.json +1628 -0
  132. propax/fluids/data/r41.json +703 -0
  133. propax/fluids/data/r410a.json +793 -0
  134. propax/fluids/data/r507a.json +990 -0
  135. propax/fluids/data/rc318.json +598 -0
  136. propax/fluids/data/ses36.json +585 -0
  137. propax/fluids/data/sulfurdioxide.json +602 -0
  138. propax/fluids/data/sulfurhexafluoride.json +1023 -0
  139. propax/fluids/data/tetrahydrofuran.json +740 -0
  140. propax/fluids/data/toluene.json +842 -0
  141. propax/fluids/data/trans-2-butene.json +840 -0
  142. propax/fluids/data/vinylchloride.json +1762 -0
  143. propax/fluids/data/xenon.json +668 -0
  144. propax/fluids/generic/__init__.py +17 -0
  145. propax/fluids/generic/conductivity/__init__.py +2 -0
  146. propax/fluids/generic/conductivity/rational_polynomial.py +154 -0
  147. propax/fluids/generic/conductivity/schema_cond.py +185 -0
  148. propax/fluids/generic/conductivity/slots.py +266 -0
  149. propax/fluids/generic/eos/__init__.py +1 -0
  150. propax/fluids/generic/eos/helmholtz.py +345 -0
  151. propax/fluids/generic/eos/mixture_terms.py +4 -0
  152. propax/fluids/generic/eos/pure_terms.py +424 -0
  153. propax/fluids/generic/eos/schema_eos.py +167 -0
  154. propax/fluids/generic/transport_registry.py +138 -0
  155. propax/fluids/generic/viscosity/__init__.py +2 -0
  156. propax/fluids/generic/viscosity/rainwater_friend.py +95 -0
  157. propax/fluids/generic/viscosity/schema_visc.py +198 -0
  158. propax/fluids/generic/viscosity/slots.py +249 -0
  159. propax/fluids/schema.py +132 -0
  160. propax/make_fluid.py +110 -0
  161. propax/py.typed +0 -0
  162. propax/utils/__init__.py +0 -0
  163. propax/utils/build_utils/__init__.py +32 -0
  164. propax/utils/build_utils/assemble.py +608 -0
  165. propax/utils/build_utils/dome.py +166 -0
  166. propax/utils/build_utils/helpers.py +128 -0
  167. propax/utils/build_utils/single_phase.py +65 -0
  168. propax/utils/exact/__init__.py +31 -0
  169. propax/utils/exact/constants.py +69 -0
  170. propax/utils/exact/critical.py +261 -0
  171. propax/utils/exact/curve.py +116 -0
  172. propax/utils/exact/fit.py +214 -0
  173. propax/utils/exact/precision.py +57 -0
  174. propax/utils/exact/superancillary.py +297 -0
  175. propax/utils/make_utils/__init__.py +3 -0
  176. propax/utils/make_utils/build.py +321 -0
  177. propax/utils/make_utils/conductivity.py +138 -0
  178. propax/utils/make_utils/coverage.py +241 -0
  179. propax/utils/make_utils/eos.py +290 -0
  180. propax/utils/make_utils/source.py +99 -0
  181. propax/utils/make_utils/viscosity.py +120 -0
  182. propax/utils/numerics.py +26 -0
  183. propax/utils/solvers.py +165 -0
  184. propax/utils/types.py +7 -0
  185. propax-0.1.0.dist-info/METADATA +247 -0
  186. propax-0.1.0.dist-info/RECORD +189 -0
  187. propax-0.1.0.dist-info/WHEEL +5 -0
  188. propax-0.1.0.dist-info/licenses/LICENSE +21 -0
  189. 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())
@@ -0,0 +1,3 @@
1
+ from .interface import Interface
2
+
3
+ __all__ = ["Interface"]
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
+ ]