pyfuga 0.2.0__tar.gz → 0.3.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 (48) hide show
  1. {pyfuga-0.2.0 → pyfuga-0.3.0}/PKG-INFO +20 -6
  2. {pyfuga-0.2.0 → pyfuga-0.3.0}/README.md +16 -5
  3. {pyfuga-0.2.0 → pyfuga-0.3.0}/pyfuga/__init__.py +7 -6
  4. {pyfuga-0.2.0 → pyfuga-0.3.0}/pyfuga/_fuga.py +79 -48
  5. {pyfuga-0.2.0 → pyfuga-0.3.0}/pyfuga/common.py +12 -6
  6. pyfuga-0.3.0/pyfuga/constants.py +33 -0
  7. {pyfuga-0.2.0 → pyfuga-0.3.0}/pyfuga/file_readers.py +27 -18
  8. {pyfuga-0.2.0 → pyfuga-0.3.0}/pyfuga/flut.py +219 -143
  9. {pyfuga-0.2.0 → pyfuga-0.3.0}/pyfuga/paths.py +37 -4
  10. {pyfuga-0.2.0 → pyfuga-0.3.0}/pyfuga/preludium_eq_routines.py +9 -6
  11. {pyfuga-0.2.0 → pyfuga-0.3.0}/pyfuga/preluts.py +76 -42
  12. {pyfuga-0.2.0 → pyfuga-0.3.0}/pyfuga/preluts_generator/generator.py +23 -5
  13. {pyfuga-0.2.0 → pyfuga-0.3.0}/pyfuga/preluts_generator/integration.py +22 -4
  14. {pyfuga-0.2.0 → pyfuga-0.3.0}/pyfuga/preluts_generator/matrices.py +40 -40
  15. {pyfuga-0.2.0 → pyfuga-0.3.0}/pyfuga/preluts_generator/nodes.py +5 -5
  16. pyfuga-0.3.0/pyfuga/profiling.py +109 -0
  17. {pyfuga-0.2.0 → pyfuga-0.3.0}/pyfuga/trafalgar.py +35 -21
  18. pyfuga-0.3.0/pyfuga/utils.py +351 -0
  19. {pyfuga-0.2.0 → pyfuga-0.3.0}/pyfuga/z0.py +11 -10
  20. {pyfuga-0.2.0 → pyfuga-0.3.0}/pyfuga.egg-info/PKG-INFO +20 -6
  21. {pyfuga-0.2.0 → pyfuga-0.3.0}/pyfuga.egg-info/SOURCES.txt +2 -0
  22. {pyfuga-0.2.0 → pyfuga-0.3.0}/pyfuga.egg-info/requires.txt +3 -0
  23. {pyfuga-0.2.0 → pyfuga-0.3.0}/pyproject.toml +97 -8
  24. pyfuga-0.3.0/tests/test_api.py +410 -0
  25. pyfuga-0.3.0/tests/test_common.py +6 -0
  26. {pyfuga-0.2.0 → pyfuga-0.3.0}/tests/test_flut.py +131 -16
  27. {pyfuga-0.2.0 → pyfuga-0.3.0}/tests/test_preluts_jit.py +1 -1
  28. pyfuga-0.3.0/tests/test_profiling.py +12 -0
  29. pyfuga-0.3.0/tests/test_trafalgar.py +110 -0
  30. pyfuga-0.2.0/pyfuga/constants.py +0 -33
  31. pyfuga-0.2.0/pyfuga/profiling.py +0 -86
  32. pyfuga-0.2.0/pyfuga/utils.py +0 -228
  33. pyfuga-0.2.0/tests/test_api.py +0 -145
  34. pyfuga-0.2.0/tests/test_trafalgar.py +0 -55
  35. {pyfuga-0.2.0 → pyfuga-0.3.0}/LICENSE +0 -0
  36. {pyfuga-0.2.0 → pyfuga-0.3.0}/pyfuga/preluts_generator/__init__.py +0 -0
  37. {pyfuga-0.2.0 → pyfuga-0.3.0}/pyfuga/preluts_generator/transforms.py +0 -0
  38. {pyfuga-0.2.0 → pyfuga-0.3.0}/pyfuga/typing.py +0 -0
  39. {pyfuga-0.2.0 → pyfuga-0.3.0}/pyfuga.egg-info/dependency_links.txt +0 -0
  40. {pyfuga-0.2.0 → pyfuga-0.3.0}/pyfuga.egg-info/top_level.txt +0 -0
  41. {pyfuga-0.2.0 → pyfuga-0.3.0}/setup.cfg +0 -0
  42. {pyfuga-0.2.0 → pyfuga-0.3.0}/tests/test_jit_signatures.py +0 -0
  43. {pyfuga-0.2.0 → pyfuga-0.3.0}/tests/test_paths.py +0 -0
  44. {pyfuga-0.2.0 → pyfuga-0.3.0}/tests/test_preluts.py +0 -0
  45. {pyfuga-0.2.0 → pyfuga-0.3.0}/tests/test_readers.py +0 -0
  46. {pyfuga-0.2.0 → pyfuga-0.3.0}/tests/test_typing.py +0 -0
  47. {pyfuga-0.2.0 → pyfuga-0.3.0}/tests/test_utils.py +0 -0
  48. {pyfuga-0.2.0 → pyfuga-0.3.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.3.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.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,6 +123,7 @@ 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
 
@@ -121,60 +133,79 @@ def get_luts(
121
133
  luts.attrs["name"] = luts_path.stem
122
134
  return luts
123
135
 
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)
136
+ fluts = get_fluts(
137
+ folder,
138
+ zeta0,
139
+ nkz0,
140
+ nbeta,
141
+ diameter,
142
+ zhub,
143
+ z0,
144
+ zi,
145
+ zlow,
146
+ zhigh,
147
+ lut_vars,
148
+ jit,
149
+ resolved_n_cpu["fluts"],
150
+ n_cpu_stage={"preluts": resolved_n_cpu["preluts"]},
151
+ )
152
+ luts = Trafalgar(fluts, nx, ny, dx, dy).make_luts(interpolation_order=3, n_cpu=resolved_n_cpu["trafalgar"])
126
153
  luts.to_netcdf(luts_path)
127
154
  luts.attrs["name"] = luts_path.stem
128
155
  return luts
129
156
 
130
157
 
131
158
  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
159
+ folder: str | Path,
160
+ zeta0: float,
161
+ nkz0: int,
162
+ nbeta: int,
163
+ diameter: float,
164
+ zhub: float,
165
+ z0: float,
166
+ zi: float,
167
+ zlow: float,
168
+ zhigh: float,
169
+ lut_vars: list[str] = UVW_LT,
170
+ jit: bool = True,
171
+ n_cpu: int | None = None,
172
+ n_cpu_stage: NCpuStageConfig | None = None,
173
+ ) -> ComplexXRDataset: # pragma: no cover
146
174
  folder = Path(folder)
