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,477 @@
1
+ """Planar Cartesian probe grids on arbitrary coordinate frames.
2
+
3
+ Pipeline role: the controlled replacement for FlightStream volume
4
+ sections. `CREATE_NEW_RECTANGLE_VOLUME_SECTION` places its sampling
5
+ points internally (SRC-003 p.366); a :class:`PlanarProbeGrid` instead
6
+ prescribes every probe position: element size or point distribution
7
+ per in-plane axis, on a plane defined by an explicit
8
+ :class:`FrameDefinition` (origin plus axes in the reference frame,
9
+ mirroring the EDIT_COORDINATE_SYSTEM grammar). Points are transformed
10
+ to the reference frame in Python and imported with FRAME 1, so the
11
+ geometry culling of :mod:`pyflightstream.probes.geometry` always
12
+ operates on the same coordinates the solver sees.
13
+
14
+ The grid is serializable like the cylindrical survey lattice: the
15
+ JSON round trip is the cross-consumer contract, and the deterministic
16
+ point ordering (base nodes row-major in u then v; refined nodes
17
+ appended cell-major) is what a probe-export parser keys its rows on.
18
+
19
+ Units: every stored length is in simulation length units; the frame
20
+ axes are dimensionless unit vectors in the reference frame.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import json
26
+ from dataclasses import dataclass
27
+ from typing import Literal
28
+
29
+ import numpy as np
30
+ from pydantic import BaseModel, ConfigDict, model_validator
31
+
32
+ __all__ = [
33
+ "FrameDefinition",
34
+ "AxisSpec",
35
+ "RefinementBand",
36
+ "PlanarProbeGrid",
37
+ "GeometryGateReport",
38
+ "PlannedProbes",
39
+ ]
40
+
41
+ _DEGENERATE = 1e-10
42
+
43
+
44
+ def _unit(vector: tuple[float, float, float], name: str) -> np.ndarray:
45
+ array = np.asarray(vector, dtype=float)
46
+ norm = float(np.linalg.norm(array))
47
+ if norm < _DEGENERATE:
48
+ raise ValueError(
49
+ f"{name} has near-zero length; a frame axis must be a direction, "
50
+ "and a zero vector defines none"
51
+ )
52
+ return array / norm
53
+
54
+
55
+ class FrameDefinition(BaseModel):
56
+ """A local coordinate frame expressed in the reference frame.
57
+
58
+ Mirrors what EDIT_COORDINATE_SYSTEM sends to the solver (origin
59
+ plus axis vectors in the reference frame, coordinate_systems
60
+ chapter): the same object can therefore both place probe grids in
61
+ Python and, optionally, create the matching solver-side frame via
62
+ :func:`pyflightstream.script.helpers.coordinate_frame`.
63
+
64
+ ``x_axis`` and ``y_axis`` are normalized on construction and
65
+ ``y_axis`` is orthogonalized against ``x_axis`` (Gram-Schmidt);
66
+ ``z_axis`` is their cross product, so the frame is always
67
+ right-handed orthonormal.
68
+
69
+ Attributes
70
+ ----------
71
+ origin : tuple of float
72
+ Frame origin in the reference frame (simulation length units).
73
+ x_axis, y_axis : tuple of float
74
+ In-plane directions of the grid, reference-frame components;
75
+ stored normalized and orthogonal.
76
+ """
77
+
78
+ model_config = ConfigDict(extra="forbid", frozen=True)
79
+
80
+ origin: tuple[float, float, float] = (0.0, 0.0, 0.0)
81
+ x_axis: tuple[float, float, float]
82
+ y_axis: tuple[float, float, float]
83
+
84
+ @model_validator(mode="before")
85
+ @classmethod
86
+ def _orthonormalize(cls, data: dict) -> dict:
87
+ if not isinstance(data, dict) or "x_axis" not in data or "y_axis" not in data:
88
+ return data
89
+ x = _unit(tuple(data["x_axis"]), "x_axis")
90
+ y_raw = np.asarray(data["y_axis"], dtype=float)
91
+ y_perp = y_raw - np.dot(y_raw, x) * x
92
+ if float(np.linalg.norm(y_perp)) < _DEGENERATE:
93
+ raise ValueError(
94
+ "y_axis is parallel to x_axis; two independent in-plane "
95
+ "directions are needed to define a plane"
96
+ )
97
+ y = y_perp / float(np.linalg.norm(y_perp))
98
+ data = dict(data)
99
+ data["x_axis"] = tuple(float(c) for c in x)
100
+ data["y_axis"] = tuple(float(c) for c in y)
101
+ return data
102
+
103
+ @property
104
+ def z_axis(self) -> tuple[float, float, float]:
105
+ """Plane normal, the right-handed cross product of x and y axes."""
106
+ z = np.cross(self.x_axis, self.y_axis)
107
+ return tuple(float(c) for c in z)
108
+
109
+ @property
110
+ def basis(self) -> np.ndarray:
111
+ """Rows are the x, y, z axes in reference-frame components."""
112
+ return np.asarray([self.x_axis, self.y_axis, self.z_axis], dtype=float)
113
+
114
+ def to_reference(self, local_points: np.ndarray) -> np.ndarray:
115
+ """Transform local ``(..., 3)`` coordinates into the reference frame.
116
+
117
+ Parameters
118
+ ----------
119
+ local_points : numpy.ndarray
120
+ Coordinates along the frame axes (simulation length units).
121
+
122
+ Returns
123
+ -------
124
+ numpy.ndarray
125
+ Same shape, reference-frame coordinates.
126
+ """
127
+ return np.asarray(self.origin) + np.asarray(local_points, dtype=float) @ self.basis
128
+
129
+ def from_reference(self, points: np.ndarray) -> np.ndarray:
130
+ """Inverse of :meth:`to_reference`."""
131
+ return (np.asarray(points, dtype=float) - np.asarray(self.origin)) @ self.basis.T
132
+
133
+
134
+ class AxisSpec(BaseModel):
135
+ """Point distribution along one in-plane axis of a grid.
136
+
137
+ The primary path prescribes the element size directly
138
+ (``spacing``, uniform); the alternative prescribes a point
139
+ ``count`` with a ``distribution``. Endpoints are always included
140
+ exactly.
141
+
142
+ Attributes
143
+ ----------
144
+ start, stop : float
145
+ Axis extent in the local frame (simulation length units).
146
+ spacing : float, optional
147
+ Uniform element size; snapped so an integer number of elements
148
+ fills the extent exactly (the snapped size never exceeds the
149
+ requested one by more than the fill rounding).
150
+ count : int, optional
151
+ Number of points (at least 2); required for the nonuniform
152
+ distributions.
153
+ distribution : str
154
+ ``uniform``, ``cosine`` (smooth clustering), or ``geometric``
155
+ (element sizes in geometric progression away from the
156
+ clustered end).
157
+ ratio : float, optional
158
+ Geometric growth ratio, greater than 1; geometric only.
159
+ cluster : str
160
+ Which end(s) get the small elements: ``start``, ``end``, or
161
+ ``both``.
162
+ """
163
+
164
+ model_config = ConfigDict(extra="forbid", frozen=True)
165
+
166
+ start: float
167
+ stop: float
168
+ spacing: float | None = None
169
+ count: int | None = None
170
+ distribution: Literal["uniform", "cosine", "geometric"] = "uniform"
171
+ ratio: float | None = None
172
+ cluster: Literal["start", "end", "both"] = "both"
173
+
174
+ @model_validator(mode="after")
175
+ def _specification_is_consistent(self) -> AxisSpec:
176
+ if self.stop <= self.start:
177
+ raise ValueError("stop must exceed start: an axis needs a positive extent")
178
+ if self.distribution == "uniform":
179
+ if (self.spacing is None) == (self.count is None):
180
+ raise ValueError(
181
+ "a uniform axis takes exactly one of spacing (element size) "
182
+ "or count (number of points)"
183
+ )
184
+ if self.ratio is not None:
185
+ raise ValueError("ratio only applies to the geometric distribution")
186
+ else:
187
+ if self.count is None or self.spacing is not None:
188
+ raise ValueError(
189
+ f"the {self.distribution} distribution varies its element size, "
190
+ "so it takes count, never spacing"
191
+ )
192
+ if self.distribution == "geometric" and (self.ratio is None or self.ratio <= 1.0):
193
+ raise ValueError(
194
+ "the geometric distribution needs ratio > 1: it is the growth "
195
+ "factor of consecutive element sizes away from the clustered end"
196
+ )
197
+ if self.distribution == "cosine" and self.ratio is not None:
198
+ raise ValueError("ratio only applies to the geometric distribution")
199
+ if self.spacing is not None and self.spacing <= 0.0:
200
+ raise ValueError("spacing must be positive: it is the element size")
201
+ if self.count is not None and self.count < 2:
202
+ raise ValueError("count must be at least 2: an axis needs both endpoints")
203
+ return self
204
+
205
+ @property
206
+ def extent(self) -> float:
207
+ """Axis length, ``stop - start``."""
208
+ return self.stop - self.start
209
+
210
+ def points(self) -> np.ndarray:
211
+ """Return the axis points, increasing, endpoints exact."""
212
+ if self.distribution == "uniform":
213
+ if self.spacing is not None:
214
+ cells = max(1, round(self.extent / self.spacing))
215
+ else:
216
+ cells = self.count - 1
217
+ return np.linspace(self.start, self.stop, cells + 1)
218
+ t = np.linspace(0.0, 1.0, self.count)
219
+ if self.distribution == "cosine":
220
+ if self.cluster == "both":
221
+ s = 0.5 * (1.0 - np.cos(np.pi * t))
222
+ elif self.cluster == "start":
223
+ s = 1.0 - np.cos(0.5 * np.pi * t)
224
+ else:
225
+ s = np.sin(0.5 * np.pi * t)
226
+ else:
227
+ cells = self.count - 1
228
+ index = np.arange(cells, dtype=float)
229
+ if self.cluster == "start":
230
+ sizes = self.ratio**index
231
+ elif self.cluster == "end":
232
+ sizes = self.ratio ** index[::-1]
233
+ else:
234
+ sizes = self.ratio ** np.minimum(index, cells - 1 - index)
235
+ s = np.concatenate([[0.0], np.cumsum(sizes)]) / float(np.sum(sizes))
236
+ points = self.start + self.extent * s
237
+ points[0], points[-1] = self.start, self.stop
238
+ return points
239
+
240
+
241
+ class RefinementBand(BaseModel):
242
+ """Two-level near-surface refinement of a planar grid.
243
+
244
+ Base cells whose center lies within ``distance`` of the body
245
+ surface (and outside the body) are re-sampled with elements
246
+ ``factor`` times finer in both in-plane directions, the
247
+ boundary-layer band of the survey.
248
+
249
+ Attributes
250
+ ----------
251
+ distance : float
252
+ Band thickness measured from the surface (simulation length
253
+ units), positive.
254
+ factor : int
255
+ Integer subdivision factor per direction, at least 2.
256
+ """
257
+
258
+ model_config = ConfigDict(extra="forbid", frozen=True)
259
+
260
+ distance: float
261
+ factor: int
262
+
263
+ @model_validator(mode="after")
264
+ def _band_is_physical(self) -> RefinementBand:
265
+ if self.distance <= 0.0:
266
+ raise ValueError(
267
+ "distance must be positive: it is the thickness of the "
268
+ "near-surface band that gets the finer sampling"
269
+ )
270
+ if self.factor < 2:
271
+ raise ValueError("factor must be at least 2, otherwise nothing is refined")
272
+ return self
273
+
274
+
275
+ class PlanarProbeGrid(BaseModel):
276
+ """A planar Cartesian probe grid on an explicit frame.
277
+
278
+ Attributes
279
+ ----------
280
+ frame : FrameDefinition
281
+ Plane placement; grid points live at local ``(u, v, 0)``.
282
+ u, v : AxisSpec
283
+ In-plane distributions along the frame x and y axes.
284
+ refinement : RefinementBand, optional
285
+ Near-surface two-level refinement, applied by the geometry
286
+ gate (it needs the body mesh to know where the surface is).
287
+ """
288
+
289
+ model_config = ConfigDict(extra="forbid", frozen=True)
290
+
291
+ frame: FrameDefinition
292
+ u: AxisSpec
293
+ v: AxisSpec
294
+ refinement: RefinementBand | None = None
295
+
296
+ def local_points(self) -> np.ndarray:
297
+ """Grid nodes in the local frame, shape ``(n_u, n_v, 3)``."""
298
+ u = self.u.points()
299
+ v = self.v.points()
300
+ nodes = np.zeros((len(u), len(v), 3))
301
+ nodes[..., 0] = u[:, None]
302
+ nodes[..., 1] = v[None, :]
303
+ return nodes
304
+
305
+ def base_points(self) -> np.ndarray:
306
+ """Grid nodes in the reference frame, flat, row-major in (u, v).
307
+
308
+ This ordering (u index major, v index minor) is part of the
309
+ serialization contract: probe-export rows map back to grid
310
+ nodes through it.
311
+ """
312
+ return self.frame.to_reference(self.local_points()).reshape(-1, 3)
313
+
314
+ @property
315
+ def shape(self) -> tuple[int, int]:
316
+ """Node counts ``(n_u, n_v)`` of the base grid."""
317
+ return len(self.u.points()), len(self.v.points())
318
+
319
+ @property
320
+ def point_count(self) -> int:
321
+ """Base node count, before culling and refinement."""
322
+ n_u, n_v = self.shape
323
+ return n_u * n_v
324
+
325
+ def to_json(self) -> str:
326
+ """Serialize the defining data; points are always derived."""
327
+ return self.model_dump_json()
328
+
329
+ @classmethod
330
+ def from_json(cls, text: str) -> PlanarProbeGrid:
331
+ """Rebuild a grid from :meth:`to_json` output (cross-consumer contract)."""
332
+ return cls.model_validate_json(text)
333
+
334
+
335
+ class GeometryGateReport(BaseModel):
336
+ """Accounting of what the geometry gate did to a grid.
337
+
338
+ Every run states how many probes it kept, discarded inside the
339
+ body, and added in the refinement band, so no probe budget is
340
+ spent silently (the design rule of the survey work: masked or
341
+ dropped content is always reported, never implied).
342
+
343
+ Attributes
344
+ ----------
345
+ base_total : int
346
+ Base grid nodes before the gate.
347
+ base_culled : int
348
+ Base nodes discarded because they fall inside the body.
349
+ base_standoff_culled : int
350
+ Base nodes discarded because they hug the surface closer than
351
+ the standoff margin; a probe on or against the wall samples
352
+ the body's surface state, not the flow (the on-surface probe
353
+ of reports/RPT-004 exported zero velocity).
354
+ refined_added : int
355
+ Refinement-band nodes added (after their own culling).
356
+ refined_culled : int
357
+ Refinement-band candidates discarded inside the body.
358
+ refined_standoff_culled : int
359
+ Refinement-band candidates discarded by the standoff margin.
360
+ band_distance : float, optional
361
+ Refinement band thickness used; None when no refinement ran.
362
+ standoff : float, optional
363
+ Standoff margin used; None when no margin was requested.
364
+ mesh_path : str, optional
365
+ Surface mesh file the gate tested against; None when the gate
366
+ ran without a mesh (no culling, no refinement).
367
+ """
368
+
369
+ model_config = ConfigDict(extra="forbid", frozen=True)
370
+
371
+ base_total: int
372
+ base_culled: int
373
+ refined_added: int
374
+ refined_culled: int
375
+ base_standoff_culled: int = 0
376
+ refined_standoff_culled: int = 0
377
+ band_distance: float | None = None
378
+ standoff: float | None = None
379
+ mesh_path: str | None = None
380
+
381
+ @property
382
+ def kept(self) -> int:
383
+ """Probes that survive the gate."""
384
+ return self.base_total - self.base_culled - self.base_standoff_culled + self.refined_added
385
+
386
+ @property
387
+ def culled_fraction(self) -> float:
388
+ """Fraction of base nodes discarded inside the body."""
389
+ return self.base_culled / self.base_total if self.base_total else 0.0
390
+
391
+
392
+ @dataclass(frozen=True)
393
+ class PlannedProbes:
394
+ """Final probe positions of a grid after the geometry gate.
395
+
396
+ Attributes
397
+ ----------
398
+ grid : PlanarProbeGrid
399
+ The prescribing grid.
400
+ points : numpy.ndarray
401
+ Reference-frame positions, shape ``(n, 3)``, in the
402
+ deterministic order the export parser relies on: surviving
403
+ base nodes first (row-major in u, v), then surviving refined
404
+ nodes (cell-major, sub-row-major).
405
+ report : GeometryGateReport
406
+ The probe accounting of the gate.
407
+ """
408
+
409
+ grid: PlanarProbeGrid
410
+ points: np.ndarray
411
+ report: GeometryGateReport
412
+
413
+ def verify_positions(self, positions: np.ndarray, *, rtol: float = 5e-4) -> None:
414
+ """Assert exported probe rows follow this plan's order and count.
415
+
416
+ The 26.120 round trip (reports/RPT-004) shows the solver
417
+ preserves the count and row order of imported probes; this
418
+ check re-validates that contract on every load, because a
419
+ silent reordering would attach fields to the wrong positions.
420
+ The default tolerance matches the export's four significant
421
+ mantissa digits.
422
+
423
+ Parameters
424
+ ----------
425
+ positions : numpy.ndarray
426
+ Exported probe positions, shape ``(n, 3)``, for example
427
+ ``ProbePointsReport.positions``.
428
+ rtol : float
429
+ Largest tolerated relative coordinate mismatch.
430
+
431
+ Raises
432
+ ------
433
+ ValueError
434
+ If the count differs or any row-aligned position deviates
435
+ beyond ``rtol``: the export does not belong to this plan,
436
+ or the solver reordered the probes.
437
+ """
438
+ positions = np.asarray(positions, dtype=float)
439
+ if positions.shape != self.points.shape:
440
+ raise ValueError(
441
+ f"the export holds {len(positions)} probes but this plan placed "
442
+ f"{len(self.points)}; the export does not belong to this plan"
443
+ )
444
+ scale = np.maximum(np.abs(self.points), 1e-3)
445
+ mismatch = np.abs(positions - self.points) / scale
446
+ worst = int(np.argmax(mismatch.max(axis=1)))
447
+ if float(mismatch.max()) > rtol:
448
+ raise ValueError(
449
+ f"probe row {worst} sits at {positions[worst].tolist()} but the plan "
450
+ f"placed {self.points[worst].tolist()}; the row order contract is "
451
+ "broken (solver reordering, or an export from a different plan)"
452
+ )
453
+
454
+ def to_json(self) -> str:
455
+ """Serialize grid, points, and report together.
456
+
457
+ The points are stored explicitly (not re-derived) because they
458
+ depend on the surface mesh the gate ran against; the file is
459
+ the complete loading contract for parsing the probe export.
460
+ """
461
+ return json.dumps(
462
+ {
463
+ "grid": json.loads(self.grid.to_json()),
464
+ "points": np.asarray(self.points).tolist(),
465
+ "report": json.loads(self.report.model_dump_json()),
466
+ }
467
+ )
468
+
469
+ @classmethod
470
+ def from_json(cls, text: str) -> PlannedProbes:
471
+ """Rebuild from :meth:`to_json` output."""
472
+ payload = json.loads(text)
473
+ return cls(
474
+ grid=PlanarProbeGrid.model_validate(payload["grid"]),
475
+ points=np.asarray(payload["points"], dtype=float),
476
+ report=GeometryGateReport.model_validate(payload["report"]),
477
+ )
@@ -0,0 +1,59 @@
1
+ """Probe harness and physics regression tooling.
2
+
3
+ Pipeline role: produces the evidence behind the command database. Tier 2
4
+ probes (:mod:`pyflightstream.qa.probes`) execute each database command in
5
+ a minimal script on a licensed machine and classify it as verified or
6
+ broken (a command that runs but does nothing is broken, not verified);
7
+ :mod:`pyflightstream.qa.compat` writes the compat report under
8
+ ``reports/compat/`` and promotes database statuses from it. Tier 3 (the
9
+ physics regression matrix, milestone M4) will land here too. The
10
+ ``pyfs-qa`` CLI (:mod:`pyflightstream.qa.cli`) drives both.
11
+ """
12
+
13
+ from pyflightstream.qa.compat import (
14
+ COMPAT_SCHEMA,
15
+ apply_compat,
16
+ read_compat_report,
17
+ write_compat_report,
18
+ )
19
+ from pyflightstream.qa.probes import (
20
+ DEFAULT_ERROR_PATTERNS,
21
+ ProbeArtifacts,
22
+ ProbeEnvironmentError,
23
+ ProbeOutcome,
24
+ ProbeResult,
25
+ ProbeRun,
26
+ ProbeSpec,
27
+ Requires,
28
+ dump_changed,
29
+ dump_gained,
30
+ file_effect,
31
+ generate_probe_script,
32
+ printed_line,
33
+ probe_version,
34
+ region_printed,
35
+ )
36
+ from pyflightstream.qa.specs import PROBE_SPECS
37
+
38
+ __all__ = [
39
+ "COMPAT_SCHEMA",
40
+ "DEFAULT_ERROR_PATTERNS",
41
+ "PROBE_SPECS",
42
+ "ProbeArtifacts",
43
+ "ProbeEnvironmentError",
44
+ "ProbeOutcome",
45
+ "ProbeResult",
46
+ "ProbeRun",
47
+ "ProbeSpec",
48
+ "Requires",
49
+ "apply_compat",
50
+ "dump_changed",
51
+ "dump_gained",
52
+ "file_effect",
53
+ "generate_probe_script",
54
+ "printed_line",
55
+ "probe_version",
56
+ "read_compat_report",
57
+ "region_printed",
58
+ "write_compat_report",
59
+ ]