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,844 @@
1
+ """Curated helpers for the common FlightStream workflows (SAD Section 4.3).
2
+
3
+ Pipeline role: a small, curated set of thin typed functions sitting on
4
+ top of :class:`~pyflightstream.script.Script`. Each helper only
5
+ translates its typed arguments into ``emit()`` calls, so every line
6
+ still passes the database validation, phase ordering, and
7
+ cross-reference checks of the builder. Helpers own the conditional
8
+ argument combinations the manual documents in prose (which extras each
9
+ SET_FREESTREAM type takes, when INITIALIZE_SOLVER takes per-surface
10
+ lines or a PERIODIC copy count), because the database records grammar,
11
+ not conditionality.
12
+
13
+ One generated function per command was rejected in the SAD: it would
14
+ reproduce the shape of the AGPL package, create a huge surface, and
15
+ teach nothing. The curated workflows are: free stream and atmosphere,
16
+ actuator disc (SRC-003 pp.323-324), rotary motion (pp.332-333), solver
17
+ settings (pp.339-343), solver initialization (p.337), sweeps (p.406),
18
+ analysis and export selection (pp.350-354), and probe management
19
+ (pp.362-363).
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import warnings
25
+ from collections.abc import Sequence
26
+ from typing import Literal
27
+
28
+ from pyflightstream.script import CommandArgumentError, Script
29
+
30
+
31
+ def _toggle(value: bool) -> str:
32
+ return "ENABLE" if value else "DISABLE"
33
+
34
+
35
+ def free_stream(
36
+ script: Script,
37
+ kind: str = "CONSTANT",
38
+ *,
39
+ frame: int | None = None,
40
+ axis: str | None = None,
41
+ rpm: float | None = None,
42
+ profile: str | None = None,
43
+ filetype: str | None = None,
44
+ ) -> None:
45
+ """Set the free-stream velocity definition (SRC-003 p.322).
46
+
47
+ Parameters
48
+ ----------
49
+ script : Script
50
+ Script under construction.
51
+ kind : str
52
+ ``CONSTANT`` (uniform free stream, the magnitude comes later
53
+ from the solver settings), ``ROTATION`` (rotating frame free
54
+ stream for hover and propeller analyses), or ``CUSTOM``
55
+ (velocity profile imported from a file).
56
+ frame : int, optional
57
+ ROTATION only: local coordinate system carrying the rotation
58
+ axis; it must exist earlier in the script.
59
+ axis : str, optional
60
+ ROTATION only: rotation axis of ``frame``, ``X``, ``Y``, or
61
+ ``Z``.
62
+ rpm : float, optional
63
+ ROTATION only: angular velocity in rev/min.
64
+ profile : str, optional
65
+ CUSTOM only: path of the velocity profile file.
66
+ filetype : str, optional
67
+ CUSTOM only: ``STRUCTURED`` or ``UNSTRUCTURED`` profile file.
68
+ """
69
+ upper = kind.upper()
70
+ rotation_given = [frame is not None, axis is not None, rpm is not None]
71
+ custom_given = [profile is not None, filetype is not None]
72
+ if upper == "ROTATION":
73
+ if not all(rotation_given) or any(custom_given):
74
+ raise CommandArgumentError(
75
+ "SET_FREESTREAM ROTATION takes exactly frame, axis, and rpm: the rotating "
76
+ "free stream needs the axis frame, the axis, and the angular velocity "
77
+ "(SRC-003 p.322)"
78
+ )
79
+ script.emit("SET_FREESTREAM", upper, frame=frame, axis=axis, angular_velocity=rpm)
80
+ elif upper == "CUSTOM":
81
+ if not all(custom_given) or any(rotation_given):
82
+ raise CommandArgumentError(
83
+ "SET_FREESTREAM CUSTOM takes exactly filetype and profile: the imported "
84
+ "velocity profile needs its file structure and path (SRC-003 p.322)"
85
+ )
86
+ script.emit("SET_FREESTREAM", upper, filetype=filetype, filename=profile)
87
+ else:
88
+ if any(rotation_given) or any(custom_given):
89
+ raise CommandArgumentError(
90
+ "SET_FREESTREAM CONSTANT takes no further input; the free-stream magnitude "
91
+ "is a solver setting (SOLVER_SET_VELOCITY, SRC-003 p.339)"
92
+ )
93
+ script.emit("SET_FREESTREAM", upper)
94
+
95
+
96
+ def atmosphere(
97
+ script: Script,
98
+ *,
99
+ altitude: float | None = None,
100
+ altitude_units: str = "METERS",
101
+ density: float | None = None,
102
+ pressure: float | None = None,
103
+ temperature: float | None = None,
104
+ viscosity: float | None = None,
105
+ specific_heat_ratio: float | None = None,
106
+ ) -> None:
107
+ """Set the working fluid state (SRC-003 p.328).
108
+
109
+ Either from a standard-atmosphere altitude (AIR_ALTITUDE) or from
110
+ the five explicit fluid properties (FLUID_PROPERTIES); the two
111
+ paths are mutually exclusive. Sonic velocity is derived from
112
+ temperature and specific heat ratio and is no longer an input.
113
+
114
+ Parameters
115
+ ----------
116
+ script : Script
117
+ Script under construction.
118
+ altitude : float, optional
119
+ Standard-atmosphere altitude, in ``altitude_units``.
120
+ altitude_units : str
121
+ ``METERS`` or ``FEET``.
122
+ density : float, optional
123
+ Fluid density in kg/m^3.
124
+ pressure : float, optional
125
+ Static pressure in Pa.
126
+ temperature : float, optional
127
+ Static temperature in K.
128
+ viscosity : float, optional
129
+ Dynamic viscosity in Pa s.
130
+ specific_heat_ratio : float, optional
131
+ Ratio of specific heats (1.4 for air).
132
+ """
133
+ properties = (density, pressure, temperature, viscosity, specific_heat_ratio)
134
+ if altitude is not None:
135
+ if any(value is not None for value in properties):
136
+ raise CommandArgumentError(
137
+ "atmosphere takes either an altitude or the five explicit fluid "
138
+ "properties, not both: AIR_ALTITUDE already sets the whole standard "
139
+ "atmosphere state (SRC-003 p.328)"
140
+ )
141
+ script.emit("AIR_ALTITUDE", altitude, altitude_units)
142
+ return
143
+ if any(value is None for value in properties):
144
+ raise CommandArgumentError(
145
+ "atmosphere without an altitude needs all five fluid properties (density, "
146
+ "pressure, temperature, viscosity, specific_heat_ratio), because "
147
+ "FLUID_PROPERTIES sets the complete fluid state (SRC-003 p.328)"
148
+ )
149
+ script.emit(
150
+ "FLUID_PROPERTIES",
151
+ density=density,
152
+ pressure=pressure,
153
+ temperature=temperature,
154
+ viscosity=viscosity,
155
+ specific_heat_ratio=specific_heat_ratio,
156
+ )
157
+
158
+
159
+ def actuator_disc(
160
+ script: Script,
161
+ name: str,
162
+ *,
163
+ frame: int,
164
+ axis: str,
165
+ offset: float,
166
+ r_tip: float,
167
+ r_hub: float,
168
+ rpm: float,
169
+ thrust: float | None = None,
170
+ thrust_type: str = "NEWTONS",
171
+ profile: str | None = None,
172
+ profile_force_unit: str = "NEWTONS",
173
+ n_blades: int | None = None,
174
+ swirl: float | None = None,
175
+ enable: bool = True,
176
+ ) -> int:
177
+ """Create and configure one propeller actuator disc (SRC-003 pp.323-324).
178
+
179
+ The disc is the linearized propeller slipstream surrogate
180
+ (SRC-003 pp.185-187). Exactly one thrust specification is taken:
181
+ a net ``thrust`` (ELLIPTICAL profile) or a radial force
182
+ distribution file ``profile`` (CUSTOM profile, which also needs
183
+ ``n_blades``).
184
+
185
+ Parameters
186
+ ----------
187
+ script : Script
188
+ Script under construction.
189
+ name : str
190
+ Actuator name shown in the interface.
191
+ frame : int
192
+ Local coordinate system carrying the disc axis (index greater
193
+ than 1); it must exist earlier in the script.
194
+ axis : str
195
+ Disc axis within ``frame``: ``X``, ``Y``, or ``Z``.
196
+ offset : float
197
+ Disc position along the axis, in simulation length units.
198
+ r_tip, r_hub : float
199
+ Tip and hub radii, in simulation length units.
200
+ rpm : float
201
+ Rotational speed in rev/min; the sign selects the rotation
202
+ direction about the axis.
203
+ thrust : float, optional
204
+ Net thrust for the ELLIPTICAL model, in ``thrust_type`` units.
205
+ thrust_type : str
206
+ ``COEFFICIENT``, ``NEWTONS``, or ``POUNDS``. The manual
207
+ recommends dimensional thrust because the coefficient
208
+ convention must match the solver formulation (SRC-003 p.187).
209
+ profile : str, optional
210
+ Path of the radial thrust profile file for the CUSTOM model.
211
+ profile_force_unit : str
212
+ Force unit used inside the profile file: ``NEWTONS``,
213
+ ``KILO-NEWTONS``, ``POUND-FORCE``, or ``KILOGRAM-FORCE``.
214
+ n_blades : int, optional
215
+ Blade count; required with ``profile``.
216
+ swirl : float, optional
217
+ Fraction between 0 and 1 of the swirl velocity kept
218
+ downstream; below 1 mimics a de-swirling stator
219
+ (SRC-003 p.186).
220
+ enable : bool
221
+ Emit ENABLE_ACTUATOR at the end.
222
+
223
+ Returns
224
+ -------
225
+ int
226
+ Index of the created actuator, for later citations.
227
+ """
228
+ if (thrust is None) == (profile is None):
229
+ raise CommandArgumentError(
230
+ "actuator_disc takes exactly one thrust specification: a net thrust "
231
+ "(ELLIPTICAL model) or a radial profile file (CUSTOM model) "
232
+ "(SRC-003 pp.185-187)"
233
+ )
234
+ if profile is not None and n_blades is None:
235
+ raise CommandArgumentError(
236
+ "actuator_disc with a profile file needs n_blades, because the imported "
237
+ "radial distribution is per blade (SRC-003 pp.323-324)"
238
+ )
239
+ if swirl is not None and not 0.0 <= swirl <= 1.0:
240
+ raise CommandArgumentError(
241
+ f"actuator_disc swirl must lie between 0 and 1, got {swirl}: it is the "
242
+ "fraction of the swirl velocity kept downstream (SRC-003 p.186)"
243
+ )
244
+ subtype = "ELLIPTICAL" if thrust is not None else "CUSTOM"
245
+ script.emit("CREATE_NEW_ACTUATOR", "PROPELLER", subtype=subtype, name=name)
246
+ index = script.num_actuators
247
+ script.emit("SET_ACTUATOR_AXIS", index, frame, axis, offset)
248
+ script.emit("SET_ACTUATOR_RADIUS", index, r_tip, r_hub)
249
+ script.emit("SET_PROP_ACTUATOR_RPM", index, rpm)
250
+ if thrust is not None:
251
+ script.emit("SET_PROP_ACTUATOR_THRUST", index, thrust, thrust_type)
252
+ else:
253
+ script.emit("SET_PROP_ACTUATOR_PROFILE", index, profile_force_unit, n_blades, profile)
254
+ if swirl is not None:
255
+ script.emit("SET_PROP_ACTUATOR_SWIRL", index, swirl)
256
+ if enable:
257
+ script.emit("ENABLE_ACTUATOR", index)
258
+ return index
259
+
260
+
261
+ def rotary_motion(
262
+ script: Script,
263
+ *,
264
+ frame: int,
265
+ axis: str,
266
+ rpm: float,
267
+ boundaries: Sequence[int] | Literal["all"] = "all",
268
+ moving_frames: Sequence[int] | Literal["all"] | None = None,
269
+ start_time: float | None = None,
270
+ wake_stabilization_blades: int | None = None,
271
+ ) -> int:
272
+ """Create and configure one rotary motion (SRC-003 pp.332-333).
273
+
274
+ Rotary motion is the blade-resolved alternative to the actuator
275
+ disc surrogate (SRC-003 p.234); it requires the unsteady solver
276
+ (see :func:`unsteady_solver`).
277
+
278
+ Parameters
279
+ ----------
280
+ script : Script
281
+ Script under construction.
282
+ frame : int
283
+ Local coordinate system of the rotation (index greater than
284
+ 1); it must exist earlier in the script.
285
+ axis : str
286
+ Rotor axis within ``frame``: ``X``, ``Y``, or ``Z``.
287
+ rpm : float
288
+ Rotor speed in rev/min.
289
+ boundaries : sequence of int or ``"all"``
290
+ Geometry boundaries assigned to the motion; ``"all"`` selects
291
+ every boundary (-1 form).
292
+ moving_frames : sequence of int, ``"all"``, or None
293
+ Local frames attached to the motion; None attaches none.
294
+ start_time : float, optional
295
+ Motion start within the solver physical time, in s; a positive
296
+ value converges a steady base flow before the motion begins.
297
+ wake_stabilization_blades : int, optional
298
+ Enables slipstream wake stabilization with this blade count.
299
+
300
+ Returns
301
+ -------
302
+ int
303
+ Identifier of the created motion, for later citations.
304
+ """
305
+ script.emit("CREATE_NEW_MOTION", "ROTARY")
306
+ motion_id = script.num_motions
307
+ if boundaries == "all":
308
+ script.emit("SET_MOTION_BOUNDARIES", motion_id, -1)
309
+ else:
310
+ script.emit("SET_MOTION_BOUNDARIES", motion_id, len(boundaries), list(boundaries))
311
+ if moving_frames == "all":
312
+ script.emit("SET_MOTION_MOVING_FRAMES", motion_id, -1)
313
+ elif moving_frames is not None:
314
+ script.emit("SET_MOTION_MOVING_FRAMES", motion_id, len(moving_frames), list(moving_frames))
315
+ script.emit("SET_MOTION_COORDINATE_SYSTEM", motion_id, frame)
316
+ script.emit("SET_MOTION_ROTOR_AXIS", motion_id, axis)
317
+ script.emit("SET_MOTION_ROTOR_RPM", motion_id, rpm)
318
+ if start_time is not None:
319
+ script.emit("SET_MOTION_START_TIME", motion_id, start_time)
320
+ if wake_stabilization_blades is not None:
321
+ script.emit(
322
+ "SET_MOTION_SLIPSTREAM_WAKE_STABILIZATION",
323
+ motion_id,
324
+ "ENABLE",
325
+ wake_stabilization_blades,
326
+ )
327
+ return motion_id
328
+
329
+
330
+ def unsteady_solver(script: Script, *, time_iterations: int, delta_time: float) -> None:
331
+ """Select unsteady physical time stepping (SRC-003 p.341).
332
+
333
+ For rotary cases the manual recommends 8 to 12 degrees of blade
334
+ rotation per time step and at least two full rotations
335
+ (SRC-003 p.210).
336
+
337
+ Parameters
338
+ ----------
339
+ script : Script
340
+ Script under construction.
341
+ time_iterations : int
342
+ Number of physical time steps.
343
+ delta_time : float
344
+ Physical time step in s.
345
+ """
346
+ script.emit("SET_SOLVER_UNSTEADY", time_iterations, delta_time)
347
+
348
+
349
+ def solver_settings(
350
+ script: Script,
351
+ *,
352
+ aoa: float | None = None,
353
+ sideslip: float | None = None,
354
+ velocity: float | None = None,
355
+ mach: float | None = None,
356
+ ref_velocity: float | None = None,
357
+ ref_mach: float | None = None,
358
+ ref_area: float | None = None,
359
+ ref_length: float | None = None,
360
+ iterations: int | None = None,
361
+ convergence: float | None = None,
362
+ forced_iterations: bool | None = None,
363
+ max_threads: int | None = None,
364
+ boundary_layer: str | None = None,
365
+ viscous_coupling: bool | None = None,
366
+ viscous_excluded: Sequence[int] | None = None,
367
+ ) -> None:
368
+ """Set the solver runtime settings that were given (SRC-003 pp.339-343).
369
+
370
+ Only the provided settings are emitted, so the helper serves both
371
+ the initial setup and the re-emission between campaign points.
372
+
373
+ Parameters
374
+ ----------
375
+ script : Script
376
+ Script under construction.
377
+ aoa : float, optional
378
+ Angle of attack in deg, magnitude below 90.
379
+ sideslip : float, optional
380
+ Side-slip angle in deg, magnitude below 90.
381
+ velocity : float, optional
382
+ Free-stream velocity magnitude in m/s.
383
+ mach : float, optional
384
+ Free-stream Mach number.
385
+ ref_velocity : float, optional
386
+ Reference velocity in m/s for coefficient normalization; for
387
+ rotary or hover cases use the largest characteristic velocity,
388
+ such as the rotor tip speed (SRC-003 p.201).
389
+ ref_mach : float, optional
390
+ Reference Mach number.
391
+ ref_area : float, optional
392
+ Reference area S_ref in simulation length units squared
393
+ (Q*S_ref force normalization, SRC-003 p.223).
394
+ ref_length : float, optional
395
+ Reference length L_ref in simulation length units
396
+ (Q*S_ref*L_ref moment normalization, SRC-003 p.223).
397
+ iterations : int, optional
398
+ Solver iteration count.
399
+ convergence : float, optional
400
+ Residual threshold declaring convergence (SRC-003 p.200).
401
+ forced_iterations : bool, optional
402
+ Run the full iteration count regardless of convergence.
403
+ max_threads : int, optional
404
+ Parallel core count.
405
+ boundary_layer : str, optional
406
+ ``LAMINAR``, ``TRANSITIONAL``, or ``TURBULENT``; the default
407
+ transitional model is stated valid for chord Reynolds numbers
408
+ between 500000 and 1500000 (SRC-003 p.203).
409
+ viscous_coupling : bool, optional
410
+ Couple the semi-empirical boundary layer model to the
411
+ potential flow solution (attached-flow viscosity only,
412
+ SRC-003 pp.207-208).
413
+ viscous_excluded : sequence of int, optional
414
+ Boundaries excluded from viscous coupling.
415
+ """
416
+ scalar_commands = (
417
+ ("SOLVER_SET_AOA", aoa),
418
+ ("SOLVER_SET_SIDESLIP", sideslip),
419
+ ("SOLVER_SET_VELOCITY", velocity),
420
+ ("SOLVER_SET_MACH_NUMBER", mach),
421
+ ("SOLVER_SET_REF_VELOCITY", ref_velocity),
422
+ ("SOLVER_SET_REF_MACH_NUMBER", ref_mach),
423
+ ("SOLVER_SET_REF_AREA", ref_area),
424
+ ("SOLVER_SET_REF_LENGTH", ref_length),
425
+ ("SOLVER_SET_ITERATIONS", iterations),
426
+ ("SOLVER_SET_CONVERGENCE", convergence),
427
+ ("SET_MAX_PARALLEL_THREADS", max_threads),
428
+ )
429
+ for command, value in scalar_commands:
430
+ if value is not None:
431
+ script.emit(command, value)
432
+ if forced_iterations is not None:
433
+ script.emit("SOLVER_SET_FORCED_ITERATIONS", _toggle(forced_iterations))
434
+ if boundary_layer is not None:
435
+ script.emit("SET_BOUNDARY_LAYER_TYPE", boundary_layer)
436
+ if viscous_coupling is not None:
437
+ script.emit("SET_SOLVER_VISCOUS_COUPLING", _toggle(viscous_coupling))
438
+ if viscous_excluded is not None:
439
+ script.emit(
440
+ "SET_VISCOUS_EXCLUDED_BOUNDARIES", len(viscous_excluded), list(viscous_excluded)
441
+ )
442
+
443
+
444
+ def initialize_solver(
445
+ script: Script,
446
+ *,
447
+ solver_model: str = "INCOMPRESSIBLE",
448
+ surfaces: Sequence[tuple[int, bool]] | Literal["all"] = "all",
449
+ wake_termination_x: float | str = "DEFAULT",
450
+ symmetry: str = "NONE",
451
+ periodic_copies: int | None = None,
452
+ wall_collision_avoidance: bool | None = None,
453
+ ) -> None:
454
+ """Initialize the solver, covering the extended forms (SRC-003 p.337).
455
+
456
+ Parameters
457
+ ----------
458
+ script : Script
459
+ Script under construction.
460
+ solver_model : str
461
+ ``INCOMPRESSIBLE``, ``SUBSONIC_PRANDTL_GLAUERT``,
462
+ ``TRANSONIC_FIELD_PANEL``, ``TANGENT_CONE``, or
463
+ ``MODIFIED_NEWTONIAN``.
464
+ surfaces : sequence of (int, bool) pairs or ``"all"``
465
+ ``"all"`` initializes every boundary (-1 form); a sequence of
466
+ ``(surface_index, quad_mesher)`` pairs initializes those
467
+ surfaces with the quad mesher toggled per surface.
468
+ wake_termination_x : float or str
469
+ X location of wake termination in the reference frame, or
470
+ ``DEFAULT`` for auto-computation.
471
+ symmetry : str
472
+ ``NONE``, ``MIRROR``, or ``PERIODIC``. Initializing MIRROR
473
+ with a full (non-half) model diverges instantly
474
+ (SRC-003 p.217).
475
+ periodic_copies : int, optional
476
+ Number of periodic copies; required with PERIODIC symmetry
477
+ and forbidden otherwise.
478
+ wall_collision_avoidance : bool, optional
479
+ Applies to solver models 1 to 3.
480
+ """
481
+ if (symmetry.upper() == "PERIODIC") != (periodic_copies is not None):
482
+ raise CommandArgumentError(
483
+ "INITIALIZE_SOLVER: PERIODIC symmetry appends the number of copies, so "
484
+ "periodic_copies is required with PERIODIC and forbidden otherwise "
485
+ "(SRC-003 p.337)"
486
+ )
487
+ if periodic_copies is not None and periodic_copies < 1:
488
+ raise CommandArgumentError(
489
+ f"INITIALIZE_SOLVER: periodic_copies must be a positive count, got "
490
+ f"{periodic_copies} (SRC-003 p.337)"
491
+ )
492
+ arguments: dict[str, object] = {
493
+ "solver_model": solver_model,
494
+ "wake_termination_x": str(wake_termination_x),
495
+ "symmetry": symmetry,
496
+ }
497
+ if surfaces == "all":
498
+ arguments["surfaces"] = -1
499
+ else:
500
+ arguments["surfaces"] = len(surfaces)
501
+ arguments["surface_toggles"] = [
502
+ f"{index},{_toggle(quad_mesher)}" for index, quad_mesher in surfaces
503
+ ]
504
+ if periodic_copies is not None:
505
+ arguments["symmetry_copies"] = periodic_copies
506
+ if wall_collision_avoidance is not None:
507
+ arguments["wall_collision_avoidance"] = _toggle(wall_collision_avoidance)
508
+ script.emit("INITIALIZE_SOLVER", **arguments)
509
+
510
+
511
+ def sweep(
512
+ script: Script,
513
+ *,
514
+ aoa: Sequence[float] | None = None,
515
+ beta: Sequence[float] | None = None,
516
+ velocity_file: str | None = None,
517
+ clear_solution: bool | None = None,
518
+ ref_velocity_same: bool | None = None,
519
+ post_run_script: str | None = None,
520
+ start: bool = True,
521
+ export_spreadsheet: str | None = None,
522
+ ) -> None:
523
+ """Configure and run a Sweeper Toolbox sweep (SRC-003 p.406).
524
+
525
+ Parameters
526
+ ----------
527
+ script : Script
528
+ Script under construction.
529
+ aoa : sequence of float, optional
530
+ Custom angle of attack values in deg.
531
+ beta : sequence of float, optional
532
+ Custom side-slip values in deg.
533
+ velocity_file : str, optional
534
+ Path of the custom velocity list file.
535
+ clear_solution : bool, optional
536
+ Clear the solution between sweep runs instead of reusing it.
537
+ ref_velocity_same : bool, optional
538
+ Keep the reference velocity equal to the free-stream velocity
539
+ at every sweep point.
540
+ post_run_script : str, optional
541
+ Script executed after each sweep point, for example a surface
542
+ section extraction script.
543
+ start : bool
544
+ Emit SWEEPER_START after the configuration.
545
+ export_spreadsheet : str, optional
546
+ Path of the sweep results spreadsheet export.
547
+ """
548
+ if aoa is None and beta is None and velocity_file is None:
549
+ raise CommandArgumentError(
550
+ "sweep needs at least one axis (aoa, beta, or velocity_file); a sweep "
551
+ "without values has nothing to run (SRC-003 p.406)"
552
+ )
553
+ if aoa is not None:
554
+ script.emit("SWEEPER_SET_AOA_SWEEP", "CUSTOM", list(aoa))
555
+ if beta is not None:
556
+ script.emit("SWEEPER_SET_BETA_SWEEP", "CUSTOM", list(beta))
557
+ if velocity_file is not None:
558
+ script.emit("SWEEPER_SET_VELOCITY_SWEEP", "CUSTOM", velocity_file)
559
+ if clear_solution is not None:
560
+ script.emit("SWEEPER_CLEAR_SOLUTION", _toggle(clear_solution))
561
+ if ref_velocity_same is not None:
562
+ script.emit("SWEEPER_REF_VELOCITY_SAME", _toggle(ref_velocity_same))
563
+ if post_run_script is not None:
564
+ script.emit("SWEEPER_POST_RUN_SCRIPT", "ENABLE", post_run_script)
565
+ if start:
566
+ script.emit("SWEEPER_START")
567
+ if export_spreadsheet is not None:
568
+ script.emit("SWEEPER_EXPORT_SPREADSHEET", export_spreadsheet)
569
+
570
+
571
+ def analysis_setup(
572
+ script: Script,
573
+ *,
574
+ loads_frame: int | None = None,
575
+ moments_model: str | None = None,
576
+ symmetry_loads: bool | None = None,
577
+ load_units: str | None = None,
578
+ boundaries: Sequence[int] | None = None,
579
+ inviscid_only: bool | None = None,
580
+ vorticity_drag_boundaries: Sequence[int] | Literal["all"] | None = None,
581
+ ) -> None:
582
+ """Select how loads and moments are analyzed (SRC-003 pp.350-351).
583
+
584
+ Parameters
585
+ ----------
586
+ script : Script
587
+ Script under construction.
588
+ loads_frame : int, optional
589
+ Coordinate system for evaluating loads and moments; index 1 is
590
+ the reference frame.
591
+ moments_model : str, optional
592
+ ``PRESSURE`` (solver default) or ``VORTICITY``.
593
+ symmetry_loads : bool, optional
594
+ Include symmetry boundary loads; relevant to half-model runs.
595
+ load_units : str, optional
596
+ ``COEFFICIENTS``, ``NEWTONS``, ``KILO-NEWTONS``,
597
+ ``POUND-FORCE``, or ``KILOGRAM-FORCE``.
598
+ boundaries : sequence of int, optional
599
+ Boundaries enabled in the analysis; boundaries not listed are
600
+ disabled (SRC-003 p.351).
601
+ inviscid_only : bool, optional
602
+ Restrict the analysis to inviscid loads and moments.
603
+ vorticity_drag_boundaries : sequence of int, ``"all"``, or None
604
+ Boundaries whose induced drag comes from surface vorticity
605
+ integration; boundaries without trailing-edge boundary
606
+ conditions silently report zero induced drag (SRC-003 p.202).
607
+ """
608
+ # symmetry_loads first: it is an init-phase setting consumed by the
609
+ # in-solve monitors (per-step force plots), so a call mixing it
610
+ # with the analysis-phase selections is only valid before
611
+ # START_SOLVER; pass it alone in that position.
612
+ if symmetry_loads is not None:
613
+ script.emit("SET_ANALYSIS_SYMMETRY_LOADS", _toggle(symmetry_loads))
614
+ if loads_frame is not None:
615
+ script.emit("SET_SOLVER_ANALYSIS_LOADS_FRAME", loads_frame)
616
+ if moments_model is not None:
617
+ script.emit("SET_ANALYSIS_MOMENTS_MODEL", moments_model)
618
+ if load_units is not None:
619
+ script.emit("SET_LOADS_AND_MOMENTS_UNITS", load_units)
620
+ if boundaries is not None:
621
+ script.emit("SET_SOLVER_ANALYSIS_BOUNDARIES", len(boundaries), list(boundaries))
622
+ if inviscid_only is not None:
623
+ script.emit("SET_INVISCID_LOADS", _toggle(inviscid_only))
624
+ if vorticity_drag_boundaries == "all":
625
+ script.emit("SET_VORTICITY_DRAG_BOUNDARIES", -1)
626
+ elif vorticity_drag_boundaries is not None:
627
+ script.emit(
628
+ "SET_VORTICITY_DRAG_BOUNDARIES",
629
+ len(vorticity_drag_boundaries),
630
+ list(vorticity_drag_boundaries),
631
+ )
632
+
633
+
634
+ def export_results(
635
+ script: Script,
636
+ *,
637
+ spreadsheet: str | None = None,
638
+ tecplot: str | None = None,
639
+ vtk: str | None = None,
640
+ vtk_boundaries: Sequence[int] | Literal["all"] = "all",
641
+ vtk_variables: Sequence[str] | Literal["all"] | None = None,
642
+ vtk_wake: bool = False,
643
+ force_distributions: str | None = None,
644
+ ) -> None:
645
+ """Export the solver results that were requested (SRC-003 pp.352-354).
646
+
647
+ Parameters
648
+ ----------
649
+ script : Script
650
+ Script under construction.
651
+ spreadsheet : str, optional
652
+ Path of the loads and moments spreadsheet, the primary
653
+ quantitative output of a steady run.
654
+ tecplot : str, optional
655
+ Path of the Tecplot .dat export.
656
+ vtk : str, optional
657
+ Path of the VTK export.
658
+ vtk_boundaries : sequence of int or ``"all"``
659
+ Boundaries included in the VTK export.
660
+ vtk_variables : sequence of str, ``"all"``, or None
661
+ Variables selected before the VTK export; None keeps the
662
+ current selection. ``CP`` is flagged for depreciation in favor
663
+ of ``CP_REFERENCE`` and ``CP_FREESTREAM`` (SRC-003 p.352); the
664
+ helper warns when it is requested.
665
+ vtk_wake : bool
666
+ Include the wake in the VTK variable selection.
667
+ force_distributions : str, optional
668
+ Path of the force distribution vectors export, all boundaries.
669
+ """
670
+ if spreadsheet is not None:
671
+ script.emit("EXPORT_SOLVER_ANALYSIS_SPREADSHEET", spreadsheet)
672
+ if tecplot is not None:
673
+ script.emit("EXPORT_SOLVER_ANALYSIS_TECPLOT", tecplot)
674
+ if vtk_variables == "all":
675
+ script.emit("SET_VTK_EXPORT_VARIABLES", -1, _toggle(vtk_wake))
676
+ elif vtk_variables is not None:
677
+ if any(variable.upper() == "CP" for variable in vtk_variables):
678
+ warnings.warn(
679
+ "the CP export variable is flagged for depreciation; prefer "
680
+ "CP_REFERENCE or CP_FREESTREAM (SRC-003 p.352)",
681
+ stacklevel=2,
682
+ )
683
+ script.emit(
684
+ "SET_VTK_EXPORT_VARIABLES", len(vtk_variables), _toggle(vtk_wake), list(vtk_variables)
685
+ )
686
+ if vtk is not None:
687
+ if vtk_boundaries == "all":
688
+ script.emit("EXPORT_SOLVER_ANALYSIS_VTK", vtk, -1)
689
+ else:
690
+ script.emit(
691
+ "EXPORT_SOLVER_ANALYSIS_VTK", vtk, len(vtk_boundaries), list(vtk_boundaries)
692
+ )
693
+ if force_distributions is not None:
694
+ script.emit("EXPORT_SOLVER_ANALYSIS_FORCE_DISTRIBUTIONS", force_distributions, -1)
695
+
696
+
697
+ def probe_points(
698
+ script: Script,
699
+ points: Sequence[tuple[float, float, float]],
700
+ *,
701
+ kind: str = "VOLUME",
702
+ ) -> None:
703
+ """Create individual probe points (SRC-003 p.362).
704
+
705
+ Parameters
706
+ ----------
707
+ script : Script
708
+ Script under construction.
709
+ points : sequence of (x, y, z) triples
710
+ Probe positions in the reference frame, simulation length
711
+ units.
712
+ kind : str
713
+ ``VOLUME`` or ``SURFACE`` probes.
714
+ """
715
+ for x, y, z in points:
716
+ script.emit("NEW_PROBE_POINT", kind, x, y, z)
717
+
718
+
719
+ def probe_line(
720
+ script: Script,
721
+ *,
722
+ points: int,
723
+ start: tuple[float, float, float],
724
+ end: tuple[float, float, float],
725
+ ) -> None:
726
+ """Create a survey line of probe points (SRC-003 p.362).
727
+
728
+ Parameters
729
+ ----------
730
+ script : Script
731
+ Script under construction.
732
+ points : int
733
+ Number of probe vertices between start and end.
734
+ start, end : (x, y, z) triples
735
+ Line ends in the reference frame, simulation length units.
736
+ """
737
+ script.emit("NEW_PROBE_LINE", points, *start, *end)
738
+
739
+
740
+ def probes_from_file(script: Script, path: str, *, units: str, frame: int = 1) -> None:
741
+ """Import a probe lattice from a CSV file (SRC-003 pp.362-363).
742
+
743
+ The file rows are X,Y,Z,TYPE with TYPE 0 for surface and 1 for
744
+ volume probes; the first line holds the point count. This is the
745
+ programmatic path for probe lattice generation.
746
+
747
+ Parameters
748
+ ----------
749
+ script : Script
750
+ Script under construction.
751
+ path : str
752
+ Probe lattice CSV path.
753
+ units : str
754
+ Length unit of the file coordinates (``METER``, ``INCH``, and
755
+ the other simulation length units).
756
+ frame : int
757
+ Coordinate system of the file coordinates; index 1 is the
758
+ reference frame.
759
+ """
760
+ script.emit("PROBE_POINTS_IMPORT", units, frame, path)
761
+
762
+
763
+ def export_probes(script: Script, path: str, *, update: bool = True) -> None:
764
+ """Export the probe values, refreshing them first (SRC-003 pp.362-363).
765
+
766
+ Parameters
767
+ ----------
768
+ script : Script
769
+ Script under construction.
770
+ path : str
771
+ Export file path.
772
+ update : bool
773
+ Emit UPDATE_PROBE_POINTS first, so the export reflects the
774
+ current solution; the manual instructs refreshing before
775
+ exporting (SRC-003 p.362).
776
+ """
777
+ if update:
778
+ script.emit("UPDATE_PROBE_POINTS")
779
+ script.emit("EXPORT_PROBE_POINTS", path)
780
+
781
+
782
+ def coordinate_frame(
783
+ script: Script,
784
+ *,
785
+ name: str,
786
+ origin: Sequence[float],
787
+ x_axis: Sequence[float],
788
+ y_axis: Sequence[float],
789
+ z_axis: Sequence[float] | None = None,
790
+ ) -> int:
791
+ """Create and define a local coordinate system, returning its index.
792
+
793
+ Emits CREATE_NEW_COORDINATE_SYSTEM followed by
794
+ EDIT_COORDINATE_SYSTEM with the origin and the three axis vectors
795
+ in the reference frame (coordinate_systems chapter). Use it when
796
+ the solver should carry the same plane a probe grid was
797
+ prescribed on; probe positions themselves are always imported in
798
+ the reference frame (frame 1), so this helper is presentation,
799
+ not placement.
800
+
801
+ Parameters
802
+ ----------
803
+ script : Script
804
+ Script under construction.
805
+ name : str
806
+ Name of the new coordinate system.
807
+ origin : sequence of float
808
+ Frame origin in the reference frame (simulation length units).
809
+ x_axis, y_axis : sequence of float
810
+ Axis direction vectors in the reference frame.
811
+ z_axis : sequence of float, optional
812
+ Third axis; computed as the right-handed cross product of
813
+ x_axis and y_axis when omitted.
814
+
815
+ Returns
816
+ -------
817
+ int
818
+ Index of the created frame (the reference frame is 1; created
819
+ local frames follow).
820
+ """
821
+ if z_axis is None:
822
+ ax, ay, az = x_axis
823
+ bx, by, bz = y_axis
824
+ z_axis = (ay * bz - az * by, az * bx - ax * bz, ax * by - ay * bx)
825
+ script.emit("CREATE_NEW_COORDINATE_SYSTEM")
826
+ frame_index = script.num_local_frames + 1
827
+ script.emit(
828
+ "EDIT_COORDINATE_SYSTEM",
829
+ frame=frame_index,
830
+ name=name,
831
+ origin_x=origin[0],
832
+ origin_y=origin[1],
833
+ origin_z=origin[2],
834
+ vector_x_x=x_axis[0],
835
+ vector_x_y=x_axis[1],
836
+ vector_x_z=x_axis[2],
837
+ vector_y_x=y_axis[0],
838
+ vector_y_y=y_axis[1],
839
+ vector_y_z=y_axis[2],
840
+ vector_z_x=z_axis[0],
841
+ vector_z_y=z_axis[1],
842
+ vector_z_z=z_axis[2],
843
+ )
844
+ return frame_index