147
175
  folder.mkdir(parents=True, exist_ok=True)
176
+ resolved_n_cpu = utils.resolve_n_cpu_stage(n_cpu, n_cpu_stage)
148
177
 
149
178
  fluts_path = get_fluts_path(folder, zeta0, nkz0, nbeta, diameter, zhub, z0, zi, zlow, zhigh, lut_vars)
150
179
  if fluts_path.exists():
151
180
  return ComplexXRDataset.from_netcdf(fluts_path)
152
181
 
153
182
  utils.compile(jit)
154
- preluts = get_preluts(folder, zeta0, nkz0, nbeta, jit, n_cpu)
183
+ preluts = get_preluts(folder, zeta0, nkz0, nbeta, jit, resolved_n_cpu["preluts"], None)
155
184
 
156
185
  low_level_out, high_level_out = get_level_range(zlow, zhigh, zhub, z0)
157
186
 
158
187
  flut_generator = FourierLUTGenerator(preluts, zhub, diameter, zi)
159
188
  if low_level_out == high_level_out == 9999:
160
- fluts = flut_generator.make_hubheight_luts(z0, lut_vars, n_cpu=n_cpu)
189
+ fluts = flut_generator.make_hubheight_luts(z0, lut_vars, n_cpu=resolved_n_cpu["fluts"])
161
190
  else:
162
- fluts = flut_generator.make_lut(z0, low_level_out, high_level_out, lut_vars, n_cpu=n_cpu)
191
+ fluts = flut_generator.make_lut(z0, low_level_out, high_level_out, lut_vars, n_cpu=resolved_n_cpu["fluts"])
163
192
 
164
193
  fluts.save(fluts_path)
165
194
  return fluts
166
195
 
167
196
 
168
197
  def get_preluts(
169
- folder,
170
- zeta0,
171
- nkz0,
172
- nbeta,
173
- jit=True,
174
- n_cpu=1,
175
- ): # pragma: no cover
198
+ folder: str | Path,
199
+ zeta0: float,
200
+ nkz0: int,
201
+ nbeta: int,
202
+ jit: bool = True,
203
+ n_cpu: int | None = None,
204
+ n_cpu_stage: NCpuStageConfig | None = None,
205
+ ) -> PreLUTs: # pragma: no cover
176
206
  folder = Path(folder)
177
207
  folder.mkdir(parents=True, exist_ok=True)
208
+ resolved_n_cpu = utils.resolve_n_cpu_stage(n_cpu, n_cpu_stage)
178
209
 
179
210
  preluts_path = get_preluts_path(folder, zeta0, nkz0, nbeta)
180
211
  if preluts_path.exists():
@@ -189,7 +220,7 @@ def get_preluts(
189
220
  ds=DS,
190
221
  accgoal=0.0001,
191
222
  jit=jit,
192
- n_cpu=n_cpu,
223
+ n_cpu=resolved_n_cpu["preluts"],
193
224
  )
194
225
  preluts.attrs["nkz0"] = nkz0
195
226
  preluts.attrs["nbeta"] = nbeta
@@ -197,7 +228,7 @@ def get_preluts(
197
228
  return preluts
198
229
 
199
230
 
200
- def main():
231
+ def main() -> None:
201
232
  if __name__ == "__main__": # pragma: no cover
202
233
  luts = get_luts(
203
234
  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,33 @@
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
+ # Other constants
29
+ UVW_LT: list[str] = ["UL", "UT", "VL", "VT", "WL", "WT", "PL", "PT"]
30
+
31
+ # Coordinate system indicators
32
+ COORD_T: int = 1 # t = u0 * KAPPA
33
+ 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,11 +120,12 @@ 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
131
  lut = np.fromfile(filename, complex, -1).reshape((len(parameters.kz0_lst) + 1, len(parameters.beta_lst)))
@@ -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