grid-generator 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.
- {grid_generator-0.2.0 → grid_generator-0.3.0}/PKG-INFO +1 -1
- {grid_generator-0.2.0 → grid_generator-0.3.0}/pyproject.toml +4 -0
- {grid_generator-0.2.0 → grid_generator-0.3.0}/src/grid_generator/body_fitted.py +115 -24
- {grid_generator-0.2.0 → grid_generator-0.3.0}/src/grid_generator/cartesian.py +26 -1
- {grid_generator-0.2.0 → grid_generator-0.3.0}/src/grid_generator/cli/app.py +18 -7
- grid_generator-0.3.0/src/grid_generator/cli/callbacks.py +47 -0
- {grid_generator-0.2.0 → grid_generator-0.3.0}/src/grid_generator/cli/cmd_geometry.py +5 -0
- grid_generator-0.3.0/src/grid_generator/cli/examples_command.py +179 -0
- {grid_generator-0.2.0 → grid_generator-0.3.0}/src/grid_generator/config/body_fitted.py +93 -8
- {grid_generator-0.2.0 → grid_generator-0.3.0}/src/grid_generator/config/cartesian.py +28 -2
- grid_generator-0.3.0/src/grid_generator/config/markers.py +89 -0
- {grid_generator-0.2.0 → grid_generator-0.3.0}/src/grid_generator/config/templates.py +40 -1
- grid_generator-0.3.0/src/grid_generator/examples/cone/cone7_m7_rn2p5/cone.toml +46 -0
- grid_generator-0.3.0/src/grid_generator/outer.py +312 -0
- {grid_generator-0.2.0 → grid_generator-0.3.0}/src/grid_generator.egg-info/PKG-INFO +1 -1
- {grid_generator-0.2.0 → grid_generator-0.3.0}/src/grid_generator.egg-info/SOURCES.txt +6 -0
- {grid_generator-0.2.0 → grid_generator-0.3.0}/src/grid_generator.egg-info/scm_file_list.json +6 -0
- grid_generator-0.3.0/src/grid_generator.egg-info/scm_version.json +8 -0
- {grid_generator-0.2.0 → grid_generator-0.3.0}/tests/test_body_fitted.py +76 -1
- {grid_generator-0.2.0 → grid_generator-0.3.0}/tests/test_cartesian.py +42 -7
- grid_generator-0.3.0/tests/test_examples.py +99 -0
- grid_generator-0.3.0/tests/test_outer.py +231 -0
- grid_generator-0.2.0/src/grid_generator/cli/callbacks.py +0 -19
- grid_generator-0.2.0/src/grid_generator.egg-info/scm_version.json +0 -8
- {grid_generator-0.2.0 → grid_generator-0.3.0}/.github/workflows/docs.yml +0 -0
- {grid_generator-0.2.0 → grid_generator-0.3.0}/.github/workflows/package.yml +0 -0
- {grid_generator-0.2.0 → grid_generator-0.3.0}/.github/workflows/publish.yml +0 -0
- {grid_generator-0.2.0 → grid_generator-0.3.0}/.github/workflows/test.yml +0 -0
- {grid_generator-0.2.0 → grid_generator-0.3.0}/.gitignore +0 -0
- {grid_generator-0.2.0 → grid_generator-0.3.0}/LICENSE +0 -0
- {grid_generator-0.2.0 → grid_generator-0.3.0}/README.md +0 -0
- {grid_generator-0.2.0 → grid_generator-0.3.0}/docs/index.md +0 -0
- {grid_generator-0.2.0 → grid_generator-0.3.0}/docs/installation.md +0 -0
- {grid_generator-0.2.0 → grid_generator-0.3.0}/mkdocs.yml +0 -0
- {grid_generator-0.2.0 → grid_generator-0.3.0}/setup.cfg +0 -0
- {grid_generator-0.2.0 → grid_generator-0.3.0}/src/grid_generator/__init__.py +0 -0
- {grid_generator-0.2.0 → grid_generator-0.3.0}/src/grid_generator/cli/__init__.py +0 -0
- {grid_generator-0.2.0 → grid_generator-0.3.0}/src/grid_generator/cli/cmd_cartesian.py +0 -0
- {grid_generator-0.2.0 → grid_generator-0.3.0}/src/grid_generator/cli/cmd_distribution.py +0 -0
- {grid_generator-0.2.0 → grid_generator-0.3.0}/src/grid_generator/cli/help_text.py +0 -0
- {grid_generator-0.2.0 → grid_generator-0.3.0}/src/grid_generator/config/__init__.py +0 -0
- {grid_generator-0.2.0 → grid_generator-0.3.0}/src/grid_generator/config/distribution.py +0 -0
- {grid_generator-0.2.0 → grid_generator-0.3.0}/src/grid_generator/config/points.py +0 -0
- {grid_generator-0.2.0 → grid_generator-0.3.0}/src/grid_generator/distributions/__init__.py +0 -0
- {grid_generator-0.2.0 → grid_generator-0.3.0}/src/grid_generator/distributions/catalog.py +0 -0
- {grid_generator-0.2.0 → grid_generator-0.3.0}/src/grid_generator/distributions/equidistant.py +0 -0
- {grid_generator-0.2.0 → grid_generator-0.3.0}/src/grid_generator/distributions/generate.py +0 -0
- {grid_generator-0.2.0 → grid_generator-0.3.0}/src/grid_generator/distributions/geometric.py +0 -0
- {grid_generator-0.2.0 → grid_generator-0.3.0}/src/grid_generator/distributions/models.py +0 -0
- {grid_generator-0.2.0 → grid_generator-0.3.0}/src/grid_generator/distributions/parse.py +0 -0
- {grid_generator-0.2.0 → grid_generator-0.3.0}/src/grid_generator/distributions/polynomial.py +0 -0
- {grid_generator-0.2.0 → grid_generator-0.3.0}/src/grid_generator/distributions/segmented.py +0 -0
- {grid_generator-0.2.0 → grid_generator-0.3.0}/src/grid_generator/distributions/tanh.py +0 -0
- {grid_generator-0.2.0 → grid_generator-0.3.0}/src/grid_generator/distributions/writers.py +0 -0
- {grid_generator-0.2.0 → grid_generator-0.3.0}/src/grid_generator/quality.py +0 -0
- {grid_generator-0.2.0 → grid_generator-0.3.0}/src/grid_generator.egg-info/dependency_links.txt +0 -0
- {grid_generator-0.2.0 → grid_generator-0.3.0}/src/grid_generator.egg-info/entry_points.txt +0 -0
- {grid_generator-0.2.0 → grid_generator-0.3.0}/src/grid_generator.egg-info/requires.txt +0 -0
- {grid_generator-0.2.0 → grid_generator-0.3.0}/src/grid_generator.egg-info/top_level.txt +0 -0
- {grid_generator-0.2.0 → grid_generator-0.3.0}/tests/test_distribution_cli.py +0 -0
- {grid_generator-0.2.0 → grid_generator-0.3.0}/tests/test_distributions.py +0 -0
- {grid_generator-0.2.0 → grid_generator-0.3.0}/tests/test_package.py +0 -0
|
@@ -54,6 +54,10 @@ Documentation = "https://uahypersonics.github.io/grid-generator"
|
|
|
54
54
|
[tool.setuptools.packages.find]
|
|
55
55
|
where = ["src"]
|
|
56
56
|
|
|
57
|
+
[tool.setuptools.package-data]
|
|
58
|
+
# bundled example cases: examples/<geometry type>/<name>/<files>
|
|
59
|
+
"grid_generator" = ["examples/*/*/*"]
|
|
60
|
+
|
|
57
61
|
[tool.setuptools_scm]
|
|
58
62
|
|
|
59
63
|
[tool.pytest.ini_options]
|
|
@@ -12,8 +12,17 @@ polar about the nose centre), then the configured body distribution starting fro
|
|
|
12
12
|
the nose spacing, so the spacing is continuous across the junction. The body may be
|
|
13
13
|
straight (plate, wedge, cone) or curved (tangent ogive, with an optional cylinder).
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
H is either constant ([domain] height, picked large enough to clear the bow shock),
|
|
16
|
+
or the grid has a shock-aligned outer boundary ([outer]): a smooth curve a margin
|
|
17
|
+
beyond the shock, with the outer points spaced smoothly along it (see
|
|
18
|
+
grid_generator.outer). Each line then leaves the wall along the normal and bends
|
|
19
|
+
toward its outer point,
|
|
20
|
+
|
|
21
|
+
x(i, j) = wall(s_i) + n_j * normal(s_i) + (n_j / h_i)^2 * shift_i,
|
|
22
|
+
|
|
23
|
+
with h_i the outer point's distance along the normal and shift_i its offset across
|
|
24
|
+
it. The wall-normal distribution keeps its first spacing and point count on every
|
|
25
|
+
line; only its stretching follows the line's height.
|
|
17
26
|
|
|
18
27
|
With a [span] table (plate and wedge), the 2D plane is extruded in z: the same
|
|
19
28
|
(x, y) plane at every z of the span distribution, giving a 3D grid.
|
|
@@ -37,26 +46,27 @@ from grid_generator.config.body_fitted import (
|
|
|
37
46
|
load_body_fitted_config,
|
|
38
47
|
)
|
|
39
48
|
from grid_generator.distributions import DistributionSpec, EquidistantSpec, build_distribution
|
|
49
|
+
from grid_generator.outer import outer_points, read_shock_curve
|
|
40
50
|
|
|
41
51
|
# --------------------------------------------------
|
|
42
52
|
# constants
|
|
43
53
|
# --------------------------------------------------
|
|
44
54
|
|
|
45
|
-
#
|
|
46
|
-
# line ahead of the nose, imax the outflow at x_end, jmin the wall,
|
|
47
|
-
# boundary
|
|
48
|
-
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
55
|
+
# grid side of each [markers] key (the names themselves come from the config): imin is
|
|
56
|
+
# the stagnation line ahead of the nose, imax the outflow at x_end, jmin the wall,
|
|
57
|
+
# jmax the outer boundary
|
|
58
|
+
MARKER_SIDES = {
|
|
59
|
+
"symmetry": "imin",
|
|
60
|
+
"outflow": "imax",
|
|
61
|
+
"wall": "jmin",
|
|
62
|
+
"outer": "jmax",
|
|
53
63
|
}
|
|
54
64
|
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
|
|
58
|
-
"
|
|
59
|
-
"
|
|
65
|
+
# the two extra sides of an extruded (3D) grid: the first and last z-planes; the
|
|
66
|
+
# solver decides what they are (periodic, symmetry, ...)
|
|
67
|
+
SPAN_MARKER_SIDES = {
|
|
68
|
+
"span_min": "kmin",
|
|
69
|
+
"span_max": "kmax",
|
|
60
70
|
}
|
|
61
71
|
|
|
62
72
|
|
|
@@ -88,16 +98,12 @@ def generate_body_fitted(source: BodyFittedConfig | str | Path) -> Dataset:
|
|
|
88
98
|
# arc lengths of the wall points: nose, then body
|
|
89
99
|
s_wall = wall_arc_lengths(profile, config.points.nose.n_points, config.body_distribution())
|
|
90
100
|
|
|
91
|
-
# wall-normal distances: from the wall up to the height
|
|
92
|
-
n_dist = build_distribution(config.normal_distribution())
|
|
93
|
-
|
|
94
101
|
# exact wall points and normals
|
|
95
102
|
x_wall, y_wall = profile.point(s_wall)
|
|
96
103
|
nx, ny = profile.normal(s_wall)
|
|
97
104
|
|
|
98
|
-
# j-lines
|
|
99
|
-
x_plane = x_wall
|
|
100
|
-
y_plane = y_wall[:, None] + n_dist[None, :] * ny[:, None]
|
|
105
|
+
# j-lines from the wall out to the outer boundary: (ni, nj)
|
|
106
|
+
x_plane, y_plane = build_grid_lines(config, x_wall, y_wall, nx, ny)
|
|
101
107
|
|
|
102
108
|
# span: the z-planes to extrude to, or the single plane z = 0 for a 2D grid
|
|
103
109
|
if config.span is not None:
|
|
@@ -112,10 +118,13 @@ def generate_body_fitted(source: BodyFittedConfig | str | Path) -> Dataset:
|
|
|
112
118
|
z = np.broadcast_to(z_axis[None, None, :], x.shape).copy()
|
|
113
119
|
grid = StructuredGrid(x=x, y=y, z=z)
|
|
114
120
|
|
|
115
|
-
# markers: four sides in 2D, plus the two z-planes in 3D
|
|
116
|
-
markers =
|
|
121
|
+
# markers: four sides in 2D, plus the two z-planes in 3D, named by [markers]
|
|
122
|
+
markers = {}
|
|
123
|
+
for key, side in MARKER_SIDES.items():
|
|
124
|
+
markers[side] = getattr(config.markers, key)
|
|
117
125
|
if nk > 1:
|
|
118
|
-
|
|
126
|
+
for key, side in SPAN_MARKER_SIDES.items():
|
|
127
|
+
markers[side] = getattr(config.markers, key)
|
|
119
128
|
dimensionality = 3 if nk > 1 else 2
|
|
120
129
|
|
|
121
130
|
# describe the grid; markers are what cfd-io's SU2 writer needs
|
|
@@ -139,6 +148,88 @@ def generate_body_fitted(source: BodyFittedConfig | str | Path) -> Dataset:
|
|
|
139
148
|
return dataset
|
|
140
149
|
|
|
141
150
|
|
|
151
|
+
def build_grid_lines(
|
|
152
|
+
config: BodyFittedConfig,
|
|
153
|
+
x_wall: NDArray[np.float64],
|
|
154
|
+
y_wall: NDArray[np.float64],
|
|
155
|
+
nx: NDArray[np.float64],
|
|
156
|
+
ny: NDArray[np.float64],
|
|
157
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
|
|
158
|
+
"""The j-lines: from each wall point out to the outer boundary.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
config: Validated configuration.
|
|
162
|
+
x_wall: Wall points x [m].
|
|
163
|
+
y_wall: Wall points y [m].
|
|
164
|
+
nx: Unit wall normals, x component.
|
|
165
|
+
ny: Unit wall normals, y component.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
Grid x and y [m], shape (ni, nj).
|
|
169
|
+
|
|
170
|
+
Raises:
|
|
171
|
+
ValueError: If a wall normal misses the outer boundary, or [points.normal]
|
|
172
|
+
doesn't give every line the same number of points.
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
# constant height: straight lines along the wall normals, all with the same distribution
|
|
176
|
+
if config.outer is None:
|
|
177
|
+
n_line = build_distribution(config.normal_distribution())
|
|
178
|
+
x_plane = x_wall[:, None] + n_line[None, :] * nx[:, None]
|
|
179
|
+
y_plane = y_wall[:, None] + n_line[None, :] * ny[:, None]
|
|
180
|
+
return x_plane, y_plane
|
|
181
|
+
|
|
182
|
+
# shock-aligned: the outer end of every line, on a smooth curve beyond the shock
|
|
183
|
+
shock_x, shock_y = read_shock_curve(config.outer.shock)
|
|
184
|
+
outer_x, outer_y = outer_points(
|
|
185
|
+
x_wall,
|
|
186
|
+
y_wall,
|
|
187
|
+
nx,
|
|
188
|
+
ny,
|
|
189
|
+
shock_x,
|
|
190
|
+
shock_y,
|
|
191
|
+
config.outer.margin_factor,
|
|
192
|
+
config.outer.min_offset,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# each line: along the normal at the wall, bending toward its outer point
|
|
196
|
+
lines_x = []
|
|
197
|
+
lines_y = []
|
|
198
|
+
for i in range(x_wall.size):
|
|
199
|
+
# split the reach into a part along the normal and a shift across it
|
|
200
|
+
reach_x = outer_x[i] - x_wall[i]
|
|
201
|
+
reach_y = outer_y[i] - y_wall[i]
|
|
202
|
+
height = reach_x * nx[i] + reach_y * ny[i]
|
|
203
|
+
shift_x = reach_x - height * nx[i]
|
|
204
|
+
shift_y = reach_y - height * ny[i]
|
|
205
|
+
|
|
206
|
+
# the same normal distribution (first spacing, points) stretched to this height
|
|
207
|
+
try:
|
|
208
|
+
n_line = build_distribution(config.normal_distribution(float(height)))
|
|
209
|
+
except ValueError as error:
|
|
210
|
+
raise ValueError(
|
|
211
|
+
f"[points.normal] at a line height of {height:.4g} m: {error}"
|
|
212
|
+
) from None
|
|
213
|
+
|
|
214
|
+
# the shift grows quadratically, so the line leaves the wall along the normal
|
|
215
|
+
fraction = n_line / height
|
|
216
|
+
lines_x.append(x_wall[i] + n_line * nx[i] + fraction**2 * shift_x)
|
|
217
|
+
lines_y.append(y_wall[i] + n_line * ny[i] + fraction**2 * shift_y)
|
|
218
|
+
|
|
219
|
+
# check: a structured grid needs the same number of points on every line
|
|
220
|
+
counts = {line.size for line in lines_x}
|
|
221
|
+
if len(counts) > 1:
|
|
222
|
+
raise ValueError(
|
|
223
|
+
"[points.normal] gives the lines different numbers of points "
|
|
224
|
+
f"({min(counts)} to {max(counts)}); set n_points for a shock-aligned grid"
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
x_plane = np.vstack(lines_x)
|
|
228
|
+
y_plane = np.vstack(lines_y)
|
|
229
|
+
|
|
230
|
+
return x_plane, y_plane
|
|
231
|
+
|
|
232
|
+
|
|
142
233
|
def wall_arc_lengths(
|
|
143
234
|
profile: Profile2D, n_nose: int, body_spec: DistributionSpec
|
|
144
235
|
) -> NDArray[np.float64]:
|
|
@@ -11,8 +11,22 @@ import numpy as np
|
|
|
11
11
|
from cfd_io import Dataset, StructuredGrid
|
|
12
12
|
|
|
13
13
|
from grid_generator.config import CartesianConfig, load_cartesian_config
|
|
14
|
+
from grid_generator.config.cartesian import AXIS_MARKER_KEYS
|
|
14
15
|
from grid_generator.distributions import build_distribution
|
|
15
16
|
|
|
17
|
+
# --------------------------------------------------
|
|
18
|
+
# constants
|
|
19
|
+
# --------------------------------------------------
|
|
20
|
+
# grid side of each [markers] key (the names themselves come from the config)
|
|
21
|
+
MARKER_SIDES = {
|
|
22
|
+
"x_min": "imin",
|
|
23
|
+
"x_max": "imax",
|
|
24
|
+
"y_min": "jmin",
|
|
25
|
+
"y_max": "jmax",
|
|
26
|
+
"z_min": "kmin",
|
|
27
|
+
"z_max": "kmax",
|
|
28
|
+
}
|
|
29
|
+
|
|
16
30
|
|
|
17
31
|
# --------------------------------------------------
|
|
18
32
|
# public API
|
|
@@ -24,7 +38,8 @@ def generate_cartesian(source: CartesianConfig | str | Path) -> Dataset:
|
|
|
24
38
|
source: Validated configuration or configuration file path.
|
|
25
39
|
|
|
26
40
|
Returns:
|
|
27
|
-
Grid-only CFD dataset with physical coordinate arrays
|
|
41
|
+
Grid-only CFD dataset with physical coordinate arrays and the boundary markers
|
|
42
|
+
of the axes the grid has.
|
|
28
43
|
"""
|
|
29
44
|
|
|
30
45
|
# load configuration when the caller supplied an artifact path
|
|
@@ -42,6 +57,15 @@ def generate_cartesian(source: CartesianConfig | str | Path) -> Dataset:
|
|
|
42
57
|
grid_x, grid_y, grid_z = np.meshgrid(x_axis, y_axis, z_axis, indexing="ij")
|
|
43
58
|
grid = StructuredGrid(x=grid_x, y=grid_y, z=grid_z)
|
|
44
59
|
|
|
60
|
+
# markers: both ends of each axis the grid has, named by [markers]
|
|
61
|
+
active_axes = {"x": config.x, "y": config.y, "z": config.z}
|
|
62
|
+
markers = {}
|
|
63
|
+
for axis_name, axis_spec in active_axes.items():
|
|
64
|
+
if axis_spec is None:
|
|
65
|
+
continue
|
|
66
|
+
for key in AXIS_MARKER_KEYS[axis_name]:
|
|
67
|
+
markers[MARKER_SIDES[key]] = getattr(config.markers, key)
|
|
68
|
+
|
|
45
69
|
dimensionality = 1 + int(config.y is not None) + int(config.z is not None)
|
|
46
70
|
dataset = Dataset(
|
|
47
71
|
grid=grid,
|
|
@@ -54,6 +78,7 @@ def generate_cartesian(source: CartesianConfig | str | Path) -> Dataset:
|
|
|
54
78
|
"ni": grid.shape[0],
|
|
55
79
|
"nj": grid.shape[1],
|
|
56
80
|
"nk": grid.shape[2],
|
|
81
|
+
"markers": {"structured": markers},
|
|
57
82
|
},
|
|
58
83
|
)
|
|
59
84
|
return dataset
|
|
@@ -7,7 +7,7 @@ from typing import Annotated
|
|
|
7
7
|
|
|
8
8
|
import typer
|
|
9
9
|
|
|
10
|
-
from grid_generator.cli.callbacks import version_callback
|
|
10
|
+
from grid_generator.cli.callbacks import verbose_callback, version_callback
|
|
11
11
|
from grid_generator.cli.cmd_cartesian import cartesian_app
|
|
12
12
|
from grid_generator.cli.cmd_distribution import distribution_app
|
|
13
13
|
from grid_generator.cli.cmd_geometry import GEOMETRY_HELP, build_geometry_app
|
|
@@ -38,20 +38,31 @@ def main(
|
|
|
38
38
|
help="Show version and exit.",
|
|
39
39
|
),
|
|
40
40
|
] = False,
|
|
41
|
+
verbose: Annotated[
|
|
42
|
+
bool,
|
|
43
|
+
typer.Option(
|
|
44
|
+
"--verbose",
|
|
45
|
+
"-v",
|
|
46
|
+
callback=verbose_callback,
|
|
47
|
+
help="Enable verbose (debug) output.",
|
|
48
|
+
),
|
|
49
|
+
] = False,
|
|
41
50
|
) -> None:
|
|
42
51
|
"""Generate structured computational grids."""
|
|
43
52
|
|
|
44
|
-
del version
|
|
53
|
+
del version, verbose
|
|
45
54
|
|
|
46
55
|
|
|
47
56
|
# --------------------------------------------------
|
|
48
|
-
# register subcommands
|
|
57
|
+
# register subcommands (note: order of registration is order of appearance in the CLI help)
|
|
49
58
|
# --------------------------------------------------
|
|
59
|
+
|
|
60
|
+
# 1D point distributions: reference (info) and 1D point files (init / run)
|
|
61
|
+
app.add_typer(distribution_app, name="distribution")
|
|
62
|
+
|
|
63
|
+
# Cartesian grids: structured grids aligned with the coordinate axes
|
|
50
64
|
app.add_typer(cartesian_app, name="cartesian")
|
|
51
65
|
|
|
52
|
-
# body-fitted grids, one command group per geometry: plate, wedge, cone
|
|
66
|
+
# body-fitted grids, one command group per geometry: plate, wedge, cone, ogive, ... (add more as needed)
|
|
53
67
|
for geometry_type in GEOMETRY_HELP:
|
|
54
68
|
app.add_typer(build_geometry_app(geometry_type), name=geometry_type)
|
|
55
|
-
|
|
56
|
-
# 1D point distributions: reference (info) and 1D point files (init / run)
|
|
57
|
-
app.add_typer(distribution_app, name="distribution")
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Shared callbacks for the grid-generator CLI."""
|
|
2
|
+
|
|
3
|
+
# --------------------------------------------------
|
|
4
|
+
# load necessary modules
|
|
5
|
+
# --------------------------------------------------
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from grid_generator import __version__
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# --------------------------------------------------
|
|
14
|
+
# callbacks
|
|
15
|
+
# --------------------------------------------------
|
|
16
|
+
def version_callback(value: bool) -> None:
|
|
17
|
+
"""Print the installed package version and exit."""
|
|
18
|
+
|
|
19
|
+
if value:
|
|
20
|
+
typer.echo(f"grid-generator {__version__}")
|
|
21
|
+
raise typer.Exit()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# --------------------------------------------------
|
|
25
|
+
# verbose callback
|
|
26
|
+
# --------------------------------------------------
|
|
27
|
+
|
|
28
|
+
# loggers that --verbose switches to debug: this tool and the uahypersonics
|
|
29
|
+
# packages it calls
|
|
30
|
+
DEBUG_LOGGERS = ("grid_generator", "canonical_geometries", "cfd_io")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def verbose_callback(value: bool) -> None:
|
|
34
|
+
"""Configure console logging; --verbose adds debug output from our own packages.
|
|
35
|
+
|
|
36
|
+
By default only warnings are shown. With --verbose, this tool and the
|
|
37
|
+
packages it calls (e.g. cfd-io writing a grid) log at debug level, while
|
|
38
|
+
third-party libraries (h5py, ...) stay quiet.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
# default console logging for every logger: warnings only
|
|
42
|
+
logging.basicConfig(level=logging.WARNING, format="%(levelname)-8s %(message)s")
|
|
43
|
+
|
|
44
|
+
# debug output from this tool and the uahypersonics packages it calls
|
|
45
|
+
if value:
|
|
46
|
+
for logger_name in DEBUG_LOGGERS:
|
|
47
|
+
logging.getLogger(logger_name).setLevel(logging.DEBUG)
|
|
@@ -12,6 +12,7 @@ from typing import Annotated
|
|
|
12
12
|
import typer
|
|
13
13
|
|
|
14
14
|
from grid_generator.body_fitted import generate_body_fitted
|
|
15
|
+
from grid_generator.cli.examples_command import build_examples_command, list_examples
|
|
15
16
|
from grid_generator.cli.help_text import format_examples
|
|
16
17
|
from grid_generator.config import (
|
|
17
18
|
BODY_BLOCKS,
|
|
@@ -187,4 +188,8 @@ def build_geometry_app(geometry_type: str) -> typer.Typer:
|
|
|
187
188
|
typer.echo(quality.summary())
|
|
188
189
|
typer.echo(f"Wrote: {output_path}")
|
|
189
190
|
|
|
191
|
+
# examples: bundled cases, copied into ./<name>/ to run (only types that ship some)
|
|
192
|
+
if list_examples(geometry_type):
|
|
193
|
+
geometry_app.command("examples")(build_examples_command(geometry_type))
|
|
194
|
+
|
|
190
195
|
return geometry_app
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Bundled example cases and the `grid-generator <type> examples` command.
|
|
2
|
+
|
|
3
|
+
Each example is a folder under ``grid_generator/examples/<type>/`` holding the
|
|
4
|
+
files of one case, with the config named like run's default (e.g. ``cone.toml``).
|
|
5
|
+
The folder name is the example's name and the first text line of its config is
|
|
6
|
+
the description, so the folders are the registry: adding an example means
|
|
7
|
+
adding a folder.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
# --------------------------------------------------
|
|
11
|
+
# load necessary modules
|
|
12
|
+
# --------------------------------------------------
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from collections.abc import Callable
|
|
16
|
+
from importlib.resources import files
|
|
17
|
+
from importlib.resources.abc import Traversable
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Annotated
|
|
20
|
+
|
|
21
|
+
import typer
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# --------------------------------------------------
|
|
25
|
+
# helpers
|
|
26
|
+
# --------------------------------------------------
|
|
27
|
+
def _type_root(geometry_type: str) -> Traversable:
|
|
28
|
+
"""Folder of one geometry type's examples inside the installed package."""
|
|
29
|
+
return files("grid_generator") / "examples" / geometry_type
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _description(config_text: str) -> str:
|
|
33
|
+
"""First text line of a config's header comment (dividers skipped)."""
|
|
34
|
+
|
|
35
|
+
# walk the leading comment block
|
|
36
|
+
for line in config_text.splitlines():
|
|
37
|
+
stripped = line.strip()
|
|
38
|
+
|
|
39
|
+
# the header ends at the first line that isn't a comment
|
|
40
|
+
if stripped and not stripped.startswith("#"):
|
|
41
|
+
break
|
|
42
|
+
|
|
43
|
+
# skip blank comment lines and dividers like "# -----"
|
|
44
|
+
text = stripped.lstrip("#").strip()
|
|
45
|
+
if text and set(text) - set("-=#"):
|
|
46
|
+
return text
|
|
47
|
+
|
|
48
|
+
return ""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# --------------------------------------------------
|
|
52
|
+
# public API
|
|
53
|
+
# --------------------------------------------------
|
|
54
|
+
def list_examples(geometry_type: str) -> dict[str, str]:
|
|
55
|
+
"""Return a name -> description mapping of one geometry type's examples.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
geometry_type: Geometry type, e.g. "cone".
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Dict of {name: description}, sorted by name (empty without examples).
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
# a type without examples has no folder
|
|
65
|
+
root = _type_root(geometry_type)
|
|
66
|
+
if not root.is_dir():
|
|
67
|
+
return {}
|
|
68
|
+
|
|
69
|
+
# every folder with the type's config is an example
|
|
70
|
+
examples: dict[str, str] = {}
|
|
71
|
+
config_name = f"{geometry_type}.toml"
|
|
72
|
+
for folder in sorted(root.iterdir(), key=lambda item: item.name):
|
|
73
|
+
config = folder / config_name
|
|
74
|
+
if folder.is_dir() and config.is_file():
|
|
75
|
+
examples[folder.name] = _description(config.read_text(encoding="utf-8"))
|
|
76
|
+
|
|
77
|
+
return examples
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def copy_example(
|
|
81
|
+
geometry_type: str, name: str, destination: str | Path, force: bool = False
|
|
82
|
+
) -> list[Path]:
|
|
83
|
+
"""Copy one example's files into a folder.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
geometry_type: Geometry type, e.g. "cone".
|
|
87
|
+
name: Example name (see list_examples).
|
|
88
|
+
destination: Folder to create and copy the files into.
|
|
89
|
+
force: Overwrite files that already exist in the folder.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
The paths of the written files.
|
|
93
|
+
|
|
94
|
+
Raises:
|
|
95
|
+
KeyError: If the type has no example of that name.
|
|
96
|
+
FileExistsError: If the folder exists and force is False.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
# check: a known example
|
|
100
|
+
if name not in list_examples(geometry_type):
|
|
101
|
+
raise KeyError(name)
|
|
102
|
+
|
|
103
|
+
# check: don't overwrite an earlier copy unless asked to
|
|
104
|
+
destination = Path(destination)
|
|
105
|
+
if destination.exists() and not force:
|
|
106
|
+
raise FileExistsError(f"{destination} already exists (use --force to overwrite)")
|
|
107
|
+
|
|
108
|
+
# copy every file of the example folder
|
|
109
|
+
destination.mkdir(parents=True, exist_ok=True)
|
|
110
|
+
written: list[Path] = []
|
|
111
|
+
source = _type_root(geometry_type) / name
|
|
112
|
+
for item in sorted(source.iterdir(), key=lambda entry: entry.name):
|
|
113
|
+
if item.is_file():
|
|
114
|
+
target = destination / item.name
|
|
115
|
+
target.write_bytes(item.read_bytes())
|
|
116
|
+
written.append(target)
|
|
117
|
+
|
|
118
|
+
return written
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def build_examples_command(geometry_type: str) -> Callable[..., None]:
|
|
122
|
+
"""Build the `examples` subcommand of one geometry type.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
geometry_type: Geometry type, e.g. "cone" (also the examples folder).
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
The command function to register as ``<geometry_app>.command("examples")``.
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
def cmd_examples(
|
|
132
|
+
name: Annotated[
|
|
133
|
+
str | None,
|
|
134
|
+
typer.Argument(help="Example to copy into ./<name>/. Omit to list the examples."),
|
|
135
|
+
] = None,
|
|
136
|
+
force: Annotated[
|
|
137
|
+
bool,
|
|
138
|
+
typer.Option("--force", "-f", help="Overwrite an existing ./<name>/ folder."),
|
|
139
|
+
] = False,
|
|
140
|
+
) -> None:
|
|
141
|
+
"""List the bundled examples, or copy one into a new folder to run."""
|
|
142
|
+
|
|
143
|
+
# list: name and description of every example
|
|
144
|
+
examples = list_examples(geometry_type)
|
|
145
|
+
if name is None:
|
|
146
|
+
width = max(len(example_name) for example_name in [*examples, "<name>"])
|
|
147
|
+
typer.echo("")
|
|
148
|
+
typer.echo(f" {'<name>':<{width}} description")
|
|
149
|
+
typer.echo(f" {'-' * width} {'-' * 11}")
|
|
150
|
+
for example_name, description in examples.items():
|
|
151
|
+
typer.echo(f" {example_name:<{width}} {description}")
|
|
152
|
+
typer.echo("")
|
|
153
|
+
typer.echo("command sequence:")
|
|
154
|
+
typer.echo(" - check out the example")
|
|
155
|
+
typer.echo(f" grid-generator {geometry_type} examples <name>")
|
|
156
|
+
typer.echo(" - change into its folder")
|
|
157
|
+
typer.echo(" cd <name>")
|
|
158
|
+
typer.echo(" - run the example")
|
|
159
|
+
typer.echo(f" grid-generator {geometry_type} run")
|
|
160
|
+
typer.echo("")
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
# copy the example into ./<name>/
|
|
164
|
+
try:
|
|
165
|
+
written = copy_example(geometry_type, name, Path(name), force=force)
|
|
166
|
+
except KeyError:
|
|
167
|
+
known = ", ".join(examples)
|
|
168
|
+
typer.echo(f"error: unknown example {name!r}; available: {known}", err=True)
|
|
169
|
+
raise typer.Exit(1) from None
|
|
170
|
+
except FileExistsError as error:
|
|
171
|
+
typer.echo(f"error: {error}", err=True)
|
|
172
|
+
raise typer.Exit(1) from None
|
|
173
|
+
|
|
174
|
+
# report the files and the next step
|
|
175
|
+
for path in written:
|
|
176
|
+
typer.echo(f"wrote {path}")
|
|
177
|
+
typer.echo(f"then: cd {name} && grid-generator {geometry_type} run")
|
|
178
|
+
|
|
179
|
+
return cmd_examples
|