Catalog PyPI
Back to packageRELEASE DETAILS

0.5.0

PyPI

aeroid

Aerospace system identification, model validation, and uncertainty quantification

aerospaceflight-testparameter-estimationsystem-identificationuncertainty-quantification
Published26 Aug 2026
Direct dependencies7
Transitive dependenciesNot calculated

README

Version 0.5.0 documentation

AeroID

PyPI Python Downloads License

What the data says the vehicle really is — with error bars.

AeroID connects experimental aerospace data with physics-based models. Given flight-test (or bench-test) time series and an ODE model of the vehicle, it estimates the model's physical parameters, quantifies their uncertainty, validates the model against the data, designs the next maneuver to fly, and propagates parameter uncertainty into the engineering quantities you actually care about. It is deliberately not another simulator: bring your own dynamics function — or plug in JSBSim, RocketPy, or AeroSandbox — and AeroID closes the loop between measurement and model.

Every estimate ± uncertainty · Every algorithm pinned to analytic truth · Data → model → decision in one chain


Quick Install

pip install aeroid
# or
uv add aeroid

Requires Python ≥ 3.11. Runtime dependencies: NumPy and SciPy. Everything else is an optional extra: aeroid[parquet], aeroid[jax], aeroid[jsbsim], aeroid[rocketpy], aeroid[aerosandbox].

30-Second Example

import aeroid, numpy as np


def short_period(t, x, u, p):
    alpha, q = x
    return np.array(
        [
            p["Z_alpha"] * alpha + q,
            p["M_alpha"] * alpha + p["M_q"] * q + p["M_delta"] * u[0],
        ]
    )


model = aeroid.Model(
    states=["alpha", "q"],
    controls=["elevator"],
    parameters={"Z_alpha": -0.8, "M_alpha": -4.0, "M_q": -1.5, "M_delta": -6.0},
    dynamics=short_period,
)

data = aeroid.load_flight_test("flight_042.csv")  # columns: time, alpha, q, elevator
result = aeroid.identify(model, data, ["Z_alpha", "M_alpha", "M_q", "M_delta"])
result.parameters["M_alpha"]  # -5.926 (truth: -6.0)
result.stderr["M_alpha"]  # ±0.105

Table of Contents


Overview

AeroID is the interface layer between measurement and model: physics engines are abundant, but the connective tissue — measurements → parameter estimation → calibration → uncertainty → validation → engineering decision — is what this package standardizes. A Model (or a wrapped external simulator) plus a FlightData record feeds every stage of that chain through one consistent API.

Key Features

  • identify() — output-error parameter estimation (nonlinear least squares) with standard errors, full covariance, and identifiability diagnostics; exact JAX Jacobians optional.
  • validate() — simulation vs. measurement: RMSE, bias with confidence intervals, fit score, residual whiteness (Ljung–Box), and residual–input cross-correlation.
  • filter_states() — EKF/UKF state estimation with RTS smoothing; identify(method="filter_error") fits on whitened Kalman innovations when process noise (turbulence, model error) matters.
  • sensitivities() — Fisher information and Cramér–Rao bounds: which parameters can this trajectory actually constrain, before you fit?
  • frequency_response() — empirical vs. model transfer functions with coherence weighting.
  • infer() — Bayesian inference via a built-in affine-invariant ensemble sampler (no extra dependencies), with prior objects, estimated sensor noise, and convergence diagnostics.
  • propagate() — Monte Carlo propagation of covariance or full posterior samples through any metric you define.
  • design_experiment() — D-/A-optimal input design over maneuver families: what should the next flight fly?
  • Simulator adapters — JSBSim, RocketPy, and AeroSandbox plug into the identical pipeline as optional extras.
  • report() — one Markdown engineering report tying it all together.

Validated Accuracy

Every numerical algorithm in AeroID is pinned to an analytic closed form, a conjugate solution, or seeded recovery of known truth — these checks are the test suite, and they gate CI:

