Flight conditions¶
A run states the flow condition it wants, and the package resolves the
state. This page is what the FLIGHT_CONDITION cell of a run matrix
means, what you may write in it, and what it refuses.
The idea, in one sentence¶
A flight condition is a set of CONSTRAINTS on one flow state, and the keys you give decide which quantity gets solved for.
It is not a record with optional fields and it is not a lookup. The same resolver answers both of these by solving for a different unknown each time:
FLIGHT_CONDITION: MACH:0.20, REmi:5.5
FLIGHT_CONDITION: TASmps:68.08, ALTFT:10000, dISA:5
The first states a Mach number and a Reynolds number and no temperature, so the state is sea level and the density moves to meet the Reynolds number. The second states a speed at an altitude, so it is an atmosphere point and the Reynolds number comes out of it.
The keys, and the units ride the names¶
The set is CLOSED. Ten keys constrain or pin the AIR, and each carries its unit in its own spelling, deliberately. Five CONSTRAIN the state:
| Key | Unit | What it constrains |
|---|---|---|
MACH |
dimensionless | velocity, through the speed of sound at the state's own temperature |
TASmps |
metres per second | velocity directly (true airspeed) |
REmi |
millions -- 5.5 means 5 500 000 |
density [^re] |
ALTFT |
feet | pressure, and temperature through the standard lapse |
dISA |
Celsius, as a DELTA | temperature, as an offset on the standard value |
and five PIN it, each replacing what the standard atmosphere would otherwise supply, and each recorded on the run as pinned (v0.11.0):
| Key | Unit | What it replaces |
|---|---|---|
RHOkgm3 |
kilograms per cubic metre | density; a contradiction beside REmi, which solves it |
MUPas |
pascal seconds | dynamic viscosity, which is what a Reynolds number is taken against |
ASMPS |
metres per second | the speed of sound, which is what MACH is taken against |
TK |
kelvin | static temperature |
PPA |
pascals | static pressure |
A pin says the state rather than implying it, which is what a reproduction of someone else's run needs: their tool wrote 1.789e-5 and 340.29 where this package's own sea-level atmosphere gives 1.7892976260350732e-05 and 340.293988026089, and that is a different fluid in the fourth digit.
[^re]: A Reynolds number is density times velocity times reference
length over viscosity. Velocity and the length come from elsewhere on
the row, so stating REmi is what pins the density.
The names are ugly on purpose. ALTITUDE:10000 would be ambiguous
between feet and metres, and this repository has already shipped a solver
command whose metres argument three builds read as feet. A unit in the
key cannot be lost the way a unit in a comment can.
Keys are matched case-insensitively, so remi, REmi and REMI are
one key. A key written twice is refused rather than taking the last one.
The same cell carries the attitude, since 0.15.0¶
The ten keys above are about the AIR. Three more are about what the aircraft is doing in it, and the same cell carries them because a point is both (FR-69, FR-70):
| Key | Unit | What it states |
|---|---|---|
ALPHA |
degrees | the incidence of the free stream |
BETA |
degrees | the sideslip of the free stream |
ADVANCE_RATIO |
dimensionless | the speed of every motion of the row that states none of its own |
RPM |
rev/min, signed | the speed of every motion of the row that states none of its own (0.21.0) |
roll_rate |
deg/s, signed | the roll rate of the aircraft, about the REF's moment point (0.21.0) |
pitch_rate |
deg/s, signed | the pitch rate, about the same point (0.21.0) |
yaw_rate |
deg/s, signed | the yaw rate, about the same point (0.21.0) |
They are parsed here and NEVER handed to the resolver above: an angle constrains no fluid property, and asking the resolver about one would be asking it a question it has no answer to. The reader splits the cell where it reads it, and the attitude rides on the row.
A ROW STATES THE ANGLE IT IS NOT SWEEPING. Before this release there was nowhere to write one: a row sweeping the advance ratio reached the solver at incidence zero, and the only record of the incidence was that nobody had written one. An angle the row holds is carried at every point of the sweep, so it names the run, which is the subject of its own section below.
One variable sweeps, and it is one that defines the condition¶
The rule of 2026-09-10, and it is the whole shape of a row: a sweep is
applied to a variable that DEFINES the flight condition, and to exactly
one variable. The swept key carries the word sweep where its value
would be, and SWEEP_VALUES holds its values:
MACH:0.14, REmi:5.6, ALPHA:sweep, BETA:0 | 0,2,4
A row with no sweep, or with two, is refused naming the keys. Two swept
variables were the paired AL/BE sweep, which retires with the
SWEEP_TYPE column: a row that varies BOTH angles becomes one row per
sideslip, and pyfs-matrix upgrade refuses it by name rather than
inventing a POL for each new row. A paired row whose second axis held one
value is not one of those: it varied one variable all along, and it
converts with the same rows.
A script that WRITES a matrix should not repeat the literal sweep. The
word is one constant, pyflightstream.cases.matrix.SWEEP_WORD, and that
is who it is for: a generator building rows in Python spells the
declaration by importing the name, so a row it writes is a row this reader
accepts, and the two cannot drift apart. A person typing a cell into a
text editor imports nothing and needs none of this.
from pyflightstream.cases.matrix import SWEEP_WORD
# The cell a generator is about to write out.
condition = {"MACH": 0.2, "REmi": 5.5, "ALPHA": SWEEP_WORD, "BETA": 0.0}
swept = [key for key, value in condition.items() if value == SWEEP_WORD]
# -> ['ALPHA']
Reading a matrix back is a different question and has a different
answer. Ask the row: row.sweep.type is alpha, beta or
advance_ratio, and row.flight_condition holds the flow state with the
swept key ALREADY TAKEN OUT, because a resolver has no use for a word
where it expects a number. The mapping above, with the word still in it,
is what the cell parser hands the reader internally and is not something
read_matrix gives you.
The reader folds case and strips spaces around it, so SWEEP, Sweep and
sweep all mean the same thing in a file a person typed; the constant is
the canonical spelling the package stores.
Every key of the cell can be swept since 0.21.0: the five that fix the state, the five pins, the two angles, the advance ratio and the rotor speed. A key that does not define the condition is still refused naming the ones that do, rather than accepted and quietly run as a single point.
A swept FLOW variable is resolved at every point. MACH:sweep, REmi:5.5
over 0.1,0.2,0.3 is three points at three velocities and three densities,
each resolved from the cell as that point states it. Such a row is one solver
job PER POINT: the air state is a setup command the solver takes before it is
initialised, so one process cannot hold two of them. A row that sweeps an
attitude is the one warm job it has always been.
An angle the row HOLDS still names the point. ALPHA:sweep, BETA:0.0
over -4,0,4 plans three runs named AL-040BE+000, AL+000BE+000 and
AL+040BE+000, which are the same three points the paired row it was
upgraded from produced (a-04.0_b+00.0 and its siblings, under the tag of
0.20.x). The point's name ends the run_id in every manifest, so the
converter is held to more than lossless content: it does not rename a run,
and a --resume after an upgrade finds the records it already has.
Only the two ANGLES do that. An ADVANCE_RATIO the row holds stays on
the row and is read there, because the 0.20 point tag never carried one and
putting it in would have renamed every run that has one, which is the same
cost running the other way. Since 0.21.0 the NAME of a point carries every
variable the cell declares, held or swept, which is a different question
from what the point MAPPING carries; see
How a point is named.
The rotor speed, the advance ratio and the velocity¶
They are one relation, V = J x (RPM/60) x D, with D the diameter of the rotor
CLOCK_MOTION names. State any two and the third follows:
RPMwith a velocity: the ratio is what the two work out to, as before.ADVANCE_RATIOwith a velocity: the speed is derived, n = V / (J D), as before.RPMwithADVANCE_RATIOand no velocity (0.21.0): the VELOCITY is computed from them. This is the form a rotor study writes, where the rig states what the rotor does and the free stream follows. A row in this form naming noCLOCK_MOTIONrotor with a diameter is refused by name, because there is no D to measure against.- All three: refused by name. The three over-state the point and nothing here can know which two were meant.
A MOTIONS record naming its own speed keeps it, whatever the cell says: the
cell states the row's speed for every motion that states none.
A rotating free stream¶
A row states ONE body rate, in deg/s and in flight-mechanics signs, and the script writes
SET_FREESTREAM ROTATION <frame of the moment point> <axis> <rev/min>
instead of SET_FREESTREAM CONSTANT. That is how a run states a pull-up, a
roll or a yaw rather than straight flight: the free stream turns about the
MOMENT REFERENCE POINT of the row's REF, at the rate the row wrote
(test_goal024_freestream_rotation_a_pitch_rate_turns_the_free_stream). The
rate is a key of the cell like any other, so a row can sweep it
(test_goal024_freestream_rotation_a_rate_sweeps_like_any_other_variable).
Which axis is which belongs to the configuration. A mesh is built in whatever orientation its author chose, so the reference artifact says it:
[body_axes]
roll = "X"
pitch = "Y"
yaw = "Z"
A row stating a rate against a reference that declares none is refused by name
(test_goal024_freestream_rotation_a_reference_declaring_no_axes_is_refused_by_name),
and so is a row stating two non-zero rates: the free stream turns about one
axis at one speed, and two rates would be composed into an axis the row does
not write (test_goal024_freestream_rotation_two_non_zero_rates_are_refused_by_name,
test_goal024_freestream_rotation_a_case_authored_in_python_is_refused_too).
Every rate zero, or no rate at all, writes CONSTANT
(test_goal024_freestream_rotation_every_rate_zero_writes_constant,
test_goal024_freestream_rotation_a_row_with_no_rate_at_all_writes_constant).
The sign of the emitted rotation is the sign of its body axis in the
geometry's frame (since 0.27.0). Positive p is right wing down, q nose up and
r nose right, about body axes that point forward, right and down. The geometry's
frame, the one a loads export states its forces in, points x aft, y right and
z up, and the solver turns the free stream as a right-hand rotation about the
frame axis it is given. So p = -omega_x, q = +omega_y and r = -omega_z: a row
stating roll_rate:40 writes ROTATION <frame> X -6.667, pitch_rate:40
writes Y 6.667, and yaw_rate:40 writes Z -6.667 (rev/min, rounded here;
test_goal024_freestream_rotation_each_rate_turns_in_the_flight_mechanics_sense,
test_goal024_freestream_rotation_each_rate_takes_its_own_axis). The sign per
axis is the diagonal of the turn the post takes a loads export into body axes
by, so the two cannot part
(test_goal024_freestream_rotation_the_sign_per_rate_is_the_export_to_body_turn).
The measured sense of all three, on FlightStream 26.124: for each rate, the
line the package emits today for +40 deg/s, and what the solver answered to
that very line in the licensed probes
(test_the_emitted_rotation_is_the_one_the_probe_recorded_producing_that_rate
looks the line up in the recorded evidence and checks the sign of the answer):
| rate | positive is | emitted for +40 deg/s | the solver's answer to that line | measured by |
|---|---|---|---|---|
roll_rate |
right wing down | ROTATION <frame> X -6.667 |
the meshed left wing loses lift (-0.0998 in its lift coefficient) and the rolling moment opposes the roll: the damping of a positive roll rate | T11, RPT-060 |
pitch_rate |
nose up | ROTATION <frame> Y 6.667 |
the airframe gains lift (+0.0227) and the pitching moment turns nose down (-0.0012): the damping of a positive pitch rate | RPT-052 |
yaw_rate |
nose right | ROTATION <frame> Z -6.667 |
the meshed left wing, now advancing, gains lift (+0.0089): the response of a positive yaw rate | T11, RPT-060 |
What the solver does with a positive angular velocity is not documented by any edition of the manual, so the sense was MEASURED on FlightStream 26.124, one axis at a time, on one half wing-body:
- Pitch, three rates, reported in
reports/RPT-052_the-sense-of-a-rotating-free-stream_2026-09-15.md: a positive rotation about y came back with the nose-down moment increment that opposes a nose-up rotation. - Roll and yaw, the licensed probe T11 on build 8172026 of 26.124, seven
converged solves, reported in
reports/RPT-060_roll-and-yaw-rates-are-emitted-reversed_2026-09-23.md: a positive rotation about x gave the left wing more lift and a positive rolling increment, the damping of a NEGATIVE roll rate, and a positive rotation about z gave the left wing less lift, the response of a NEGATIVE yaw rate.
A row of 0.21.0 to 0.26.0 stating roll_rate or yaw_rate was solved at the
opposite rate. Those releases emitted all three rates with one sign of +1,
right for pitch (RPT-052) and reversed for roll and yaw (T11, RPT-060), so the
coefficients of such a row are those of -p or -r. Such a point is found by its
name, which carries the rate (P for roll, R for yaw), or by the script its
run record names, whose free-stream line turns about X or Z with the sign
of the rate the row states. A row stating pitch_rate is unaffected. The
reversal is measured on 26.124 and on one configuration's axes; on another
build the same line was emitted and its response is unmeasured.
A [body_axes] table that permutes the axes (roll = "Y", say) turns about
the axis it declares with its rate's sign above
(test_goal024_freestream_rotation_a_permuted_axis_turns_with_its_rates_sign);
no probe has measured such a mesh.
Which quantity gets solved for¶
ALTFTanddISAfix the temperature, and therefore the speed of sound and the viscosity. Both have defaults: an absent altitude is sea level and an absentdISAis zero. Those defaults are what make a short set legal at all.- Exactly one of
MACHorTASmpsfixes the velocity. State exactly one of the two: stating neither leaves the state under-determined and is refused, and stating both is a contradiction and is refused. REmifixes the density, by solving the Reynolds definition for it. If you do not state it, density is the atmosphere's own value at the stated altitude and the Reynolds number becomes an outcome.
An ISA deviation moves temperature and leaves pressure alone¶
That is the standard reading of "ISA+5", and it is worth stating because the other reading, shifting pressure too, is what a reader who has not met the convention will assume. Density follows from the offset temperature at the unchanged pressure altitude.
A solved density is not a point in any atmosphere¶
Holding a Mach number and a Reynolds number together at a fixed
temperature is what a wind tunnel does, and what a validation case needs.
The resulting state has an implied pressure that is not any altitude's:
MACH:0.20, REmi:5.5 against a one-metre reference solves to about
1.446 kg/m3 against a sea-level 1.225.
That is the design, not a defect. For a panel method it is expected to be harmless: density scales forces and viscosity sets Reynolds, and nothing in this package reads the pressure.
One thing to know before you read that as a guarantee. The package does not read the pressure, but it does WRITE it. The emitted fluid state carries the pressure and temperature of the stated altitude beside the solved density, so on this branch the three are not a consistent triple: at the worked figures above, a density 18 percent above sea level is emitted next to a sea-level pressure. Whether the solver re-derives anything from that argument is a question about the solver, not about this package, and it is open: it is recorded for the domain-expert seat rather than answered here, because settling it needs a licensed run.
It stops being harmless the moment a consumer reads that state
as an altitude, which is why the run record carries which branch
produced the density, in its density_source field, beside the resolved
density, temperature and viscosity and the condition as written.
Where a pin may live: the row, or the setup it names¶
A pin states a constant of the FLUID, and a constant of the fluid is
usually a constant of the whole campaign rather than of one point. So it
may live in the setup artifact the row's SET cell names:
# inputs/setups/s001.toml
NITER = 500
convergence = 1e-5
[flight_condition]
MUPas = 1.789e-5
ASMPS = 340.29
TK = 288.15
PPA = 101325
and the rows then state only what varies:
3207 | ... | MACH:0.2, REmi:11.7716754 | ... | s001 | ...
Pin names in that table match case-insensitively, exactly as they do
in a cell, so mupas and MUPas are one key -- and stating both is
refused as one pin written twice rather than taken last.
The row wins, key by key. A row stating ASMPS:335.0 against that setup
resolves at 335.0 and inherits the other three. A row stating nothing
inherits all four. Nothing about the resolution changes: the same state
comes out, whichever file the number was written in.
Why bother: a thirteen-point polar repeats those four numbers thirteen times in the column, and a single mistyped digit on one row is a physics nobody selected on that row alone, which is the version of the mistake that is hardest to see in the results.
The table holds the five pins and nothing else, and the two exclusions are the design rather than caution.
MACH,TASmpsandREmiare what the resolver solves for, and a preset several rows share cannot state them. A row inheriting a Mach number would be a case nobody wrote, and it would look exactly like a row that stated one.ALTFTanddISAselect a point IN the atmosphere, which the pins exist to replace rather than to locate.
One default can be superseded without contradicting anything, and it is
RHOkgm3. A ROW stating both it and REmi is refused, because each
fixes the density; a SETUP pinning a density is not making that statement
about any one row, so a row that solves its own density simply drops that
default. Refusing instead would mean a setup carrying a density could
never serve a Reynolds row.
The run record says where each number came from: flight_condition is
the row as written, and flight_condition_defaults is what the setup
supplied, with the values used.
A Reynolds number needs a reference length¶
REmi on a row that names no REF is refused, naming both. The
dependency is not bookkeeping: the same MACH:0.20, REmi:5.5 against a
unit chord gives a density about 18 percent above sea level, and against
a rotor's mean face length of about 0.15 m gives nearly eight times sea
level, at an implied pressure near 797 kPa. The length is stated so the
figure can be reproduced rather than taken.
Read those two numbers carefully, because it is easy to combine them wrongly: 1.18 and 7.87 are each against SEA LEVEL. The ratio between the two states is neither, it is 1 divided by 0.15, near seven, because on this branch density is inversely proportional to the reference length and to nothing else. Same inputs, same resolver, a state that is ordinary or absurd depending on a number that comes from somewhere else entirely.
The length actually used is recorded beside the resolved state, because the state cannot be checked without it.
What it refuses, and why each refusal exists¶
Every one of these arrives before a script is emitted and before a solver exists, which is the whole point of stating a condition rather than typing numbers into columns.
| You write | What happens | Exception |
|---|---|---|
KEAS:120 |
refused, naming the key and listing the accepted set | MatrixError |
MACH:fast |
refused, naming the key, the value and the unit expected | MatrixError |
MACH:nan, REmi:inf |
refused: a flow condition cannot be a NaN or an infinity | MatrixError |
MACH:0.2, MACH:0.35 |
refused as a duplicate, rather than taking the last | MatrixError |
MACH 0.2 (no colon) |
refused: the cell holds KEY:value pairs |
MatrixError |
MACH:0.20,,REmi:5.5 |
refused: an empty entry between commas is a typo, not a constraint | MatrixError |
| an empty cell | refused: the cell is mandatory, exactly as RE and MACH were |
MatrixError |
ALTFT:10000 alone |
refused as under-determined, naming which keys supply a velocity | FlightConditionError |
MACH:0.20, TASmps:68.08 |
refused as a contradiction: each fixes the velocity alone | FlightConditionError |
ALTFT:70000 |
refused naming the altitude range the model covers | AtmosphereError |
And in a setup's [flight_condition] table, which is judged when the FILE
is read rather than when a row is resolved:
| You write there | What happens | Exception |
|---|---|---|
MACH, TASmps or REmi |
refused: those state the POINT, and a preset several rows share cannot | FlightConditionError |
ALTFT or dISA |
refused: those locate a point in the atmosphere the pins replace | FlightConditionError |
VISCOSITY = 1.8e-5 |
refused: not a pin in any spelling, and the accepted five are listed | FlightConditionError |
MUPas = 1.8e-5 twice, in two casings |
refused as one pin stated twice, since the names match case-insensitively | FlightConditionError |
MUPas = 0 |
refused: a pin is a positive quantity | FlightConditionError |
flight_condition = 5 |
refused: it is a TABLE of pins | InputArtifactError |
MUPas = "thin", = true, = nan, = inf |
refused: a pin is a finite number | InputArtifactError |
[flight_conditions], misspelt |
refused as the table misspelt, and NOT as an unknown solver setting | InputArtifactError |
Which exception, and why there are three¶
The table is ordered by the three, because the boundary between them is the
thing worth knowing before you write an except clause:
MatrixErrormeans the cell is malformed. The defect is in the file, and no flight condition was ever constructed to complain about. Every parse refusal above is this one.FlightConditionErrormeans the cell parsed and the state cannot be resolved: no velocity, two velocities, a Reynolds number on a row with no reference length, or a condition that states nothing at all.AtmosphereErrormeans the cell is fine, the state is determined, and the physics does not reach there: an altitude outside the model's range, or a temperature at or below absolute zero.
A fourth class, InputArtifactError, reaches a reader through a
flight-condition mistake made in a SETUP file rather than in a cell, and
that is the boundary rather than an exception to it: a malformed
artifact is the artifact reader's refusal, and a well-formed artifact
stating the wrong CONSTANT is a FlightConditionError like any other.
The three above are what a FLIGHT_CONDITION CELL can raise.
All four descend from the package's base exception, so except
PyflightstreamError catches every one of them and is the right clause if you
do not need to tell them apart.
REmi also cannot be resolved without the reference length the row's
REF names, and the resolver refuses naming both if it ever lacks one.
You are unlikely to meet that message from a matrix: REF is itself a
mandatory column and a blank or unresolvable one is refused earlier. It
guards a caller reaching the resolver directly.
The set being closed is what makes the first row possible. A key the package does not know is refused rather than ignored, which is the difference between a typo that costs a message and a typo that costs a campaign.
Upgrading a matrix written before v0.9.0¶
The RE and MACH columns are gone. A file written under either older
layout is recognised and refused naming its converter:
pyfs-matrix upgrade your_matrix.fs --in-place
Without --in-place the upgraded matrix goes to standard output, so you
can diff before you overwrite. It needs no recipes, no version and no
executable.
Read this before you upgrade a campaign you have already run. The
conversion is lossless as a FILE -- the values move across verbatim -- but
it is not neutral as a RESULT. Under v0.8.x the RE column was
recorded metadata that reached no emitted line, so a declared Reynolds
number changed nothing about what the solver was asked to do. From
v0.9.0 REmi is a constraint that solves for density. Every legacy row
carried both RE and MACH, so every upgraded row now takes the
solved-density branch and emits an explicit fluid state it never emitted
before.
Measured on this repository's own committed fixture, a row at RE 4.38,
MACH 0.1441 against a 1.2 m reference solves at 1.3319 kg/m3, 8.7
percent above sea level, where the same row previously emitted no fluid
state at all. The numbers will differ, and they differ because the old
behaviour was the defect this release fixes.
If you want the previous behaviour, state the condition without
REmi and let the Reynolds number be derived: MACH:0.1441 alone
resolves at the standard density for its altitude. Keep REmi when you
meant it as a constraint.
What this page does not cover¶
The atmosphere model itself is ISO 2533:1975 with a Sutherland
viscosity law. The one fact you may hit is its RANGE: the model covers
-2000 m to 20000 m, which is -6561 ft to 65616 ft, and an ALTFT
outside that is refused naming the range rather than extrapolated. It is
stated here rather than pointed at, because the module that implements
it is private and a pointer there resolves nowhere for a reader of this
page.
Nothing here is a claim about a particular day's weather: the model says what the standard defines at a pressure altitude.