Skip to content

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:

  • RPM with a velocity: the ratio is what the two work out to, as before.
  • ADVANCE_RATIO with a velocity: the speed is derived, n = V / (J D), as before.
  • RPM with ADVANCE_RATIO and 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 no CLOCK_MOTION rotor 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

  • ALTFT and dISA fix the temperature, and therefore the speed of sound and the viscosity. Both have defaults: an absent altitude is sea level and an absent dISA is zero. Those defaults are what make a short set legal at all.
  • Exactly one of MACH or TASmps fixes 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.
  • REmi fixes 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, TASmps and REmi are 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.
  • ALTFT and dISA select 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:

  • MatrixError means 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.
  • FlightConditionError means 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.
  • AtmosphereError means 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.