Skip to content

House conventions

Naming conventions

Units ride the names

Every physical quantity carries its SI unit as a name suffix: area_m2, chord_m, pitch_deg, timeout_s, mass_per_length_kg_per_m; sectional line densities end in _per_m. A number without a unit suffix is dimensionless by declaration (mach, ratio, fraction, relaxation), never an unstated unit.

Reference frames are explicit

Positions and axes name their frame: artifact points live in the simulation geometry frame, planar probe grids prescribe an explicit origin-plus-axes FrameDefinition, and the cylindrical probe lattice pins the z-up convention by test. No quantity changes frame silently.

Two name registers, never mixed

FlightStream native commands keep the manual's exact UPPER_SNAKE spelling (SET_AOA, START_SOLVER) and are emitted, cited, and stored verbatim; everything the library itself owns (functions, parameters, artifact kinds) is lowercase snake_case English.

Versions use the canonical scheme

FlightStream versions are canonical YY.XXX identifiers with exactly three fractional digits (26.120), the vendor display name is recorded as an alias (26.12), and ordering comes only from the registered list position, never from parsing the identifier. An alias resolves only where it names exactly one build: the vendor reuses a release name across builds, so both 26.12 and 26.1 name more than one, each refused with every candidate and its vendor build number named rather than resolving to one. The members are not written out here, and that is deliberate: a hand-written list of them went stale on the two registrations that followed it, so the refusal itself is the list. Reuse is not descent either, and which family is which matters: 26.12 is a release with its hotfixes, while 26.1 is two separate releases that happened to share a name. The registry states descent per build rather than leaving it to be read off the identifier.

Indices state their base

Boundary, frame, and other solver entity indices are 1-based, following the FlightStream convention, and every entity-citing argument also accepts a declared label; Python-side sequences stay 0-based. Docstrings state the base wherever an index crosses the boundary between the two worlds.

Ids are stems, not paths

Workspace input artifacts are selected by id, and an id is the file name stem inside the library (letters, digits, dot, underscore, hyphen); it is never a path, and naming templates are output-only (the manifest stays the identity authority). Since 0.8.0 a reference, setup or group id also DECLARES ITS KIND with a leading letter (r, s, e), so a number mistyped between two columns of a run matrix is refused instead of resolving to another artifact's file; geometries and profiles still resolve by bare stem. A library written before that is migrated by migrate_input_ids.

Rotor signs are stated by the row, not derived

Two different signs meet at a rotor and the package keeps them apart. The AZIMUTH INCREMENT, which way round the disc the blades are numbered, is DERIVED, by script.helpers.ROTATION_SENSE_SIGN, from the sense a caller passes to blade_frames, where counterclockwise about the rotor axis is taken as the positive sense. The ROTOR SPEED sign is MEASURED and stated by the matrix row that opens the mesh (RPM_SIGN beside ADVANCE_RATIO, or the sign inside RPM), since 0.11.0 (PFS-2029.08): until then a propeller reference carried rpm_sign_installed and rpm_sign_isolated, which no emitter read and which named a configuration the reference cannot know, the mesh a ROW opens. Getting the sign from a published sense needs the rotor axis, the side of the aircraft and the handedness of the mesh actually loaded, so it is stated rather than derived. Of the recorded rotor block the package reads position (the hub frame each rotor of the reference carries, _SMRP) and the diameter behind the advance ratio; the rest is recorded. Three things are open rather than settled here and are recorded in RPT-036: where azimuth zero sits, which viewed-from-behind sense a datasheet's inboard vocabulary means, and whether a descriptor's counterclockwise, which is stated as seen from behind, is the positive sense about the rotor axis as passed. That last one is the report's own weakest link and it is what the taken-as above stands on.

Refusals teach