WhatChecked againstVerified within
ODE simulation (Model.simulate)closed-form x0·e^(−kt)1e-6
Output-error identify, noise-freeknown truth parametersrel 1e-4
Output-error identify, noisy flighttruth within ±3σ and 5 %pass
Parameter scaling invariance×1000 magnitude reparameterizationrel 1e-4
EKF steady statescalar discrete Riccati closed formrel 1e-3
UKF vs EKF on a linear systemmust coinciderel 1e-6
Correctly-specified filter innovationswhiteness + unit variancepass
Filter-error identifytruth within 5 %, σ² ≈ 1pass
Sensitivitiesanalytic partials of first-order step responserel 1e-3
Cramér–Rao boundidentify covariance on the same datarel 0.3
Empirical transfer functionexact short-period H(s) where γ² > 0.9510 % mag / 0.15 rad
Ensemble sampler2-D Gaussian target momentsrel 0.15
infer() posteriorconjugate Normal–Normal closed formrel 0.05
Autocorrelation timeAR(1) chains with known τrel 0.2
JAX RK4 rolloutsolve_ivp on the same model1e-7
JAX Jacobiananalytic sensitivity of e^(−kt)rel 1e-6
Posterior resampling in propagateevery draw an exact posterior rowexact
Experiment designamplitude pinned to bound (monotone information)rel 1e-2
Designed-experiment CRBstderr of actually fitting the flown designrel 0.3–0.5
JSBSim adapterinjected property recovered; bit-identical resetsrel 2e-2 / exact
RocketPy adapterinjected drag scale recoveredrel 3e-2
AeroSandbox adapterinjected coefficient scales recoveredrel 2e-2

The Quickstart numbers in this README are real program output, and the README workflows run verbatim as integration tests — the docs cannot drift from the code.

Determinism and Honesty

  • Every stochastic entry point takes a seed — Monte Carlo propagation, the MCMC sampler, the experiment-design optimizer, and every synthetic test dataset are bit-reproducible for a fixed seed.
  • Results are immutable records — frozen dataclasses holding read-only arrays, each with a summary() and a matching report() section.
  • Uncertainty is never optional — estimates ship with standard errors and correlations; posteriors ship with credible intervals, R-hat, and effective sample size.
  • Degeneracy is loud — when the data cannot distinguish parameters, an IdentifiabilityWarning names the offending combinations and the result is flagged, instead of silently reporting a huge or truncated covariance.
  • No silent extrapolation of trust — colored residuals, low spectral coherence, and unconverged chains are all reported as such.

Identification and Validation

Identify the short-period longitudinal dynamics of an aircraft from a flight-test log (continuing the 30-second example):

print(result.summary())
| parameter | estimate | std. error | rel. error |
|-----------|----------|------------|------------|
| Z_alpha   | -1.17    | 0.04187    | 3.6%       |
| M_alpha   | -5.926   | 0.1048     | 1.8%       |
| M_q       | -2.473   | 0.085      | 3.4%       |
| M_delta   | -8.927   | 0.1682     | 1.9%       |

Validate the fitted model against the data and propagate the parameter uncertainty into a quantity you care about — here the short-period natural frequency:

import math

validation = aeroid.validate(model, data, result)


def natural_frequency(p):
    return math.sqrt(p["M_q"] * p["Z_alpha"] - p["M_alpha"])


mc = aeroid.propagate(result, natural_frequency, n_samples=2000, seed=0)
print(aeroid.report(validation, result, mc))
| statistic | value  |
|-----------|--------|
| mean      | 2.969  |
| std       | 0.0254 |
| P2.5      | 2.922  |
| P50       | 2.969  |
| P97.5     | 3.019  |

The true natural frequency of the synthetic aircraft is 3.0 rad/s — inside the interval. The report also covers per-channel RMSE, bias with its confidence interval, residual whiteness, and residual–input correlation.

State Estimation and Filter-Error Identification

filter_states runs an extended (or unscented) Kalman filter over the record and, by default, an RTS smoothing pass — useful for reconstructing states between noisy sensors and for checking noise assumptions via the innovations:

