Files
flightplanner/crates/core/src/perf/profile.rs
T
Alexandre 2cb633e99d Initial commit: offline flight planner (Rust, PFPX-class)
Clean-room reproduction of PFPX: route generation + IFPS validation + OFP.
Independent design — official ICAO/EUROCONTROL data only (RAD, FRA points,
IFPUV oracle); no community FPL sources.

Workspace crates: core (routing/discover/rad/navdata/perf/export),
cli, server, rad (Annex parser), gui (Tauri v2 + React + MapLibre).
Route discovery: oracle-in-the-loop repair against Eurocontrol IFPUV.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-21 11:10:35 +02:00

148 lines
4.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Aircraft performance profile, loaded from `data/aircraft/<icao>.json`.
use std::path::Path;
use serde::{Deserialize, Serialize};
use crate::error::{CoreError, Result};
use crate::perf::openap::PerfModel;
/// Per-phase speeds and fuel flow plus regulatory reserves for one aircraft type.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AircraftProfile {
pub icao: String,
pub name: String,
pub default_cruise_fl: i32,
pub phases: Phases,
pub reserves: Reserves,
/// Optional physics-based (OpenAP) coefficients. When present, fuel flow is
/// computed from drag at the actual mass/altitude instead of the fixed
/// per-phase `fuel_flow_kgph` figures.
#[serde(default)]
pub openap: Option<OpenApParams>,
}
/// OpenAP-derived aerodynamic and engine coefficients plus mass envelope.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct OpenApParams {
pub wing_area_m2: f64,
pub cd0: f64,
pub induced_k: f64,
pub n_engines: u32,
pub engine: String,
pub max_thrust_n: f64,
pub tsfc_cruise_g_per_ns: f64,
pub ff_idle_kgs: f64,
pub ref_cruise_alt_ft: f64,
pub oew_kg: f64,
pub mtow_kg: f64,
pub mlw_kg: f64,
pub max_fuel_kg: f64,
pub cruise_mach: f64,
}
impl OpenApParams {
/// Build the runtime physics model from these coefficients.
pub fn model(&self) -> PerfModel {
PerfModel {
wing_area_m2: self.wing_area_m2,
cd0: self.cd0,
induced_k: self.induced_k,
n_engines: self.n_engines,
max_thrust_n: self.max_thrust_n,
tsfc_cruise_g_per_ns: self.tsfc_cruise_g_per_ns,
ff_idle_kgs: self.ff_idle_kgs,
ref_cruise_alt_ft: self.ref_cruise_alt_ft,
}
}
/// Default payload (kg) when the caller gives none: 70 % of a max-payload
/// proxy (MLW OEW). A stand-in until real max-zero-fuel-weight data is
/// added; yields a realistic ~medium load factor.
pub fn default_payload_kg(&self) -> f64 {
0.70 * (self.mlw_kg - self.oew_kg)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Phases {
pub climb: ClimbPhase,
pub cruise: CruisePhase,
pub descent: DescentPhase,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ClimbPhase {
pub ias_kt: f64,
pub mach: f64,
pub tas_kt: f64,
pub fuel_flow_kgph: f64,
pub roc_fpm: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CruisePhase {
pub mach: f64,
pub tas_kt: f64,
pub fuel_flow_kgph: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DescentPhase {
pub ias_kt: f64,
pub mach: f64,
pub tas_kt: f64,
pub fuel_flow_kgph: f64,
pub rod_fpm: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Reserves {
pub final_reserve_min: f64,
pub contingency_pct: f64,
pub taxi_kg: f64,
}
impl AircraftProfile {
/// Deserialize a profile from a JSON string.
pub fn from_json_str(json: &str) -> Result<Self> {
Ok(serde_json::from_str(json)?)
}
/// Load `<dir>/<icao lowercased>.json`.
pub fn load(dir: &Path, icao: &str) -> Result<Self> {
let path = dir.join(format!("{}.json", icao.to_lowercase()));
let json = std::fs::read_to_string(&path).map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
CoreError::NotFound(format!("aircraft profile '{icao}' ({})", path.display()))
} else {
CoreError::Io(e)
}
})?;
Self::from_json_str(&json)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn loads_bundled_a320_profile() {
// data/aircraft lives at the workspace root, two levels up from this crate.
let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../data/aircraft");
let p = AircraftProfile::load(&dir, "A320").unwrap();
assert_eq!(p.icao, "A320");
assert_eq!(p.default_cruise_fl, 360);
assert!(p.phases.cruise.tas_kt > 400.0);
assert_eq!(p.reserves.final_reserve_min, 30.0);
}
#[test]
fn missing_profile_is_not_found() {
let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../data/aircraft");
let err = AircraftProfile::load(&dir, "ZZZZ").unwrap_err();
assert!(err.to_string().contains("ZZZZ"), "{err}");
}
}