PIANO performance data parser - #159
Conversation
Move the TOML-emitting helpers out of make_performance_model.py into _performance_model_toml.py and replace write_legacy_performance_toml with a single write_performance_toml shared by every model type. Sections are now an ordered mapping of section name to table, so a model can emit more than the three legacy flight performance sections. Three additions prepare the writer for PIANO data without changing legacy output: crossover_altitude_m in the speed key order, a None guard on speed keys, and the PIANO column comments. Add tests/data/performance/legacy_golden.toml and a byte-identity regression test. The test was landed and confirmed green on the unmodified writer before the refactor, and stays green after it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QymTe3DRs64MtZvFcDJ7Xu
PerformanceTableInput had nothing legacy-specific in it. Split it: TableInput carries the column name normalization, duplicate check and row size check; PerformanceTableInput adds only the required-column check. TableInput goes to performance/types.py rather than models/base.py. It is plain tabular data, and a parser that needs it should not have to import the model layer, which drags in the config system, the EDB reader and pandas. PerformanceTableInput stays in models/base.py, where the required-column rule belongs. Add TableInput.column() for lookup by column name. Keep tolerating rows with more data columns than labels, which PerformanceTable.from_input relies on when it truncates rows. Accept an empty table, which a parser can legitimately produce and which the row size checks previously hit with an IndexError. legacy.py re-exports PerformanceTableInput, so existing imports and the docs autoclass reference still resolve. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QymTe3DRs64MtZvFcDJ7Xu
PIANO exports report mass in pounds, thrust and drag in pounds-force, and fuel flow in pounds per hour. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QymTe3DRs64MtZvFcDJ7Xu
Parse PIANO cruise, climb and descent text exports into SI tables. Every column PIANO reports is kept except buffet onset and the NOx, HC and CO emission indices. Climb and descent true airspeed is derived from the airspeed schedule and the ISA standard atmosphere, reusing AEIC.utils.standard_atmosphere. Each block cross-checks the derived speed against its own distance and time, and its final cumulative burn against its header total. Both warn only. Cruise rows are keyed on (fl, mass, mach). A labelled reference Mach can land on the swept grid and disagree with it, so the swept row wins and the disagreeing columns are named in a warning. Relax SpeedData.cas_low and cas_high to optional, add crossover_altitude_m, and make Speeds.cruise optional. PIANO's cruise table states no CAS schedule and no single cruise Mach, and neither CAS field is read anywhere today. Add anonymized PIANO exports as test fixtures. Their numbers are dummy values and are internally inconsistent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QymTe3DRs64MtZvFcDJ7Xu
Add the fields a PIANO performance model file carries: the three phase tables, the cruise reference Mach table, the descent idle thrust table and an optional operating empty mass. Fix the model_type discriminator, which was Literal['Piano'] and so could never match. PerformanceModel.normalize_model_type lowercases model_type before discrimination, so a piano file failed to load with union_tag_invalid. bada.py and tasopt.py carry the same bug and are left alone here. empty_mass raises unless an operating empty mass was supplied: PIANO exports do not contain one, and unlike BADA the sweep gives no basis for deriving it. evaluate_impl raises; evaluation over the sweep comes later. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QymTe3DRs64MtZvFcDJ7Xu
Factor the nine options every subcommand takes into a shared decorator, along with the APU check and LTO resolution both subcommands run. Flag names, types, defaults and help text are unchanged; only the order they list in --help changes, which now follows declaration order. The piano subcommand reads a set of PIANO exports and writes a model_type = "piano" file with five table sections. Cruise speed data is written only when --cruise-mach is given, so no cruise Mach assumption is baked into a file whose sweep covers many of them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QymTe3DRs64MtZvFcDJ7Xu
Cover the reader's parsing rules: block counts, the mass count mismatch error, both airspeed schedules and their overrides, a labelled Mach that lands on the swept grid, incomplete reference Mach groups, dropped zero-time rows, unit conversions on one row of each phase, rate-of-climb signs, row order and the non-zero Delta-ISA error. The duplicate-row disagreement path gets its own test built on a modified copy of the cruise fixture, because the fixture's own colliding rows agree on every column. Cover the subcommand end to end: the written file loads back as a PianoPerformanceModel, every emitted column survives the round trip, maximum_mass spans the three phase tables, empty_mass needs an operating empty mass, and cruise speed data appears only with --cruise-mach. The fixtures are anonymized dummy data whose numbers are internally inconsistent, so the tests assert structure and units only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QymTe3DRs64MtZvFcDJ7Xu
ian-ross
left a comment
There was a problem hiding this comment.
Just took a quick look at this. I know it's only a draft and you're likely to make changes, but I wanted to head off some stuff that you (or Claude) are doing that's not good.
|
Oh yeah, to add to my review comments: in principle, this looks great, and it's a good first step to adding more and better performance models. We just need to wrangle the Claude. |
7466d88 to
101cb31
Compare
|
I've refactored the code such that:
On top of updating the doc, there are couple of decisions left which are more science related:
I think point 1. is the only functionality I would want decided/finalized before considering this to be done (barring code improvements). Point 2. and 3. could wait. |
|
This is looking good. There are only a few things remaining:
|
|
Thanks for the feedback, I'll make the changes. For the failing test, all that's needed is to add the scoping to the pytest fixtures. Now the cached function is called only when the fixture is instantiated at the module or session scope (removing the cache decorator / moving the cached function body into the fixture does not change anything). # conftest.py
@pytest.fixture(scope="module")
def lto() -> LTOPerformanceInput: ...
@pytest.fixture(scope="session")
def piano_data() -> PianoData: ...Setting either of these fixtures to # Example with lto() set with scope="module"
ERROR tests/test_legacy_performance_model.py::test_built_model_round_trips_through_the_loader - ValueError: AEIC configuration is not set
ERROR tests/test_make_performance_model.py::test_legacy_output_byte_identical - ValueError: AEIC configuration is not set
ERROR tests/test_model_writer.py::test_table_sections_are_written_in_spec_order - ValueError: AEIC configuration is not set
ERROR tests/test_model_writer.py::test_empty_table_writes_its_columns_and_no_rows - ValueError: AEIC configuration is not set
ERROR tests/test_model_writer.py::test_none_table_omits_its_whole_section - ValueError: AEIC configuration is not set
ERROR tests/test_model_writer.py::test_apu_name_omitted_when_unset - ValueError: AEIC configuration is not set
ERROR tests/test_model_writer.py::test_unset_speed_phase_is_omitted - ValueError: AEIC configuration is not set
ERROR tests/test_model_writer.py::test_isa_offset_round_trips - ValueError: AEIC configuration is not set
ERROR tests/test_model_writer.py::test_model_type_without_a_write_spec_raises - ValueError: AEIC configuration is not set
ERROR tests/test_piano_performance_model.py::test_written_file_loads_as_a_piano_model - ValueError: AEIC configuration is not set
ERROR tests/test_piano_performance_model.py::test_every_emitted_column_survives_the_round_trip - ValueError: AEIC configuration is not set
ERROR tests/test_piano_performance_model.py::test_empty_mass_requires_an_operating_empty_mass - ValueError: AEIC configuration is not set
ERROR tests/test_piano_performance_model.py::test_cruise_speeds_written_only_with_cruise_mach - ValueError: AEIC configuration is not set
ERROR tests/test_piano_performance_model.py::test_name_and_altitude_overrides - ValueError: AEIC configuration is not set
ERROR tests/test_piano_performance_model.py::test_isa_offset_comes_from_the_exports - ValueError: AEIC configuration is not setI think it's because fixture are created from largest scope to smallest scope such that the session or module level fixtures get created before the Without caching, the tests (with |
|
I think the PR is ready for review. I have:
I have marked two things as questions in the doc (
|
This PR is part of #158 and implements a converter of PIANO performance outputs to AEIC
.tomlfiles that follow existing conventions + carry additional information not contained in.PTFfiles (most notably cruise performance is a function of(FL, mass, mach).This PR implements a stub of the
PianoPerformanceModelinperformance/piano.pybased on data stored in the.tomlbut does not implement the actual performance evaluation methodevaluate_impl(), this is for a future PR.PIANO output description
PIANO outputs with dummy data have been added to
tests/data/piano/and are representative of real outputs. Data is stored in 3 files, one for cruise, one for climb and one for descend. I recommend taking a look at these files first to make sense of the rest of the description here.Climb
PIANO climb outputs assume a fixed speed schedule with PIANO-default values: CAS=250 kts when
altitude < FL100, aircraft specific constant CAS forFL100 < altitude < crossover_altitude, and an aircraft specific constant Mach foraltitude > crossover_altitude.The table contains the following columns:
Altitude,time,distance,cumulative fuel burn,net thrust per engine,rate of climb,drag.This can be computed for a range of starting masses, such that there are multiple climb tables in a single climb file. However climb table headers which contain (starting mass, speed schedule...) only appear above a table when the cruise has reached its target altitude (which it does not always reach):
PianoOverridesdataclassCruise
PIANO computes a performance sweep at cruise as a function of
(FL, mass, mach)and provides:TAS,CAS,Drag,Max. Cruise Rating,L/D,FuelFlow,SFC,Specific Air Range,Max Climb Rating / engine,Rate of climb at Max climb rating for a fixed Mach,Rate of Climb at max climb rating for a fixed CAS(+ some others, I am only storing these for now).On top of these "parameter sweep" points, it also provides performance at notable operating points:
max specific air range,99% specific air range,maxLim. These points are appended at the end of a mach sweep: their(FL, mass, mach)coordinate may be repeated from the sweep but there is no guarantee that the values associated are the exact same as the ones from the sweep (probably some rounding in the PIANO output). Currently I overwrite this with data from the sweep but we could do the opposite.Descent
Very similar to climb, except that they in my experience always succeed such that the descent table header is always present and can be parsed to get the speed schedule and mass, simplifying the processing (no need for
PianoOverrides. PIANO also provides the altitude after which thrust is set to idle, this is also recorded in the.tomlfile if we ever have use for it.PIANO.toml schema
A PIANO based
.tomlfile looks like this (dummy numbers again):Code refactors
.tomlwriter functions from the legacy write code incommands/make_performance_models.pytocommands/_performance_models_toml.pyand only kept legacy specific and PIANO specific CLI entry points incommands/make_performance_models.pyPerformanceTableInputdata structure into a table specific class, and a performance table class. The newTableInputdataclass (inperformance/types.py) accepts empty tables and is responsible for normalizing columns, checking shape consistency and provides a column accessor. NowPerformanceTableInputinherits from it and only performs validation on the columns making sure they contain the minimum performance data required.These refactors do not change the behavior of the existing CLI command for
legacymodels as tested with the referencetests/data/performance/legacy_golden.tomlgenerated with the old code, andtests/test_make_performance_model.pywhich verifies that the new code leads to a byte identical file.New code to support PIANO
Mainly in
parsers/piano_reader.pyandtests/.More details to come
In progress
Code:
Modeling decisions:
Simplify climb speed schedule parsing logic: assume that a PIANO climb output file will contain at least 1 valid header block describing the speed schedule (leavingDeferred to later when the database is updated (current code would still work then anyway)PianoOverridesto only include climb masses)Speedvalue for a PIANO which does not have a set cruise machoew = min_mass / 1.2, whereas PIANO can provide the exact number (though it is not in the outputs we parse)...but the rest of the row is valid