Fly a published ANP procedure into a Doc 29 flight profile (#644)
* Fly a published ANP procedure into a Doc 29 flight profile
Most ANP entries publish a procedure rather than a trajectory, and where its
steps put the aeroplane over the ground depends on the field, so there is
nothing to tabulate. The six coefficient tables shipped for exactly this sat
unread, as their PROVENANCE.md said.
This implements ECAC Doc 29 Vol. 2 Appendix B and wires it into the ANP bridge:
flight_profile flies a procedure at an aerodrome, procedural_profile hands the
result back in the units the noise chain speaks, and event_level and
noise_contour take an aerodrome for the same reason. That takes the shipped
fleet from 33 aircraft and operation pairs that can run the chain to 309 of 310.
All 314 reference profile points of Doc 29 Volume 3 Part 2 are reproduced, all
four columns, at a tolerance derived from the printed precision rather than
chosen. Three defects in the published standard are registered in docs/ERRATA.md
with the rendered page cited: the mid-step propeller airspeed of Eq. (B-21),
which the reference results refuse by 545 ft; the doubled slope correction of
Eq. (B-41); and the missing k^2 of Eq. (B-18), which no reference case can
arbitrate because the case sheet has no runway-gradient column.
* Correct the aircraft overview at its source, and rebuild the llms artifacts
The scope claim was fixed in docs/aircraft/index.md, which is a generated
mirror of site/src/content/docs/aircraft/index.mdx, so the source still said
the performance model was not implemented and the mirror check failed.
* Normalise the ANP operation on read, and keep the guides in step
The aerodynamic table picks its speed column by an exact match on the operation
while PerformanceAircraft.flap folds case when it looks the row up again, so an
export spelling the operation "d" stored the landing coefficient under a key a
departure still finds. No row of the bundled table fills both columns, so there
the wrong column is empty and the Take-off step refuses outright, but a table
filling both would take a rotation speed from the landing coefficient and say
nothing. The fixed-point table failed a third way, reporting no profile at all.
Also: the mirror and the Spanish overview still said the performance model was
outside the bridge; the worked example flew at 35 degrees C, outside the below
30 degrees C envelope the NPD tables are valid in and which no Appendix D
conversion is implemented for; and the Eq. (B-21) errata called the propeller
branch "every piston and turboprop aeroplane" when B4.1 and B4.2 split the
turboprops without a rule, 11 of the 20 in ANP v2.3 reaching Eq. (B-12) and the
other 9 Eq. (B-9).
An ApproachStep now refuses a negative length, which is what lets the rollout
say "no length to travel" as an inequality rather than as a float equality.
* Let the maximum-weight bin through the chain, and refuse a bin between bins
Three findings from the review that the diff view does not show.
The stage-length label reached flight_profile as int | str so the "M"
maximum-weight procedure survives the lookup, but event_level and noise_contour
still annotated it int. The runtime already handled it, so the annotation was
refusing valid code; the fixed-point path is the one that genuinely cannot take
"M", and it now says so instead of surfacing int()'s own ValueError quoting a
bare string.
_stage_label truncated a number between the bins: "1.5" came back as bin 1, so a
caller got a profile flown at a different trip length and no word about the
substitution. It is refused now.
And the guides said flight_profile "asks for the aerodrome that profile has no
use for", which reads as a limitation of the aerodrome rather than of the
tabulated profile. Both editions say plainly why it is required.
* Split three long functions into one helper per case
The ANP performance parser walked five tables in one body, ApproachStep's
validation branched on the step kind inside its __post_init__, and the backwards
approach solver carried five thrust laws inline. Each is now a dispatcher over
named helpers that cite the equation they implement, so a reader follows one
case at a time instead of holding all of them.
No behaviour moved: every parsed coefficient of the shipped export, every point
of every published procedure at three aerodromes as float.hex() literals, and
93312 ApproachStep constructions with their exact exception type and message
hash the same before and after.
* Ask whether there is a deceleration, rather than comparing one to zero
The test is the same for a float, negative zero included, and the library has
no other float equality comparison, so a lone one here would have been the odd
spelling rather than the precedent.
Give the tree a formatter, and a gate that keeps it (#602)
* Give the tree a formatter, and a gate that keeps it
Ruff was the linter and nothing was the formatter. There was no
`[tool.ruff]`, no `ruff.toml`, no pre-commit and no `.editorconfig`, and CI
ran `ruff check .` on ruff's default rules alone, which are four families of
outright errors. Nothing looked at shape, so 673 of the 812 Python files had
drifted from any single style.
The line length is 88 because that is what the tooling defaults to and,
measured, what moves this tree least: 100 rewrites 708 files and 120 rewrites
717, since a longer limit rejoins lines that were split by hand. Only 0,8 %
of the lines here were over 88 to begin with.
Markdown stays out of it. Ruff formats fenced Python inside `.md` and does
not recognise `.mdx` at all, and every snippet in `docs/` is mirrored by hand
into the site in English and Spanish, so formatting one copy would drift the
three apart with nothing to notice. Those snippets also align their trailing
comments to show what each line prints, which is a teaching device.
The linter now selects the families this tree already satisfied or nearly
did, and the fifty-odd findings they raised are fixed. Two of those fixes are
worth naming because the formatter caused them: reflowing a call moves a
`# type: ignore` onto a line that no longer holds the error it silenced, and
mypy caught four such cases. One rename is reverted with the reason written
down, because renaming an unused binding moves the fingerprint the clip
freshness gate reads, and that is not worth re-rendering an FDTD animation.
* Keep the reformat out of git blame
A tree-wide reformat stands in front of every line it touched. This file
names it so `git blame` walks past, which GitHub honours without any
local configuration and which `git config blame.ignoreRevsFile` turns on
for a checkout.
* Stop selecting the rule that reads a constant as a literal
SIM300 calls any upper-case name a literal, and on that reading it gets
three idioms in this tree backwards.
`spectrum[OCTAVE_BANDS == f]` is a numpy mask, not a condition.
`res.transmission_loss[BANDS <= f0]` reads "the bands at or below f0" until
it becomes `f0 >= BANDS`. And `value == pytest.approx(x)` is how pytest is
written here: 1755 of the 2065 `pytest.raises`-adjacent assertions in the
suite already put the expected value on the right.
It is right about two of the nineteen it finds and wrong about the other
seventeen, and SonarCloud caught the result as `python:S3415`, assertions
whose argument order no longer agrees within a file. The nineteen are
reverted by rebuilding those files as they would have been had the rule
never been selected, which keeps the `C420` and formatting changes two of
them also carry.
A Spanish figure writes its decimal comma on every panel it draws, and two guards keep it that way (#777)
Every helper that writes text takes a language and defaults to English, so a call that forgets to pass it on raises nothing and puts an English decimal point into a Spanish figure. The plotting modules, the figure generators and the fiches now hand the caller's language to every helper that takes one, and localize_axes is called once per axes a plot builds rather than once per figure: a twin axis, a colorbar and the z of a 3-D panel each carry a formatter of their own, and each was shipping in English beside a panel already in Spanish. What decides whether an axis is localised is its formatter and not its scale, so a log axis of distances still writing its own numbers is reached and a pinned frequency axis is left to format_frequency_axis. A zoom inset and a contour colorbar, which the save-time pass of the figure generators cannot reach, are localised where they are built. The band column of the room criterion sheet and of the whole sound-power fiche family is written in the sheet's language too.
scripts/check_language_forwarding.py fails on any call to a helper that takes a language, made from anywhere a language is in scope, that does not pass it on. It reads module and class bodies as well as functions, follows a helper through a local alias and a functools.partial, names the shapes it cannot reach, keys an exemption by its line so one approved call cannot cover another, and reports a method name only some of whose namesakes take the language, which silently switched the untyped-receiver check off.
scripts/check_figure_decimal_point.py reads the committed Spanish figures and fails on a tick label that is a number with a point in it, whichever pass was meant to write the comma. It found six figures, regenerated here. Both run from the Makefile and in CI.
Every verdict reads its pass and fail flags, grades and classes from the values it holds (#949)
A result no longer stores a verdict beside the readings it came from. Sixty-two result classes used to keep the flag, grade or class their function had worked out (`passes`, `complies`, `qualified`, the `*_met` and `*_ok` flags, the advisories, the per-band classes of the IEC 61260-1, IEC 61672-1 and IEC 61043 verdicts, the ISO 9614-2 grade, the ISO/PAS 1996-3 category, the IEC TS 61400-11-2 block status and the rest), so a result built by hand or rewritten with `dataclasses.replace` could hold readings that fail and a flag that says they pass. Each of them is now a read-only property, read from the values the result keeps and the limit the standard prints, and so is any figure that only fed it, such as the rounded level of an RD 1367/2007 period or the acceptance region of a trend test.
The unit goes in the name, where the caller writes the number (#752)
* The unit goes in the name, where the caller writes the number
A static pressure of 101 325 and a static pressure of 101.325 are both
legitimate values in this tree: the ISO 3740 family prints kilopascals,
IEC 61094-2 and ISO 15186-3 Annex A print pascals. Nothing separates them
by magnitude, because either reading is a real duty somewhere else, and
the result of the mix-up is still a float.
146 public parameters take their unit as a suffix. No value changes:
every function keeps the unit it already documented, so no oracle, no
printed figure and no conformance row moves.
temperature -> temperature_c across 39 callables, with temperature1_c,
temperature2_c, temperatures_c and temperature_celsius folded into the
same spelling; GasStream.inlet_temperature, which IEC 60534-8-3 states in
kelvin, becomes inlet_temperature_k. relative_humidity and the ISO 354
humidity both become relative_humidity_percent. The ambient pressure of
the propagation and aircraft chains becomes atmospheric_pressure_kpa, the
name air_density_astm and air_density_iso10534 already used; the ISO 3740
family takes static_pressure_kpa and the pascals of ISO 15186-3 Annex A,
ISO 9053-2 and ISO 7235 take static_pressure_pa, matching fluids.air.
Every other member of the family takes _pa.
Acoustic pressure keeps its bare name: a waveform is always in pascals
and a Signal carries its own calibration, so a suffix would restate the
type rather than resolve an ambiguity.
scripts/check_parameter_units.py holds the rule from here on. It walks
the imported package, not the source tree, because ReportMetadata is
published from the root and defined in a private module. Against main it
reports 154 offenders; here it reports none.
* Regenerate the API reference and the llms artifacts after the rewrap
* Pin the renamed argument in the four refusals nothing was reading
Results keep their own copy of the arrays they were given (#939)
np.asarray hands back the very array it is given when that array is already
float64, and so does every validation helper built on it, so a result built
from such an array kept the caller's array, or a view of it: changing that
array afterwards changed a result already computed, and nothing raised. Every
array a function or a method of the library keeps from its arguments is now a
read-only copy of its own, through the private helper read_only_copy, and the
objects that keep arrays to hand out later (SilencerChain, AnpDatabase,
FDTD2D, ElasticFDTD2D, window_metrics) do the same; a Signal keeps a writeable
copy of its samples. The values do not move. scripts/check_array_aliasing.py
(make array-aliasing and a CI job of its own) follows each parameter of every
function of the package to the places an array is kept and fails on one kept
without a copy of its own.
The unit of critical_angle depended on which function you called (#757)
* The unit of critical_angle depended on which function you called
#755 closed the bare names of the geometry. These are the compounds built
on the same words, and one of them is worse than anything the first pass
found: in weston_regimes.py, WestonRegimeBoundaries.critical_angle is in
radians and weston_regime_boundaries(critical_angle=...) one screen below
it takes degrees, while seabed_reflection.py has it in degrees again. The
same name, 57 times apart. The field is critical_angle_rad now and the two
overrides critical_angle_deg, which is what the function converts to
anyway.
The rest, read off each docstring: degrees for bank_angle, path_angle(s),
grazing_angle, launch_angles, arrival_angles, source_angle and
limiting_angle; radians for incidence_angle and angle_limit; metres for
the thirteen diameters; steradians for solid_angle; per second for
sound_speed_gradient; nepers per radian for reflection_loss_gradient and
its override. runway_gradient becomes runway_gradient_ratio, because its
own docstring says dimensionless.
Two public functions were caught by the sweep and put back:
critical_angle(c1, c2) and reflection_loss_gradient(seabed). The rule is
about parameters; what a function returns is said by its return type.
element_diameter and pitch_diameter stay bare in the guard's exemptions,
because the bearing formulae use them only as the ratio d/D.
The guard matches the geometric quantities as whole words inside a name
now, so critical_angle and duct_diameter are held to the rule and
triangles is not, and the two tuples of the first pass collapse into one.
No value changes: the same 995 conformance checks with the same numbers.
One clip is re-rendered, because its scene reaches the renamed parameters.
* The geometric conditions with a default are written by name too
The rule of #756 now reaches the names #755 and this branch renamed.
Thirteen more conditions carry a default and could still be passed as a
bare number.
piston_directivity_pattern and hydrophone_depths take a star before
their angle argument, which keeps the quantity the call is about
positional.
Nine result dataclasses take KW_ONLY after the fields they are about,
among them ApproachStep and DepartureStep, whose ANP steps were being
written as five numbers in a row against a column of sheet C-6.2.
* Say both halves of the rule in the guard's own docstring
Results keep their own copy of the arrays they were given (#939)
np.asarray hands back the very array it is given when that array is already
float64, and so does every validation helper built on it, so a result built
from such an array kept the caller's array, or a view of it: changing that
array afterwards changed a result already computed, and nothing raised. Every
array a function or a method of the library keeps from its arguments is now a
read-only copy of its own, through the private helper read_only_copy, and the
objects that keep arrays to hand out later (SilencerChain, AnpDatabase,
FDTD2D, ElasticFDTD2D, window_metrics) do the same; a Signal keeps a writeable
copy of its samples. The values do not move. scripts/check_array_aliasing.py
(make array-aliasing and a CI job of its own) follows each parameter of every
function of the package to the places an array is kept and fails on one kept
without a copy of its own.