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.
Files changed (71) hide show
  1. pyflightstream/__init__.py +27 -0
  2. pyflightstream/cases/__init__.py +332 -0
  3. pyflightstream/cases/matrix_legacy.py +337 -0
  4. pyflightstream/commands/__init__.py +544 -0
  5. pyflightstream/commands/_meta.yaml +18 -0
  6. pyflightstream/commands/actuators.yaml +167 -0
  7. pyflightstream/commands/advanced_settings.yaml +136 -0
  8. pyflightstream/commands/aeroelastic_coupling.yaml +172 -0
  9. pyflightstream/commands/boundary_conditions.yaml +153 -0
  10. pyflightstream/commands/ccs_wing_mesh.yaml +74 -0
  11. pyflightstream/commands/coordinate_systems.yaml +123 -0
  12. pyflightstream/commands/file_io.yaml +82 -0
  13. pyflightstream/commands/mesh_import_export.yaml +72 -0
  14. pyflightstream/commands/motion_definitions.yaml +185 -0
  15. pyflightstream/commands/probe_points.yaml +116 -0
  16. pyflightstream/commands/runtime_settings.yaml +160 -0
  17. pyflightstream/commands/scenes.yaml +14 -0
  18. pyflightstream/commands/script_controls.yaml +39 -0
  19. pyflightstream/commands/simulation_controls.yaml +30 -0
  20. pyflightstream/commands/solver_analysis.yaml +118 -0
  21. pyflightstream/commands/solver_export.yaml +138 -0
  22. pyflightstream/commands/solver_initialization.yaml +95 -0
  23. pyflightstream/commands/solver_settings.yaml +120 -0
  24. pyflightstream/commands/streamlines.yaml +63 -0
  25. pyflightstream/commands/surface_sections.yaml +145 -0
  26. pyflightstream/commands/sweeper.yaml +115 -0
  27. pyflightstream/commands/unsteady_solver.yaml +68 -0
  28. pyflightstream/commands/volume_sections.yaml +148 -0
  29. pyflightstream/farfield/__init__.py +613 -0
  30. pyflightstream/files/__init__.py +375 -0
  31. pyflightstream/fsi/__init__.py +57 -0
  32. pyflightstream/fsi/beam.py +417 -0
  33. pyflightstream/fsi/centrifugal.py +418 -0
  34. pyflightstream/fsi/cli.py +206 -0
  35. pyflightstream/fsi/config.py +316 -0
  36. pyflightstream/fsi/driver.py +501 -0
  37. pyflightstream/fsi/kinematics.py +153 -0
  38. pyflightstream/fsi/loads.py +740 -0
  39. pyflightstream/fsi/nodes.py +463 -0
  40. pyflightstream/fsi/state.py +181 -0
  41. pyflightstream/post/__init__.py +18 -0
  42. pyflightstream/post/writers.py +195 -0
  43. pyflightstream/probes/__init__.py +453 -0
  44. pyflightstream/probes/geometry.py +281 -0
  45. pyflightstream/probes/planar.py +477 -0
  46. pyflightstream/qa/__init__.py +59 -0
  47. pyflightstream/qa/cli.py +364 -0
  48. pyflightstream/qa/compat.py +285 -0
  49. pyflightstream/qa/drift.py +406 -0
  50. pyflightstream/qa/geometry.py +421 -0
  51. pyflightstream/qa/physics.py +1744 -0
  52. pyflightstream/qa/probes.py +1023 -0
  53. pyflightstream/qa/references/PHY-01.yaml +38 -0
  54. pyflightstream/qa/references/PHY-02.yaml +28 -0
  55. pyflightstream/qa/references/PHY-05.yaml +29 -0
  56. pyflightstream/qa/references/PHY-06.yaml +90 -0
  57. pyflightstream/qa/references/SMI-01.yaml +28 -0
  58. pyflightstream/qa/references/SMI-02.yaml +28 -0
  59. pyflightstream/qa/specs.py +1405 -0
  60. pyflightstream/reference.py +465 -0
  61. pyflightstream/results/__init__.py +547 -0
  62. pyflightstream/run/__init__.py +677 -0
  63. pyflightstream/script/__init__.py +474 -0
  64. pyflightstream/script/helpers.py +844 -0
  65. pyflightstream/versions.py +191 -0
  66. pyflightstream-0.2.0.dist-info/METADATA +88 -0
  67. pyflightstream-0.2.0.dist-info/RECORD +71 -0
  68. pyflightstream-0.2.0.dist-info/WHEEL +5 -0
  69. pyflightstream-0.2.0.dist-info/entry_points.txt +3 -0
  70. pyflightstream-0.2.0.dist-info/licenses/LICENSE +21 -0
  71. 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())