pyflightstream 0.2.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.
- pyflightstream/__init__.py +27 -0
- pyflightstream/cases/__init__.py +332 -0
- pyflightstream/cases/matrix_legacy.py +337 -0
- pyflightstream/commands/__init__.py +544 -0
- pyflightstream/commands/_meta.yaml +18 -0
- pyflightstream/commands/actuators.yaml +167 -0
- pyflightstream/commands/advanced_settings.yaml +136 -0
- pyflightstream/commands/aeroelastic_coupling.yaml +172 -0
- pyflightstream/commands/boundary_conditions.yaml +153 -0
- pyflightstream/commands/ccs_wing_mesh.yaml +74 -0
- pyflightstream/commands/coordinate_systems.yaml +123 -0
- pyflightstream/commands/file_io.yaml +82 -0
- pyflightstream/commands/mesh_import_export.yaml +72 -0
- pyflightstream/commands/motion_definitions.yaml +185 -0
- pyflightstream/commands/probe_points.yaml +116 -0
- pyflightstream/commands/runtime_settings.yaml +160 -0
- pyflightstream/commands/scenes.yaml +14 -0
- pyflightstream/commands/script_controls.yaml +39 -0
- pyflightstream/commands/simulation_controls.yaml +30 -0
- pyflightstream/commands/solver_analysis.yaml +118 -0
- pyflightstream/commands/solver_export.yaml +138 -0
- pyflightstream/commands/solver_initialization.yaml +95 -0
- pyflightstream/commands/solver_settings.yaml +120 -0
- pyflightstream/commands/streamlines.yaml +63 -0
- pyflightstream/commands/surface_sections.yaml +145 -0
- pyflightstream/commands/sweeper.yaml +115 -0
- pyflightstream/commands/unsteady_solver.yaml +68 -0
- pyflightstream/commands/volume_sections.yaml +148 -0
- pyflightstream/farfield/__init__.py +613 -0
- pyflightstream/files/__init__.py +375 -0
- pyflightstream/fsi/__init__.py +57 -0
- pyflightstream/fsi/beam.py +417 -0
- pyflightstream/fsi/centrifugal.py +418 -0
- pyflightstream/fsi/cli.py +206 -0
- pyflightstream/fsi/config.py +316 -0
- pyflightstream/fsi/driver.py +501 -0
- pyflightstream/fsi/kinematics.py +153 -0
- pyflightstream/fsi/loads.py +740 -0
- pyflightstream/fsi/nodes.py +463 -0
- pyflightstream/fsi/state.py +181 -0
- pyflightstream/post/__init__.py +18 -0
- pyflightstream/post/writers.py +195 -0
- pyflightstream/probes/__init__.py +453 -0
- pyflightstream/probes/geometry.py +281 -0
- pyflightstream/probes/planar.py +477 -0
- pyflightstream/qa/__init__.py +59 -0
- pyflightstream/qa/cli.py +364 -0
- pyflightstream/qa/compat.py +285 -0
- pyflightstream/qa/drift.py +406 -0
- pyflightstream/qa/geometry.py +421 -0
- pyflightstream/qa/physics.py +1744 -0
- pyflightstream/qa/probes.py +1023 -0
- pyflightstream/qa/references/PHY-01.yaml +38 -0
- pyflightstream/qa/references/PHY-02.yaml +28 -0
- pyflightstream/qa/references/PHY-05.yaml +29 -0
- pyflightstream/qa/references/PHY-06.yaml +90 -0
- pyflightstream/qa/references/SMI-01.yaml +28 -0
- pyflightstream/qa/references/SMI-02.yaml +28 -0
- pyflightstream/qa/specs.py +1405 -0
- pyflightstream/reference.py +465 -0
- pyflightstream/results/__init__.py +547 -0
- pyflightstream/run/__init__.py +677 -0
- pyflightstream/script/__init__.py +474 -0
- pyflightstream/script/helpers.py +844 -0
- pyflightstream/versions.py +191 -0
- pyflightstream-0.2.0.dist-info/METADATA +88 -0
- pyflightstream-0.2.0.dist-info/RECORD +71 -0
- pyflightstream-0.2.0.dist-info/WHEEL +5 -0
- pyflightstream-0.2.0.dist-info/entry_points.txt +3 -0
- pyflightstream-0.2.0.dist-info/licenses/LICENSE +21 -0
- pyflightstream-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""Geometry gate of the probe planner: culling and near-surface refinement.
|
|
2
|
+
|
|
3
|
+
Pipeline role: keeps the probe budget honest. Probe points that fall
|
|
4
|
+
inside the body sample nothing physical, so the gate discards them
|
|
5
|
+
against the surface mesh (an ``.obj``/``.stl`` exported from the
|
|
6
|
+
simulation, see :func:`pyflightstream.run.export_surface_mesh`), and
|
|
7
|
+
the optional boundary-layer band re-samples the cells within a
|
|
8
|
+
distance ``d`` of the surface with finer elements. Every decision is
|
|
9
|
+
counted in the :class:`~pyflightstream.probes.planar.GeometryGateReport`:
|
|
10
|
+
no probe is dropped or added silently.
|
|
11
|
+
|
|
12
|
+
The containment and distance queries come from trimesh (public
|
|
13
|
+
library per the engineering policy; ``pip install pyflightstream[geom]``,
|
|
14
|
+
license evidence reports/RPT-003). This module never runs the solver:
|
|
15
|
+
it consumes a mesh file that already exists.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
import numpy as np
|
|
23
|
+
|
|
24
|
+
from pyflightstream.probes.planar import (
|
|
25
|
+
GeometryGateReport,
|
|
26
|
+
PlanarProbeGrid,
|
|
27
|
+
PlannedProbes,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"GeometryEngineMissingError",
|
|
32
|
+
"OpenMeshError",
|
|
33
|
+
"load_surface_mesh",
|
|
34
|
+
"apply_geometry_gate",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class GeometryEngineMissingError(ImportError):
|
|
39
|
+
"""The optional geometry engine is not installed.
|
|
40
|
+
|
|
41
|
+
Point-in-body containment and distance-to-surface queries need
|
|
42
|
+
trimesh with its spatial index; the plain installation leaves the
|
|
43
|
+
grids fully usable without culling.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class OpenMeshError(ValueError):
|
|
48
|
+
"""The surface mesh is not watertight.
|
|
49
|
+
|
|
50
|
+
Inside/outside is undefined for an open surface (a half model cut
|
|
51
|
+
by a symmetry plane, an unclosed trailing edge): the ray-parity
|
|
52
|
+
test would return arbitrary answers, so the gate refuses instead
|
|
53
|
+
of guessing.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _trimesh():
|
|
58
|
+
try:
|
|
59
|
+
import trimesh
|
|
60
|
+
except ImportError as error:
|
|
61
|
+
raise GeometryEngineMissingError(
|
|
62
|
+
"the geometry gate needs trimesh for point-in-body containment and "
|
|
63
|
+
"distance-to-surface queries; install the geom extra "
|
|
64
|
+
"(pip install pyflightstream[geom]) or run the grid without culling"
|
|
65
|
+
) from error
|
|
66
|
+
return trimesh
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def load_surface_mesh(path: str | Path, *, require_watertight: bool = True):
|
|
70
|
+
"""Load a surface mesh file for the geometry gate.
|
|
71
|
+
|
|
72
|
+
Parameters
|
|
73
|
+
----------
|
|
74
|
+
path : str or pathlib.Path
|
|
75
|
+
Mesh file (``.obj`` or ``.stl``), in simulation length units
|
|
76
|
+
and the reference frame, as EXPORT_SURFACE_MESH writes it
|
|
77
|
+
(SRC-003 pp.307-308).
|
|
78
|
+
require_watertight : bool
|
|
79
|
+
Refuse open meshes (the default): containment is undefined on
|
|
80
|
+
them. Disable only for distance-only uses.
|
|
81
|
+
|
|
82
|
+
Returns
|
|
83
|
+
-------
|
|
84
|
+
trimesh.Trimesh
|
|
85
|
+
The loaded mesh.
|
|
86
|
+
|
|
87
|
+
Raises
|
|
88
|
+
------
|
|
89
|
+
GeometryEngineMissingError
|
|
90
|
+
If trimesh is not installed.
|
|
91
|
+
OpenMeshError
|
|
92
|
+
If the mesh is not watertight and containment would be
|
|
93
|
+
undefined.
|
|
94
|
+
"""
|
|
95
|
+
trimesh = _trimesh()
|
|
96
|
+
mesh = trimesh.load(str(path), force="mesh")
|
|
97
|
+
if require_watertight and not mesh.is_watertight:
|
|
98
|
+
raise OpenMeshError(
|
|
99
|
+
f"the surface mesh {path} is not watertight, so inside/outside is "
|
|
100
|
+
"undefined and containment culling would guess. Close the geometry "
|
|
101
|
+
"(mirror a half model, close open ends) or run the gate with "
|
|
102
|
+
"cull disabled."
|
|
103
|
+
)
|
|
104
|
+
return mesh
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _fine_axis(values: np.ndarray, factor: int) -> np.ndarray:
|
|
108
|
+
"""Subdivide each axis cell linearly into ``factor`` fine cells."""
|
|
109
|
+
segments = [
|
|
110
|
+
np.linspace(values[i], values[i + 1], factor + 1)[:-1] for i in range(len(values) - 1)
|
|
111
|
+
]
|
|
112
|
+
return np.concatenate([*segments, [values[-1:]]], axis=None)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _surface_distance(mesh, points: np.ndarray) -> np.ndarray:
|
|
116
|
+
from trimesh.proximity import ProximityQuery
|
|
117
|
+
|
|
118
|
+
_, distance, _ = ProximityQuery(mesh).on_surface(points)
|
|
119
|
+
return np.asarray(distance)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def apply_geometry_gate(
|
|
123
|
+
grid: PlanarProbeGrid,
|
|
124
|
+
mesh=None,
|
|
125
|
+
*,
|
|
126
|
+
mesh_path: str | Path | None = None,
|
|
127
|
+
cull: bool = True,
|
|
128
|
+
standoff: float = 0.0,
|
|
129
|
+
) -> PlannedProbes:
|
|
130
|
+
"""Run the geometry gate of a planar grid and return the final probes.
|
|
131
|
+
|
|
132
|
+
Parameters
|
|
133
|
+
----------
|
|
134
|
+
grid : PlanarProbeGrid
|
|
135
|
+
The prescribing grid; its optional ``refinement`` band is
|
|
136
|
+
honored here, because only the mesh knows where the surface
|
|
137
|
+
is.
|
|
138
|
+
mesh : trimesh.Trimesh, optional
|
|
139
|
+
Loaded surface mesh; alternatively give ``mesh_path``.
|
|
140
|
+
mesh_path : str or pathlib.Path, optional
|
|
141
|
+
Mesh file to load through :func:`load_surface_mesh`.
|
|
142
|
+
cull : bool
|
|
143
|
+
Discard points inside the body. Disabling keeps every point
|
|
144
|
+
while still allowing band refinement.
|
|
145
|
+
standoff : float
|
|
146
|
+
Also discard points closer to the surface than this margin
|
|
147
|
+
(simulation length units). A probe on or hugging the wall
|
|
148
|
+
samples the body's surface state, not the flow: the 26.120
|
|
149
|
+
round trip exported zero velocity for a node sitting exactly
|
|
150
|
+
on the leading edge, and near-wall probes carry
|
|
151
|
+
boundary-layer columns regardless of the viscous toggles
|
|
152
|
+
(reports/RPT-004). Zero disables the margin.
|
|
153
|
+
|
|
154
|
+
Returns
|
|
155
|
+
-------
|
|
156
|
+
PlannedProbes
|
|
157
|
+
Final reference-frame points in the deterministic parser
|
|
158
|
+
order, plus the probe accounting report.
|
|
159
|
+
|
|
160
|
+
Raises
|
|
161
|
+
------
|
|
162
|
+
ValueError
|
|
163
|
+
If the grid asks for refinement, or a positive standoff is
|
|
164
|
+
requested, without a mesh: both are measured from the
|
|
165
|
+
surface, so they cannot exist without one.
|
|
166
|
+
"""
|
|
167
|
+
resolved_path = str(mesh_path) if mesh_path is not None else None
|
|
168
|
+
if mesh is None and mesh_path is not None:
|
|
169
|
+
mesh = load_surface_mesh(mesh_path)
|
|
170
|
+
if grid.refinement is not None and mesh is None:
|
|
171
|
+
raise ValueError(
|
|
172
|
+
"the grid prescribes a near-surface refinement band, but no surface "
|
|
173
|
+
"mesh was given; the band is measured from the surface, so pass mesh "
|
|
174
|
+
"or mesh_path (export one with pyflightstream.run.export_surface_mesh)"
|
|
175
|
+
)
|
|
176
|
+
if standoff < 0.0:
|
|
177
|
+
raise ValueError("standoff must be zero or positive: it is a wall margin")
|
|
178
|
+
if standoff > 0.0 and mesh is None:
|
|
179
|
+
raise ValueError(
|
|
180
|
+
"a standoff margin is measured from the surface, so it needs a mesh; "
|
|
181
|
+
"pass mesh or mesh_path, or drop the margin"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
base = grid.base_points()
|
|
185
|
+
if mesh is not None and cull:
|
|
186
|
+
inside = np.asarray(mesh.contains(base), dtype=bool)
|
|
187
|
+
else:
|
|
188
|
+
inside = np.zeros(len(base), dtype=bool)
|
|
189
|
+
if standoff > 0.0:
|
|
190
|
+
near = (_surface_distance(mesh, base) < standoff) & ~inside
|
|
191
|
+
else:
|
|
192
|
+
near = np.zeros(len(base), dtype=bool)
|
|
193
|
+
kept_base = base[~(inside | near)]
|
|
194
|
+
|
|
195
|
+
refined_points: list[np.ndarray] = []
|
|
196
|
+
refined_culled = 0
|
|
197
|
+
refined_standoff_culled = 0
|
|
198
|
+
if grid.refinement is not None:
|
|
199
|
+
refined, refined_culled, refined_standoff_culled = _refine_band(
|
|
200
|
+
grid, mesh, cull=cull, standoff=standoff
|
|
201
|
+
)
|
|
202
|
+
if len(refined):
|
|
203
|
+
refined_points.append(refined)
|
|
204
|
+
|
|
205
|
+
points = np.vstack([kept_base, *refined_points]) if refined_points else kept_base
|
|
206
|
+
report = GeometryGateReport(
|
|
207
|
+
base_total=len(base),
|
|
208
|
+
base_culled=int(inside.sum()),
|
|
209
|
+
base_standoff_culled=int(near.sum()),
|
|
210
|
+
refined_added=sum(len(block) for block in refined_points),
|
|
211
|
+
refined_culled=refined_culled,
|
|
212
|
+
refined_standoff_culled=refined_standoff_culled,
|
|
213
|
+
band_distance=grid.refinement.distance if grid.refinement else None,
|
|
214
|
+
standoff=standoff if standoff > 0.0 else None,
|
|
215
|
+
mesh_path=resolved_path,
|
|
216
|
+
)
|
|
217
|
+
return PlannedProbes(grid=grid, points=points, report=report)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _refine_band(
|
|
221
|
+
grid: PlanarProbeGrid, mesh, *, cull: bool, standoff: float = 0.0
|
|
222
|
+
) -> tuple[np.ndarray, int, int]:
|
|
223
|
+
"""Fine nodes of the cells within the band distance of the surface.
|
|
224
|
+
|
|
225
|
+
Cells are flagged by their center: within ``distance`` of the
|
|
226
|
+
surface and not inside the body. Flagged cells are re-sampled
|
|
227
|
+
``factor`` times finer per direction; nodes already present in the
|
|
228
|
+
base grid are skipped, and nodes shared between adjacent flagged
|
|
229
|
+
cells are emitted once, in cell-major order. Candidates inside the
|
|
230
|
+
body or within the standoff margin are discarded and counted.
|
|
231
|
+
"""
|
|
232
|
+
factor = grid.refinement.factor
|
|
233
|
+
distance = grid.refinement.distance
|
|
234
|
+
u = grid.u.points()
|
|
235
|
+
v = grid.v.points()
|
|
236
|
+
|
|
237
|
+
centers_local = np.zeros(((len(u) - 1) * (len(v) - 1), 3))
|
|
238
|
+
centers_u = 0.5 * (u[:-1] + u[1:])
|
|
239
|
+
centers_v = 0.5 * (v[:-1] + v[1:])
|
|
240
|
+
centers_local[:, 0] = np.repeat(centers_u, len(centers_v))
|
|
241
|
+
centers_local[:, 1] = np.tile(centers_v, len(centers_u))
|
|
242
|
+
centers = grid.frame.to_reference(centers_local)
|
|
243
|
+
|
|
244
|
+
center_distance = _surface_distance(mesh, centers)
|
|
245
|
+
center_inside = np.asarray(mesh.contains(centers), dtype=bool)
|
|
246
|
+
flagged = (center_distance < distance) & ~center_inside
|
|
247
|
+
flagged = flagged.reshape(len(centers_u), len(centers_v))
|
|
248
|
+
|
|
249
|
+
fine_u = _fine_axis(u, factor)
|
|
250
|
+
fine_v = _fine_axis(v, factor)
|
|
251
|
+
seen: set[tuple[int, int]] = set()
|
|
252
|
+
nodes_local: list[tuple[float, float]] = []
|
|
253
|
+
for iu in range(len(u) - 1):
|
|
254
|
+
for iv in range(len(v) - 1):
|
|
255
|
+
if not flagged[iu, iv]:
|
|
256
|
+
continue
|
|
257
|
+
for a in range(factor + 1):
|
|
258
|
+
gu = iu * factor + a
|
|
259
|
+
for b in range(factor + 1):
|
|
260
|
+
gv = iv * factor + b
|
|
261
|
+
if gu % factor == 0 and gv % factor == 0:
|
|
262
|
+
continue # a base-grid node
|
|
263
|
+
if (gu, gv) in seen:
|
|
264
|
+
continue # shared with an adjacent flagged cell
|
|
265
|
+
seen.add((gu, gv))
|
|
266
|
+
nodes_local.append((fine_u[gu], fine_v[gv]))
|
|
267
|
+
if not nodes_local:
|
|
268
|
+
return np.empty((0, 3)), 0, 0
|
|
269
|
+
|
|
270
|
+
local = np.zeros((len(nodes_local), 3))
|
|
271
|
+
local[:, :2] = np.asarray(nodes_local)
|
|
272
|
+
candidates = grid.frame.to_reference(local)
|
|
273
|
+
if cull:
|
|
274
|
+
inside = np.asarray(mesh.contains(candidates), dtype=bool)
|
|
275
|
+
else:
|
|
276
|
+
inside = np.zeros(len(candidates), dtype=bool)
|
|
277
|
+
if standoff > 0.0:
|
|
278
|
+
near = (_surface_distance(mesh, candidates) < standoff) & ~inside
|
|
279
|
+
else:
|
|
280
|
+
near = np.zeros(len(candidates), dtype=bool)
|
|
281
|
+
return candidates[~(inside | near)], int(inside.sum()), int(near.sum())
|