filtered = aeroid.filter_states(
    model,
    data,
    result,
    process_noise={"alpha": 1e-8, "q": 1e-8},  # continuous PSD
    measurement_noise={"alpha": 0.005**2, "q": 0.01**2},  # variances
)
filtered.smoothed_state("alpha")  # best state estimate using the full record
filtered.log_likelihood  # for comparing noise models

When real flights contain turbulence or model error, output-error fits are biased; identify(method="filter_error") fits on whitened innovations instead, using the same noise specification — and sigma2 ≈ 1 doubles as a check that your noise levels are consistent with the data.

Sensitivity Analysis

Before flying (or fitting), ask which parameters the maneuver can constrain:

sens = aeroid.sensitivities(model, data, noise={"alpha": 0.005, "q": 0.01})
print(sens.summary())  # per-parameter sensitivity norms + Cramér–Rao bounds

An unidentifiable parameter combination triggers an IdentifiabilityWarning naming the offending parameters; the Cramér–Rao bound is directly comparable to the covariance identify will achieve.

Bayesian Inference

When a point estimate with error bars isn't enough, sample the full posterior. Priors are plain objects, sensor noise is estimated by default (as sigma_<output> parameters), and no extra dependencies are needed:

posterior = aeroid.infer(
    model,
    data,
    parameters={
        "Z_alpha": aeroid.Normal(-1.0, 1.0),
        "M_alpha": aeroid.Normal(-4.0, 4.0),
        "M_q": aeroid.Normal(-2.0, 2.0),
        "M_delta": aeroid.Normal(-6.0, 6.0),
    },
    initial=result,  # start the walkers at the least-squares fit
    n_steps=2000,
    seed=0,
)
print(posterior.summary())  # means, credible intervals, R-hat, ESS

mc = aeroid.propagate(posterior, natural_frequency, seed=0)
print(aeroid.report(validation, inference=posterior))

The result plugs into validate, sensitivities, and propagate exactly like a least-squares fit — and propagate resamples actual posterior rows, preserving skew, bounds, and correlations rather than assuming a Gaussian.

Frequency-Domain Validation

Compare empirical and model transfer functions, weighted by coherence so only frequencies the data actually excites count:

freq = aeroid.frequency_response(model, data, result, frequency_range=(0.2, 3.0))
freq.channel("elevator", "q").mismatch  # coherence-weighted relative error
print(aeroid.report(validation, result, frequency=freq))

Experiment Design

Before the next flight, ask what maneuver would constrain the parameters best. design_experiment optimizes a maneuver family's variables (for example per-line multisine amplitudes) against a D- or A-optimal Fisher-information criterion, within your amplitude limits:

design = aeroid.design_experiment(
    model,
    aeroid.Multisine("elevator", frequencies=(0.2, 0.5, 1.0, 1.5), amplitude=(0.0, 0.05)),
    ["M_q", "M_delta"],
    values=result,  # design at the identified point
    noise={"alpha": 0.005, "q": 0.01},
    duration=10.0,
    sample_rate=50.0,
    seed=0,
)
print(design.recommendation)
# designed multisine on 'elevator': dominant energy 0.5-1.5 Hz, peak
# amplitude 0.05; predicted stderr improves 2.1x over the initial design for M_q
flight_plan = design.to_flight_data()  # fly-ready control history

Doublet, Multisine (Schroeder phases), and Chirp are built in; any object satisfying the ManeuverFamily protocol works. The amplitude bound encodes your safety/actuator limit — for near-linear dynamics the optimizer will use all of it; the interesting freedom is in the frequency content.

Simulator Adapters

Bring existing simulators' physics into the same pipeline. Each adapter is an optional extra:

pip install "aeroid[jsbsim]"      # JSBSim flight dynamics
pip install "aeroid[rocketpy]"    # RocketPy rocket flights
pip install "aeroid[aerosandbox]" # AeroSandbox aerodynamics
from aeroid.adapters.jsbsim import JsbsimModel

