Skip to content

Calculation fixes

Writing the Calculations section from the code turned up five numbers that do not show what they say. The owner asked for all five to be fixed on 2026-10-20. Three are on both platforms, two only on iOS (Android has no recording or weather):

#What the pilot seesPlatformsSection
1Height lost in thermals never counts: no deduction in the rating’s Decisions part, and “Where I lost height in thermals” is never offered (P5-4)iOS, AndroidHeight lost
2“Lowest entry altitude” on the Thermals page shows the flight’s lowest point, not the lowest thermal entryiOS, AndroidLowest entry
3The statistics’ “Total altitude” (Thermals) sums each flight’s largest single climb, not its height gained in thermalsiOS, AndroidThermal height
4The in-flight AGL cell shows metres next to the pilot’s altitude unitiOSAGL cell
5METAR cloud layers reported as broken (BKN) are droppediOSBroken clouds

Fixes 1 and 3 change numbers pilots already know (the rating, the statistics), so each lands on both platforms in the same release.

Thermal.altitudeLost is the sum of the drops between consecutive fixes, so it is 0 or negative (Thermals). Every reader below asks for altitudeLost > 0, which is never true. The rule from now on: the height lost in a thermal is −altitudeLost (0 or positive). altitudeLost itself keeps its sign, so the thermal sheet (which shows it as a drop) does not change.

ReaderTodayNew
Question “Where I lost height in thermals”, appliesa thermal with altitudeLost > 0 (never)a flight with a thermal whose height lost is above 0
Its answerthermals with altitudeLost > 0, by altitudeLost, most first, top 5thermals with height lost above 0, by height lost, most first, top 5; each row shows “lost h” with h positive, then the gain and the vario; the summary’s “lost” is Σ height lost
Rating, Decisions partlost = Σ altitudeLost; −10 when gained > 0 and lost / gained > 0.10 (never); fact when lost > 0 (never)lost = Σ height lost over the scope’s thermals; the same −10 and the same fact, which now apply

Everything else in the rating stays: gained = Σ altitudeGained over the same thermals, the threshold 0.10, the 10 points. Worked example (from ios-changes P5-4): a thermal through 1000, 1050, 1030, 1080 m has altitudeLost = −20, so it lost 20 m; a flight with it offers the question and lists that thermal.

Effect: a flight that lost more than a tenth of its thermal gains again inside the thermals loses 10 points in Decisions, about 20 points off the 0–1000 rating (Decisions weighs 0.20). The rating is computed when the Debrief or Compare opens, so no stored data changes.

The Thermals page’s “Lowest entry altitude” shows lowestThermalEntryFix’s pressure altitude: the lowest entryAltitude of a thermal that is not an attempt, which the pipeline already computes (Flight analysis §5.4). With no such thermal the row shows no value, as any missing value does. lowestAltitudeFix keeps its other uses (the barogram, the Debrief’s scope).

“Total altitude” in the statistics’ Thermals group becomes the sum, over the year’s logbook flights, of each flight’s height gained in thermals: Σ altitudeGained of its thermals that are not attempts (in metres, as an integer). The label stays. The stored altitudeGained (the largest single climb in a gliding block) keeps its other uses: the logbook row’s figure and the sync summary.

The statistics read stored values, not the IGC, so this needs a new stored value; see Data.

The iPhone’s in-flight AGL cell formats its value like the MSL cell: converted to the pilot’s altitude unit (altitudeString), not the raw metres (stringValue). The Live Activity already does this.

CloudData reads coverage BKN as Broken (it reads BKC today, which METARs never use). BKN030 gives Broken at 3,000 ft. SKC, CLR and VV keep their current handling.

One new per-flight value, for fix 3:

FieldTypeMeaning
thermalAltitudeGainedinteger, metres, optionalΣ altitudeGained of the flight’s thermals that are not attempts. Absent: not computed yet
  • iOS: a new optional CoreFlight attribute (Core Data lightweight migration), set in CoreFlight+CoreData insert and update next to altitudeGained.
  • Android: a new FlightEntity column (Room migration), set in FlightRows.withAnalysis, and a new key in the Firestore summary map. Update Sync schema §summary: thermalAltitudeGained, optional; an older app ignores it.
  • Existing flights: Android fills it by bumping ANALYSIS_VERSION, which re-analyses every flight. iOS has no re-analysis of all flights and gets no backfill (decision 5).
  • Statistics while a flight has no value yet: the flight adds 0 to “Total altitude” (as a flight without thermals does), and the row’s other years are unaffected.

None.

None new. The “lost h” value of the height-lost answer reuses flight.debrief.value.lost, now with a positive height.

#QuestionDecisionDate
1Fix the five numbers?Yes, all five, on both platforms where they exist.2026-10-20
2P5-4: how to read the height lost?As −altitudeLost at the three readers; altitudeLost keeps its sign. This settles P5-4.2026-10-20
3“Total altitude”: rename the row or change the value?Change the value to the height gained in thermals; keep the label.2026-10-20
4RouteAn analysis, built by the iOS and Android sessions (the app repos are read-only from vulture-docs).2026-10-20
5Backfill thermalAltitudeGained for old iOS flights?No. An old iOS flight gets the value when it is next updated; until then it adds 0 to “Total altitude”.2026-10-20

None.

#Code (vulture-ios)Change
1Packages/FlightScreens/Sources/FlightScreens/FlightDebrief/FlightQuestion.swift (applies(to:), .heightLost), FlightQuestionAnswering+Breakdown.swift (heightLost(in:)), FlightRating/FlightRating+Compute.swift (thermallingDeductions(in:))Read −altitudeLost; update FlightQuestionBreakdownTests (it counts altitudeLost > 0 too) and add the 1000/1050/1030/1080 m test
2Packages/FlightScreens/Sources/FlightScreens/FlightDetail/Tabs/DetailTabThermalsViewModel.swiftlowestThermalEntryFix instead of lowestAltitudeFix
3Packages/Persistence/.../CoreFlight.swift, CoreFlight+CoreData.swift, the Core Data model; Packages/FlightScreens/Sources/FlightScreens/Statistics/FlightsStatisticsProviding.swift, FlightsStatisticsViewModel.swift (.totalAltiudeGained)New attribute, set on insert and update; statistics sum it
4Modules/App/Sources/Map/InFlight/InFlightMapViewModel.swift (aglDataItem.value = agl?.stringValue)altitudeString, as the MSL cell
5Packages/AviationWeather/Sources/AviationWeather/Models/CloudData.swiftcase "BKN"; a test for BKN030
#Code (vulture-android)Change
1domain/debrief/.../FlightQuestion.kt (HEIGHT_LOST), FlightQuestionAnsweringBreakdown.kt, FlightRating.kt (thermalling deductions)Read -altitudeLost; drop the three PARITY comments; update FlightQuestionBreakdownTest and add the same test as iOS
2domain/flightdetail/.../ThermalsTab.ktlowestThermalEntryFix
3data/db/.../entity/FlightEntity.kt + migration, data/db/.../FlightRows.kt, domain/sync/.../FlightCodec.kt and data/sync/.../FlightAdapter.kt (summary), domain/logbook/.../LogbookStatistics.kt, StatisticsFlight.kt (and its doc comment); bump ANALYSIS_VERSIONNew column and summary key; statistics sum it

Both: CHANGELOG.md entries (the rating can drop for flights that lost height in thermals; the statistics’ total altitude now counts thermal height), and parity golden data if the rating or statistics are in it.

No changes.