pyfuga 0.2.0__tar.gz → 0.4.0__tar.gz

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 (50) hide show
  1. {pyfuga-0.2.0 → pyfuga-0.4.0}/PKG-INFO +20 -6
  2. {pyfuga-0.2.0 → pyfuga-0.4.0}/README.md +16 -5
  3. {pyfuga-0.2.0 → pyfuga-0.4.0}/pyfuga/__init__.py +7 -6
  4. {pyfuga-0.2.0 → pyfuga-0.4.0}/pyfuga/_fuga.py +92 -53
  5. {pyfuga-0.2.0 → pyfuga-0.4.0}/pyfuga/common.py +12 -6
  6. pyfuga-0.4.0/pyfuga/constants.py +44 -0
  7. {pyfuga-0.2.0 → pyfuga-0.4.0}/pyfuga/file_readers.py +29 -20
  8. {pyfuga-0.2.0 → pyfuga-0.4.0}/pyfuga/flut.py +219 -143
  9. {pyfuga-0.2.0 → pyfuga-0.4.0}/pyfuga/paths.py +37 -4
  10. {pyfuga-0.2.0 → pyfuga-0.4.0}/pyfuga/preludium_eq_routines.py +9 -6
  11. {pyfuga-0.2.0 → pyfuga-0.4.0}/pyfuga/preluts.py +77 -42
  12. {pyfuga-0.2.0 → pyfuga-0.4.0}/pyfuga/preluts_generator/generator.py +23 -5
  13. {pyfuga-0.2.0 → pyfuga-0.4.0}/pyfuga/preluts_generator/integration.py +22 -4
  14. {pyfuga-0.2.0 → pyfuga-0.4.0}/pyfuga/preluts_generator/matrices.py +40 -40
  15. {pyfuga-0.2.0 → pyfuga-0.4.0}/pyfuga/preluts_generator/nodes.py +5 -5
  16. pyfuga-0.4.0/pyfuga/profiling.py +109 -0
  17. {pyfuga-0.2.0 → pyfuga-0.4.0}/pyfuga/trafalgar.py +35 -21
  18. pyfuga-0.4.0/pyfuga/utils.py +420 -0
  19. {pyfuga-0.2.0 → pyfuga-0.4.0}/pyfuga/z0.py +11 -10
  20. {pyfuga-0.2.0 → pyfuga-0.4.0}/pyfuga.egg-info/PKG-INFO +20 -6
  21. {pyfuga-0.2.0 → pyfuga-0.4.0}/pyfuga.egg-info/SOURCES.txt +3 -0
  22. {pyfuga-0.2.0 → pyfuga-0.4.0}/pyfuga.egg-info/requires.txt +3 -0
  23. {pyfuga-0.2.0 → pyfuga-0.4.0}/pyproject.toml +108 -8
  24. pyfuga-0.4.0/tests/test_api.py +500 -0
  25. pyfuga-0.4.0/tests/test_common.py +6 -0
  26. {pyfuga-0.2.0 → pyfuga-0.4.0}/tests/test_flut.py +136 -16
  27. pyfuga-0.4.0/tests/test_lut_schema.py +120 -0
  28. {pyfuga-0.2.0 → pyfuga-0.4.0}/tests/test_preluts_jit.py +10 -6
  29. pyfuga-0.4.0/tests/test_profiling.py +12 -0
  30. pyfuga-0.4.0/tests/test_trafalgar.py +110 -0
  31. pyfuga-0.4.0/tests/test_utils.py +105 -0
  32. pyfuga-0.2.0/pyfuga/constants.py +0 -33
  33. pyfuga-0.2.0/pyfuga/profiling.py +0 -86
  34. pyfuga-0.2.0/pyfuga/utils.py +0 -228
  35. pyfuga-0.2.0/tests/test_api.py +0 -145
  36. pyfuga-0.2.0/tests/test_trafalgar.py +0 -55
  37. pyfuga-0.2.0/tests/test_utils.py +0 -19
  38. {pyfuga-0.2.0 → pyfuga-0.4.0}/LICENSE +0 -0
  39. {pyfuga-0.2.0 → pyfuga-0.4.0}/pyfuga/preluts_generator/__init__.py +0 -0
  40. {pyfuga-0.2.0 → pyfuga-0.4.0}/pyfuga/preluts_generator/transforms.py +0 -0
  41. {pyfuga-0.2.0 → pyfuga-0.4.0}/pyfuga/typing.py +0 -0
  42. {pyfuga-0.2.0 → pyfuga-0.4.0}/pyfuga.egg-info/dependency_links.txt +0 -0
  43. {pyfuga-0.2.0 → pyfuga-0.4.0}/pyfuga.egg-info/top_level.txt +0 -0
  44. {pyfuga-0.2.0 → pyfuga-0.4.0}/setup.cfg +0 -0
  45. {pyfuga-0.2.0 → pyfuga-0.4.0}/tests/test_jit_signatures.py +0 -0
  46. {pyfuga-0.2.0 → pyfuga-0.4.0}/tests/test_paths.py +0 -0
  47. {pyfuga-0.2.0 → pyfuga-0.4.0}/tests/test_preluts.py +0 -0
  48. {pyfuga-0.2.0 → pyfuga-0.4.0}/tests/test_readers.py +0 -0
  49. {pyfuga-0.2.0 → pyfuga-0.4.0}/tests/test_typing.py +0 -0
  50. {pyfuga-0.2.0 → pyfuga-0.4.0}/tests/test_z0.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyfuga
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: Linearised CFD wake model (PyFuga)
5
5
  Author-email: PyFuga team <lalc@dtu.dk>
