(heirloom + air: every frame inherits from its ancestors, and the gallery
is the family archive. Formerly framevo; the old CLI name still works
as an alias.)
A fully automated, headless research loop that evolves quadcopter frame geometry to minimize energy per distance (Wh/km) flown through a portfolio of adverse-weather scenarios. Everything except the frame — battery, motors, propellers, flight controller — is fixed. Runs unattended, is resumable after a crash, and produces a ranked, visual archive of every frame it ever evaluated.
make demo # 3 generations x population 8, ~2-4 min on 8 cores
make test # pytest suite
.venv/bin/airloom run --generations 100 # starts, or RESUMES the
# latest run by default
.venv/bin/airloom run --generations 100 --fresh # snapshot + CLEAR results,
# then a brand-new run
.venv/bin/airloom lineage <genome_hash> # ancestor chain w/ fitness
.venv/bin/airloom gallery # rebuild artifacts from db
.venv/bin/airloom robustness # is the RANKING an artifact
# of the model knobs?
.venv/bin/airloom verify-champions # refined structural check of
# the top frames + print-
# and-test protocol
While a run is live, type quit + enter (or ctrl-c) to stop gracefully — the
tasks in flight finish and the run stays resumable. If the latest run's
genomes predate the current genome spec, airloom run starts a fresh run
automatically; a run.db written by an older schema is archived (never
deleted) and recreated.
Local-first: plain pip install -e . on macOS (Apple Silicon) or Linux x86,
CPU-only, no Docker/GPU/EGL. Thumbnails render through headless matplotlib.
Worker count auto-scales to the core count.
Every airloom run first commits a snapshot of results/ into its own
nested repo (results/.git — separate from the project repo, so big run
artifacts never bloat it). --fresh then actually clears the folder.
Restore any past state with git -C results log +
git -C results checkout <hash> -- .
index.html— the gallery: static, no frameworks, no server, open withfile://(auto-reloads every 30 s while a run is live — locally only); also mirrored intodocs/, the GitHub Pages root, on every refresh. Tufte-styled (cream paper, ink, one rust accent, hairlines). At the top: a progress chart of every candidate in evaluation order — gray dots, a best-so-far step line with labeled improvements, invalid candidates as rust ×, generation ticks, and two horizontal benchmark lines (≈5.0 Wh/km aggregate = current 7-inch long-range practice, ≈4.0 = record-class stretch; both set inscenarios.yaml) — so you can see what moved the needle and where the search plateaued. Below: one row per generation sorted by fitness, then detail blocks showing a from-below still of each candidate — click it to open a full-screen overlay with two tabs: the interactive 3D model (depth-buffered WebGL, drag/scroll/double-click), and a side-by-side comparison with the oldest ancestor of its lineage whose rotation and zoom stay in sync. A metric/value table, the genome table, parent thumbnails and the candidate's lab-notebook notes (hypothesis / method / result — see the narrator below) sit beside each still. Parts are colored by role: rust arms and near-black deck plates are the evolved geometry; the blue Li-Ion pack, gray motor cans and pale translucent prop disks are the fixed kit.leaderboard.md— top 10 with all metrics and per-scenario columns.lineage.html(+ rawlineage.svg,lineage.dot) — a dedicated family tree page: nodes shaded by fitness (dark = better), hollow nodes = invalid, edges colored by operator, elite carry-overs as dotted pass-throughs.convergence.png— fitness vs. generation.frames/gen_XXXX/<hash>.stl+.png— every candidate, including invalid ones (_INVALIDsuffix; the failures are instructive).gen_XXXX_best.stl— the generation's champion (fused), plusgen_XXXX_best_parts/— the same frame as separate flat, printable pieces.designer_log.md— the designer rounds' proposals with their one-line rationales (see below).robustness.md(on demand,airloom robustness) — the top candidates re-flown under perturbed model knobs (±30 % handbook Cds, ±10 % rotor tables, rain knobs, wash term): rank correlation, top-5 overlap and champion identity vs the baseline ordering, with a STABLE / MODERATE / FRAGILE verdict. Answers "is the leaderboard a frame property or a model artifact?" and names the knob where Phase B fidelity would matter first.champion_check.md(on demand,airloom verify-champions) — the top frames re-analyzed with the same station-by-station variable-section bending model the in-loop constraint now runs (net-section stress concentration at holes/cutouts via Peterson Kt, and the taper gene), adding the one thing the fast in-loop gate deliberately doesn't apply: as-built strength knockdowns for printed materials — plus a bench print-and-test protocol with hold and predicted-failure loads.run.db— SQLite: genomes, lineage (self-referencing,WITH RECURSIVEancestry), per-scenario metrics, populations, lab-notebook notes, config snapshot, git hash. Written by an older schema? It gets archived automatically (never deleted) and recreated.glossary.html(copied fromdocs/glossary.html, linked from the gallery) — definitions of both the evolutionary-algorithm terms and the domain terms (genes, Wh/km, constraints, physics vocabulary).
Matched to the official DroneAid Collective (drone-aid.de) workshop kit, a 7-inch long-range build in the most popular open-source plate-frame archetype — the TBS Source One family (open-source: 2 mm deck plates, M3 standoffs, sandwiched plate arms; the upstream repo carries no license file, so terms are unconfirmed). Kit components as modeled: 4× 2806-class ~1300KV motors with 7×4-class 3-blade props (ground truth: UIUC Master Airscrew GF 7×4 measurements), a self-built 6S1P 21700 Li-Ion pack (4.2 Ah, 470 g, strapped on top of the deck), 30.5 mm Betaflight FC + 4-in-1 ESC stack between the plates, micro FPV camera, VTX + antenna, ELRS receiver. Fixed non-frame mass: 0.78 kg; baseline AUW ≈ 1.0 kg.
The generation-0 baseline genome is measured from the official Source One
V6 7in DC plate drawing (cached with provenance notes in
data/source_one/): 106.6×48.5 mm bottom plate, 2 mm plates, 6 mm arms,
M3×30 standoffs, ~160 mm arms. Assembly is modeled the way the real frame
bolts together: arm root tongues rest on the bottom plate inside the
sandwich (tongues may not collide — hard constraint), the FC/ESC boards sit
in the gap (and count as a bluff body for drag), the battery wedge hinges on
its front bottom edge so it never sinks into the plate, and the XT60/lead
are modeled visually. The best candidate of each generation is also exported
as individual print/cut-ready pieces in gen_XXXX_best_parts/
(bottom_plate, top_plate, arm ×4).
The genome no longer describes primitives — it deforms the official
Source One V6 7in DC plate drawings (data/source_one/, parsed from the
DXF with arcs, cutouts and every bolt hole) under zone constraints that keep
each candidate a printable/cuttable derivative of the real design:
arm_length_scale,arm_width_scale,arm_waist_scale,arm_thickness— stretch/reshape the real arm outlines; the bolt tongue and the 16×19 mm motor-mount end stay rigid.tip_thickness_scale— mid-shaft thickness dip as a fraction of root thickness (a bump profile: full thickness stays at the tongue and the motor mount, where it's needed for the plate-sandwich fit and screw engagement; 1.0 = uniform, no dip).arm_cutout_scale— three lightening holes drilled along each arm's shaft centerline, radius scaled off the local width there (0 = none).front_sweep_deg,rear_sweep_deg— rotate the arms about their drawing-registered plate anchors (front anchors are exact bolt-pattern registrations; DC arms keep their true left/right mirrored chirality).plate_length_scale,plate_width_scale,plate_thickness_scale,deck_gap— stretch the real main/mid/top plates (the 30.5 mm stack pattern stays pinned exactly), set the standoff length.battery_wedge_deg— tilt of the pack on the top plate.material— carbon plate / PA12-CF / PET-CF / PLA+ / PETG / ASA library.
Gene value 1.0 on every scale gene reproduces the real V6 exactly (the
generation-0 seed; its frame mass comes out at 144 g — the real frame weighs
~145 g). Every candidate assembles the full kit: 6×21700 cell pack, FC/ESC
stack, 2806 motors, 3-blade props, camera, VTX + ELRS antennas, GPS, XT60
and routed wire looms — dimension-accurate meshes (components.py).
Hard constraints (fitness = ∞, no simulation): the FC/ESC stack must fit the
deck gap, placed arm outlines must not overlap (the real notch interlock is
not redesigned), each tongue's bolt pair must land on main-plate material,
rotors need ≥ 5 mm from each other, and prop disks are checked against the
deck/battery in 3D (the real V6's rear props sweep below the top-plate
corners; deck_gap trades that margin).
Six scenarios (config/scenarios.yaml): calm_warm, cold_headwind, storm
(25 mm/h rain), crosswind, gusty_light (severe Dryden gusts),
hot_thin (1500 m density altitude). Every candidate flies all of them
over the identical mission: 2 km north + 2 km south at 12 m/s, 30 m AGL.
Each scenario has a fixed turbulence seed, so all candidates fly identical
gust histories — fitness differences are frame differences, never gust luck.
Designer rounds (headless Claude): every 6 generations (config:
designer in evolution.yaml), a headless claude -p call receives the
elite genomes with per-scenario results and the invalid-design histogram,
and proposes new genome vectors as design hypotheses — injected as operator
designer (violet in the family tree), rationales appended to
results/designer_log.md. The brief includes the known constraint couplings
(deck gap vs. FC stack, sweep vs. tongue collisions and bolt registration).
Fail-soft: if the CLI is unavailable the round is skipped.
Lab-notebook narrator (headless Claude): after each generation, ONE
claude -p call writes three short notes for every new candidate —
hypothesis (the pre-flight "why": what the search had observed and what
bet this design makes), method (what concretely changed vs. the parents),
result (outcome vs. parents and best-so-far, or the constraint that killed
it, plus one idea worth testing next). Rule-based fallback notes are written
instantly; the Claude enrichment runs on a background thread off the
loop's critical path and upgrades them in place. The notes appear in the
gallery detail blocks and the family-tree hover cards. Config: narrator
in evolution.yaml; fail-soft like the designer.
Patience: if the best-so-far stalls for 6 generations (no ≥0.5 %
improvement), the loop pivots — it crosses tournament winners with far
parents (the most genetically distant still-decent candidates in the run's
history) under boosted mutation, escalating to random parents if the plateau
persists. Derived from persisted history, so it survives --resume;
configured under ga.patience in config/evolution.yaml.
Fitness = mean Wh/km + λ·worst Wh/km (λ = 0.5), or pure worst-case with
aggregation: minimax. Thrust limiting (rotor RPM ceiling incl. battery
sag, per-motor power, pack deliverable power) clamps rather than
instantly failing — a gust transient costs tracking and energy, like real
flight. A scenario fails on sustained infeasibility: thrust-limited
beyond 10 % of the nominal mission time (blamed on the dominant limiter),
altitude/track divergence, mission not completed within 1.6× nominal time,
or the structural check. Any scenario failure ⇒ invalid. An optional
early-reject screen
(early_reject.enabled) flies calm_warm first and gives clearly-losing
candidates a finite penalized fitness without flying the other five.
| piece | model | source / check |
|---|---|---|
| rotor | CT(J), CP(J) interpolated from measured tables; T = ρn²D⁴CT, P = ρn³D⁵CP / 0.85 (motor+ESC) |
UIUC Propeller DB, Master Airscrew GF 7×4 (static + 4 RPM sweeps), cached in data/uiuc/; unit-tested against tabulated points |
| frame drag | component buildup: projected areas from rasterizing the STL along the flow at 0–60° tilt; handbook per-class Cds corrected by CFD-fitted tilt-dependent ratios and an assembly interference factor (wake shielding + silhouette double-count, −6 %/−27 %/−37 % at 0°/20°/40°); rotor-wash download on arm planform under the disks | calibrated against 12 OpenFOAM v2512 k-ω SST cases (cfd/calibration.md); round-trip unit-tested to ±3 % of the measured CdAs |
| turbulence | Dryden, MIL-F-8785C low-altitude forms, spectral synthesis, fixed seeds | variance unit-tested against σ² spec |
| rain | (a) water-film added mass ∝ top area, (b) momentum drag via equivalent suspended-water density + vertical impact force, (c) 15 % thrust-coefficient penalty | empirical knobs, see NASA TP-2671 (Dunham et al.) heavy-rain research; all in config |
| flight | 100 Hz point-mass 3-DOF sim; quasi-static attitude (the quad tilts into the relative wind), P velocity loop with accel limits; rotor speeds solved each step from the CT tables; electrical energy integrated | zero-wind mission unit-tested against a quasi-static analytic power balance (±6 %) |
| battery | quasi-static pack model: V = V0 − I·R, current solved from demanded motor power each step; energy integrated at the pack terminals (incl. I²R loss); RPM ceiling scales with sagged voltage; deliverable-power (V0²/4R ≈ 820 W) and cell-current (45 A) limits feed the saturation clock | P42A-class DC IR + discharge-curve sag; unit-tested (quadratic solve, IR-loss share, storm clamp behavior) |
| structure | station-by-station variable-section Euler–Bernoulli bending along the real morphed arm outline (net width minus any hole/cutout, Peterson Kt where one pierces a station, genome-selected thickness taper): worst-case per-rotor thrust across all scenarios × 1.5 safety factor; max stress ≤ material strength, tip deflection ≤ 5 % L, first bending mode outside ±15 % of hover 1P | stress/deflection unit-tested against hand calculations on a plain (untapered, unpierced) section, which reduces to the classic constant-section formulas |
Sanity anchors (unit-tested): the original spec anchor — a 5-inch quad at 1.1 kg AUW hovers at ~195 W (180–280 W band), checked against the cached GWS 5×4.3 dataset — plus the shipped platform: the baseline 7-inch deck (~0.34 m wheelbase, ~1.0 kg AUW) hovers in a plausible 90–230 W band on the MA GF 7×4 data.
- Phase A drag is approximate. Component buildup with handbook Cd values and a cos²-blend between body-x and body-y flow azimuths; no interference drag beyond the rotor-wash term, no Reynolds corrections.
- Rotor model is axial-flow data stretched to edgewise flight. UIUC wind tunnel sweeps measure propellers in axial advance; a quadcopter in cruise sees mostly edgewise inflow. J is computed from the axial inflow component only, and there is no rotor–rotor interaction.
- The rain model is empirical. Film mass, momentum drag and the 15 % thrust penalty are literature-inspired config knobs, not physics.
- No rotational dynamics. Attitude is quasi-static; control effort in gusts appears as thrust modulation, not motor differential torques. Gust series are synthesized at the commanded cruise speed rather than the instantaneous airspeed.
- UIUC data was measured at 2.5k–7k RPM and on a 2-blade prop; the kit flies 3-blade 7×4s and hovers near 9k RPM. CT/CP are treated as Re-independent and blade-count effects are absorbed into the tables.
- Print-material properties are XY/datasheet values. FDM parts are
weaker across layer lines (often 40–60 % in Z); in the LOOP the 1.5
safety factor is the only allowance for anisotropy, print quality, or
temperature — datasheet strength, not as-built.
airloom verify-championsre-checks the top frames with per-material as-built strength knockdowns on top of the same station-by-station model, and emits a bench print-and-test protocol. - Structural model checks the arms only (station-by-station stress along the shaft, tip deflection, resonance off the root station's stiffness); the deck is assumed rigid and the standoffs ideal.
- The battery model is quasi-static. Constant open-circuit voltage at the nominal 3.7 V/cell (no state-of-charge curve — conservative for a short mission that only uses ~15 % of the pack), constant DC internal resistance (no temperature or transient effects), no thermal limits for pack or motors. It exists to catch physically undeliverable power demands, not to predict voltage traces.
- Printability is enforced by real-outline geometry constraints, not full DFM. Parts are the actual (morphed) V6 outlines with their holes and cutouts, exported as separate flat pieces; but tongue bolt holes are assumed re-cut with the plates when sweep deviates from stock, the rear anchor is a best-fit (98 % clamp coverage) rather than a hole registration, and tolerances/print-orientation strength are not modeled.
- CMA-ES mode (
--optimizer cmaes) has no discrete parents — it samples from an adapted Gaussian — so the family tree is skipped and distribution-level provenance (mean, σ per generation) is stored incma_stateinstead.
airloom cfd-calibrate generated 12 OpenFOAM cases (arms alone, deck/kit
alone, full assembly, and a contrasting genome's full assembly — each at
0°/20°/40° tilt, freestream BCs, one snappyHexMesh per geometry) and the
overnight solve produced cfd/calibration.md. Findings: the handbook
buildup overestimated drag ~2× at cruise tilt; assembly interference
(wake shielding + silhouette double-count) is −27 % at 20°; and the
contrast genome's ratios matched the baseline's within ~2 points at every
angle — the error is systematic, not gene-dependent. The corrections
now live in aero.py (CAL_* tables, applied in
drag_table_from_areas), and the post-calibration airloom robustness
verdict is STABLE (min Spearman 0.958, no champion flips): the
ranking is a frame property, not a model artifact. Consequence: the
originally-planned per-candidate CFD pipeline (top-N per generation,
cached by genome hash) is not needed; the seam (DragTable from
AreaTable) remains if a future genome expansion moves outside the
calibrated shape envelope. Note: absolute Wh/km dropped with the
calibration, so the gallery's 5.0/4.0 benchmark lines predate it and need
re-derivation.
config/ platform.yaml (fixed hardware), scenarios.yaml (mission,
portfolio, aggregation + benchmark lines), evolution.yaml
(GA, patience, designer, narrator, execution)
data/uiuc/ cached UIUC propeller measurements (scripts/fetch_uiuc.py refreshes)
data/source_one/ official Source One V6 7in DC plate DXF + provenance notes
docs/ glossary.html (copied into results/ next to the gallery)
src/airloom/ config, genome, realgeo (DXF outline parsing), frame_gen,
components, meshutil, rotor_model, aero, dryden, simulator,
structures, evaluate, parallel, evolution, designer,
narrator, loop, dbstore, lineage, gallery, render, cli
tests/ frame validity (baseline reproduces the real V6), kit
components, materials, designer parsing, patience/pivot,
UIUC points, beam hand-calc, Dryden variance, zero-wind
energy, hover sanity anchors
results/ everything a run produces (see above)
airloom is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. See LICENSE for the full text.