model = JsbsimModel(
    "c172x",
    parameters={"pitch_trim": ("fcs/pitch-trim-cmd-norm", 0.0)},
    controls={"elevator": "fcs/elevator-cmd-norm"},
    outputs={"q_rad_s": "velocities/q-rad_sec"},
    initial_conditions={"ic/h-sl-ft": 5000.0, "ic/vc-kts": 120.0},
)
result = aeroid.identify(model, data, ["pitch_trim"])  # works unchanged
  • JSBSim and RocketPy models are stepped black boxes (SimulatorModel): they work with identify (output-error, finite differences), validate, frequency_response, sensitivities, infer (numpy backend), propagate, and report; they are rejected with clear errors by filter_states, filter-error identification, and the JAX paths, which need a continuous dynamics callable.
  • AeroSandbox (aerosandbox_model(...)) returns a genuine Model (longitudinal 3-DOF from tabulated AeroBuildup aerodynamics), so everything except gradient="jax" applies.
  • load_jsbsim_output() and rocketpy_flight_data() bring each simulator's native output in as FlightData.
  • RocketPy builders should pass tight Flight tolerances (rtol=1e-9, atol=1e-9) so finite-difference identification stays smooth.

JAX Acceleration

With pip install "aeroid[jax]", write the dynamics with jax.numpy (the same function works in every aeroid path) and get exact Jacobians instead of finite differences, from a jit-compiled fixed-step RK4 rollout:

result = aeroid.identify(model, data, ["Z_alpha", "M_alpha", "M_q", "M_delta"], gradient="jax")
posterior = aeroid.infer(model, data, priors, backend="jax")

aeroid.simulate_rk4(...) exposes the underlying integrator for parity checks against Model.simulate. Estimates can differ from the adaptive integrator at roughly the 1e-4 relative level; raise substeps for coarse sample rates.

Defining a Model

aeroid.Model wraps any ODE vehicle model

x_dot = f(t, x, u, parameters)
y     = h(t, x, u, parameters)

where x is the state vector, u the control inputs (interpolated piecewise-linearly from your measured channels), and parameters a plain dict of named physical parameters. The measurement function is optional and defaults to y = x. Data channels are matched to states, controls, and outputs by name, so a FlightData loaded from CSV or Parquet plugs straight in. Simulation uses scipy.integrate.solve_ivp, evaluated exactly on the measurement time grid — AeroID never resamples your data.


API Reference

All names below (except the adapters) are importable from the top-level aeroid namespace.

Models and data

NameKindPurpose
Model(states, controls, parameters, dynamics, outputs, measurement, name)classFrozen ODE model; .simulate(time, controls, x0, parameters, ...), .simulate_data(data, ...)
SimulationResultclassTrajectory on the requested grid; .state(name), .output(name)
SimulatorModelprotocolDuck type for external stepped simulators accepted by the pipeline
FlightData(time, channels, units)classImmutable time series; .array(names), .window(t0, t1), [name]
load_flight_test(path, *, time_column, channels, units)functionCSV / Parquet loader (name-matched channels)
simulate_rk4(model, time, controls, x0, parameters, *, substeps)functionJit-compiled differentiable RK4 (aeroid[jax])

Identification and filtering

NameKindPurpose
identify(model, data, parameters, *, method, gradient, ...)functionOutput-error or filter-error NLS → IdentificationResult
IdentificationResultclass.parameters, .stderr, .covariance, .correlation, .identifiable, .summary()
filter_states(model, data, parameters, *, process_noise, measurement_noise, method, smooth, ...)functionEKF/UKF + RTS → FilterResult
FilterResultclass.state(name), .smoothed_state(name), .state_std(name), .innovation(name), .log_likelihood

Validation

NameKindPurpose
validate(model, data, parameters, *, confidence, whiteness_lags, ...)functionResidual statistics per output → ValidationResult
ValidationResult / OutputMetricsclass.metrics[name]: RMSE, bias ± CI, fit %, Ljung–Box whiteness, input correlation
frequency_response(model, data, parameters, *, nperseg, frequency_range, coherence_threshold, ...)functionWelch/CSD transfer functions → FrequencyResponseResult
FrequencyResponseResult / ChannelFrequencyResponseclass.channel(input, output): magnitude, phase, coherence, mismatch

Sensitivity and experiment design