6
6
  License-Expression: MIT
@@ -43,6 +43,9 @@ Provides-Extra: lint
43
43
  Requires-Dist: ruff>0.14; extra == "lint"
44
44
  Requires-Dist: black[jupyter]>=26.1.0; extra == "lint"
45
45
  Requires-Dist: mypy>=1.19.1; extra == "lint"
46
+ Requires-Dist: packaging>=24; extra == "lint"
47
+ Requires-Dist: scipy-stubs<1.18,>=1.10; extra == "lint"
48
+ Requires-Dist: types-tqdm>=4.0; extra == "lint"
46
49
  Provides-Extra: docs
47
50
  Requires-Dist: sphinx==7.4.7; extra == "docs"
48
51
  Requires-Dist: sphinx-autobuild==2024.10.3; extra == "docs"
@@ -69,9 +72,8 @@ PyFuga is part of the [**PyWake**](https://gitlab.windenergy.dtu.dk/TOPFARM/PyWa
69
72
 
70
73
  [![pipeline status](https://gitlab.windenergy.dtu.dk/TOPFARM/PyFuga/badges/main/pipeline.svg)](https://gitlab.windenergy.dtu.dk/TOPFARM/PyFuga/-/commits/main)
71
74
  [![coverage report](https://gitlab.windenergy.dtu.dk/TOPFARM/PyFuga/badges/main/coverage.svg)](https://gitlab.windenergy.dtu.dk/TOPFARM/PyFuga/-/commits/main)
72
- [![documentation](https://img.shields.io/badge/docs-latest-brightgreen.svg)](https://topfarm.pages.windenergy.dtu.dk/PyFuga/main/index.html)
73
- <!-- Replace 000000000 with the Zenodo badge id after first deposit -->
74
- [![DOI](https://zenodo.org/badge/000000000.svg)](https://zenodo.org/badge/latestdoi/000000000)
75
+ [![documentation](https://img.shields.io/badge/docs-latest-brightgreen.svg)](https://topfarm.pages.windenergy.dtu.dk/PyFuga)
76
+ [![All versions DOI (Zenodo)](https://zenodo.org/badge/DOI/10.5281/zenodo.19569518.svg)](https://doi.org/10.5281/zenodo.19569518)
75
77
 
76
78
  ## Installation
77
79
 
@@ -97,7 +99,7 @@ A minimal example to get you started is available in **[the QuickStart Jupyter n
97
99
 
98
100
  ## Documentation
99
101
 
100
- Learn more about PyFuga at the **[official documentation](https://topfarm.pages.windenergy.dtu.dk/PyFuga/main/index.html)**.
102
+ Learn more about PyFuga at the **[official documentation](https://topfarm.pages.windenergy.dtu.dk/PyFuga)**.
101
103
 
102
104
  ## Release history
103
105
 
@@ -113,7 +115,19 @@ Issues and feature requests can be submitted through the project's [**GitLab iss
113
115
 
114
116
  ## Citing PyFuga
115
117
 
116
- Please cite both the underlying Fuga theory and the PyFuga software implementation. See the "Citing PyFuga" section in the documentation for recommended references and BibTeX entries.
118
+ Please cite both the underlying Fuga theory and the PyFuga software implementation.
119
+
120
+ For PyFuga itself, **use the DOI on the Zenodo record for the release version you used** (this is the canonical source of truth). The repository `CITATION.cff` mirrors this metadata for convenience.
121
+
122
+ The badge above points to the latest release DOI. For scientific citation, always use the DOI for the exact release version used in your work.
123
+
124
+ If you have PyFuga installed, you can print a Zenodo search link for your installed version:
125
+
126
+ ```bash
127
+ python scripts/citation.py
128
+ ```
129
+
130
+ See the "Citing PyFuga" section in the documentation for recommended references and BibTeX entries.
117
131
 
118
132
  ## Authors and acknowledgements
119
133
 
@@ -8,9 +8,8 @@ PyFuga is part of the [**PyWake**](https://gitlab.windenergy.dtu.dk/TOPFARM/PyWa
8
8
 
9
9
  [![pipeline status](https://gitlab.windenergy.dtu.dk/TOPFARM/PyFuga/badges/main/pipeline.svg)](https://gitlab.windenergy.dtu.dk/TOPFARM/PyFuga/-/commits/main)
10
10
  [![coverage report](https://gitlab.windenergy.dtu.dk/TOPFARM/PyFuga/badges/main/coverage.svg)](https://gitlab.windenergy.dtu.dk/TOPFARM/PyFuga/-/commits/main)
11
- [![documentation](https://img.shields.io/badge/docs-latest-brightgreen.svg)](https://topfarm.pages.windenergy.dtu.dk/PyFuga/main/index.html)
12
- <!-- Replace 000000000 with the Zenodo badge id after first deposit -->
13
- [![DOI](https://zenodo.org/badge/000000000.svg)](https://zenodo.org/badge/latestdoi/000000000)
11
+ [![documentation](https://img.shields.io/badge/docs-latest-brightgreen.svg)](https://topfarm.pages.windenergy.dtu.dk/PyFuga)
12
+ [![All versions DOI (Zenodo)](https://zenodo.org/badge/DOI/10.5281/zenodo.19569518.svg)](https://doi.org/10.5281/zenodo.19569518)
14
13
 
15
14
  ## Installation
16
15
 
@@ -36,7 +35,7 @@ A minimal example to get you started is available in **[the QuickStart Jupyter n
36
35
 
37
36
  ## Documentation
38
37
 
39
- Learn more about PyFuga at the **[official documentation](https://topfarm.pages.windenergy.dtu.dk/PyFuga/main/index.html)**.
38
+ Learn more about PyFuga at the **[official documentation](https://topfarm.pages.windenergy.dtu.dk/PyFuga)**.
40
39
 
41
40
  ## Release history
42
41
 
@@ -52,7 +51,19 @@ Issues and feature requests can be submitted through the project's [**GitLab iss
52
51
 
53
52
  ## Citing PyFuga
54
53
 
55
- Please cite both the underlying Fuga theory and the PyFuga software implementation. See the "Citing PyFuga" section in the documentation for recommended references and BibTeX entries.
54
+ Please cite both the underlying Fuga theory and the PyFuga software implementation.
55
+
56
+ For PyFuga itself, **use the DOI on the Zenodo record for the release version you used** (this is the canonical source of truth). The repository `CITATION.cff` mirrors this metadata for convenience.
57
+
58
+ The badge above points to the latest release DOI. For scientific citation, always use the DOI for the exact release version used in your work.
59
+
60
+ If you have PyFuga installed, you can print a Zenodo search link for your installed version:
61
+
62
+ ```bash
63
+ python scripts/citation.py
64
+ ```
65
+
66
+ See the "Citing PyFuga" section in the documentation for recommended references and BibTeX entries.
56
67
 
57
68
  ## Authors and acknowledgements
58
69
 
@@ -1,6 +1,7 @@
1
1
  import functools
2
2
  import inspect
3
3
  from importlib.metadata import PackageNotFoundError, version
4
+ from typing import Any
4
5
 
5
6
  from . import _fuga
6
7
 
@@ -13,20 +14,20 @@ __all__ = ["get_luts", "get_fluts", "get_preluts", "__version__"]
13
14
 
14
15
 
15
16
  @functools.wraps(_fuga.get_luts)
16
- def get_luts(*args, **kwargs):
17
+ def get_luts(*args: Any, **kwargs: Any) -> Any:
17
18
  return _fuga.get_luts(*args, **kwargs)
18
19
 
19
20
 
20
21
  @functools.wraps(_fuga.get_fluts)
21
- def get_fluts(*args, **kwargs):
22
+ def get_fluts(*args: Any, **kwargs: Any) -> Any:
22
23
  return _fuga.get_fluts(*args, **kwargs)
23
24
 
24
25
 
25
26
  @functools.wraps(_fuga.get_preluts)
26
- def get_preluts(*args, **kwargs):
27
+ def get_preluts(*args: Any, **kwargs: Any) -> Any:
27
28
  return _fuga.get_preluts(*args, **kwargs)
28
29
 
29
30
 
30
- get_luts.__signature__ = inspect.signature(_fuga.get_luts)
31
- get_fluts.__signature__ = inspect.signature(_fuga.get_fluts)
32
- get_preluts.__signature__ = inspect.signature(_fuga.get_preluts)
31
+ get_luts.__signature__ = inspect.signature(_fuga.get_luts) # type: ignore[attr-defined]
32
+ get_fluts.__signature__ = inspect.signature(_fuga.get_fluts) # type: ignore[attr-defined]
33
+ get_preluts.__signature__ = inspect.signature(_fuga.get_preluts) # type: ignore[attr-defined]
@@ -27,28 +27,29 @@ from pyfuga.flut import FourierLUTGenerator
27
27
  from pyfuga.paths import get_fluts_path, get_level_range, get_luts_path, get_preluts_path
28
28
  from pyfuga.preluts import PreLUTs
29
29
  from pyfuga.trafalgar import Trafalgar
30
- from pyfuga.utils import ComplexXRDataset, get_beta_lst, get_kz0_lst
30
+ from pyfuga.utils import ComplexXRDataset, NCpuStageConfig, get_beta_lst, get_kz0_lst
31
31
 
32
32
 
33
33
  def get_luts(
34
- folder,
35
- zeta0,
36
- nkz0,
37
- nbeta,
38
- diameter,
39
- zhub,
40
- z0,
41
- zi,
42
- zlow,
43
- zhigh,
44
- lut_vars=UVW_LT,
45
- nx=2048,
46
- ny=512,
47
- dx=None,
48
- dy=None,
49
- jit=True,
50
- n_cpu=1,
51
- ): # pragma: no cover
34
+ folder: str | Path,
35
+ zeta0: float,
36
+ nkz0: int,
37
+ nbeta: int,
38
+ diameter: float,
39
+ zhub: float,
40
+ z0: float,
41
+ zi: float,
42
+ zlow: float,
43
+ zhigh: float,
44
+ lut_vars: list[str] = UVW_LT,
45
+ nx: int = 2048,
46
+ ny: int = 512,
47
+ dx: float | None = None,
48
+ dy: float | None = None,
49
+ jit: bool = True,
50
+ n_cpu: int | None = None,
51
+ n_cpu_stage: NCpuStageConfig | None = None,
52
+ ) -> xr.Dataset: # pragma: no cover
52
53
  """Generate physical-space wake look-up tables via the full PyFuga pipeline.
53
54
 
54
55
  This function orchestrates the three-stage PyFuga pipeline:
@@ -97,7 +98,17 @@ def get_luts(
97
98
  jit : bool, optional
98
99
  Enable JIT compilation (Numba). Default is True.
99
100
  n_cpu : int or None, optional
100
- CPU cores for parallel computation. None uses all; 1 disables parallelisation.
101
+ Optional global worker-count override. If ``None`` (default), stage-aware
102
+ defaults are used and all stages run on all CPUs. If an integer is provided, that
103
+ worker count is applied to all stages unless overridden by
104
+ ``n_cpu_stage``. Large ``nx``/``ny`` and multiple workers can increase
105
+ memory use substantially.
106
+ n_cpu_stage : dict, optional
107
+ Optional per-stage overrides for worker counts. Supported keys are
108
+ ``"preluts"``, ``"fluts"``, and ``"trafalgar"``. Provided keys override
109
+ the resolved default/global setting for that stage. Precedence is:
110
+ defaults -> global ``n_cpu`` (if provided) -> ``n_cpu_stage`` keys.
111
+ A stage value may be ``None`` meaning all CPUs for that stage only.
101
112
 
102
113
  Returns
103
114
  -------
@@ -112,73 +123,100 @@ def get_luts(
112
123
  """
113
124
  folder = Path(folder)
114
125
  folder.mkdir(parents=True, exist_ok=True)
126
+ resolved_n_cpu = utils.resolve_n_cpu_stage(n_cpu, n_cpu_stage)
115
127
  dx = dx or diameter / 4
116
128
  dy = dy or diameter / 16
117
129
 
118
130
  luts_path = get_luts_path(folder, zeta0, nkz0, nbeta, diameter, zhub, z0, zi, zlow, zhigh, lut_vars, nx, ny, dx, dy)
119
131
  if luts_path.exists():
120
132
  luts = xr.load_dataset(luts_path)
121
- luts.attrs["name"] = luts_path.stem
122
- return luts
123
-
124
- fluts = get_fluts(folder, zeta0, nkz0, nbeta, diameter, zhub, z0, zi, zlow, zhigh, lut_vars, jit, n_cpu)
125
- luts = Trafalgar(fluts, nx, ny, dx, dy).make_luts(3)
133
+ if utils.load_lut_if_current(luts, luts_path):
134
+ luts.attrs["name"] = luts_path.stem
135
+ return luts
136
+
137
+ fluts = get_fluts(
138
+ folder,
139
+ zeta0,
140
+ nkz0,
141
+ nbeta,
142
+ diameter,
143
+ zhub,
144
+ z0,
145
+ zi,
146
+ zlow,
147
+ zhigh,
148
+ lut_vars,
149
+ jit,
150
+ resolved_n_cpu["fluts"],
151
+ n_cpu_stage={"preluts": resolved_n_cpu["preluts"]},
152
+ )
153
+ luts = Trafalgar(fluts, nx, ny, dx, dy).make_luts(interpolation_order=3, n_cpu=resolved_n_cpu["trafalgar"])
154
+ utils.stamp_lut_metadata(luts)
126
155
  luts.to_netcdf(luts_path)
127
156
  luts.attrs["name"] = luts_path.stem
128
157
  return luts
129
158
 
130
159
 
131
160
  def get_fluts(
132
- folder,
133
- zeta0,
134
- nkz0,
135
- nbeta,
136
- diameter,
137
- zhub,
138
- z0,
139
- zi,
140
- zlow,
141
- zhigh,
142
- lut_vars=UVW_LT,
143
- jit=True,
144
- n_cpu=1,
145
- ): # pragma: no cover
161
+ folder: str | Path,
162
+ zeta0: float,
163
+ nkz0: int,
164
+ nbeta: int,
165
+ diameter: float,
166
+ zhub: float,
167
+ z0: float,
168
+ zi: float,
169
+ zlow: float,
170
+ zhigh: float,
171
+ lut_vars: list[str] = UVW_LT,
172
+ jit: bool = True,
173
+ n_cpu: int | None = None,
174
+ n_cpu_stage: NCpuStageConfig | None = None,
175
+ ) -> ComplexXRDataset: # pragma: no cover
146
176
  folder = Path(folder)
147
177
  folder.mkdir(parents=True, exist_ok=True)
178
+ resolved_n_cpu = utils.resolve_n_cpu_stage(n_cpu, n_cpu_stage)
148
179
 
149
180
  fluts_path = get_fluts_path(folder, zeta0, nkz0, nbeta, diameter, zhub, z0, zi, zlow, zhigh, lut_vars)
150
181
  if fluts_path.exists():
151
- return ComplexXRDataset.from_netcdf(fluts_path)
182
+ fluts = ComplexXRDataset.from_netcdf(fluts_path)
183
+ if utils.load_lut_if_current(fluts, fluts_path):
184
+ return fluts
152
185
 
153
186
  utils.compile(jit)
154
- preluts = get_preluts(folder, zeta0, nkz0, nbeta, jit, n_cpu)
187
+ preluts = get_preluts(folder, zeta0, nkz0, nbeta, jit, resolved_n_cpu["preluts"], None)
155
188
 
156
189
  low_level_out, high_level_out = get_level_range(zlow, zhigh, zhub, z0)
157
190
 
158
191
  flut_generator = FourierLUTGenerator(preluts, zhub, diameter, zi)
159
192
  if low_level_out == high_level_out == 9999:
160
- fluts = flut_generator.make_hubheight_luts(z0, lut_vars, n_cpu=n_cpu)
193
+ fluts = flut_generator.make_hubheight_luts(z0, lut_vars, n_cpu=resolved_n_cpu["fluts"])
161
194
  else:
162
- fluts = flut_generator.make_lut(z0, low_level_out, high_level_out, lut_vars, n_cpu=n_cpu)
195
+ fluts = flut_generator.make_lut(z0, low_level_out, high_level_out, lut_vars, n_cpu=resolved_n_cpu["fluts"])
163
196
 
197
+ utils.stamp_lut_metadata(fluts)
164
198
  fluts.save(fluts_path)
165
199
  return fluts
166
200
 
167
201
 
168
202
  def get_preluts(
169
- folder,
170
- zeta0,
171
- nkz0,
172
- nbeta,
173
- jit=True,
174
- n_cpu=1,
175
- ): # pragma: no cover
203
+ folder: str | Path,
204
+ zeta0: float,
205
+ nkz0: int,
206
+ nbeta: int,
207
+ jit: bool = True,
208
+ n_cpu: int | None = None,
209
+ n_cpu_stage: NCpuStageConfig | None = None,
210
+ ) -> PreLUTs: # pragma: no cover
176
211
  folder = Path(folder)
177
212
  folder.mkdir(parents=True, exist_ok=True)
213
+ resolved_n_cpu = utils.resolve_n_cpu_stage(n_cpu, n_cpu_stage)
178
214
 
179
215
  preluts_path = get_preluts_path(folder, zeta0, nkz0, nbeta)
180
216
  if preluts_path.exists():
181
- return PreLUTs.from_netcdf(preluts_path)
217
+ preluts = PreLUTs.from_netcdf(preluts_path)
218
+ if utils.load_lut_if_current(preluts, preluts_path):
219
+ return preluts
182
220
 
183
221
  utils.compile(jit)
184
222
  preluts = PreLUTs.make_preluts(
@@ -189,15 +227,16 @@ def get_preluts(
189
227
  ds=DS,
190
228
  accgoal=0.0001,
191
229
  jit=jit,
192
- n_cpu=n_cpu,
230
+ n_cpu=resolved_n_cpu["preluts"],
193
231
  )
194
232
  preluts.attrs["nkz0"] = nkz0
195
233
  preluts.attrs["nbeta"] = nbeta
234
+ utils.stamp_lut_metadata(preluts)
196
235
  preluts.save(preluts_path)
197
236
  return preluts
198
237
 
199
238
 
200
- def main():
239
+ def main() -> None:
201
240
  if __name__ == "__main__": # pragma: no cover
202
241
  luts = get_luts(
203
242
  folder=Path(__file__).parent.parent,
@@ -16,6 +16,7 @@ Key components:
16
16
  """
17
17
 
18
18
  import numpy as np
19
+ import numpy.typing as npt
19
20
 
20
21
  from pyfuga.utils import jit
21
22
 
@@ -23,7 +24,7 @@ from .constants import CM_STABLE, CM_UNSTABLE
23
24
 
24
25
 
25
26
  @jit("double(double,double)")
26
- def get_cdivkL(zeta0, kz0):
27
+ def get_cdivkL(zeta0: float, kz0: float) -> float:
27
28
  """c/(k*L), c is a stability constant: depending on the stability (0, Cm1, Cm2)"""
28
29
  if abs(zeta0) < 1e-10: # Neutral
29
30
  return 0.0
@@ -35,7 +36,7 @@ def get_cdivkL(zeta0, kz0):
35
36
 
36
37
 
37
38
  @jit("double(double,double)")
38
- def get_phi_inverse(kz, cdivkl):
39
+ def get_phi_inverse(kz: float, cdivkl: float) -> float:
39
40
  """
40
41
  Inverse of stability function phi_m
41
42
  for unstable conditions
@@ -45,7 +46,7 @@ def get_phi_inverse(kz, cdivkl):
45
46
 
46
47
 
47
48
  @jit("double(double,double,double)")
48
- def get_psi(zeta0, kz, cdivkl):
49
+ def get_psi(zeta0: float, kz: float, cdivkl: float) -> float:
49
50
  """Stability function -psi_m"""
50
51
  if zeta0 < 0:
51
52
  # Unstable: -psi_m=-ln(1/8*(1+phi_m**-2)*(1+phi_m**-1)**2)+2*atan(phi_m**-1)-pi/2
@@ -58,7 +59,7 @@ def get_psi(zeta0, kz, cdivkl):
58
59
 
59
60
 
60
61
  @jit("double(double,double,double)")
61
- def get_phi(zeta0, kz, cdivkl):
62
+ def get_phi(zeta0: float, kz: float, cdivkl: float) -> float:
62
63
  """Stability function phi_m"""
63
64
  if zeta0 < 0: # Unstable: phi_m=(1+Cm1*z/L)**(-1/4)
64
65
  return (1.0 + cdivkl * kz) ** (-0.25)
@@ -67,13 +68,18 @@ def get_phi(zeta0, kz, cdivkl):
67
68
 
68
69
 
69
70
  @jit("double[:](complex128[:,:],int32)")
70
- def complex_norm(arr, axis=0):
71
+ def complex_norm(arr: npt.NDArray[np.complex128], axis: int = 0) -> npt.NDArray[np.float64]:
71
72
  """Compute the norm of a complex array along the first axis"""
72
73
  return np.sqrt((np.abs(arr) ** 2).sum(0))
73
74
 
74
75
 
75
76
  @jit("double(double,double,complex128[:,:],complex128[:,:])")
76
- def get_new_h2(h, acc, Yerr, Y):
77
+ def get_new_h2(
78
+ h: float,
79
+ acc: float,
80
+ Yerr: npt.NDArray[np.complex128],
81
+ Y: npt.NDArray[np.complex128],
82
+ ) -> float:
77
83
  """Get new step size based on error between integration estimates"""
78
84
  err = np.max(complex_norm(Yerr, axis=0) / complex_norm(Y, axis=0))
79
85
  return np.maximum(np.minimum(0.9 * h * (acc / err) ** (1 / 3), 4.0e-1), 1.0e-4)
@@ -0,0 +1,44 @@
1
+ """constants.py
2
+ ==============
3
+ Global constants for atmospheric stability, numerical algorithms, and data structure sizes.
4
+
5
+ Key constants:
6
+ - **Stability (MO)**: CM_STABLE, CM_UNSTABLE for Monin-Obukhov relations
7
+ - **Numerics**: KAPPA (von Kármán), MAX_NODES (max preLUT nodes),
8
+ Y_NORM_THRESHOLD (threshold to trigger QR decomposition and substations
9
+ between nodes)
10
+ - **Data**: N_EQ (number of differential equations), UVW_LT (variable names)
11
+ - **Coordinate systems**: COORD_T (log profile), COORD_S (kz)
12
+ """
13
+
14
+ # PreLUT constants
15
+ CM_STABLE: float = 5.0 # MO Stability constant in phi_m (stable)
16
+ CM_UNSTABLE: float = -19.0 # MO Stability constant in phi_m (unstable) Hogstrom (1988)
17
+ N_EQ: int = 6 # ! Number of equations
18
+ KAPPA: float = 0.4 # Von Karman constant
19
+ KAPPA_SQUARED: float = 0.16 # Square of kappa
20
+ P_SCALE: int = 1 # Pressure normalisation variable
21
+ MAX_NODES: int = 400
22
+ Y_NORM_THRESHOLD: int = 100 # Max value of Ynorm before a sublevel is made
23
+
24
+ # LUT constant
25
+ FIRST_Z_OUTPUT: float = 1.0 # First height level in output
26
+ DS: float = 0.05 # Vertical grid spacing in log-height coordinate s = ln(z/z0)
27
+
28
+ # On-disk LUT cache compatibility version. Cached LUTs (preLUTs/fLUTs/LUTs) whose
29
+ # stored ``lut_format_version`` differs from this value - or which predate versioning
30
+ # entirely (legacy PyFuga caches) - are rejected and regenerated instead of loaded.
31
+ #
32
+ # Bump this for SEMANTIC/NUMERICAL changes to LUT output that keep the same on-disk
33
+ # structure (e.g. a physics bug-fix that changes values). STRUCTURAL changes (added,
34
+ # removed or renamed variables/dimensions) are additionally caught automatically by the
35
+ # ``lut_schema_fingerprint`` attribute and by the schema guard test in
36
+ # ``tests/test_lut_schema.py``, which fails and reminds you to bump this too.
37
+ LUT_FORMAT_VERSION: int = 1
38
+
39
+ # Other constants
40
+ UVW_LT: list[str] = ["UL", "UT", "VL", "VT", "WL", "WT", "PL", "PT"]
41
+
42
+ # Coordinate system indicators
43
+ COORD_T: int = 1 # t = u0 * KAPPA
44
+ COORD_S: int = 2 # s = kz
@@ -10,26 +10,34 @@ with legacy simulation data.
10
10
 
11
11
  import os
12
12
  import struct
13
+ from io import BufferedReader
13
14
  from pathlib import Path
15
+ from typing import Any, TypeAlias
14
16
 
15
17
  import numpy as np
16
18
  import xarray as xr
17
19
  from numpy import newaxis as na
18
20
 
21
+ PathLike: TypeAlias = str | os.PathLike[str]
22
+ PrelutTuple: TypeAlias = tuple[float, float, float, float, float, float]
23
+ PrelutListEntry: TypeAlias = tuple[str, float, float, float, float, float, float]
24
+
19
25
 
20
26
  class BinaryReader:
21
- def s(self, n):
27
+ fid: BufferedReader
28
+
29
+ def s(self, n: int) -> str:
22
30
  return self.fid.read(n).decode().strip()
23
31
 
24
- def i(self):
25
- return struct.unpack("i", self.fid.read(4))[0]
32
+ def i(self) -> int:
33
+ return int(struct.unpack("i", self.fid.read(4))[0])
26
34
 
27
- def d(self):
28
- return struct.unpack("d", self.fid.read(8))[0]
35
+ def d(self) -> float:
36
+ return float(struct.unpack("d", self.fid.read(8))[0])
29
37
 
30
38
 
31
39
  class Parameters(BinaryReader):
32
- def __init__(self, folder):
40
+ def __init__(self, folder: PathLike) -> None:
33
41
  i, d, s = self.i, self.d, self.s
34
42
  with open(Path(folder) / "parameters.bin", "rb") as self.fid:
35
43
  self.prelutname = s(127)
@@ -50,7 +58,7 @@ class Parameters(BinaryReader):
50
58
 
51
59
 
52
60
  class CaseData(BinaryReader):
53
- def __init__(self, folder):
61
+ def __init__(self, folder: PathLike) -> None:
54
62
  i, d, s = self.i, self.d, self.s
55
63
 
56
64
  with open(Path(folder) / "CaseData.bin", "rb") as self.fid:
@@ -65,31 +73,31 @@ class CaseData(BinaryReader):
65
73
  self.closure = i()
66
74
 
67
75
 
68
- def eof(fid):
76
+ def eof(fid: BufferedReader) -> int:
69
77
  fid.seek(-1, 2) # go to the file end.
70
78
  eof = fid.tell() + 1 # get the end of file location
71
79
  fid.seek(0, 0) # go back to file beginning
72
80
  return eof
73
81
 
74
82
 
75
- def read_prelut_list(folder, dict=True):
83
+ def read_prelut_list(folder: PathLike, dict: bool = True) -> dict[str, PrelutTuple] | list[PrelutListEntry]:
76
84
 
77
85
  folder = Path(folder)
78
86
  with open(folder / "prelut_list.lst", "rb") as fid:
79
87
  fid.seek(18)
80
88
  n = 24 if fid.read(6) == b"\x00\x00\x00\x00\x00\x00" else 18
81
89
 
82
- N = eof(fid) // (n + 48)
90
+ n_rows = eof(fid) // (n + 48)
83
91
 
84
92
  # list of filename (18 or 18+6xNull), ds, smaxx, kz0, beta, kzmax, accgoal
85
93
  if dict:
86
- return {fid.read(n)[:18].decode(): struct.unpack("d" * 6, fid.read(8 * 6)) for _ in range(N)}
94
+ return {fid.read(n)[:18].decode(): struct.unpack("d" * 6, fid.read(8 * 6)) for _ in range(n_rows)}
87
95
 
88
96
  else:
89
- return [(fid.read(n)[:18].decode(),) + struct.unpack("d" * 6, fid.read(8 * 6)) for _ in range(N)]
97
+ return [(fid.read(n)[:18].decode(),) + struct.unpack("d" * 6, fid.read(8 * 6)) for _ in range(n_rows)]
90
98
 
91
99
 
92
- def read_pre_file(filename):
100
+ def read_pre_file(filename: PathLike) -> dict[str, tuple[list[str], Any]]:
93
101
  with open(filename, "rb") as fid:
94
102
  d = np.fromfile(fid, float, -1).reshape((-1, 291))
95
103
  Yleft, Rleft, Rright, dy = np.moveaxis((d[:, :288:2] + d[:, 1:288:2] * 1j).reshape((-1, 4, 6, 6)), 1, 0)
@@ -112,15 +120,16 @@ def read_pre_file(filename):
112
120
  }
113
121
 
114
122
 
115
- def read_lut_file(filename, prelut_folder):
123
+ def read_lut_file(filename: PathLike, prelut_folder: PathLike) -> xr.Dataset:
124
+ filename_path = Path(filename)
116
125
  parameters = Parameters(prelut_folder)
117
- casedata = CaseData(os.path.dirname(filename))
118
- var = os.path.basename(filename)[:2]
119
- level = int(filename[-8:-4])
126
+ casedata = CaseData(filename_path.parent)
127
+ var = filename_path.name[:2]
128
+ level = int(filename_path.name[-8:-4])
120
129
  z = casedata.zhub if level == 9999 else casedata.z0 * np.exp(casedata.ds * level)
121
130
 
122
- lut = np.fromfile(filename, complex, -1).reshape((len(parameters.kz0_lst) + 1, len(parameters.beta_lst)))
123
- lut = lut[1:].T
131
+ raw = np.fromfile(filename, complex, -1).reshape((len(parameters.kz0_lst) + 1, len(parameters.beta_lst)))
132
+ lut = raw[1:].T
124
133
  return xr.Dataset(
125
134
  {
126
135
  var: (("beta", "kz0", "level"), lut[:, :, na]),
@@ -133,7 +142,7 @@ def read_lut_file(filename, prelut_folder):
133
142
  )
134
143
 
135
144
 
136
- def read_lut_dat_file(filename, nx, ny, dx, dy):
145
+ def read_lut_dat_file(filename: PathLike, nx: int, ny: int, dx: float, dy: float) -> xr.DataArray:
137
146
  dat = np.fromfile(filename, np.dtype("<f"), -1).astype(float).reshape((ny // 2, nx))
138
147
  x_lst = np.arange(-nx // 4, nx * 3 / 4) * dx # rotor is located 1/4 downstream
139
148