pyflightstream 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyflightstream/__init__.py +27 -0
- pyflightstream/cases/__init__.py +332 -0
- pyflightstream/cases/matrix_legacy.py +337 -0
- pyflightstream/commands/__init__.py +544 -0
- pyflightstream/commands/_meta.yaml +18 -0
- pyflightstream/commands/actuators.yaml +167 -0
- pyflightstream/commands/advanced_settings.yaml +136 -0
- pyflightstream/commands/aeroelastic_coupling.yaml +172 -0
- pyflightstream/commands/boundary_conditions.yaml +153 -0
- pyflightstream/commands/ccs_wing_mesh.yaml +74 -0
- pyflightstream/commands/coordinate_systems.yaml +123 -0
- pyflightstream/commands/file_io.yaml +82 -0
- pyflightstream/commands/mesh_import_export.yaml +72 -0
- pyflightstream/commands/motion_definitions.yaml +185 -0
- pyflightstream/commands/probe_points.yaml +116 -0
- pyflightstream/commands/runtime_settings.yaml +160 -0
- pyflightstream/commands/scenes.yaml +14 -0
- pyflightstream/commands/script_controls.yaml +39 -0
- pyflightstream/commands/simulation_controls.yaml +30 -0
- pyflightstream/commands/solver_analysis.yaml +118 -0
- pyflightstream/commands/solver_export.yaml +138 -0
- pyflightstream/commands/solver_initialization.yaml +95 -0
- pyflightstream/commands/solver_settings.yaml +120 -0
- pyflightstream/commands/streamlines.yaml +63 -0
- pyflightstream/commands/surface_sections.yaml +145 -0
- pyflightstream/commands/sweeper.yaml +115 -0
- pyflightstream/commands/unsteady_solver.yaml +68 -0
- pyflightstream/commands/volume_sections.yaml +148 -0
- pyflightstream/farfield/__init__.py +613 -0
- pyflightstream/files/__init__.py +375 -0
- pyflightstream/fsi/__init__.py +57 -0
- pyflightstream/fsi/beam.py +417 -0
- pyflightstream/fsi/centrifugal.py +418 -0
- pyflightstream/fsi/cli.py +206 -0
- pyflightstream/fsi/config.py +316 -0
- pyflightstream/fsi/driver.py +501 -0
- pyflightstream/fsi/kinematics.py +153 -0
- pyflightstream/fsi/loads.py +740 -0
- pyflightstream/fsi/nodes.py +463 -0
- pyflightstream/fsi/state.py +181 -0
- pyflightstream/post/__init__.py +18 -0
- pyflightstream/post/writers.py +195 -0
- pyflightstream/probes/__init__.py +453 -0
- pyflightstream/probes/geometry.py +281 -0
- pyflightstream/probes/planar.py +477 -0
- pyflightstream/qa/__init__.py +59 -0
- pyflightstream/qa/cli.py +364 -0
- pyflightstream/qa/compat.py +285 -0
- pyflightstream/qa/drift.py +406 -0
- pyflightstream/qa/geometry.py +421 -0
- pyflightstream/qa/physics.py +1744 -0
- pyflightstream/qa/probes.py +1023 -0
- pyflightstream/qa/references/PHY-01.yaml +38 -0
- pyflightstream/qa/references/PHY-02.yaml +28 -0
- pyflightstream/qa/references/PHY-05.yaml +29 -0
- pyflightstream/qa/references/PHY-06.yaml +90 -0
- pyflightstream/qa/references/SMI-01.yaml +28 -0
- pyflightstream/qa/references/SMI-02.yaml +28 -0
- pyflightstream/qa/specs.py +1405 -0
- pyflightstream/reference.py +465 -0
- pyflightstream/results/__init__.py +547 -0
- pyflightstream/run/__init__.py +677 -0
- pyflightstream/script/__init__.py +474 -0
- pyflightstream/script/helpers.py +844 -0
- pyflightstream/versions.py +191 -0
- pyflightstream-0.2.0.dist-info/METADATA +88 -0
- pyflightstream-0.2.0.dist-info/RECORD +71 -0
- pyflightstream-0.2.0.dist-info/WHEEL +5 -0
- pyflightstream-0.2.0.dist-info/entry_points.txt +3 -0
- pyflightstream-0.2.0.dist-info/licenses/LICENSE +21 -0
- pyflightstream-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
"""The validating FlightStream script builder.
|
|
2
|
+
|
|
3
|
+
Pipeline role: turns typed Python calls into the ASCII script text the
|
|
4
|
+
solver executes, validating every emission against the per-version
|
|
5
|
+
command database before a single line reaches FlightStream. Errors
|
|
6
|
+
happen at build time with manual citations, because solver-side
|
|
7
|
+
failures are silent or cryptic.
|
|
8
|
+
|
|
9
|
+
A :class:`Script` is an ordinary object bound to one FlightStream
|
|
10
|
+
version; two scripts coexist safely, and there is no module-level
|
|
11
|
+
state. Emission is checked in order: command exists in the version,
|
|
12
|
+
argument binding and types, enum membership, count-versus-list
|
|
13
|
+
consistency, phase ordering, and cross references. The ``raw()``
|
|
14
|
+
escape hatch bypasses validation and flags the script for the run
|
|
15
|
+
manifest.
|
|
16
|
+
|
|
17
|
+
Cross references (SAD Section 4.2): the script counts the local
|
|
18
|
+
coordinate systems, actuators, and motions it creates, and rejects a
|
|
19
|
+
command citing an index that does not exist yet, because FlightStream
|
|
20
|
+
expects auxiliary definitions before they are referenced and fails
|
|
21
|
+
silently otherwise. Frames carried by an opened project file are
|
|
22
|
+
declared with :meth:`Script.declare_existing`.
|
|
23
|
+
|
|
24
|
+
The two gaps of the first cut are closed: the per-surface lines of
|
|
25
|
+
INITIALIZE_SOLVER when SURFACES is not -1 (``surface_toggles``) and
|
|
26
|
+
the PERIODIC symmetry copy count (``symmetry_copies``) are regular
|
|
27
|
+
database arguments now, emitted comfortably through the curated
|
|
28
|
+
helper layer in :mod:`pyflightstream.script.helpers` (SAD Section
|
|
29
|
+
4.3).
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import os
|
|
35
|
+
from collections.abc import Sequence
|
|
36
|
+
|
|
37
|
+
from pyflightstream.commands import (
|
|
38
|
+
ArgSpec,
|
|
39
|
+
ArgType,
|
|
40
|
+
CommandEntry,
|
|
41
|
+
CommandRegistry,
|
|
42
|
+
Layout,
|
|
43
|
+
ListSeparator,
|
|
44
|
+
Phase,
|
|
45
|
+
)
|
|
46
|
+
from pyflightstream.versions import FsVersion
|
|
47
|
+
|
|
48
|
+
_ORDERED_PHASES = (
|
|
49
|
+
Phase.GEOMETRY,
|
|
50
|
+
Phase.SETUP,
|
|
51
|
+
Phase.INIT,
|
|
52
|
+
Phase.EXEC,
|
|
53
|
+
Phase.ANALYSIS,
|
|
54
|
+
Phase.EXPORT,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
_COUNT_ARG_NAMES = {
|
|
58
|
+
"count",
|
|
59
|
+
"surfaces",
|
|
60
|
+
"numpts",
|
|
61
|
+
"num_boundaries",
|
|
62
|
+
"num_variables",
|
|
63
|
+
"num_frames",
|
|
64
|
+
"num_sections",
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# Cross-reference ledger (SAD Section 4.2): commands that create an
|
|
68
|
+
# indexed auxiliary object, commands that delete one, and the argument
|
|
69
|
+
# names that cite one. Frame index 1 is the reference frame and always
|
|
70
|
+
# exists (SRC-003 p.329); created local frames take indices 2 upward.
|
|
71
|
+
_CREATION_COMMANDS = {
|
|
72
|
+
"CREATE_NEW_COORDINATE_SYSTEM": "frames",
|
|
73
|
+
"CREATE_NEW_ACTUATOR": "actuators",
|
|
74
|
+
"CREATE_NEW_MOTION": "motions",
|
|
75
|
+
}
|
|
76
|
+
_DELETION_COMMANDS = {
|
|
77
|
+
"DELETE_ACTUATOR": "actuators",
|
|
78
|
+
"DELETE_MOTION": "motions",
|
|
79
|
+
}
|
|
80
|
+
_SCALAR_REFERENCE_ARGS = {
|
|
81
|
+
"frame": "frames",
|
|
82
|
+
"load_frame": "frames",
|
|
83
|
+
"coordinate_system_id": "frames",
|
|
84
|
+
"actuator_index": "actuators",
|
|
85
|
+
"motion_id": "motions",
|
|
86
|
+
}
|
|
87
|
+
_LIST_REFERENCE_ARGS = {
|
|
88
|
+
"frame_indices": "frames",
|
|
89
|
+
}
|
|
90
|
+
_REFERENCE_NOUNS = {
|
|
91
|
+
"frames": "local coordinate system",
|
|
92
|
+
"actuators": "actuator",
|
|
93
|
+
"motions": "motion",
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class ScriptOrderError(ValueError):
|
|
98
|
+
"""A command was emitted after its phase had already passed.
|
|
99
|
+
|
|
100
|
+
The script builder tracks the highest phase reached (geometry,
|
|
101
|
+
setup, init, exec, analysis, export); FlightStream expects
|
|
102
|
+
auxiliary definitions such as coordinate systems, actuators, and
|
|
103
|
+
motions before solver initialization. Control commands are exempt.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class CommandArgumentError(ValueError):
|
|
108
|
+
"""An emitted argument does not satisfy the database specification.
|
|
109
|
+
|
|
110
|
+
The message names the command, the argument, the expectation, and
|
|
111
|
+
the manual citation of the entry, so the fix can be checked against
|
|
112
|
+
the manual directly.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class ScriptReferenceError(ValueError):
|
|
117
|
+
"""A command cites a frame, actuator, or motion not created yet.
|
|
118
|
+
|
|
119
|
+
FlightStream resolves these indices at execution time and fails
|
|
120
|
+
silently or cryptically when they do not exist; the builder counts
|
|
121
|
+
the objects the script creates and rejects the citation at build
|
|
122
|
+
time instead (SAD Section 4.2). Objects already present in the
|
|
123
|
+
opened project file are declared with
|
|
124
|
+
:meth:`Script.declare_existing`.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _type_error(entry: CommandEntry, spec: ArgSpec, expected: str, value: object) -> None:
|
|
129
|
+
raise CommandArgumentError(
|
|
130
|
+
f"{entry.name}: argument {spec.name!r} expects {expected}, got {value!r} "
|
|
131
|
+
f"({entry.manual_ref})"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _match_enum(entry: CommandEntry, spec: ArgSpec, value: object) -> str:
|
|
136
|
+
if isinstance(value, str):
|
|
137
|
+
for member in spec.values:
|
|
138
|
+
if member.upper() == value.upper():
|
|
139
|
+
return member
|
|
140
|
+
_type_error(entry, spec, f"one of {', '.join(spec.values)}", value)
|
|
141
|
+
raise AssertionError("unreachable")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _check_scalar(entry: CommandEntry, spec: ArgSpec, value: object) -> object:
|
|
145
|
+
if spec.type is ArgType.INT:
|
|
146
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
147
|
+
_type_error(entry, spec, "an integer", value)
|
|
148
|
+
return value
|
|
149
|
+
if spec.type is ArgType.FLOAT:
|
|
150
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
151
|
+
_type_error(entry, spec, "a real number", value)
|
|
152
|
+
return value
|
|
153
|
+
if spec.type is ArgType.PATH:
|
|
154
|
+
if not isinstance(value, (str, os.PathLike)):
|
|
155
|
+
_type_error(entry, spec, "a path", value)
|
|
156
|
+
return str(value)
|
|
157
|
+
if spec.type is ArgType.STR:
|
|
158
|
+
if not isinstance(value, str):
|
|
159
|
+
_type_error(entry, spec, "a string", value)
|
|
160
|
+
return value
|
|
161
|
+
if spec.type is ArgType.ENUM:
|
|
162
|
+
return _match_enum(entry, spec, value)
|
|
163
|
+
if spec.type is ArgType.BOOL:
|
|
164
|
+
if not isinstance(value, bool):
|
|
165
|
+
_type_error(entry, spec, "True or False", value)
|
|
166
|
+
return value
|
|
167
|
+
raise AssertionError(f"unhandled scalar type {spec.type}")
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _check_list(entry: CommandEntry, spec: ArgSpec, value: object) -> list:
|
|
171
|
+
if isinstance(value, str) or not isinstance(value, Sequence):
|
|
172
|
+
_type_error(entry, spec, "a sequence of values", value)
|
|
173
|
+
items = list(value)
|
|
174
|
+
if spec.type is ArgType.INT_LIST:
|
|
175
|
+
for item in items:
|
|
176
|
+
if isinstance(item, bool) or not isinstance(item, int):
|
|
177
|
+
_type_error(entry, spec, "a sequence of integers", value)
|
|
178
|
+
return items
|
|
179
|
+
if spec.type is ArgType.FLOAT_LIST:
|
|
180
|
+
for item in items:
|
|
181
|
+
if isinstance(item, bool) or not isinstance(item, (int, float)):
|
|
182
|
+
_type_error(entry, spec, "a sequence of real numbers", value)
|
|
183
|
+
return items
|
|
184
|
+
if spec.type is ArgType.STR_LIST:
|
|
185
|
+
for item in items:
|
|
186
|
+
if not isinstance(item, str):
|
|
187
|
+
_type_error(entry, spec, "a sequence of strings", value)
|
|
188
|
+
return items
|
|
189
|
+
return [_match_enum(entry, spec, item) for item in items]
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class Script:
|
|
193
|
+
"""One FlightStream script under construction, bound to one version.
|
|
194
|
+
|
|
195
|
+
Parameters
|
|
196
|
+
----------
|
|
197
|
+
version : str or FsVersion
|
|
198
|
+
Target FlightStream version, canonical or alias; every
|
|
199
|
+
emission is validated against this version's command view.
|
|
200
|
+
registry : CommandRegistry, optional
|
|
201
|
+
Alternative database, used by tests; defaults to the packaged
|
|
202
|
+
one.
|
|
203
|
+
|
|
204
|
+
Attributes
|
|
205
|
+
----------
|
|
206
|
+
version : FsVersion
|
|
207
|
+
The resolved target version.
|
|
208
|
+
raw_flag : bool
|
|
209
|
+
True once ``raw()`` was used; recorded in the run manifest so
|
|
210
|
+
unvalidated scripts stay identifiable (FR-07).
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
def __init__(self, version: str | FsVersion, registry: CommandRegistry | None = None):
|
|
214
|
+
view = (registry or CommandRegistry.load()).for_version(version)
|
|
215
|
+
self._view = view
|
|
216
|
+
self.version: FsVersion = view.version
|
|
217
|
+
self.raw_flag = False
|
|
218
|
+
self._lines: list[str] = []
|
|
219
|
+
self._phase_index: int | None = None
|
|
220
|
+
self._phase_setter: tuple[str, int] | None = None
|
|
221
|
+
self._created = {"frames": 0, "actuators": 0, "motions": 0}
|
|
222
|
+
|
|
223
|
+
@property
|
|
224
|
+
def num_local_frames(self) -> int:
|
|
225
|
+
"""Local coordinate systems the script created or declared."""
|
|
226
|
+
return self._created["frames"]
|
|
227
|
+
|
|
228
|
+
@property
|
|
229
|
+
def num_actuators(self) -> int:
|
|
230
|
+
"""Actuators the script created or declared."""
|
|
231
|
+
return self._created["actuators"]
|
|
232
|
+
|
|
233
|
+
@property
|
|
234
|
+
def num_motions(self) -> int:
|
|
235
|
+
"""Motions the script created or declared."""
|
|
236
|
+
return self._created["motions"]
|
|
237
|
+
|
|
238
|
+
def declare_existing(self, *, frames: int = 0, actuators: int = 0, motions: int = 0) -> None:
|
|
239
|
+
"""Declare auxiliary objects already present in the opened project.
|
|
240
|
+
|
|
241
|
+
A simulation file loaded with OPEN can carry local coordinate
|
|
242
|
+
systems, actuators, and motions saved earlier; the builder
|
|
243
|
+
cannot see inside the file, so scripts citing those objects
|
|
244
|
+
declare them here to satisfy the cross-reference check.
|
|
245
|
+
|
|
246
|
+
Parameters
|
|
247
|
+
----------
|
|
248
|
+
frames : int
|
|
249
|
+
Local coordinate systems in the project, beyond the
|
|
250
|
+
reference frame (index 1), which always exists.
|
|
251
|
+
actuators : int
|
|
252
|
+
Actuators in the project.
|
|
253
|
+
motions : int
|
|
254
|
+
Motions in the project.
|
|
255
|
+
"""
|
|
256
|
+
for kind, extra in (("frames", frames), ("actuators", actuators), ("motions", motions)):
|
|
257
|
+
if extra < 0:
|
|
258
|
+
raise ValueError(f"declared {kind} must be zero or positive, got {extra}")
|
|
259
|
+
self._created[kind] += extra
|
|
260
|
+
|
|
261
|
+
def emit(self, name: str, /, *args: object, **kwargs: object) -> None:
|
|
262
|
+
"""Validate and append one command.
|
|
263
|
+
|
|
264
|
+
Parameters
|
|
265
|
+
----------
|
|
266
|
+
name : str
|
|
267
|
+
Command name as in the database; positional-only, so a
|
|
268
|
+
command argument may itself be called ``name`` (for
|
|
269
|
+
example CREATE_NEW_ACTUATOR).
|
|
270
|
+
*args, **kwargs
|
|
271
|
+
Argument values, positional in database order or by
|
|
272
|
+
argument name. Optional arguments may be omitted.
|
|
273
|
+
|
|
274
|
+
Raises
|
|
275
|
+
------
|
|
276
|
+
CommandNotInVersionError
|
|
277
|
+
If the command does not exist in this version; the message
|
|
278
|
+
carries the manual citation and successor when known.
|
|
279
|
+
CommandArgumentError
|
|
280
|
+
If an argument violates the typed specification.
|
|
281
|
+
ScriptOrderError
|
|
282
|
+
If the command's phase precedes the phase already reached.
|
|
283
|
+
ScriptReferenceError
|
|
284
|
+
If the command cites a frame, actuator, or motion index the
|
|
285
|
+
script has not created or declared yet.
|
|
286
|
+
"""
|
|
287
|
+
entry = self._view[name]
|
|
288
|
+
bound = self._bind(entry, args, kwargs)
|
|
289
|
+
self._check_phase(entry)
|
|
290
|
+
self._check_references(entry, bound)
|
|
291
|
+
block, multiline = self._render_command(entry, bound)
|
|
292
|
+
self._lines.extend(block)
|
|
293
|
+
if multiline:
|
|
294
|
+
self._lines.append("")
|
|
295
|
+
if entry.name in _CREATION_COMMANDS:
|
|
296
|
+
self._created[_CREATION_COMMANDS[entry.name]] += 1
|
|
297
|
+
elif entry.name in _DELETION_COMMANDS:
|
|
298
|
+
self._created[_DELETION_COMMANDS[entry.name]] -= 1
|
|
299
|
+
|
|
300
|
+
def raw(self, text: str) -> None:
|
|
301
|
+
"""Append unvalidated script text and flag the script (FR-07)."""
|
|
302
|
+
self.raw_flag = True
|
|
303
|
+
self._lines.extend(text.splitlines())
|
|
304
|
+
|
|
305
|
+
def comment(self, text: str) -> None:
|
|
306
|
+
"""Append a comment line; FlightStream ignores lines starting with #."""
|
|
307
|
+
self._lines.append(f"# {text}")
|
|
308
|
+
|
|
309
|
+
def render(self) -> str:
|
|
310
|
+
"""Return the complete script text, newline terminated."""
|
|
311
|
+
return "\n".join(self._lines) + "\n"
|
|
312
|
+
|
|
313
|
+
def _bind(self, entry: CommandEntry, args: tuple, kwargs: dict) -> dict[str, object]:
|
|
314
|
+
specs = entry.args
|
|
315
|
+
if len(args) > len(specs):
|
|
316
|
+
raise CommandArgumentError(
|
|
317
|
+
f"{entry.name} takes at most {len(specs)} arguments, got {len(args)} "
|
|
318
|
+
f"({entry.manual_ref})"
|
|
319
|
+
)
|
|
320
|
+
bound: dict[str, object] = {}
|
|
321
|
+
for spec, value in zip(specs, args, strict=False):
|
|
322
|
+
bound[spec.name] = value
|
|
323
|
+
known = {spec.name for spec in specs}
|
|
324
|
+
for key, value in kwargs.items():
|
|
325
|
+
if key not in known:
|
|
326
|
+
raise CommandArgumentError(
|
|
327
|
+
f"{entry.name} has no argument {key!r}; arguments are "
|
|
328
|
+
f"{', '.join(sorted(known)) or 'none'} ({entry.manual_ref})"
|
|
329
|
+
)
|
|
330
|
+
if key in bound:
|
|
331
|
+
raise CommandArgumentError(
|
|
332
|
+
f"{entry.name}: argument {key!r} given twice ({entry.manual_ref})"
|
|
333
|
+
)
|
|
334
|
+
bound[key] = value
|
|
335
|
+
checked: dict[str, object] = {}
|
|
336
|
+
for spec in specs:
|
|
337
|
+
if spec.name not in bound:
|
|
338
|
+
if spec.required:
|
|
339
|
+
raise CommandArgumentError(
|
|
340
|
+
f"{entry.name} requires argument {spec.name!r} ({entry.manual_ref})"
|
|
341
|
+
)
|
|
342
|
+
continue
|
|
343
|
+
value = bound[spec.name]
|
|
344
|
+
if spec.is_list:
|
|
345
|
+
checked[spec.name] = _check_list(entry, spec, value)
|
|
346
|
+
else:
|
|
347
|
+
checked[spec.name] = _check_scalar(entry, spec, value)
|
|
348
|
+
self._check_counts(entry, checked)
|
|
349
|
+
return checked
|
|
350
|
+
|
|
351
|
+
def _check_counts(self, entry: CommandEntry, bound: dict[str, object]) -> None:
|
|
352
|
+
count_value: object | None = None
|
|
353
|
+
for spec in entry.args:
|
|
354
|
+
if spec.name in _COUNT_ARG_NAMES and spec.name in bound:
|
|
355
|
+
count_value = bound[spec.name]
|
|
356
|
+
elif spec.is_list and spec.name in bound and isinstance(count_value, int):
|
|
357
|
+
if count_value >= 0 and count_value != len(bound[spec.name]):
|
|
358
|
+
raise CommandArgumentError(
|
|
359
|
+
f"{entry.name}: the declared count is {count_value} but "
|
|
360
|
+
f"{spec.name!r} holds {len(bound[spec.name])} values "
|
|
361
|
+
f"({entry.manual_ref})"
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
def _check_phase(self, entry: CommandEntry) -> None:
|
|
365
|
+
if entry.phase is Phase.CONTROL:
|
|
366
|
+
return
|
|
367
|
+
index = _ORDERED_PHASES.index(entry.phase)
|
|
368
|
+
if self._phase_index is not None and index < self._phase_index:
|
|
369
|
+
setter_name, setter_line = self._phase_setter
|
|
370
|
+
current = _ORDERED_PHASES[self._phase_index]
|
|
371
|
+
raise ScriptOrderError(
|
|
372
|
+
f"{entry.name} is a {entry.phase} command, but the script already "
|
|
373
|
+
f"reached the {current} phase ({setter_name} at line {setter_line}). "
|
|
374
|
+
"Auxiliary definitions such as coordinate systems, actuators, and "
|
|
375
|
+
"motions must precede solver initialization; the phase order is "
|
|
376
|
+
"geometry, setup, init, exec, analysis, export."
|
|
377
|
+
)
|
|
378
|
+
if self._phase_index is None or index > self._phase_index:
|
|
379
|
+
self._phase_index = index
|
|
380
|
+
self._phase_setter = (entry.name, len(self._lines) + 1)
|
|
381
|
+
|
|
382
|
+
def _check_references(self, entry: CommandEntry, bound: dict[str, object]) -> None:
|
|
383
|
+
for spec in entry.args:
|
|
384
|
+
if spec.name not in bound:
|
|
385
|
+
continue
|
|
386
|
+
kind = _SCALAR_REFERENCE_ARGS.get(spec.name)
|
|
387
|
+
if kind is not None:
|
|
388
|
+
self._check_one_reference(entry, spec.name, kind, bound[spec.name])
|
|
389
|
+
kind = _LIST_REFERENCE_ARGS.get(spec.name)
|
|
390
|
+
if kind is not None:
|
|
391
|
+
for value in bound[spec.name]:
|
|
392
|
+
if value != -1:
|
|
393
|
+
self._check_one_reference(entry, spec.name, kind, value)
|
|
394
|
+
|
|
395
|
+
def _check_one_reference(self, entry: CommandEntry, arg: str, kind: str, value: object) -> None:
|
|
396
|
+
limit = self._created[kind] + (1 if kind == "frames" else 0)
|
|
397
|
+
if isinstance(value, int) and 1 <= value <= limit:
|
|
398
|
+
return
|
|
399
|
+
noun = _REFERENCE_NOUNS[kind]
|
|
400
|
+
if kind == "frames":
|
|
401
|
+
available = (
|
|
402
|
+
f"the reference frame is index 1 and the script has created or declared "
|
|
403
|
+
f"{self._created[kind]} local frame(s), so valid indices run 1 to {limit}"
|
|
404
|
+
)
|
|
405
|
+
else:
|
|
406
|
+
available = (
|
|
407
|
+
f"the script has created or declared {self._created[kind]} {noun}(s), "
|
|
408
|
+
f"so valid indices run 1 to {limit}"
|
|
409
|
+
)
|
|
410
|
+
raise ScriptReferenceError(
|
|
411
|
+
f"{entry.name}: argument {arg!r} cites {noun} {value!r}, but {available}. "
|
|
412
|
+
"FlightStream expects auxiliary definitions before they are referenced; "
|
|
413
|
+
"create the object earlier in the script, or declare objects carried by "
|
|
414
|
+
f"the opened project with declare_existing(). ({entry.manual_ref})"
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
def _format_scalar(self, value: object) -> str:
|
|
418
|
+
return str(value)
|
|
419
|
+
|
|
420
|
+
def _list_lines(self, spec: ArgSpec, items: list) -> list[str]:
|
|
421
|
+
rendered = [self._format_scalar(item) for item in items]
|
|
422
|
+
if spec.separator is ListSeparator.NEWLINE:
|
|
423
|
+
return rendered
|
|
424
|
+
joiner = "," if spec.separator is ListSeparator.COMMA else " "
|
|
425
|
+
return [joiner.join(rendered)]
|
|
426
|
+
|
|
427
|
+
def _render_command(self, entry: CommandEntry, bound: dict) -> tuple[list[str], bool]:
|
|
428
|
+
provided = [(spec, bound[spec.name]) for spec in entry.args if spec.name in bound]
|
|
429
|
+
if entry.layout is Layout.BARE:
|
|
430
|
+
return [entry.name], False
|
|
431
|
+
if entry.layout is Layout.INLINE:
|
|
432
|
+
inline_parts = [entry.name]
|
|
433
|
+
tail_lines: list[str] = []
|
|
434
|
+
for spec, value in provided:
|
|
435
|
+
if spec.own_line:
|
|
436
|
+
tail_lines.append(self._format_scalar(value))
|
|
437
|
+
elif spec.is_list:
|
|
438
|
+
inline_parts.append(" ".join(self._format_scalar(item) for item in value))
|
|
439
|
+
else:
|
|
440
|
+
inline_parts.append(self._format_scalar(value))
|
|
441
|
+
return [" ".join(inline_parts), *tail_lines], bool(tail_lines)
|
|
442
|
+
if entry.layout is Layout.PAYLOAD_LINES:
|
|
443
|
+
inline_parts = [entry.name]
|
|
444
|
+
tail_lines = []
|
|
445
|
+
for spec, value in provided:
|
|
446
|
+
if spec.is_list:
|
|
447
|
+
tail_lines.extend(self._list_lines(spec, value))
|
|
448
|
+
else:
|
|
449
|
+
inline_parts.append(self._format_scalar(value))
|
|
450
|
+
return [" ".join(inline_parts), *tail_lines], True
|
|
451
|
+
if entry.layout is Layout.PARAM_LINES:
|
|
452
|
+
lines = [entry.name]
|
|
453
|
+
for spec, value in provided:
|
|
454
|
+
if spec.is_list:
|
|
455
|
+
lines.extend(self._list_lines(spec, value))
|
|
456
|
+
elif spec.type is ArgType.PATH:
|
|
457
|
+
lines.append(self._format_scalar(value))
|
|
458
|
+
else:
|
|
459
|
+
lines.append(f"{spec.name.upper()} {self._format_scalar(value)}")
|
|
460
|
+
return lines, True
|
|
461
|
+
lines = [entry.name]
|
|
462
|
+
for spec, value in provided:
|
|
463
|
+
if spec.is_list:
|
|
464
|
+
lines.extend(self._list_lines(spec, value))
|
|
465
|
+
elif spec.type is ArgType.BOOL:
|
|
466
|
+
# Presence keyword (SRC-003 p.307): True is the bare keyword
|
|
467
|
+
# line, False emits nothing.
|
|
468
|
+
if value:
|
|
469
|
+
lines.append(spec.name.upper())
|
|
470
|
+
elif spec.joins_previous:
|
|
471
|
+
lines[-1] += f" {self._format_scalar(value)}"
|
|
472
|
+
else:
|
|
473
|
+
lines.append(f"{spec.name.upper()} {self._format_scalar(value)}")
|
|
474
|
+
return lines, True
|