NameKindPurpose
sensitivities(model, data, parameters, *, values, noise, ...)functiondy/dθ histories, Fisher information, Cramér–Rao → SensitivityResult
SensitivityResultclass.sensitivity(output, parameter), .fisher_information, .cramer_rao_bound, .norms, .identifiable
design_experiment(model, maneuver, parameters, *, criterion, duration, sample_rate, ...)functionD-/A-optimal input design → ExperimentDesign
ExperimentDesignclass.design, .predicted_stderr, .recommendation, .to_flight_data()
Doublet / Multisine / Chirp / ManeuverFamilyclass/protocolBuilt-in maneuver families and the extension point

Bayesian inference and uncertainty

NameKindPurpose
infer(model, data, parameters, *, noise, initial, n_walkers, n_steps, backend, seed, ...)functionEnsemble MCMC posterior → InferenceResult
InferenceResultclass.samples, .parameters, .percentiles, .map_parameters, .r_hat, .effective_sample_size
Normal / Uniform / LogUniform / HalfNormal / PriorclassPrior distributions ({"M_alpha": aeroid.Normal(-4, 2)})
propagate(result, func, *, n_samples, percentiles, seed)functionMonte Carlo through any metric; resamples posteriors → PropagationResult
PropagationResultclass.mean, .std, .percentiles, .samples, .n_failed

Adapters (aeroid.adapters.*)

NameModulePurpose
JsbsimModel / load_jsbsim_outputadapters.jsbsimProperty-mapped JSBSim aircraft; JSBSim CSV loader
RocketpyModel / rocketpy_flight_dataadapters.rocketpyBuilder-parameterized rocket flights; Flight sampler
aerosandbox_modeladapters.aerosandboxLongitudinal 3-DOF Model from AeroBuildup tables

Everything else

report(validation, identification, propagation, *, experiment, inference, frequency, title, path) renders the Markdown engineering report. Package errors derive from aeroid.AeroidError (ModelDefinitionError, DataFormatError, ChannelError, SimulationError, IdentificationError, FilterError, InferenceError, AdapterError), and statistical degeneracy warns via IdentifiabilityWarning.


Units and Conventions

AeroID is unit-agnostic: use any consistent unit system and results come back in it. The conventions that do matter:

ConventionRule
Channel matchingData channels map to states/controls/outputs by name
TimeSeconds, strictly increasing; solver output lands exactly on the data grid
ControlsInterpolated piecewise-linearly between samples
ParametersPlain dict[str, float] at every user boundary
Process noiseContinuous power spectral density (state² per second)
Measurement noiseVariances in filter_states/identify, standard deviations in sensitivities/infer(noise=...)
AdaptersKeep each simulator's native units (JSBSim: imperial; RocketPy/AeroSandbox: SI) — encode the unit in the channel name

Building from Source

git clone git@github.com:alphabench/aeroid.git
cd aeroid
uv sync          # runtime + dev dependencies (incl. jax and all adapters)

Verification Test

uv run ruff check . && uv run ruff format --check .   # style
uv run mypy                                           # strict typing
uv run pytest -m "not slow"                           # fast truth-pinned subset
uv run pytest                                         # full suite incl. long MCMC/design runs

The adapter test modules skip automatically when jsbsim / rocketpy / aerosandbox are not installed; uv sync installs all of them so the full suite runs.

References

  • V. Klein and E.A. Morelli, Aircraft System Identification: Theory and Practice, AIAA (2006) — output-error method, maneuver design practice.
  • R.V. Jategaonkar, Flight Vehicle System Identification, AIAA (2015) — filter-error method.
  • G.M. Ljung and G.E.P. Box, Biometrika 65 (1978) 297 — residual whiteness test.
  • P.D. Welch, IEEE Trans. Audio Electroacoust. 15 (1967) 70 — spectral estimation for the transfer-function comparison.
  • H.E. Rauch, F. Tung and C.T. Striebel, AIAA Journal 3 (1965) 1445 — RTS smoothing.
  • S.J. Julier and J.K. Uhlmann, Proc. SPIE 3068 (1997) — unscented Kalman filtering (Merwe scaled sigma points).
  • J. Goodman and J. Weare, Comm. App. Math. Comp. Sci. 5 (2010) 65 — affine-invariant ensemble sampler.
  • A. Gelman and D.B. Rubin, Statistical Science 7 (1992) 457 — split-R-hat convergence diagnostic.
  • A.D. Sokal, Functional Integration (1997) — autocorrelation-time estimation.
  • M.R. Schroeder, IEEE Trans. Inf. Theory 16 (1970) 85 — low-crest multisine phases.
  • R. Storn and K. Price, J. Global Optimization 11 (1997) 341 — differential evolution (experiment-design optimizer).