Error messages name the physical or version cause and the remedy, main refusal wordings are pinned by test, and every exception class is importable from pyflightstream.exceptions; structured refusals carry their facts as attributes. Every CATALOGUED exception descends from PyflightstreamError, so one except clause catches the catalog, and each also keeps the standard-library base it would have had, so catching ValueError or RuntimeError still works. Read that word: a residual of bare standard-library raises survives outside the catalog. Every site the guard's walk REACHES is named in the ratchet in tests/tier1_offline/test_exceptions_catalog.py, which is the single home of that list; the walk's own reach is stated in SRS FR-39, and at least one site sits outside it. Until the residual is empty the standard-library bases are what covers it, and the plural matters: it is mostly ValueError and also holds TypeError and RuntimeError sites, so being exhaustive today means catching PyflightstreamError together with ValueError, TypeError and RuntimeError.

Options are declared knobs

Machine and QA tuning goes through the exact-key options registry (pyflightstream.options); anything that changes a physical result belongs in the case definition or workspace, recorded by the manifest, never in an option.

Behavior selectors are keyword-only

Arguments that select behavior (active_only, resume, open_browser) are keyword-only, so call sites read as prose and new parameters never break positional calls.

Toggles read both vocabularies

A solver flag is a Python bool, and the solver's own ENABLE and DISABLE are read as True and False by every helper argument and every settings field that switches one, because a setup carried over from the solver speaks that vocabulary. Each helper reads its toggles before it emits anything, and refuses a value in neither vocabulary naming the helper and the argument. Truthiness is never consulted, since a non-empty string is truthy and 'DISABLE' would otherwise silently emit ENABLE.

Diagnostics are nouns, validators say check_

A function that MEASURES a property of a result is named for the quantity it returns, as a noun: sample_coverage beside ring_sample_weights, symmetry_floor, spurious_diagnostic, mass_closure. A function that REFUSES an invalid combination carries the check_ prefix and returns nothing: check_recipe, check_state_matches_config. The two are different jobs and the name says which, so a reader knows before opening it whether a call can raise.

A validator takes the values it compares where the object would reverse the dependency direction

Where taking a configuration object would make a lower module import a higher one, a check_ function takes the individual values instead: check_state_matches_config takes two integer counts, so fsi.state keeps its import surface to pydantic and the module owning the persisted state needs nothing else. Such decomposed values are KEYWORD-ONLY, because same-typed scalars transpose silently in a positional call, which is the shape of the defect the check exists to catch. Convenience at one call site is not worth an upward import, and neither rule generalises to a validator whose object is already below it.

Axes and signs of every emitted coefficient

A loads export states one force and one moment per surface in the geometry's own frame, x aft, y right, z up. Body axes are that frame turned half a turn about y (forward, right, down), so CDB is the export's Cx and CLB its Cz; stability axes turn the body axes by -alpha_s about y, and wind axes turn those by beta_w about z, both angles read off the velocity the solver flies. Drag opposes +x and lift +z of each system; the side force keeps its sign. The moment turns as one vector and only then takes the span (CR, CN) or the chord (CM). A body rate is flight mechanics: positive p is right wing down, q nose up, r nose right. The solver turns the free stream as a right-hand rotation about the frame axis it is given, so the emitted rotation takes the sign of its body axis in the geometry's frame: p = -omega_x, q = +omega_y, r = -omega_z, the same half turn about y (since 0.27.0, G13). Measured on 26.124: pitch by RPT-052, roll and yaw by the licensed probe T11 (build 8172026), RPT-060, which found the one sign of +1 used for all three axes reversed for roll and yaw, so a row of 0.21.0 to 0.26.0 stating roll_rate or yaw_rate was solved at the opposite rate. docs/post-processing-definitions.md is the definition of record. SCORED against the solver's own recorded output (RPT-063): the body-axis forces, the wind-axis drag and the stability and wind lift of the emitted polar row (tests/tier1_offline/test_goal028_axes_recorded_exports.py), and the body rate sense of all three axes (tests/tier1_offline/test_ops2011_rate_sense_against_recorded_probes.py). NOT SCORED, each for want of a recorded export that could tell a right sign from a wrong one: the moments and the side force in stability and wind axes, the rotor coefficients, the sectional loads, the unsteady history and the far field.