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,465 @@
1
+ """Command reference and compatibility matrix rendered from the database.
2
+
3
+ Pipeline role: presentation layer sitting above ``commands`` and
4
+ ``versions``; nothing imports it back. This module is the single
5
+ rendering source for both delivery layers of the reference:
6
+ ``pyflightstream.help()`` renders a self-contained HTML page and opens
7
+ it in the default browser (layer 1, the offline fallback), and the
8
+ markdown generators feed the mkdocs site at build time (layer 2, the
9
+ published docs). Both layers read the same database through the same
10
+ extraction helpers, so they can never disagree.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import html
16
+ import tempfile
17
+ import webbrowser
18
+ from importlib import metadata
19
+ from pathlib import Path
20
+
21
+ from pyflightstream.commands import (
22
+ ArgSpec,
23
+ CommandEntry,
24
+ CommandRegistry,
25
+ Status,
26
+ VersionStatus,
27
+ )
28
+ from pyflightstream.versions import FsVersion, known_versions, manual_editions, resolve
29
+
30
+ _STYLE = """
31
+ body { font-family: system-ui, sans-serif; margin: 2rem auto; max-width: 70rem;
32
+ color: #1c2733; background: #ffffff; }
33
+ h1 { font-size: 1.5rem; } h2 { font-size: 1.15rem; margin-top: 2rem; }
34
+ table { border-collapse: collapse; width: 100%; font-size: 0.85rem; }
35
+ th, td { border: 1px solid #d4dbe3; padding: 0.4rem 0.6rem; text-align: left;
36
+ vertical-align: top; }
37
+ th { background: #eef2f6; }
38
+ code { font-family: ui-monospace, monospace; }
39
+ .notes { color: #55636f; font-size: 0.8rem; margin-top: 0.3rem; }
40
+ .status-documented { color: #1a6e2f; }
41
+ .status-removed { color: #a33030; }
42
+ .status-verified { color: #14508a; }
43
+ .status-broken { color: #a33030; font-weight: bold; }
44
+ .meta { color: #55636f; font-size: 0.85rem; }
45
+ """
46
+
47
+
48
+ # ---------------------------------------------------------------------------
49
+ # Shared extraction helpers (both rendering layers sit on these).
50
+ # ---------------------------------------------------------------------------
51
+
52
+
53
+ def _arg_text(arg: ArgSpec) -> str:
54
+ """Return the one-line plain-text description of one argument."""
55
+ text = f"{arg.name}: {arg.type}"
56
+ if arg.values:
57
+ text += " in {" + ", ".join(arg.values) + "}"
58
+ if arg.unit:
59
+ text += f" [{arg.unit}]"
60
+ if not arg.required:
61
+ text += " (optional)"
62
+ return text
63
+
64
+
65
+ def _version_records(entry: CommandEntry) -> list[tuple[str, VersionStatus]]:
66
+ """Return the entry's evidence records in registry release order."""
67
+ return [
68
+ (version.canonical, entry.versions[version.canonical])
69
+ for version in known_versions()
70
+ if version.canonical in entry.versions
71
+ ]
72
+
73
+
74
+ def _evidence_text(record: VersionStatus) -> str:
75
+ """Return the evidence citation of one per-version record."""
76
+ parts = []
77
+ if record.report:
78
+ parts.append(record.report)
79
+ if record.note:
80
+ parts.append(record.note)
81
+ if record.successor:
82
+ parts.append(f"Successor: {record.successor}")
83
+ return "; ".join(parts)
84
+
85
+
86
+ def _grouped_by_chapter(version: FsVersion | None) -> dict[str, list[CommandEntry]]:
87
+ """Group database entries by chapter, optionally scoped to one version.
88
+
89
+ Parameters
90
+ ----------
91
+ version : FsVersion or None
92
+ When given, only commands with an evidence record for this
93
+ version (hotfix inheritance included) are kept; removed
94
+ commands stay visible with their removal note. None keeps the
95
+ whole database.
96
+
97
+ Returns
98
+ -------
99
+ dict of str to list of CommandEntry
100
+ Entries per chapter file stem, names sorted inside each
101
+ chapter.
102
+ """
103
+ registry = CommandRegistry.load()
104
+ chapters: dict[str, list[CommandEntry]] = {}
105
+ for entry in registry.commands.values():
106
+ if version is not None and entry.status_in(version) is None:
107
+ continue
108
+ chapters.setdefault(entry.chapter, []).append(entry)
109
+ for members in chapters.values():
110
+ members.sort(key=lambda entry: entry.name)
111
+ return dict(sorted(chapters.items()))
112
+
113
+
114
+ def _chapter_title(chapter: str) -> str:
115
+ """Return the display title of a chapter file stem."""
116
+ return chapter.replace("_", " ").capitalize()
117
+
118
+
119
+ def _package_version() -> str:
120
+ """Return the installed package version, or ``"unknown"``."""
121
+ try:
122
+ return metadata.version("pyflightstream")
123
+ except metadata.PackageNotFoundError:
124
+ return "unknown"
125
+
126
+
127
+ def _database_meta_sentence(entry_count: int, scope: str) -> str:
128
+ """Return the provenance sentence shared by both rendering layers."""
129
+ registered = ", ".join(f"{v.canonical} ({v.alias})" for v in known_versions())
130
+ return (
131
+ f"Generated from the command database of pyflightstream "
132
+ f"{_package_version()}. Scope: {scope}. "
133
+ f"Registered versions, release order: {registered}. "
134
+ f"{entry_count} commands. Every entry paraphrases the FlightStream manual "
135
+ "and cites its page (manual_ref); statuses follow the evidence rules of "
136
+ "CLAUDE.md invariant 3."
137
+ )
138
+
139
+
140
+ # ---------------------------------------------------------------------------
141
+ # Layer 1: self-contained HTML page (offline fallback).
142
+ # ---------------------------------------------------------------------------
143
+
144
+
145
+ def _format_versions_html(entry: CommandEntry) -> str:
146
+ lines = [
147
+ f'<span class="status-{record.status}">{html.escape(canonical)}: {record.status}</span>'
148
+ for canonical, record in _version_records(entry)
149
+ ]
150
+ return "<br>".join(lines)
151
+
152
+
153
+ def _entry_row_html(entry: CommandEntry) -> str:
154
+ args = "<br>".join(html.escape(_arg_text(arg)) for arg in entry.args) or "none"
155
+ notes = f'<div class="notes">{html.escape(entry.notes)}</div>' if entry.notes else ""
156
+ return (
157
+ f"<tr><td><code>{html.escape(entry.name)}</code>{notes}</td>"
158
+ f"<td>{entry.phase}</td><td>{entry.layout}</td><td>{args}</td>"
159
+ f"<td>{_format_versions_html(entry)}</td>"
160
+ f"<td>{html.escape(entry.manual_ref)}</td></tr>"
161
+ )
162
+
163
+
164
+ def render_html(version: str | FsVersion | None = None) -> str:
165
+ """Render the command database as one self-contained HTML page.
166
+
167
+ Parameters
168
+ ----------
169
+ version : str, FsVersion, or None
170
+ When given, only commands with an evidence record for this
171
+ version (hotfix inheritance included) are rendered; removed
172
+ commands stay visible with their removal note. None renders
173
+ the whole database.
174
+
175
+ Returns
176
+ -------
177
+ str
178
+ Complete HTML document.
179
+ """
180
+ resolved = resolve(version) if version is not None else None
181
+ chapters = _grouped_by_chapter(resolved)
182
+ entry_count = sum(len(members) for members in chapters.values())
183
+
184
+ scope = f"FlightStream {resolved.canonical}" if resolved else "all registered versions"
185
+ sections = []
186
+ for chapter, members in chapters.items():
187
+ rows = "\n".join(_entry_row_html(entry) for entry in members)
188
+ title = html.escape(chapter.replace("_", " "))
189
+ sections.append(
190
+ f"<h2>{title}</h2>\n<table>\n"
191
+ "<tr><th>Command</th><th>Phase</th><th>Layout</th>"
192
+ "<th>Arguments</th><th>Versions</th><th>Manual ref</th></tr>\n"
193
+ f"{rows}\n</table>"
194
+ )
195
+
196
+ body = "\n".join(sections)
197
+ return (
198
+ "<!DOCTYPE html>\n<html><head><meta charset='utf-8'>"
199
+ f"<title>pyflightstream command reference</title><style>{_STYLE}</style></head>"
200
+ "<body>\n<h1>pyflightstream command reference</h1>\n"
201
+ f'<p class="meta">{html.escape(_database_meta_sentence(entry_count, scope))}</p>\n'
202
+ f"{body}\n</body></html>\n"
203
+ )
204
+
205
+
206
+ def help( # noqa: A001
207
+ version: str | FsVersion | None = None,
208
+ path: str | Path | None = None,
209
+ open_browser: bool = True,
210
+ ) -> Path:
211
+ """Write the HTML command reference and open it in the browser.
212
+
213
+ This is the offline fallback of the published mkdocs reference;
214
+ both are rendered from the same database by this module.
215
+
216
+ Parameters
217
+ ----------
218
+ version : str, FsVersion, or None
219
+ Optional version filter, canonical or alias; see
220
+ :func:`render_html`.
221
+ path : str or Path, optional
222
+ Where to write the page. Defaults to a stable file name in the
223
+ system temporary directory, overwritten on each call.
224
+ open_browser : bool
225
+ Whether to open the page with the default browser. Set False
226
+ in headless environments and tests.
227
+
228
+ Returns
229
+ -------
230
+ Path
231
+ Location of the written HTML file.
232
+ """
233
+ if path is None:
234
+ suffix = resolve(version).canonical if version is not None else "all"
235
+ path = Path(tempfile.gettempdir()) / f"pyflightstream_reference_{suffix}.html"
236
+ target = Path(path)
237
+ target.write_text(render_html(version), encoding="utf-8")
238
+ if open_browser:
239
+ webbrowser.open(target.as_uri())
240
+ return target
241
+
242
+
243
+ # ---------------------------------------------------------------------------
244
+ # Layer 2: markdown pages for the mkdocs site (generated at build time).
245
+ # ---------------------------------------------------------------------------
246
+
247
+
248
+ def _md_cell(text: str) -> str:
249
+ """Escape one markdown table cell (pipes and line breaks)."""
250
+ return text.replace("|", "\\|").replace("\n", " ").strip()
251
+
252
+
253
+ def _status_span(status: Status) -> str:
254
+ """Return the status word wrapped in its CSS class span."""
255
+ return f'<span class="status-{status}">{status}</span>'
256
+
257
+
258
+ def _entry_markdown(entry: CommandEntry) -> str:
259
+ """Render one command as a markdown section with anchor heading."""
260
+ lines = [f"## {entry.name}", ""]
261
+ lines.append(f"Phase `{entry.phase}`, layout `{entry.layout}`. Manual: {entry.manual_ref}.")
262
+ lines.append("")
263
+ if entry.notes:
264
+ lines.append(f"> {_md_cell(entry.notes)}")
265
+ lines.append("")
266
+ if entry.args:
267
+ lines.append("| Argument | Specification |")
268
+ lines.append("|---|---|")
269
+ for arg in entry.args:
270
+ spec = _arg_text(arg).removeprefix(f"{arg.name}: ")
271
+ lines.append(f"| `{arg.name}` | {_md_cell(spec)} |")
272
+ lines.append("")
273
+ lines.append("| Version | Status | Evidence |")
274
+ lines.append("|---|---|---|")
275
+ for canonical, record in _version_records(entry):
276
+ evidence = _evidence_text(record) or entry.manual_ref
277
+ lines.append(f"| {canonical} | {_status_span(record.status)} | {_md_cell(evidence)} |")
278
+ lines.append("")
279
+ return "\n".join(lines)
280
+
281
+
282
+ def markdown_reference_pages() -> dict[str, str]:
283
+ """Render the command reference as markdown pages for the docs site.
284
+
285
+ One page per manual chapter plus an index and a ``SUMMARY.md``
286
+ navigation file (mkdocs-literate-nav format). Paths are relative to
287
+ the ``reference/`` section of the docs; the mkdocs build generates
288
+ them through ``scripts/gen_docs_pages.py`` so the site can never
289
+ drift from the database.
290
+
291
+ Returns
292
+ -------
293
+ dict of str to str
294
+ Page content keyed by path relative to ``reference/``.
295
+ """
296
+ chapters = _grouped_by_chapter(None)
297
+ entry_count = sum(len(members) for members in chapters.values())
298
+
299
+ pages: dict[str, str] = {}
300
+ index_lines = [
301
+ "# Command reference",
302
+ "",
303
+ _database_meta_sentence(entry_count, "all registered versions"),
304
+ "",
305
+ "Offline fallback: `pyflightstream.help()` renders this same database "
306
+ "into a self-contained HTML page from the installed package, no docs "
307
+ "site needed.",
308
+ "",
309
+ "See also the [version compatibility matrix](../compatibility.md).",
310
+ "",
311
+ "| Chapter | Commands |",
312
+ "|---|---|",
313
+ ]
314
+ nav_lines = ["* [Overview](index.md)"]
315
+ for chapter, members in chapters.items():
316
+ title = _chapter_title(chapter)
317
+ index_lines.append(f"| [{title}]({chapter}.md) | {len(members)} |")
318
+ nav_lines.append(f"* [{title}]({chapter}.md)")
319
+
320
+ page_lines = [
321
+ f"# {title}",
322
+ "",
323
+ f"Commands of the `{chapter}` chapter of the database, "
324
+ f"{len(members)} entries. Statuses follow the evidence rules of "
325
+ "CLAUDE.md invariant 3: `documented` cites the manual, `verified` "
326
+ "and `broken` cite a committed probe report, `removed` records "
327
+ "the manual page stating the removal.",
328
+ "",
329
+ ]
330
+ page_lines.extend(_entry_markdown(entry) for entry in members)
331
+ pages[f"{chapter}.md"] = "\n".join(page_lines)
332
+
333
+ index_lines.append("")
334
+ pages["index.md"] = "\n".join(index_lines)
335
+ pages["SUMMARY.md"] = "\n".join(nav_lines) + "\n"
336
+ return pages
337
+
338
+
339
+ def markdown_compatibility_matrix() -> str:
340
+ """Render the version compatibility matrix as one markdown page.
341
+
342
+ Rows are commands grouped by chapter; columns are the registered
343
+ FlightStream versions in release order. Cells carry the evidence
344
+ status; an empty cell is the honest absence of recorded evidence
345
+ for that version (no status is ever guessed).
346
+
347
+ Returns
348
+ -------
349
+ str
350
+ Complete markdown page.
351
+ """
352
+ chapters = _grouped_by_chapter(None)
353
+ entry_count = sum(len(members) for members in chapters.values())
354
+ versions = known_versions()
355
+ editions = manual_editions()
356
+
357
+ counts: dict[str, dict[Status, int]] = {
358
+ version.canonical: dict.fromkeys(Status, 0) for version in versions
359
+ }
360
+ none_counts: dict[str, int] = dict.fromkeys(counts, 0)
361
+ for members in chapters.values():
362
+ for entry in members:
363
+ for version in versions:
364
+ record = entry.status_in(version)
365
+ if record is None:
366
+ none_counts[version.canonical] += 1
367
+ else:
368
+ counts[version.canonical][record.status] += 1
369
+
370
+ lines = [
371
+ "# Version compatibility matrix",
372
+ "",
373
+ _database_meta_sentence(entry_count, "all registered versions"),
374
+ "",
375
+ "An empty cell means no recorded evidence for that version: the "
376
+ "command awaits release-notes review or backfill probing, and the "
377
+ "script builder refuses it for that version until evidence lands.",
378
+ "",
379
+ "## Evidence per version",
380
+ "",
381
+ "| Version | Vendor name | Documented | Verified | Broken | Removed | No evidence "
382
+ "| Manual edition |",
383
+ "|---|---|---|---|---|---|---|---|",
384
+ ]
385
+ for version in versions:
386
+ row = counts[version.canonical]
387
+ edition = editions.get(version.canonical, "none registered")
388
+ lines.append(
389
+ f"| {version.canonical} | {version.alias} "
390
+ f"| {row[Status.DOCUMENTED]} | {row[Status.VERIFIED]} | {row[Status.BROKEN]} "
391
+ f"| {row[Status.REMOVED]} | {none_counts[version.canonical]} "
392
+ f"| {_md_cell(edition)} |"
393
+ )
394
+ lines.append("")
395
+
396
+ header = " | ".join(version.canonical for version in versions)
397
+ divider = "|---" * (len(versions) + 1) + "|"
398
+ for chapter, members in chapters.items():
399
+ lines.append(f"## {_chapter_title(chapter)}")
400
+ lines.append("")
401
+ lines.append(f"| Command | {header} |")
402
+ lines.append(divider)
403
+ for entry in members:
404
+ cells = []
405
+ for version in versions:
406
+ record = entry.status_in(version)
407
+ cells.append(_status_span(record.status) if record else "")
408
+ link = f"[{entry.name}](reference/{entry.chapter}.md#{entry.name.lower()})"
409
+ lines.append(f"| {link} | " + " | ".join(cells) + " |")
410
+ lines.append("")
411
+ return "\n".join(lines)
412
+
413
+
414
+ def percent_script_markdown(source: str) -> str:
415
+ """Render a percent-format example script as one markdown page.
416
+
417
+ Markdown cells (``# %% [markdown]``) become prose; code cells
418
+ (``# %%``) become fenced Python blocks; a leading module docstring
419
+ is dropped because it repeats the page introduction. This is the
420
+ committed rendering of the no-notebooks policy (CLAUDE.md
421
+ invariant 7): the ``.py`` file is the single source and the docs
422
+ page is generated from it at build time.
423
+
424
+ Parameters
425
+ ----------
426
+ source : str
427
+ Content of a percent-format ``.py`` example.
428
+
429
+ Returns
430
+ -------
431
+ str
432
+ Complete markdown page.
433
+ """
434
+ blocks: list[str] = []
435
+ cell_lines: list[str] = []
436
+ is_markdown = False
437
+
438
+ def flush() -> None:
439
+ lines = list(cell_lines)
440
+ while lines and not lines[0].strip():
441
+ lines.pop(0)
442
+ if not is_markdown and lines and lines[0].startswith('"""'):
443
+ first = lines.pop(0).strip()
444
+ if not (first.endswith('"""') and len(first) > 3):
445
+ while lines and '"""' not in lines[0]:
446
+ lines.pop(0)
447
+ if lines:
448
+ lines.pop(0)
449
+ body = "\n".join(lines).strip("\n")
450
+ if not body:
451
+ return
452
+ blocks.append(body if is_markdown else f"```python\n{body}\n```")
453
+
454
+ for line in source.splitlines():
455
+ marker = line.strip()
456
+ if marker.startswith("# %%"):
457
+ flush()
458
+ cell_lines = []
459
+ is_markdown = marker == "# %% [markdown]"
460
+ elif is_markdown:
461
+ cell_lines.append(line.removeprefix("#").removeprefix(" "))
462
+ else:
463
+ cell_lines.append(line)
464
+ flush()
465
+ return "\n\n".join(blocks) + "\n"