License

MIT — see LICENSE.

Changelog

Canonical history lives in CHANGELOG.md.

v0.5.0 — first public release

The complete measurement-to-decision chain:

  • Core: Model / FlightData abstractions, output-error identify with covariance and identifiability diagnostics, validate, Monte Carlo propagate, Markdown report.
  • Filtering: EKF/UKF filter_states with RTS smoothing and filter-error identification on whitened innovations.
  • Analysis: sensitivities (Fisher information, Cramér–Rao) and coherence-weighted frequency_response.
  • Bayesian: dependency-free ensemble-MCMC infer with priors, estimated sensor noise, and convergence diagnostics; posterior-aware propagate.
  • Optional JAX: differentiable RK4 rollout, exact Jacobians (gradient="jax"), fast likelihoods (backend="jax").
  • Adapters: JSBSim, RocketPy, and AeroSandbox as optional extras behind the SimulatorModel protocol.
  • Experiment design: D-/A-optimal design_experiment over Doublet / Multisine / Chirp families with predicted parameter precision.

Downloads & popularity

Awaiting observations

No metric observations yet

Download counts and popularity will appear here as the collector receives them.

Declared dependencies 7

Native requirements
7 direct targetsNot calculated transitive count
Package / targetRequirementRelationship
numpy
Environment
{
  "extras": "",
  "marker": null,
  "requires_python": ">=3.11",
  "evaluation": "NOT_EVALUATED"
}
numpy>=1.26runtime
scipy
Environment
{
  "extras": "",
  "marker": null,
  "requires_python": ">=3.11",
  "evaluation": "NOT_EVALUATED"
}
scipy>=1.11runtime
aerosandbox
Environment
{
  "extras": "",
  "marker": "extra == \"aerosandbox\"",
  "requires_python": ">=3.11",
  "evaluation": "NOT_EVALUATED"
}
aerosandbox>=4.2; extra == "aerosandbox"runtime
jax
Environment
{
  "extras": "",
  "marker": "extra == \"jax\"",
  "requires_python": ">=3.11",
  "evaluation": "NOT_EVALUATED"
}
jax>=0.4; extra == "jax"runtime
jsbsim
Environment
{
  "extras": "",
  "marker": "extra == \"jsbsim\"",
  "requires_python": ">=3.11",
  "evaluation": "NOT_EVALUATED"
}
jsbsim>=1.2; extra == "jsbsim"runtime
pyarrow
Environment
{
  "extras": "",
  "marker": "extra == \"parquet\"",
  "requires_python": ">=3.11",
  "evaluation": "NOT_EVALUATED"
}
pyarrow>=15; extra == "parquet"runtime
rocketpy
Environment
{
  "extras": "",
  "marker": "extra == \"rocketpy\"",
  "requires_python": ">=3.11",
  "evaluation": "NOT_EVALUATED"
}
rocketpy>=1.4; extra == "rocketpy"runtime

Distribution files

2 collected
aeroid-0.5.0-py3-none-any.whl

bdist_wheel · 68.83 KB · Python py3

Source file
aeroid-0.5.0.tar.gz

sdist · 220.69 KB · Python source

Source file

Metadata history

Meaningful changes
Current collected state Current

Observed since 20 Oct, 20:47 UTC

No superseded metadata states collected. Unchanged observations do not create new revisions.

Record identity

package_version:5246fbc781f050bb9bba0f6b42b3f8d8ea7eb4896dcd7d84c4308fd7ad3f4f26

Other collected versions

View all collected versions →