EL01: Why a drone hovers: forces, units, and your first design loop
On this page
- Prerequisites and observable goals
- Warm-up: spaced review (10 minutes, closed book)
- 1. Quick summary
- 2. Deep explanation
- 2.1. The question every drone design starts with
- 2.2. Quantities and units: unit algebra is exponent bookkeeping
- 2.3. Mass, weight, and the gram-force trap
- 2.4. Motion as derivatives (Module 12, just in time)
- 2.5. Newton’s second law and the free-body diagram
- 2.6. What an accelerometer measures
- 2.7. Thrust-to-weight: the control budget
- 2.8. The mass budget
- 2.9. Energy, power, and flight time
- 2.10. The mass spiral: why a bigger battery helps less and less
- 2.11. Requirements that can be verified
- 2.12. Your first part in code: build123d
- 2.13. The hardware engine, and the Python this lab adds
- 2.14. Cross-checks: how independent is the second opinion?
- 2.15. Design trade-offs: why this engine looks the way it does
- 2.16. Beginner mistakes: a field guide
- 3. The lab: build the first slice yourself
- 3.0. Setup and ground rules
- 3.1. Step 1: quantities, and mass is not weight (worked example)
- 3.2. Step 2: Newton’s second law (partly filled)
- 3.3. Step 3: the thrust-to-weight family (on your own)
- 3.4. Step 4: energy, power, and flight time (on your own)
- 3.5. Step 5: the mass budget (on your own)
- 3.6. Step 6: the arm in build123d, and its hand check (worked, then on your own)
- 3.7. Step 7: your first requirements, and your first report (on your own)
- 3.8. Step 8: the phone test (predict, measure, integrate)
- 3.9. Step 9: the tinker loop (three knobs)
- 3.10. Read and change the engine
- 4. Practice, code reading, and the gate
- 4.1. Practice set
- 4.2. Code-reading drill: PX4 estimates its own hover thrust
- 4.3. Mastery gate
- 4.4. Next steps
- 5. Development log entry
- 6. Stretch (optional)
- Hint ladders for the lab
- L1. Step 1: weight
- L2. Step 2: Newton’s second law
- L3. Step 3: thrust-to-weight
- L4. Step 4: energy and flight time
- L5. Step 5: the mass budget
- L6. Step 6: the arm and its hand check
- L7. Step 7: requirements
- L8. Step 8: integrating the phone data
- L9. Step 9: the tinker loop
- Complete solutions: EL01
- Retention and readiness
Prerequisites and observable goals#
Calculus I through the chain rule (Modules 08–11). Module 04’s section on units helps. Module 12 (motion) is taught here where it is needed, so you can meet it here first. Python basics from C01–C02; section 2.13 covers the Python this lab adds (pint, TOML files, build123d), with C++ comparisons.
Carry units through every calculation and catch a units error by dimensional analysis; tell mass, weight, and gram-force apart; draw a free-body diagram and apply Newton’s second law along a line; predict what an accelerometer reads in a hover, a climb, and free fall, and test it with your phone; compute thrust-to-weight ratio, hover thrust fraction, and climb acceleration from real motor data; compute battery energy, hover power, and flight time, and explain why a bigger battery helps less and less; write requirements that can be verified; build your first parametric part in code and check its mass two independent ways; run, read, and change the first slice of your hardware engine.
Level: Engineering Lab, Module 1 of 29. About 20 hours in nine sessions: two to three weeks at the track’s pace of about 9 hours a week. Python 3.14 plus four pinned packages, installed into the lab’s virtual environment in section 3.0. No purchases: the physical test uses your phone.
Warm-up: spaced review (10 minutes, closed book)#
If your error log has no reviews due, use these three, drawn from the friction points described in LM. All three come back in this module.
- Rewrite and as powers of . (Notation disguises. Units use exactly this bookkeeping: section 2.2 writes m/s as m s.)
- Differentiate , then differentiate the result. (Power rule. Section 2.4 reads these two derivatives as velocity and acceleration.)
- Compute and . (Fractions under pressure. Section 2.7 needs both: a hover fraction and its reciprocal.)
The answers are at the very end of this chapter’s solutions.
1. Quick summary#
A quadcopter hovering, seen from the side, so each propeller you see hides a second one behind it. Each propeller throws air downward (blue): the stream it pushes speeds up and narrows below the disc. The air pushes back up on the propeller, and that reaction is the thrust (orange). Gravity pulls down on the whole drone as if at one point, its centre of mass (the quartered disc). In a hover the four thrusts add up to exactly the weight, so the forces balance and nothing accelerates. Every number in this module is about keeping that balance, breaking it on purpose, and paying for it with a battery. EL03 derives the thrust from the air’s change of momentum.
Five ideas carry this module.
- Every number is a quantity: a value times a unit. Unit algebra is exponent bookkeeping, the same bookkeeping as the power rule. An equation whose units do not match is wrong before you plug in a single number, and your hardware engine refuses to run it.
- Mass is not weight. Mass is how much stuff there is; weight is the force gravity puts on it, . The “grams” of thrust in a motor’s data table are a force in disguise: gram-force.
- Newton’s second law, , drawn as a free-body diagram. A hover is balance, and any imbalance becomes acceleration. An accelerometer feels every force except gravity, so on a hovering drone it reads the thrust per unit mass: , not zero.
- Thrust-to-weight is the control budget. A hover spends of the motors’ maximum thrust. What remains is everything the drone has for climbing, turning, and fighting gusts.
- Energy is power times time, and mass feeds back. Flight time is usable battery energy divided by hover power. A bigger battery stores more energy but weighs more, which raises the hover power. That loop is the first design loop you will close.
What you will build. In your lab repository, your own copy of the companion code’s eng-lab folder, you write the physics core of a Python hardware engine: weight and Newton’s second law, the thrust-to-weight family, energy and flight time, a mass budget with growth allowances, your first parametric CAD part (a drone arm, checked against your own hand calculation), your first requirements, and the analysis for a real measurement you take with your phone. Eighty-two tests check them step by step. The companion code provides the rest of the engine around your code: the units system, the design-file loader, the report that reruns every analysis whenever you change the design, and the tinker command that logs your predictions next to the results. By the end of this module, changing one number in design/drone.toml changes the arm’s CAD mass, the thrust-to-weight ratio, the flight time, and every requirement’s pass or fail, in one report. The Hover Budget Explorer, on the companion website at bezhig.com/book/explorers/el01-hover-budget.html, runs the same model in your browser: drag the battery, payload, motor count, and arm thickness, and watch the thrust arrows, the flight time, the mass spiral, and the accelerometer’s reading move.
| Session | Work | Where |
|---|---|---|
| 1 | Warm-up; units as exponent bookkeeping; mass, weight, gram-force | 1, 2.1–2.3; EL01.01–02 |
| 2 | Motion as derivatives; Newton’s second law and free-body diagrams; lab setup, steps 1–2 | 2.4–2.5; 3.0–3.2; EL01.03–04 |
| 3 | What an accelerometer feels; write your phone-test prediction | 2.6; 3.8 (predictions only); EL01.05 |
| 4 | Thrust-to-weight; lab step 3; the thrust table | 2.7; 3.3; EL01.06–07 |
| 5 | Mass budget, energy, flight time; lab steps 4–5 | 2.8–2.9; 3.4–3.5; EL01.08–09 |
| 6 | The mass spiral; requirements; lab step 7 and your first report | 2.10–2.11; 3.7; EL01.10–11 |
| 7 | Code-CAD: the arm and its hand check; lab step 6; the engine, its Python, and its trade-offs | 2.12–2.15; 3.6; EL01.12 |
| 8 | The phone test: measure, integrate, compare; the tinker loop; read and change the engine; the field guide of beginner mistakes | 2.16; 3.8–3.10; EL01.13–17 |
| 9 | Code-reading drill, mastery gate, development log; stretch if you want it | 4.2–4.4; 5; 6 |
2. Deep explanation#
2.1. The question every drone design starts with#
Before anyone draws a part, a drone designer asks two questions: will it lift itself with room to spare, and for how long? Both answers depend on the mass, and the mass depends on the answers. A longer flight needs a bigger battery; a bigger battery is heavier; a heavier drone needs more thrust; more thrust takes more power; more power drains the battery faster. Engineers call this a design loop, and the first job of a design is to close it: to find numbers that agree with each other all the way around.
The loop this module closes. Each box is a section of this chapter and a stage of your hardware engine. Change any number and every box downstream changes; your report reruns the whole loop in seconds.
This module gives you every box at its simplest: forces along one vertical line, efficiency read from a manufacturer’s test table, and a single part in CAD. Later modules replace each box with a better model: EL02 adds moments and balance, EL03 derives hover power from how propellers move air, EL04 adds the electrical side, and EL05 turns the loop into a full trade study.
2.2. Quantities and units: unit algebra is exponent bookkeeping#
A physical number is never just a number. “The arm is 150” means nothing until you know 150 what: millimetres, metres, or grams. A quantity is a value times a unit, and the unit is part of the quantity: is one length written two ways.
Base units. The International System of Units (SI) builds everything this module needs from four base units: the metre (m) for length, the kilogram (kg) for mass, the second (s) for time, and the ampere (A) for electric current. Every other unit is a product of powers of these. Here are the ones you will use, each with its exponents written out:
| Quantity | Unit | In base units | What it means |
|---|---|---|---|
| velocity | m/s | m s | metres gained per second |
| acceleration | m/s | m s | velocity gained per second |
| force | newton, N | kg m s | the push that gives 1 kg an acceleration of 1 m/s |
| energy (work) | joule, J N m | kg m s | a force of 1 N pushing through 1 m |
| power | watt, W J/s | kg m s | 1 joule every second |
| charge | coulomb, C A s | A s | 1 ampere flowing for 1 second |
| voltage | volt, V W/A | kg m s A | energy per unit charge |
Unit algebra is the power rule’s cousin. Multiplying quantities multiplies their units, and multiplying powers of the same unit adds exponents. So acceleration times time is
a velocity, as it should be. Dividing subtracts exponents. This is the same slip that turns into in a calculus answer: a dropped minus sign on an exponent. In units, a dropped sign turns seconds into per-second, and the answer is off by a factor of the time squared. Use the Parentheses Rewrite: write every unit as a product of powers before simplifying.
Converting is multiplying by 1. Since , the fraction equals 1, and multiplying by it changes the units without changing the quantity. Chain as many of these “ones” as you need, and cancel units exactly like factors.
Worked example: the energy in a drone battery. A battery label says 6S (six cells in series) and 1300 mAh. A lithium-polymer (LiPo) cell has a nominal voltage of 3.7 V, so the pack’s nominal voltage is . The “mAh” is milliampere-hours: a charge, current times time. Convert it to coulombs, then multiply by the voltage:
Check the units of the product: C V (A s)(W/A) W s J. The amperes cancel, which is why charge times voltage is energy.
Your turn (faded). A 4S 5000 mAh pack. Pack voltage: V. Charge: A h. Energy: W h. Check: 14.8 V, 5 A h, 74 W h. The drone simulator in the author’s FateMMO engine stores exactly this number, batteryCapacityWh = 74.0; EL01.10 audits it.
Dimensional homogeneity. You can only add, subtract, or compare quantities of the same dimension (length with length, force with force). Every term of a correct equation has the same units. And the input of , , or must be a pure number, with no units at all. That gives you a free test of any formula: check its units before trusting its numbers.
A units error that destroyed a spacecraft. NASA lost the Mars Climate Orbiter on 23 September 1999. One team’s ground software reported thruster impulse in pound-force seconds; the navigation software that used it expected newton-seconds, as the interface specification required. The navigation software therefore underestimated the effect of every small thruster firing on the trajectory by a factor of 4.45, the conversion from pound-force to newtons; the spacecraft reached Mars about 170 km lower than planned, and it was destroyed. No single line of code was “wrong”; the units at an interface did not match. The investigation report is listed at the end of this chapter.
Units in code. In C++ you already use a units library without calling it one: std::chrono. You cannot add a std::chrono::seconds to a plain int without saying what the int means, and seconds(2) + milliseconds(500) converts automatically. The C++ library mp-units extends the same compile-time checking to every physical dimension. Your hardware engine uses pint, which does the same thing in Python at run time: every value carries its unit, conversions are automatic, and adding metres to seconds raises an error. Section 2.13 shows how.
Say it back: why does multiplying acceleration by time give a velocity? Answer in exponents.
2.3. Mass, weight, and the gram-force trap#
Mass , in kilograms, is the amount of matter in an object and its resistance to being accelerated. It is the same on Earth, on Mars, and in orbit. Weight is a force: the pull of gravity on that mass,
where is the acceleration gravity would give the object if nothing else acted on it. Near Earth’s surface is about . For a definite number the world uses standard gravity, exactly, fixed by international agreement in 1901. Real local gravity varies by a few tenths of a percent, from about 9.780 m/s at the equator to 9.832 m/s at the poles. Weight is measured in newtons: a 1.5 kg drone weighs .
On Mars, where (NASA’s mean surface value), the same 1.5 kg weighs only 5.595 N. The mass did not change; the weight did. NASA’s Ingenuity helicopter used that to fly in an atmosphere about one percent as dense as Earth’s (EL01.14).
Scales measure force. A kitchen scale measures how hard the object pushes down on it, then divides by an assumed and displays grams. That is why hobby thrust tables are written in grams: a motor on a thrust stand pushes up on a scale, and the scale reports the “grams” that would push down equally hard. Those grams are gram-force (gf), the weight of one gram under standard gravity:
Two ways to make a scale read “1500 g”. On the left, a mass; on the right, a motor mounted upside down on a thrust stand, blowing air upward so that its thrust presses the stand down. Both push with 14.71 N. The display says grams in both cases, but only the left one is a mass; the right one is 1500 gram-force.
So a motor “making 1500 g of thrust” makes . Hobbyists like gram-force for a good reason: to hover, the total thrust in gf equals the mass in grams, so a 650 g drone hovers at 650 gf. The trap is the moment you put gram-force into a physics formula that expects newtons, such as power force speed (section 2.9): the answer comes out 9.8 times too small, with no error message. The hardware engine’s units system is what stops that.
Check yourself: a motor table lists 2.1 kg of thrust. In newtons? (20.6 N.)
2.4. Motion as derivatives (Module 12, just in time)#
Let be the drone’s height above the ground, in metres, at time in seconds. Three quantities describe its motion, and each is the derivative of the one before:
The units follow from the derivative: is a small change in height (metres) divided by a small change in time (seconds), so velocity is in m/s, and acceleration is in (m/s)/s m s. Positive velocity means rising; positive acceleration means the upward velocity is growing (or a downward velocity is shrinking). Throughout this module, up is positive.
Constant acceleration from rest. Suppose the drone starts at rest at and accelerates upward at a constant . Which function has derivative ? The function . Which function has derivative ? Try : its derivative is . It works, and both start at zero. So
Finding a function from its derivative is antidifferentiation, the subject of Module 16. For now, guessing and checking with the power rule is enough.
Worked example. A drone with (section 2.7) climbs from rest with for 1.0 s. Then m/s and m. One second of full thrust, and it is climbing at 35 km/h. (Real drones meet air drag long before this, which EL19 adds.)
Walking the chain backwards with a computer. Your phone’s accelerometer gives you at hundreds of instants a second, not a formula. To recover and , add up the small changes: over a short step , velocity changes by about , and height by about . That is exactly the update loop a game engine runs every frame, here in the grenade code of ZeroVector, the author’s tactical shooter:
grenade.velocity.y -= 9.81f * dt; // gravity changes the velocity
const vec3 displacement = grenade.velocity * dt; // the velocity changes the positionAveraging the acceleration at the start and end of each step, instead of using only the start, is called the trapezoid rule, and it is far more accurate for the same step size. Lab step 8 builds it; Module 23 proves how accurate it is.
A 2-second climb that speeds up and then slows to a stop 3 m higher (the shape your phone test will have). Top: height. Middle: velocity, the slope of height. Bottom: acceleration, the slope of velocity: positive while speeding up, negative while slowing down, and zero at the instant of fastest climb, where the height curve is steepest. Reading the chain upward is differentiation; reading it downward is integration.
Say it back: at the instant the climb is fastest, what is the acceleration, and why?
2.5. Newton’s second law and the free-body diagram#
Newton’s second law. The net force on a body equals its mass times its acceleration:
Read it as a statement about cause and effect. Forces that cancel produce no acceleration; whatever does not cancel, the net force, accelerates the body, and the heavier the body, the less it accelerates for the same net force. Force and acceleration have directions, so the law is a vector equation. In this module everything happens along one vertical line, so a sign carries the direction.
The free-body diagram: a four-step method.
- Isolate the body. Draw only the drone, cut free from everything else.
- Draw every external force where it acts: each thrust at its motor, the weight at the centre of mass, drag opposing any motion through the air, a push from the ground if the drone is sitting on it. Forces the drone’s parts exert on each other (a motor on its arm) are internal and are not drawn.
- Choose axes and a sign convention. Up is positive.
- Sum the forces and apply .
Worked example: hover. Four motors with thrusts (up), weight (down), no drag while still:
In a hover, , so . For a 1.5 kg drone, the four thrusts must total N, or 3.68 N (375 gf) each if they share it equally.
Worked example: climbing. A 1.2 kg drone runs all four motors at 5.0 N each. Net force: N upward. Acceleration: m/s upward.
Your turn (faded). A 0.80 kg drone; each motor makes 1.5 N. Net force: N. Acceleration: m/s. Is it climbing? Check: N; m/s. It accelerates downward: the thrust does not cover the weight. Whether it is moving up or down depends on its velocity, which the forces alone do not tell you.
This is the same equation as F01’s altitude model, , with drag added. F01’s lab drone has ; section 2.7 explains that choice.
Say it back: a drone rises at a constant 2 m/s. Draw its free-body diagram and say which force is larger.
2.6. What an accelerometer measures#
Your drone’s flight controller, and your phone, contain a MEMS accelerometer (micro-electro-mechanical system): a speck of silicon with a tiny proof mass held between springs. The chip measures how far the springs are stretched, which tells it the force the springs are exerting on the proof mass.
An accelerometer is a proof mass (blue) on springs inside a case. Gravity pulls the proof mass and the case equally, so gravity alone never stretches the springs: in free fall they relax and the reading is 0. The springs deflect only when another force (the table, the thrust) pushes the case, and the springs must pass that push on to the proof mass. The reading is that non-gravitational force per unit mass.
Why gravity is invisible to it. Gravity pulls on every gram of the phone equally, the case and the proof mass alike, so it accelerates them together and the springs never need to push. Only the other forces (the table, a hand, the thrust) act on the case alone, and the springs must pass them on to the proof mass. So an accelerometer measures the non-gravitational force per unit mass, called the specific force . From Newton’s second law along the vertical, with up positive: the non-gravitational forces are (whatever is needed, beyond gravity, to produce the acceleration ), so
- On a table: , so m/s upward. The phone reads “one up” while sitting still, because the table pushes it up.
- In free fall: , so . A dropped phone reads zero, even though it is accelerating as hard as gravity can make it.
- A drone: the only non-gravitational force is the thrust (plus drag), so . In a hover it reads ; climbing with acceleration it reads .
That last line is worth a pause: a drone’s accelerometer measures its thrust per unit mass. Divide the reading by and you have the drone’s instantaneous thrust-to-weight ratio. The drone controller in the author’s FateMMO engine uses exactly this relation in reverse: it computes the thrust it needs as (accelerationSetpoint + gravity) * mass, which is . And PX4, the open-source autopilot your drone will run, estimates its own hover throttle from this equation while it flies; the code-reading drill (section 4.2) walks through it.
Say it back: in one sentence each, why an accelerometer reads on a table and 0 in free fall.
2.7. Thrust-to-weight: the control budget#
Let be the number of motors and the most thrust one motor can make. The thrust-to-weight ratio compares the drone’s total maximum thrust with its weight:
It is a ratio of two forces, so it is a pure number with no units. Three quantities follow from it directly.
- Hover thrust fraction (the Greek letter phi): the share of the maximum thrust a hover uses, .
- What is left over, : the headroom for everything that is not hovering.
- Maximum climb acceleration: with every motor at full thrust, section 2.5 gives . At the drone can accelerate upward at exactly . At it cannot leave the ground.
Worked example: the drone in your design file. Its motor is a T-Motor AIR2216 II swinging a 10-inch propeller. The manufacturer’s bench test (the thrust table, below) gives gf at full throttle. Section 2.8 finds the predicted mass, g, which weighs 1158.6 gf. So
A hover uses 21.7% of the available thrust; 78% is left for manoeuvring.
Why designers ask for . A multicopter steers by making some motors push harder than others: to pitch forward, the back motors speed up and the front ones slow down. If the hover already demands most of the maximum, the motors that need to speed up cannot, the controller saturates, and the drone stops obeying. Three more demands eat into the same headroom:
- Tilting to move. A drone flies sideways by tilting. Its thrust then points partly sideways, and only the vertical part holds it up. Section 2.5’s balance at a tilt angle needs a total thrust of : 15% more than a hover at . EL01.13 shows that the steepest tilt that still holds altitude is , which is at .
- Climbing and gusts, which need and therefore thrust above .
- A sagging battery. The thrust table was measured with a bench power supply holding exactly 16 V. A real four-cell pack starts near 16.8 V and falls as it drains, and the motors’ maximum thrust falls with it. EL04 measures how much; until then, the margin covers it.
The usual answer to all four is a designers’ rule of thumb, not a law of physics: , a hover at no more than half of maximum thrust. Your requirement SYS-001 states exactly that, and section 2.11 turns it into a mass limit.
| Build | per motor | Mass | Hover uses | |
|---|---|---|---|---|
| 3-inch: T-Motor F1404 3800 KV, 3-inch prop, 4S | 324.5 gf | 350 g | 3.7 | 27% |
| 5-inch freestyle: T-Motor F60PRO V 1950 KV, 5.1-inch prop, 6S | 1990 gf | 520 g | 15.3 | 7% |
| Your worked example: AIR2216 II, 10-inch prop, 4S | 1332 gf | 1159 g | 4.60 | 22% |
| FateMMO’s simulated default quad (EL01.10) | 7.371 N | 1500 g | 2.00 | 50% |
| PX4’s simulated x500 (Gazebo) | 8.549 N | 2064 g | 1.69 | 59% |
Masses: T-Motor’s recommended take-off weights for the first two motors; section 2.8 for yours; the simulator’s parameters for FateMMO (EL01.10); PX4’s model files for its x500, at the commit PX4 v1.17.0 uses (x500_base: a 2.0 kg body plus four 16 g rotors; x500: the motor constant and top speed). Maximum thrusts: the makers’ thrust tables; FateMMO’s ; PX4’s . Sources at the end of the chapter.
Racing and freestyle quads carry huge thrust surpluses because they are flown aggressively; camera and mapping drones sit near 2 to 3 because every gram of motor is a gram of battery they cannot carry. The PX4 simulation model is surprisingly heavy for its motors, which is worth remembering when you compare your simulator with it in EL23.
Reading a thrust table. A motor maker mounts the motor and propeller on a test stand, holds the voltage fixed, steps the throttle, and records thrust, current, electrical power, and efficiency:
The AIR2216 II with a 10-inch propeller at a fixed 16 V, from the manufacturer’s table (the dots are its rows; lines join them). Left: thrust grows faster than throttle, so half throttle gives only 34% of full thrust. Right: efficiency, thrust per watt, is best at low thrust and falls as thrust rises. Your drone hovers at 289.6 gf per motor (orange): about 38% throttle, but only 21.7% of full thrust, at 8.54 gf/W.
Two lessons hide in that figure. First, throttle is not thrust. A propeller’s thrust grows roughly with the square of its speed (EL03 derives why), so the throttle setting that hovers is much larger than the hover thrust fraction . FateMMO’s drone simulator shows the same thing in its own terms: its hoverThrottle() (listed in EL01.10) returns the hover motor speed as a fraction of maximum, 0.706, while the hover thrust fraction is 0.499; and . Second, efficiency depends on where you read it. The table’s best number, 9.12 gf/W, is at 30% throttle; at full throttle it is 5.13 gf/W. Hover power must use the efficiency at the hover thrust, which the engine interpolates from the table.
Say it back: what is , and name two things the drone spends it on.
2.8. The mass budget#
A mass budget lists every component with its mass, grouped into subsystems, and adds them up. Two ideas turn that list into an engineering tool.
Numbers grow as a design matures. An early estimate forgets things: screws, wire, glue, a heavier connector than planned. Aerospace programs therefore add a mass growth allowance to each item according to how mature its number is. The standard most programs follow, ANSI/AIAA S-120A, gives ranges: roughly 10–35% for an estimate (up to 50–100% for estimated wiring harnesses), 3–25% for a preliminary design, and 1–5% for an item bought from a catalog. A measured part carries only its measurement uncertainty. Your design file uses 20% for estimates, 10% for CAD parts, 5% for catalog parts, and 1% for weighed parts:
where is the item’s mass and its allowance as a fraction.
Worked example: your design file’s budget.
| Item | Maturity | Mass | Allowance | Predicted |
|---|---|---|---|---|
| Battery, 4S 5000 mAh LiPo | catalog | 420.9 g | 5% | 442.0 g |
| Motors, 4 64 g | catalog | 256.0 g | 5% | 268.8 g |
| Arms, 4 28.75 g (your CAD, section 2.12) | CAD | 115.0 g | 10% | 126.5 g |
| Body plates, standoffs, screws | estimated | 90.0 g | 20% | 108.0 g |
| Propellers, 4 12.5 g | catalog | 50.0 g | 5% | 52.5 g |
| Wiring, connectors, battery strap | estimated | 45.0 g | 20% | 54.0 g |
| Landing gear | estimated | 40.0 g | 20% | 48.0 g |
| GPS and compass | catalog | 32.0 g | 5% | 33.6 g |
| Flight controller and 4-in-1 ESC | catalog | 21.8 g | 5% | 22.9 g |
| Radio receiver | catalog | 2.2 g | 5% | 2.3 g |
| Total | 1072.9 g | 1158.6 g |
The allowances add 85.6 g, 8% of the basic mass. The battery alone is 38% of the predicted mass, which is why section 2.10 exists.
From a requirement to a mass limit. Requirement SYS-001 says . Solve the definition for the mass: g. The mass margin is the share of the allowable mass not yet spent:
A large margin this early is comfortable: it can pay for a camera, a bigger battery, or a heavier but stronger arm later.
2.9. Energy, power, and flight time#
Work and power. A force that pushes something through a distance along its line does work (joules, N m). Power is work per unit time. If the force is constant while the distance changes, : force times speed. (This is where the units trap of section 2.3 bites: put gram-force into and the power comes out 9.8 times too small.) Energy is power accumulated over time; at constant power, .
Battery energy is charge times voltage (section 2.2): your pack stores . You should not use all of it. Makers recommend stopping at about 3.5 V per cell under load and never going below 3.0 V, where damage begins; and the last part of the charge comes out at a sagging voltage. So flight-time estimates use a usable fraction of the stored energy; 0.8 is a common choice and your design file’s default.
Hover power from the thrust table. Each motor must make gf in a hover (section 2.7). The table’s efficiency at that thrust is gf/W: every watt of electrical power buys 8.54 gram-force of thrust. So each motor draws W, and the four draw 135.7 W. The flight controller, GPS, and receiver add about 4 W, for a total of W.
Flight time at constant power is usable energy over power:
Two second opinions, and only one of them is independent. First, the hover current is A; the usable charge is A h, and h. Same answer, but it had to be: the current came from the same power, so charge over current is energy over power in disguise. That catches an arithmetic slip, never a wrong model. Second, a route that really is different: the thrust table also records the current each motor drew, measured rather than computed. Read at the hover thrust, it is 2.13 A per motor; with 0.27 A for the electronics the total is 8.78 A, and min. The two disagree by about 2 minutes, and the disagreement is information. The table was measured at 16 V, your pack sits near 14.8 V, and at the same thrust a motor needs about the same power, so it draws about times the table’s current. Energy over power accounts for that; the table’s current does not. Your report shows both checks, labelled by kind (section 2.14).
Your turn (faded). Add a 200 g camera. New predicted mass: g. Hover thrust per motor: gf. Reading the table between its 309 gf and 373 gf rows, efficiency near that thrust is about 8.3 gf/W, so motor power is W; add 4 W. Flight time: min. Check: 1398.6 g; 349.6 gf; W, plus 4 W is 172.5 W; h min. The engine interpolates the efficiency exactly (8.26 gf/W) and reports 20.5 min; run python -m hwe tinker "payload.mass=200 g" --predict "..." to see.
2.10. The mass spiral: why a bigger battery helps less and less#
A bigger battery stores more energy, but it also adds mass, and more mass raises the hover power. Where does that end?
A first model. Suppose efficiency did not depend on thrust. Let be the battery’s mass and the mass of everything else, the battery’s specific energy (energy per unit mass: your pack’s is W h/kg), and the thrust efficiency. Hover power is the total weight divided by the efficiency, so
The fraction is the battery’s share of the whole drone. It grows when grows, but it can never reach 1, so the flight time creeps toward a ceiling and never passes it. Each added gram of battery buys less flight time than the one before: diminishing returns. For your drone, minutes, which only an all-battery drone could reach.
With the real propeller, the curve turns over. Real efficiency falls as thrust rises (the thrust table’s right panel), so a heavier drone pays more watts for every gram. The engine sweeps the battery capacity from a quarter to eight times the design’s, holding the pack’s specific energy fixed, and reads the table’s efficiency at every new hover thrust:
Flight time against battery capacity for your design file’s drone, from the engine. Doubling the battery from 5000 to 10,000 mAh buys 37% more flight time, not 100%. The longest flight, 40.4 minutes, needs a 25,000 mAh pack weighing 2.1 kg, three times the rest of the drone, and by then has fallen to 1.82 (shaded: fails SYS-001). The largest battery that still meets is about 22,000 mAh.
That figure is the design loop of section 2.1, drawn. Engineering is choosing a point on it: the best flight time is not the best design, because it fails the control budget. EL03 derives the shape of this curve from how a propeller accelerates air, and uses Module 14’s optimization to find the peak with a pencil: for an ideal rotor, the best battery weighs twice everything else.
2.11. Requirements that can be verified#
A requirement states what the system shall do. The word matters: in engineering documents “shall” marks a binding requirement, “should” a goal, and “will” a statement of fact. A good requirement is one sentence, about one thing, with a number and a unit, that someone can check. Compare:
- Bad: “The drone should fly for a long time.” (No number; “should”; how long is long?)
- Good: “The drone shall hover for at least 12.0 minutes at sea level from a full charge to a resting voltage of 3.5 V per cell.” (One quantity, a unit, and the conditions under which it is measured.)
Every requirement names how it will be verified. NASA’s Systems Engineering Handbook, like most aerospace programs, uses four methods:
- Inspection: look, weigh, or measure the thing itself (weigh the drone on a scale).
- Analysis: calculations and models (the engine’s ).
- Demonstration: operate it and observe a result, without detailed measurement (it hovers).
- Test: operate it under controlled conditions with instruments, and record data (a timed hover with a logged voltage).
Derived requirements and margins. Requirements form a tree: a top-level need (a flight time) produces derived requirements (a battery energy, a mass limit) that each part must meet. Section 2.8 derived the allowable mass, 2664 g, from SYS-001. A parent field records that link so that a change can be traced to everything it affects. The margin records how comfortably the design meets each requirement, and your report prints it for every one.
Regulations are requirements too. Your drone’s mass decides which rules apply to it. United States federal rules (14 CFR 48.15) require a drone flown only for recreation to be registered with the FAA if it weighs more than 0.55 pounds, about 249.5 g, at take-off, counting everything on board; once registered, it must broadcast Remote ID, and a home-built drone does that with an FAA-accepted broadcast module. A drone flown under Part 107, the rules for non-recreational flying, must be registered whatever it weighs, and flights that produce material for a professional portfolio are safest treated as Part 107. Your worked example weighs 1159 g, so it must be registered either way; a sub-250 g design would avoid registration only for purely recreational flying. EL06 covers these rules in full, with their sources, before you build or fly anything.
2.12. Your first part in code: build123d#
CAD (computer-aided design) programs build exact 3D models of parts. Most engineers use a graphical program such as Siemens NX, CATIA, SolidWorks, or Creo: sketch a profile with the mouse, constrain its dimensions, extrude it into a solid, cut holes, and the program records every step in a feature history. Code-CAD writes the same steps as a program. You will use build123d, a Python library on top of OpenCASCADE, an open-source geometry kernel. The model is a boundary representation: a solid is stored as its exact bounding surfaces, so a circle stays a true circle rather than becoming a polygon. That is why the volume build123d reports agrees with a hand formula to about sixteen significant digits, the limit of a double-precision number: your report shows a difference near .
Why code, when industry works in NX? Three reasons that matter for this project. A part written as a function of its dimensions is parametric: change one argument and the whole part regenerates, so your report can re-run the CAD whenever the design changes. Code can be tested, diffed, and reviewed like any other code. And every feature in a graphical CAD program has a counterpart in code, so what you learn here transfers: EL11 rebuilds this arm in NX and maps each click to a line of build123d.
The arm. Your first part is a flat arm like those cut from carbon-fibre plate for small quadcopters: a bar from the frame to the motor, a square pad under the motor with four screw holes and a centre hole, and two bolt holes where it clamps into the frame.
The arm from above, with the ten parameters its function takes: length , bar width , pad size , motor-hole spacing , the frame holes’ inset and pitch, the three hole diameters, and the thickness . The pad is centred on the motor. Drawn to scale for your design file’s arm (, , , , , in mm); the worked example below uses the lab tests’ smaller arm. Every dimension is a knob: EL07 will use this same function to find the thinnest arm that survives a crash load.
The same arm in an isometric view, drawn by the engine from the CAD solid: visible edges solid, hidden edges dashed, the way engineering drawings show a part. build123d projects every edge and removes the hidden ones; hwe/cad/drawing.py only draws the result.
The units boundary. build123d, like most CAD kernels, takes plain numbers and assumes millimetres. The engine never uses a plain number. So there is exactly one border crossing, the function mm() in hwe/cad/boundary.py, which turns a length quantity into a bare number of millimetres. Hand build123d a length in metres (0.15) where it expects millimetres (150), and the part comes out 1000 times too small in every direction, with a volume times too small, and no error anywhere. Converting at one named boundary is how real systems keep that from happening; the Mars Climate Orbiter’s interface had no such guard.
Mass from geometry. For a part made of one material of uniform density (mass per unit volume), mass is density times volume, . The volume is where CAD earns its keep on complicated parts. For this flat arm you can also get it by hand, which is the point: the top-view area is the bar plus the pad, minus the rectangle where they overlap, minus the seven holes, and the volume is that area times the thickness :
where is the length, the bar width, the pad size, and , , the motor-hole, centre-hole, and frame-hole diameters. (The overlap is long because the pad is centred on the motor, so half of it lies over the bar.) For the arm the lab tests use, with , , , , , , and (all in mm):
build123d reports for the same part. Two independent methods, one answer. At the density of a typical carbon-fibre plate, about (one maker lists 1.44 to 1.61 g/cm across its solid plates; see the chapter’s sources), one arm is g. Your design file’s longer, wider arm (190 by 16 mm, section 2.8) comes out at 18,547 mm and 28.75 g.
2.13. The hardware engine, and the Python this lab adds#
The hardware engine is a pipeline. Each stage reads the design files and the stages before it, and the report shows every result with its cross-check.
The EL01 slice of the engine. Every arrow carries quantities with units. A stage whose core code you have not written yet shows “not written” in the report instead of disappearing; a stage a later module adds shows “locked” with the module that unlocks it.
The Python this lab adds, with C++ comparisons.
| C++ | Python in this lab |
|---|---|
std::chrono::duration<double> | A pint Quantity: Q_(1300, "mA*h"). Arithmetic keeps units; .to("W*h") converts; .m_as("mm") gives a bare number in a named unit, used only at a boundary. Adding incompatible units raises DimensionalityError. |
| A wrapper template around a function | A decorator. The line @quantities(mass="[mass]", returns="[force]") above a function replaces it with a wrapper that checks every argument’s dimension and the result’s before and after your code runs. It is plain Python: read it in hwe/units.py. |
A struct with named fields | A @dataclass, such as Item(name, subsystem, mass, maturity, source) in hwe/design.py. |
| A config file parsed at start-up | A TOML file read with the standard library’s tomllib: design/drone.toml holds every knob, each with its unit as a string (length = "100 mm"). |
| An RAII scope object | A with block. In build123d, with BuildPart() as arm: opens a part under construction; shapes created inside join it, and leaving the block finishes it. |
std::vector<double> plus a unit | A pint Quantity wrapping a numpy array, as the phone data arrives: t = Q_(np.array([...]), "s"); t[k] is one Quantity. |
std::accumulate(first, last, init) | sum(items, start). Without a start, sum begins from the plain int 0. pint lets 0 + mass through as a special case, so a non-empty list works, but an empty list returns the bare int 0, with no unit, and the next line breaks. Give the start a unit, Q_(0, "g"). |
The commands (run from your eng-lab folder with the virtual environment’s Python): python -m hwe report runs every unlocked stage and writes out/report/index.html; python -m hwe watch reruns it whenever you save a design, data, or code file; python -m hwe tinker KEY=VALUE --predict "..." changes knobs without editing the design file, compares against the baseline, and logs your prediction beside the result; python -m hwe explain N "..." adds your explanation to entry .
2.14. Cross-checks: how independent is the second opinion?#
An engineer who reports one number with one method is asking to be trusted. An engineer who reports it three ways is showing why they should be. The three kinds of evidence, in rising order of authority:
- A hand calculation from first principles, on paper: the arm’s volume from its geometry, the flight time from energy over power.
- An independent model: a different method, program, or data set that shares as little as possible with the first: build123d’s exact solid against your hand formula; flight time from the thrust table’s measured current against energy over power from its efficiency (section 2.9).
- A measurement: the kitchen scale, the thrust stand, the flight log. Only measurement tells you whether your model matches reality.
Not every second calculation counts, so your report labels each check by kind:
- Independent: a different method or different data. Only this kind can catch a wrong model.
- Recomputed: the same formula and inputs worked a second way, like in plain gram-force arithmetic. It catches slips in code.
- Identity: two results that must agree by algebra, like hover fraction times , or charge over a current computed from the same power. It catches arithmetic slips only.
- Assumption: a check that an input suits the model, like the pack’s voltage against the thrust table’s test voltage.
Engineers split this into verification (did we solve the equations right?) and validation (did we solve the right equations?). Hand calculation against CAD is verification: both assume a solid, uniform arm, so they can agree perfectly and both be wrong about a 3D-printed part. Only weighing the part validates the model. Your report’s cross-check table shows each pair side by side with its relative difference, and flags any pair that disagrees beyond its tolerance.
Two cautions. Independence matters: two methods that read the same wrong input will agree on the wrong answer. The current-column check shares its bench test with the efficiency column, so it can expose a table that was misread or misapplied (the field guide’s U7), but not a table that is wrong; only your own thrust stand in EL15 can do that. And the tolerance must match the method: CAD against exact geometry should agree to about ; two models built on different assumptions, like the table’s 16 V and your pack’s 14.8 V, agree only as well as those assumptions allow; a model against a measurement agrees to a few percent at best. That disagreement is information, not failure.
2.15. Design trade-offs: why this engine looks the way it does#
- Units at run time, not compile time. pint checks units while the program runs, which costs time and catches errors only on paths you execute. Measured on one desktop PC: multiplying two plain Python floats took about 11 ns, multiplying two pint quantities about 4.3 s, a few hundred times slower (between 340 and 420 times across three runs). C++ libraries such as mp-units check at compile time for free, but Python has no compiler to do it. For design analysis the cost is invisible; for a flight controller running a thousand times a second it would not be, which is why EL19’s C++ simulator uses plain doubles with units in the names and tests instead.
- A text design file, not a GUI. Every knob lives in
drone.toml, so a design change is a one-line diff your Git history records, reviewable like code. A GUI is faster to poke at but leaves no reviewable trail. - The report is rebuilt from scratch every run. No number is typed into the report by hand, so it can never disagree with the design: the same reason continuous integration beats a hand-updated spreadsheet. The price is that every analysis must be automated, including the ones you would rather eyeball.
- Data tables versus physics models. The thrust table is measured, so it is accurate inside its tested range and silent outside it (the engine refuses to extrapolate it). Momentum theory (EL03) extrapolates anywhere but must be validated against tables and tests. Real engineering uses both and watches where they disagree.
- Code-CAD versus industry CAD. Code is parametric, testable, and diffable, but slower for sculpting organic shapes and rarer in industry job postings: of the 45 postings in EL00, SolidWorks and CATIA appear in six each and NX in five. You learn the concepts here and the industry tool in EL11.
2.16. Beginner mistakes: a field guide#
These are the mistakes people really make in a first sizing calculation. Each has a symptom you can recognize and a way to catch it. For all but U7, a test in your lab catches it: that test was run against a copy of the code with the mistake planted in it, and the copy fails. U7 lives in the engine’s own code, so the report guards against it instead.
| Mistake | Symptom | How to spot it; which test catches it | |
|---|---|---|---|
| U1 | mAh treated as energy | The pack with “more mAh” is assumed to fly longer, even at a lower voltage | Units: mAh is charge. pint refuses to return it as energy; step 4’s test_more_mah_can_mean_less_energy |
| U2 | Grams of thrust used as a mass, or put into a formula expecting newtons | Power or acceleration off by a factor of 9.8 | Step 3’s tests (dividing gram-force by grams fails nine of them); test_wrong_unit_for_thrust_is_refused shows the engine refusing g where gf belongs. Write gf, never g, for thrust |
| U3 | Forgetting : weight written as | “Weight 1.5” where 14.7 N belongs | The decorator refuses a mass where a force is due; step 1 |
| U4 | One motor’s thrust used as the total, or the weight not shared among the motors | four times too small, or hover thrust per motor four times too big | Step 3’s test_ttw_counts_every_motor and test_hover_thrust_per_motor |
| U5 | Growth allowance subtracted, applied twice, or quietly zero for an unknown maturity | A mass budget that looks healthier than it is | Step 5’s allowance tests and test_unknown_maturity_is_an_error |
| U6 | Full-charge voltage (4.2 V per cell) used for energy | Flight time about 13% too optimistic | Step 4’s energy tests, which use the default voltage: test_energy_of_a_6s_1300_pack gets 32.76 W h instead of 28.86 |
| U7 | Efficiency read at the wrong throttle | A table’s best efficiency is at low thrust; using it for a hover overstates flight time | The report reads efficiency at the hover thrust and cross-checks it against the table’s measured current (section 2.9); EL01.07 |
| U8 | A sign flip in the free-body diagram | A climbing drone with negative acceleration, or weight added to thrust | Step 2’s test_acceleration_sinking_is_negative |
| U9 | “In a hover the accelerometer reads zero” | A phone at rest “should” read 0 but reads 9.8 | Step 2’s accelerometer tests; your phone (section 3.8) |
| U10 | Metres handed to CAD that expects millimetres | A part 1000 times too small and times too light | Step 6’s test_cad_volume_matches_with_other_units |
| U11 | Stripping units early (.m in mid-calculation) | Numbers in mixed units added silently | Code review: .m appears only at a named boundary. At the CAD boundary, step 6’s test_cad_volume_matches_with_other_units catches it |
| U12 | Integrating accelerometer data without removing gravity and bias | Height grows like : metres of drift in seconds | Step 8’s lift tests; section 3.8 |
3. The lab: build the first slice yourself#
3.0. Setup and ground rules#
Everything lives in your own copy of the companion code’s eng-lab folder; put it under Git, because it is designed to become your public repository. If you do not have Python 3.14 yet, install it from python.org. Then, in PowerShell, go to that folder, create the virtual environment .venv once, install the four packages pinned in pyproject.toml (pint 0.26.1, numpy 2.5.3, matplotlib 3.11.2, and build123d 0.13.0), and run the first check:
cd path\to\eng-lab
py -3.14 -m venv .venv
.venv\Scripts\python.exe -m pip install -e .
.venv\Scripts\python.exe labs\el01_hover\check.py 1On macOS or Linux the same steps are python3.14 -m venv .venv, .venv/bin/pip install -e ., and .venv/bin/python labs/el01_hover/check.py 1; this chapter writes the Windows forms. Four of step 1’s five tests stop at NotImplementedError: Step 1: weight, and the summary reports step 1 as “not written yet”. That is the starting line. The fifth, test_bare_number_is_refused, already passes: the decorator refuses a bare number before your function even runs. To type plain python instead of the long path, activate the environment first with .venv\Scripts\Activate.ps1; if PowerShell’s execution policy blocks the script, keep using the full path, which always works.
- Your files are the five in
hwe/core/that say “YOU WRITE THIS FILE” (each starts as docstrings only), plusdesign/requirements.toml,labs/el01_hover/predictions.toml, your phone data, and your tinker log. The tests inlabs/el01_hover/tests/and everything else inhwe/were written for you; read them freely, but don’t edit the tests. - Type the code yourself. Don’t paste from the book. Typing is how your hands learn the patterns.
- Predict before every run, in writing. The orange boxes say what to predict.
- When a test fails, read the test before your code. Each test’s comment shows the arithmetic it expected.
- Stuck? Climb that step’s hint ladder (after the practice set) one rung at a time.
- The tests are the judge. A step is done when
check.pyreports it passing on your own code. After an honest attempt, compare your code with the step’s worked listing or its hint ladder’s skeleton, and with the usual first failures in EL01.16’s solution; find your first incorrect step before you change anything.
3.1. Step 1: quantities, and mass is not weight (worked example)#
First, meet pint in the Python prompt. Start it with .venv\Scripts\python.exe, then type:
from hwe.units import Q_, G0
m = Q_(1.5, "kg")
w = m * G0
print(w) # 14.709975, in kg times m/s^2: correct, not yet simplified
print(w.to("N")) # 14.709975 N
print(Q_(1500, "gf").to("N")) # a thrust table's "1500 g"
print(Q_(1300, "mA*h").to("C")) # charge, not energy
Q_(1, "m") + Q_(1, "s") # DimensionalityErrorNow open hwe/core/hover.py and replace the body of weight with one line:
@quantities(mass="[mass]", g="[acceleration]", returns="[force]")
def weight(mass, g=G0):
"""The weight of a mass: W = m g, the force gravity exerts on it. ..."""
return (mass * g).to("N")Line by line: the decorator line (already in the file) states the contract: mass must be a mass, g an acceleration, and the result a force; it checks all three on every call. g=G0 makes standard gravity the default while letting a caller pass Mars gravity. mass * g multiplies the magnitudes and adds the unit exponents (kg times m s); .to("N") expresses the result in newtons. Leave the docstring in place.
Run .venv\Scripts\python.exe labs\el01_hover\check.py 1.
3.2. Step 2: Newton’s second law (partly filled)#
Goal. net_force, acceleration, and accelerometer_reading. Here are the first two with their key pieces left blank; fill them from section 2.5:
def net_force(thrusts, mass, g=G0):
total = sum(thrusts, ____) # a start value WITH units: zero newtons
return ____ - weight(mass, g)
def acceleration(thrusts, mass, g=G0):
return (____ / mass).to("m/s^2") # Newton's second law, solved for aWhy give sum a start value? Unless told otherwise it begins from the plain integer 0. pint makes a special exception for adding or subtracting 0, so here the arithmetic would still work. But where sum’s result is returned directly, as in step 5’s mass budget, an empty list would hand back the bare int 0, a number with no unit, and the caller’s next .to("g") would fail. Writing the start as zero newtons states the unit and makes every case, even the empty one, a force: a habit worth keeping. Now write accelerometer_reading on your own from section 2.6: one line.
Run check.py 2.
3.3. Step 3: the thrust-to-weight family (on your own)#
Goal. thrust_to_weight, hover_thrust_per_motor, hover_thrust_fraction, and max_climb_acceleration, from section 2.7 and their docstrings. No code shown this time. Reuse weight; each function is one or two lines.
Run check.py 3.
3.4. Step 4: energy, power, and flight time (on your own)#
Goal. battery_energy, propulsion_power, and flight_time in hwe/core/energy.py, from section 2.9. flight_time must refuse nonsense: a usable fraction outside , or a power that is not positive, raises ValueError (Python’s raise ValueError("...")).
Run check.py 4.
3.5. Step 5: the mass budget (on your own)#
Goal. The five functions in hwe/core/massbudget.py, from section 2.8. Read the module docstring first: it names the Item fields and the one Python trap in this file.
Run check.py 5.
3.6. Step 6: the arm in build123d, and its hand check (worked, then on your own)#
Goal. arm() builds the part in section 2.12’s plan view; arm_volume_by_hand() computes its volume from geometry alone. Type arm() from this listing:
def arm(length, width, thickness, pad_size, motor_hole_spacing, motor_hole_diameter,
center_hole_diameter, frame_hole_diameter, frame_hole_inset, frame_hole_pitch):
from build123d import (BuildPart, BuildSketch, Circle, GridLocations, Locations,
Mode, Rectangle, extrude)
L, w, t, a = mm(length), mm(width), mm(thickness), mm(pad_size) # the units boundary
s, dm, dc = mm(motor_hole_spacing), mm(motor_hole_diameter), mm(center_hole_diameter)
df, x1, p = mm(frame_hole_diameter), mm(frame_hole_inset), mm(frame_hole_pitch)
with BuildPart() as part:
with BuildSketch(): # a flat profile on the XY plane
with Locations((L / 2, 0)):
Rectangle(L, w) # the bar, centred at L/2
with Locations((L, 0)):
Rectangle(a, a) # the pad, centred on the motor
with Locations((L, 0)):
with GridLocations(s, s, 2, 2): # a 2 x 2 grid, s apart
Circle(dm / 2, mode=Mode.SUBTRACT) # motor screw holes
Circle(dc / 2, mode=Mode.SUBTRACT) # centre clearance hole
with Locations((x1, 0), (x1 + p, 0)):
Circle(df / 2, mode=Mode.SUBTRACT) # frame bolt holes
extrude(amount=t) # the profile, t thick, upward
return part.partLine by line: every length crosses the units boundary once, at the top, into plain millimetres. BuildPart opens a solid under construction and BuildSketch a flat profile inside it; a shape created inside a Locations block is centred at each listed point. Shapes add to the profile by default; mode=Mode.SUBTRACT cuts them out instead. Circle takes a radius, so every diameter is halved. extrude sweeps the finished profile upward into a solid, and part.part is that solid.
Now write arm_volume_by_hand() on your own from section 2.12’s formula. Keep the units: your function receives quantities and returns a volume.
Run check.py 6. These tests build real CAD, so the first run takes several seconds while build123d loads its geometry kernel.
3.7. Step 7: your first requirements, and your first report (on your own)#
Goal. At least four more requirements in design/requirements.toml, beside the worked SYS-001, following section 2.11 and the file’s own comments. Between them, use the verification methods Analysis, Test, and Inspection; derive at least one from another (its parent); give every threshold a unit when its result has one; and write a one-sentence rationale for each. A requirement the current design fails is fine: that is information, not a mistake.
Then run the whole engine for the first time and open the result in your browser:
.venv\Scripts\python.exe -m hwe report --openRun check.py 7.
3.8. Step 8: the phone test (predict, measure, integrate)#
This is your first physical test, and it follows the rule every test in this track follows: the prediction is written, and saved, before the measurement is taken. check.py compares the files’ timestamps and reports whether your prediction came first.
(a) The prediction. Open labs/el01_hover/predictions.toml and fill in every value its comments ask for (you may have done this in session 3): the reading at rest, whether it rises or falls while you lift and while you stop, the height your double integration will report compared with the tape measure, and an honest error band.
(b) The measurement. Install phyphox, a free physics app from RWTH Aachen University, on your phone. Use its “Acceleration with g” experiment, which records the raw accelerometer, gravity included.
- Lay the phone screen up on a thick book on a table, next to a wall with a tape measure or ruler taped vertically. Start the recording and keep everything still for 2 seconds.
- Lift the book and phone smoothly straight up by about 30 cm, taking about a second, and hold it steady at the top for 2 seconds. Stop the recording. Read the lift height off the tape measure to the nearest half centimetre.
- Export the data as CSV (comma separator, decimal point), copy it to your computer, and save it as
data/tests/el01_phone/lift.csv. Its columns areTime (s)andAcceleration x (m/s^2)throughAbsolute acceleration (m/s^2), so the engine reads the units straight from the header. - Check the sign: look at the first second of
Acceleration z. If it reads about instead of , your phone reports the opposite convention; setsign = -1under[tests.phone]indesign/drone.toml.
(c) The analysis code. Write cumulative_trapezoid, remove_bias, and velocity_and_height in hwe/core/kinematics.py on your own, from section 2.4 and the docstrings. The tests use synthetic lifts with a known answer, including one with random sensor noise and one that ends while still moving. Run check.py 8.
(d) Predicted versus measured. Run python -m hwe report and open the phone section: your raw reading, the velocity and height you integrated, and the final height.
3.9. Step 9: the tinker loop (three knobs)#
Every concept in this track comes with a knob. The loop: predict what changing it will do, run it, then explain the result, especially when your prediction was wrong. The engine enforces the order: tinker refuses to run without a written prediction.
.venv\Scripts\python.exe -m hwe tinker "battery.capacity=8000 mA*h" --predict "flight time up, but less than the 60 percent the capacity grew: the pack gets heavier. T/W down"
.venv\Scripts\python.exe -m hwe explain 1 "..."Turn at least three different knobs, one at a time. Good first choices, each tied to a section:
Each entry lands in logs/tinker.csv and in your report. Run check.py 9.
3.10. Read and change the engine#
The engine is yours to read and change, line by line. Three short tasks, in order:
- Read
hwe/units.py(about 150 lines). Explain in writing whatrequiredoes when it receives a bare float, and how the@quantitiesdecorator finds the argument namedmassamong positional arguments. (Hint:inspect.signature(func).bind.) - Read
stage_hoverandstage_energyinhwe/pipeline.py. For each cross-check they record, say what the two methods are and whether they are truly independent (section 2.14). Then compare your answers with the Kind column of your report’s cross-check table. - Change the report: add “Hover current” (the result
energy.hover_current, in amperes) to the tiles at the top ofhwe/report/html.py. One line inKEY_RESULTSdoes it. Runpython -m hwe watchin a second terminal first, and watch the report rebuild when you save.
When check.py reports all nine steps passing, you have the first slice of a hardware engine whose physics you wrote. Copy the summary line into your development log (section 5).
4. Practice, code reading, and the gate#
4.1. Practice set#
F = foundation; C = standard; T = transfer; S = optional stretch; L = lab; R = code reading; G = mastery gate. Follow the session plan in section 1. Attempt each problem completely before opening its solution, carry units through every line, and log every miss in your error log with its friction family (LM shows the columns).
Units as exponents. Write each in base units (kg, m, s, A) with exponents, then simplify and name the quantity: (a) N/kg; (b) W s; (c) (m/s)/s; (d) J/(A s); (e) C V/s.
Multiply by 1. Convert, showing each factor of 1: (a) 2200 mAh to coulombs; (b) the energy of a 6S 2200 mAh LiPo in W h and in kJ (3.7 V per cell); (c) 1.8 kgf to newtons; (d) a thrust-table entry of “350 g” to newtons; (e) is 100 W h the same as 360 kJ?
Mass, weight, gram-force. A 650 g quadcopter. (a) Its weight in newtons. (b) Each motor’s hover thrust, in gram-force and in newtons. (c) A motor’s data sheet lists “2.1 kg” of maximum thrust. What is that in newtons, and could four of them lift a 9 kg camera drone?
Motion from rest. A drone climbs from rest with a constant upward acceleration of 2.5 m/s for 4.0 s, then holds that climb speed. (a) Its speed and height at 4.0 s; check by differentiating it twice. (b) When does it reach 60 m?
Free-body diagram. A 2.0 kg drone’s motors push 6.0, 6.0, 5.0, and 5.0 N. (a) Draw the free-body diagram with a sign convention. (b) Net force. (c) Acceleration. (d) What total thrust holds a steady 3 m/s climb, ignoring drag? (EL02 will show that unequal thrusts also tip the drone; here only their sum matters.)
What the accelerometer reads. An up-pointing accelerometer, in m/s: (a) a phone on a table; (b) in an elevator speeding up upward at 1.2 m/s; (c) in the same elevator rising at a constant speed; (d) slowing to a stop on the way up at 1.0 m/s; (e) a drone hovering on Mars ( m/s).
Reading a thrust table. Use the AIR2216 II table (section 2.7’s figure; rows in data/catalog/). (a) Maximum thrust in newtons. (b) A 1.4 kg quad: each motor’s hover thrust, and its hover throttle by linear interpolation between rows. (c) The efficiency at that thrust, and each motor’s hover power. (d) Why is the table’s best efficiency, 9.12 gf/W, the wrong number for (c)?
The thrust-to-weight family. A hexacopter uses six AIR2216 II motors (1332 gf each) and weighs 2.4 kg. Find , the hover thrust fraction, the maximum climb acceleration, and the steepest tilt that still holds altitude (EL01.13’s result, or derive it now).
A mass budget by hand. Frame 250 g (estimated); four motors at 64 g (catalog); battery 421 g (catalog); four propellers at 12.5 g (catalog); avionics 56 g (catalog); wiring 60 g (estimated). Allowances: estimated 20%, catalog 5%. Find the basic mass, the predicted mass, the allowable mass for with four 1332 gf motors, and the mass margin.
A real simulator, audited: FateMMO’s drone simulator. The author’s FateMMO engine includes a quadcopter simulator in C++. Below are its vehicle parameters, the function that computes its hover command, and the lines that fill three fields of MAVLink’s BATTERY_STATUS telemetry message. (a) Compute the simulated quad’s from thrustCoefficient, maxMotorSpeed, and mass, and compare with the comment above the struct. (b) What does hoverThrottle() return, and why is it not ? (c) Check batteryCapacityWh = 74.0 against a 4S 5000 mAh pack. (d) The telemetry converts used energy back to charge with usedWh / (kNominalCellVoltage * params.batteryCells) * 1000.0. Check its units. What does usedWh * 36.0 produce, and why 36? (e) Audit the struct’s fields: which state no unit at all? Propose one change to the struct that would make a units slip in those fields a compile error.
// Physical description of a quad-X airframe. Defaults describe a 1.5 kg,
// 450 mm (motor-to-motor) quad on 10-inch propellers and a 4S 5000 mAh LiPo,
// with a thrust-to-weight ratio of about 2.
struct VehicleParams {
double mass = 1.5;
double armLength = 0.225; // centre to motor axis
Vec3 inertia{0.015, 0.015, 0.027}; // principal moments about the body axes
double thrustCoefficient = 9.1e-6; // N per (rad/s)^2
double torqueCoefficient = 1.5e-7; // N m per (rad/s)^2
double motorTimeConstant = 0.03; // first-order rotor speed lag, s
double maxMotorSpeed = 900.0; // rad/s at command 1
double minMotorSpeed = 150.0; // idle speed the controller holds while armed, rad/s
double linearDragCoefficient = 0.15; // N per (m/s) of air-relative velocity
double quadraticDragCoefficient = 0.02; // N per (m/s)^2
double angularDragCoefficient = 0.003; // N m per (rad/s)
double batteryCapacityWh = 74.0;
int batteryCells = 4;
double batteryInternalResistance = 0.03; // whole pack, ohm
double powertrainEfficiency = 0.8; // rotor shaft power / electrical power
double avionicsPower = 3.0; // constant electrical load, W
// ... member functions
};
double VehicleParams::hoverThrottle(double gravity) const {
// ... input checks
const double hoverSpeed = std::sqrt(mass * gravity / (4.0 * thrustCoefficient));
return std::clamp(hoverSpeed / maxMotorSpeed, 0.0, 1.0);
}
// ... in the battery telemetry
const double usedWh = std::max(0.0, params.batteryCapacityWh - s.batteryEnergyWh);
m.currentBattery = saturate<std::int16_t>(s.batteryCurrent * 100.0);
m.currentConsumed = saturate<std::int32_t>(usedWh / (kNominalCellVoltage * params.batteryCells) * 1000.0);
m.energyConsumed = saturate<std::int32_t>(usedWh * 36.0);Frontier application: Falcon 9 at lift-off. SpaceX publishes Falcon 9’s total mass as 549,054 kg and the sea-level thrust of its first stage, nine Merlin engines, as 7607 kN. (a) The lift-off thrust-to-weight ratio. (b) The initial upward acceleration, ignoring drag. (c) What an accelerometer in the payload reads at lift-off, in m/s and in . (d) As the first stage burns its propellant, assuming constant thrust, what happens to and to the accelerometer’s reading? (e) Why can a rocket lift off with a that would leave a multicopter almost uncontrollable?
Requirements. Rewrite each as a verifiable requirement with a number, a unit, conditions, and a verification method: (a) “The drone should be light.” (b) “The drone shall fly for a long time and be easy to repair.” (c) “The arm shall be strong.” Then derive one child requirement from your (b) and name its parent.
Tilted flight. A drone tilted by an angle from level has its total thrust tilted by the same angle. (a) Using right-triangle trigonometry (Module 04), write the vertical part of and the thrust needed to hold altitude. (b) Show that the steepest tilt that still holds altitude is . (c) Evaluate it for , 2, and 4.60.
Frontier application: Ingenuity on Mars. NASA’s Ingenuity helicopter had a mass of about 1.8 kg, and Mars’s surface gravity is 3.73 m/s. (a) Its weight on Mars and on Earth. (b) A kitchen scale calibrated on Earth is carried to Mars: what does it display for Ingenuity, and why? (c) The thrust needed to hover on Mars, in newtons and in gram-force. (d) Mars’s air is about one hundredth as dense as Earth’s. Without equations yet, say why that might force a rotor 1.2 m across; EL03 answers it with momentum theory.
The best battery, with calculus (after Module 14). (a) For the constant-efficiency model , use the quotient rule to show for every : no interior maximum. (b) Momentum theory (EL03) will show that hover power grows like mass to the power , so . Find the that maximizes . Watch the exponent: . (c) Compare with section 2.10’s figure.
Complete lab steps 1–9 until check.py reports every step passing on your own code. Record the first test that failed and its first incorrect step in your error log.
Mutation hunt. Make each change, predict which step fails first and what the message says, run check.py, and undo it: (a) weight returns mass; (b) net_force adds the weight instead of subtracting it; (c) battery_energy’s default cell voltage becomes 4.2 V; (d) arm converts lengths with .m_as("m"); (e) remove_bias averages the whole record instead of the still window. One sentence each on what the failing test’s name told you.
4.2. Code-reading drill: PX4 estimates its own hover thrust#
PX4, the open-source autopilot your drone will run, learns its hover throttle in flight. It watches the vertical acceleration and the thrust it is commanding, and fits the one number that makes Newton’s second law agree with them. The code is C++ written by other people, and reading someone else’s code is a skill of its own; its physics is exactly this module’s. Read these three files at this pinned commit (tag v1.17.0); the line numbers below refer to it:
github.com/PX4/PX4-Autopilot/blob/d6f12ad1c4f70ad3230afd7d86e971421e02fef4/src/modules/mc_hover_thrust_estimator/zero_order_hover_thrust_ekf.hpp
github.com/PX4/PX4-Autopilot/blob/d6f12ad1c4f70ad3230afd7d86e971421e02fef4/src/modules/mc_hover_thrust_estimator/zero_order_hover_thrust_ekf.cpp
github.com/PX4/PX4-Autopilot/blob/d6f12ad1c4f70ad3230afd7d86e971421e02fef4/src/modules/mc_hover_thrust_estimator/MulticopterHoverThrustEstimator.cpp
Protocol. Read the header comment of the .hpp (lines 34–62) and the whole .cpp once, top to bottom, without stopping. Words you have not met yet (Kalman gain, covariance, innovation variance) belong to EL22; for now, treat them as machinery that adjusts one number. Then write your explanation for each question before opening its solution.
.hpp lines 46–48 and .cpp lines 102–105. Here thrust and _hover_thr are fractions of the maximum thrust, between 0 and 1. Show that g * thrust / _hover_thr - g is Newton’s second law for a multicopter, as in section 2.5. What does a positive value mean?
.cpp lines 84–87. computeH returns the derivative of the predicted acceleration with respect to the hover thrust. Rewrite the prediction as and differentiate it with respect to yourself; compare your answer with line 86. Why is it negative, physically?
MulticopterHoverThrustEstimator.cpp, line 157 and lines 181–192. (a) What unit must local_pos.timestamp be in, given * 1e-6f? (b) PX4’s position estimate uses a north-east-down frame, in which points down. Explain both minus signs in fuseAccZ(-local_pos.az, -thrust_allocated(2)) using the comment on lines 183–184.
.hpp lines 81 and 97–99. The hover-thrust estimate starts at 0.5 and is kept between 0.1 and 0.9. What thrust-to-weight ratio does each of those three numbers correspond to? Why might a designer refuse estimates outside that range?
.hpp lines 102–109: a dimensional audit. The innovation (line 108) is an acceleration. The innovation variance is computed in .cpp line 93 as , where is _acc_var. A variance has the square of its quantity’s unit. Which unit comments are inconsistent with that, and what should they say? (This comment is unchanged on PX4’s main branch as of 26 September 2026: see stretch S4.)
4.3. Mastery gate#
A 3S 2200 mAh LiPo (3.7 V per cell). Its energy in W h and in joules, showing every factor of 1. Then show, with exponents, that J/s is a watt.
A 1.6 kg drone runs four motors at 4.5 N each. Draw the free-body diagram; find the net force, the acceleration, and the accelerometer’s reading; then find the thrust per motor that would hold a hover, in newtons and gram-force.
Four motors, each 1100 gf maximum; predicted mass 1.3 kg; a 4S 4000 mAh LiPo (3.7 V per cell, 80% usable); hover efficiency 7.5 gf/W; electronics 5 W. Find , the hover thrust fraction, the maximum climb acceleration, the hover power, and the flight time.
Each snippet has one bug. For each: name it (field-guide number from section 2.16, or describe it if none fits), give the smallest input that exposes it and the wrong result, and fix it.
# (a)
def thrust_to_weight(max_thrust_per_motor, motor_count, mass, g=G0):
return max_thrust_per_motor / (mass * g)
# (b)
def battery_energy(capacity, cells, cell_voltage=Q_(3.7, "V")):
return (capacity * cell_voltage).to("W*h")
# (c)
def velocity_and_height(t, accel, still_until):
v = cumulative_trapezoid(t, accel)
return v, cumulative_trapezoid(t, v)4.4. Next steps#
- Record the module. Add an EL01 row to your progress record, a spreadsheet with one row per module (first attempt, gate score, teach-back, tests passing, first error, next review in seven days), and log every miss in your error log.
- The calculus modules. Module 12 is section 2.4 with more examples; Module 14’s optimization is EL01.15; Module 16’s antiderivatives and Module 23’s trapezoid rule are lab step 8.
- EL02 turns the vertical line into a plane: moments, the centre of mass, why unequal thrusts tip the drone, and the matrix that turns “roll, pitch, yaw, thrust” into four motor commands. Your arm gains a place on a frame.
5. Development log entry#
Keep a running DEVLOG.md at the top of your eng-lab repository. It becomes public evidence of what you built, so write every claim so that a stranger could check it. For this module, one entry:
6. Stretch (optional)#
- S1. The best battery. EL01.15, after Module 14: the calculus behind section 2.10’s curve.
- S2. Drag in a climb. Add quadratic drag, , to section 2.5’s climb and find the steady climb speed at full thrust for N s/m (the value FateMMO’s drone simulator uses, EL01.10). EL19 builds the full model.
- S3. Units at compile time, in C++23 (portfolio). Write a tiny
Quantity<M, L, T>template whose exponents are template parameters, so thatmass * accelhas typeQuantity<1, 1, -2>and adding it to a length does not compile. Portweightandthrust_to_weightto it, then compare your design with mp-units. It is exactly the tool theVehicleParamsstruct of EL01.10 is missing. - S4. Your first open-source contribution. EL01.22 finds unit comments in PX4 that are dimensionally inconsistent, still present on the main branch as of 26 September 2026. Check the current main branch first; if they are still there, read PX4’s contribution guide and open a one-line pull request that corrects them. A merged fix, however small, is public evidence of careful reading.
- S5. How good is your phone’s accelerometer? Record 60 seconds with the phone perfectly still. Compute the mean and standard deviation of the vertical reading. Predict the height error after 10 seconds of double integration from a bias error of one standard deviation divided by the square root of the number of still samples, then measure it by integrating the still record itself. This is the first step toward EL16 (sensor noise) and EL22 (Kalman filtering).
Frontier connections: where this module’s ideas show up at scale#
Read these after the gate, for orientation.
- Units at interfaces (2.2). The Mars Climate Orbiter Mishap Investigation Board’s Phase I report (NASA, 1999): llis.nasa.gov/llis_lib/pdf/1009464main1_0641-mr.pdf.
- Requirements and verification (2.11). NASA Systems Engineering Handbook, NASA/SP-2016-6105 Rev2: www.nasa.gov/wp-content/uploads/2018/09/nasa_systems_engineering_handbook_0.pdf.
- Mass growth allowance (2.8). ANSI/AIAA S-120A’s table as reproduced in NASA/TM-20240014203: ntrs.nasa.gov/api/citations/20240014203/downloads/11-15-24%2020240014203.pdf.
- Newton’s second law in flight software (2.6, 4.2). PX4’s hover thrust estimator, the drill’s source: github.com/PX4/PX4-Autopilot/tree/d6f12ad1c4f70ad3230afd7d86e971421e02fef4/src/modules/mc_hover_thrust_estimator.
- Flying where the air is thin (EL01.14). NASA’s Ingenuity Mars Helicopter: science.nasa.gov/mission/mars-2020-perseverance/ingenuity-mars-helicopter/.
Sources for this chapter (checked 26 September 2026)#
Thrust tables and recommended take-off weights: uav-en.tmotor.com/2022/Air-Gear_0510/798.html (AIR2216 II, the same sheet Holybro links as its X500 V2 motor specification: holybro.com/products/x500-v2-kits); www.t-hobby.com/products/micro-fpv-drones-brushless-motor (F1404); www.t-hobby.com/products/brushless-motor-for-fpv-drones-60pro-v-2207-5 (F60PRO V). Components: gensace.de/products/gens-ace-g-tech-5000mah-14-8v-4s1p-60c-lipo-battery-pack-with-xt90-plug-bashing-series; holybro.com/products/kakute-h7-v2-stacks; docs.holybro.com/gps-and-rtk-system/m8n-m9n-m10-gps/standard-m10-m9n-m8n-gps/overview; radiomasterrc.com/products/rp1-expresslrs-2-4ghz-nano-receiver; PX4’s simulated x500, at the PX4-gazebo-models commit PX4 v1.17.0 pins: github.com/PX4/PX4-gazebo-models/blob/b6127f4ec20de867e215fb5f78ae88b80f371909/models/x500_base/model.sdf (masses) and github.com/PX4/PX4-gazebo-models/blob/b6127f4ec20de867e215fb5f78ae88b80f371909/models/x500/model.sdf (motor constant, top speed). LiPo voltages (3.7 V nominal, 4.2 V full, 3.5 V recommended cut-off): genstattu.com/bw. Constants and standards: standard gravity, physics.nist.gov/cgi-bin/cuu/Value?gn; Mars’s mean surface gravity, 3.73 m/s, NASA’s Mars Fact Sheet (last updated 19 May 2025), nssdc.gsfc.nasa.gov/planetary/factsheet/marsfact.html; gram-force and kilogram-force, www.nist.gov/pml/special-publication-811/nist-guide-si-appendix-b-conversion-factors/nist-guide-si-appendix-b9; mass growth allowance, the NASA technical memorandum in Frontier connections; carbon-fibre plate density, dragonplate.com/solid-carbon-fiber-sheets-plates (1.44 to 1.61 g/cm across that maker’s solid plates). Regulations: www.ecfr.gov/current/title-14/section-48.15; www.faa.gov/uas/recreational_flyers; www.faa.gov/uas/getting_started/remote_id; www.faa.gov/uas/commercial_operators. Frontier examples: Falcon 9, www.spacex.com/vehicles/falcon-9; Mars Climate Orbiter, as in Frontier connections; Ingenuity’s mass, rotor span, and air density, JPL’s quick facts, www.jpl.nasa.gov/news/press_kits/ingenuity/landing/quick_facts/. Software: phyphox, phyphox.org/; MAVLink’s BATTERY_STATUS units, at the commit PX4 v1.17.0 pins, github.com/mavlink/mavlink/blob/33af200d25ec6f0925b49b1ba82bbf1294ea5f72/message_definitions/v1.0/common.xml; pint, pint.readthedocs.io/; build123d, build123d.readthedocs.io/. The author’s own code: the drone simulator in the FateMMO engine and the grenade system of ZeroVector, read 26 September 2026.
Hint ladders for the lab#
Climb one rung at a time, and try again after each one. Rung 1 is a question, rung 2 points to the idea, rung 3 is a skeleton with the key parts left for you.
L1. Step 1: weight#
Rung 1. Which two quantities multiply to give a weight, and which unit should the answer be expressed in?
Rung 2. Section 2.3, ; section 3.1’s pint tour for .to().
Rung 3. return (____ * ____).to("N")
L2. Step 2: Newton’s second law#
Rung 1. In section 2.5’s free-body diagram, which forces point up and which point down? Which of them does thrusts hold?
Rung 2. Section 2.5 for the net force and ; section 2.6 for the reading, ; section 2.13’s table for sum with a start value.
Rung 3.
total = sum(thrusts, Q_(0, "N"))
return total - ____ # net_force
return (net_force(____, ____, g) / ____).to("m/s^2") # acceleration
return (____ + g).to("m/s^2") # accelerometer_readingL3. Step 3: thrust-to-weight#
Rung 1. How much thrust can all the motors make together? How much does the drone weigh? Which of those is on top?
Rung 2. Section 2.7: , , and . Reuse weight.
Rung 3.
return motor_count * ____ / weight(____, g) # thrust_to_weight
return weight(mass, g) / ____ # hover_thrust_per_motor
return 1 / thrust_to_weight(____) # hover_thrust_fraction
return g * (thrust_to_weight(____) - ____) # max_climb_accelerationL4. Step 4: energy and flight time#
Rung 1. What is the pack’s voltage when cells cells of cell_voltage each sit in series? What is charge times voltage?
Rung 2. Section 2.2’s worked example and section 2.9’s hover power and flight time. raise ValueError("...") rejects bad inputs.
Rung 3.
return (capacity * cells * ____).to("W*h") # battery_energy
return motor_count * (____ / ____) # propulsion_power
if not (0 < usable_fraction <= 1): raise ValueError("usable_fraction must be in (0, 1]")
if power <= Q_(0, "W"): raise ValueError("power must be positive")
return (____ * ____ / ____).to("min") # flight_timeL5. Step 5: the mass budget#
Rung 1. For one item, what is its predicted mass? How do you look up its allowance?
Rung 2. Section 2.8, ; the module docstring’s Python traps.
Rung 3.
return sum((item.mass * (1 + allowances[____]) for item in items), Q_(0, "g")).to("g")
split = {}
for item in items:
split[item.subsystem] = split.get(item.subsystem, Q_(0, "g")) + ____For allowable_mass, solve for on paper first.
L6. Step 6: the arm and its hand check#
Rung 1. (For arm_volume_by_hand.) Sketch the top view. Which rectangles add, which rectangle is counted twice, and which circles come out?
Rung 2. Section 2.12’s formula. Circles take radii in build123d but your formula uses diameters: .
Rung 3.
area = length * width + pad_size**2 - (pad_size / 2) * ____
holes = math.pi / 4 * (4 * ____**2 + ____**2 + 2 * ____**2)
return ((area - holes) * ____).to("mm^3")L7. Step 7: requirements#
Rung 1. For each requirement: could someone who has never met you check it, and how? Does it name one quantity, with a unit?
Rung 2. Section 2.11 and the file’s comments; the list of results in tests/test_requirements.py.
Rung 3. A derived requirement in the file’s format: id = "SYS-002", text “The drone’s predicted take-off mass shall not exceed 2664 g.”, verification = "Analysis", result = "mass.predicted", op = "<=", threshold = "2664 g", parent = "SYS-001". Now write one of your own that is verified by Test and one by Inspection.
L8. Step 8: integrating the phone data#
Rung 1. If you know the acceleration at two instants apart, by how much did the velocity change between them? Where does the running total start?
Rung 2. Section 2.4’s trapezoid rule and section 3.8’s bias warning. A boolean mask, t < still_until, selects the still samples.
Rung 3.
total = 0 * y[0] * t[0] # zero, already in y's unit times t's unit
values = [total]
for k in range(1, len(t)):
dt = t[k] - t[k - 1]
total = total + ____ # one trapezoid: the average of its ends, times dt
values.append(total)
return Q_.from_list(values) # one Quantity array; the units never left
still = accel[t < still_until]
return accel - ____ # remove_biasL9. Step 9: the tinker loop#
Rung 1. Which section does each knob belong to, and what does that section’s equation say will happen?
Rung 2. Section 3.9’s knob list; write the prediction as a number with a direction (“flight time up about 30%”), not a feeling.
Rung 3. After each run, answer in the explanation: which equation explains the result, and where did your prediction differ, and why?
Complete solutions: EL01#
Open a solution only after you have written your own attempt. Compare at the first step where your work differs. A corrected answer is not yet an independent success.
EL01.01Show solution
Exponents in, quantity out
(a) , an acceleration. That is why gravity is sometimes written 9.81 N/kg: newtons of weight per kilogram of mass. (b) , a joule: energy. (c) , an acceleration; the exponent is . (d) , a volt: energy per unit charge. (e) , a watt: power. Final sweep: every negative exponent kept its sign.
Back to EL01.01EL01.02Show solution
A chain of ones
(a) . (b) Pack voltage V; , and . (c) . (d) “350 g” of thrust is 350 gf . (e) : yes.
Back to EL01.02EL01.03Show solution
Grams of thrust are gram-force
(a) . (b) Each motor carries a quarter: . (c) “2.1 kg” is . Four make 8.4 kgf, but a 9 kg drone weighs 9 kgf: . No: it cannot even hover.
Back to EL01.03EL01.04Show solution
Constant acceleration, then constant speed
(a) and . Check: gives (velocity) and (acceleration). (b) The last 40 m at 10 m/s take 4.0 s, so it reaches 60 m at .
Back to EL01.04EL01.05Show solution
Sum the forces, then divide by the mass
(a) Up positive; four thrusts up at the motors; the weight N down at the centre of mass. (b) upward. (c) upward. (d) Steady climb means , so the thrusts total exactly the weight, (4.90 N each if shared). The 3 m/s does not enter; only a change of speed needs a net force.
Back to EL01.05EL01.06Show solution
Reading
(a) . (b) . (c) Constant speed: , so . (d) Slowing on the way up is a downward acceleration: . (e) . In an elevator you feel heavier in (b) and lighter in (d) for the same reason the accelerometer reads more and less: your body, like the proof mass, feels only the floor’s push.
Back to EL01.06EL01.07Show solution
Interpolate, and read efficiency where you operate
(a) . (b) gf per motor, between the 40% row (309 gf) and the 45% row (373 gf): throttle. (c) , so each motor draws (169.5 W for four). (d) 9.12 gf/W is the efficiency at 210 gf. At 350 gf the motor works harder and less efficiently; using 9.12 would understate the power by about 9% and overstate the flight time by about 10%. This is field-guide mistake U7.
Back to EL01.07EL01.08Show solution
EL01.09Show solution
EL01.10Show solution
Your simulator, audited
(a) N per motor, 29.48 N for four; the weight is N; , matching the comment’s “about 2”. (b) hoverThrottle() returns the hover speed as a fraction of the maximum: . Thrust grows as , so the hover thrust fraction is . Both are correct; they answer different questions. (c) . (d) W h divided by volts is A h, and makes mA h: exactly what MAVLink’s BATTERY_STATUS.current_consumed field is defined in. usedWh * 36.0: hectojoules, the unit of energy_consumed; the * 100.0 in the currentBattery line is there because current_battery is in centiamperes. (e) mass (kg), armLength (m), and inertia (kg m) state no unit. A fix that makes a slip a compile error: give each a type that carries its unit, as std::chrono does for time, either with a small template (stretch S3) or with mp-units, for example quantity<si::kilogram> mass = 1.5 * si::kilogram;. Then passing a length where a mass belongs does not compile. Even renaming the fields massKg and armLengthM is a cheap, partial improvement.
EL01.11Show solution
A rocket’s thrust-to-weight
(a) kN; N kN; . (b) . (c) The reading is the thrust per unit mass, : section 2.6’s rule, on a rocket. (d) The mass falls while the thrust holds, so both and rise; at half the mass they double. The crew feels heavier and heavier as the stage empties. (e) A multicopter must hover and steers by making some motors push harder than others, so it needs thrust in reserve (section 2.7). A rocket only has to climb away; it steers by swivelling (gimballing) its engines rather than by throttling some against others, and every kilogram of extra engine is a kilogram less payload. Different missions, different requirements: the same law.
Back to EL01.11EL01.12Show solution
Verifiable rewrites
Model answers; yours may choose other numbers, but each must be one quantity, with a unit, conditions, and a method. (a) “SYS-010: The drone’s take-off mass, battery included, shall not exceed 1500 g.” Verification: Inspection (weigh on a calibrated scale). (b) Two requirements, because it held two: “SYS-011: The drone shall hover for at least 15.0 minutes at sea level in still air, from a full charge until any cell reaches 3.5 V under load.” Verification: Test (a timed hover with the voltage logged). “SYS-012: One arm shall be replaceable in no more than 10 minutes with only a 2.5 mm hex driver.” Verification: Demonstration. A child of SYS-011: “PWR-001: The battery shall store at least 43.7 W h.” (At 139.7 W, 15 minutes needs W h usable, and W h stored.) Verification: Analysis; parent SYS-011. (c) “STR-001: Each arm shall carry a static tip load of 26.1 N, twice the motor’s maximum thrust, without breaking or bending permanently.” Verification: Test (a load test, EL07). The words “strong” and “light” are not requirements until they become numbers.
Back to EL01.12EL01.13Show solution
The tilt budget
(a) The thrust’s vertical part is . Holding altitude needs , so . (b) The motors can give at most , so , which rearranges to , that is, . (c) : ; : ; : .
Back to EL01.13EL01.14Show solution
Same mass, different weight
(a) Mars: . Earth: . (b) The scale measures a force and divides by the it was calibrated with: kg, so it displays . The mass is still 1.8 kg. (c) . (d) A rotor makes thrust by pushing air downward. With a hundredth of the air in every cubic metre, it must push a far larger volume of air, faster, to get the same thrust: a bigger disk, spinning faster. EL03’s momentum theory puts numbers on it.
Back to EL01.14EL01.15Show solution
The calculus behind the mass spiral
(a) With the Modular Workbench, , , , : . The slope never reaches zero: no interior maximum, only diminishing returns. (b) Product rule on ; the new exponent is :
It is zero when , so ; the slope is positive before and negative after, so this is a maximum. (c) Your drone’s other components weigh about 717 g with allowances, so an ideal rotor would peak with a battery near 1.4 kg; your table-based curve peaks near 2.1 kg, about three times . The real motor’s efficiency falls more slowly with thrust than an ideal rotor’s, because at low thrust the motor’s own fixed losses already waste a sizeable share. EL03 and EL04 separate those effects.
Back to EL01.15EL01.16Show solution
The usual first failures
The tests are the judge: the lab is done when check.py reports every step passing on your own code. If a step will not pass after an honest attempt, check this list before anything else. The usual first failures, in order: stripping units with .m to “make the numbers work” (step 1, U11); adding the weight in net_force (step 2, U8); forgetting motor_count (step 3, U4); energy from capacity alone (step 4, U1); a sum with no start value (step 5); diameters passed where radii belong (step 6); a requirement with no unit on its threshold (step 7); and integrating without removing the bias (step 8, U12). Log the category and the first incorrect step, not just “fixed.”
EL01.17Show solution
What each mutation breaks
(a) Step 1 fails at once, and not in your arithmetic: the @quantities wrapper refuses the result. check.py names test_weight_accepts_grams first, and its message ends Cannot convert from 'g' to '[force]' (weight() result); the 1.5 kg tests say 'kg'. Steps 2 and 3 fail too, because net_force and the thrust-to-weight family call weight: one wrong function breaks everything built on it. The unit system caught the bug before any number could. (b) Step 2 fails. check.py lists tests alphabetically, so it names test_acceleration_climbing first: 26.47 m/s instead of 6.86. test_net_force_four_motors shows 30.71 N instead of 1.29 N. Both are off by exactly twice the weight ( N, and m/s): the fingerprint of a sign flip. (c) Step 4 fails. check.py names test_energy_in_joules first (117,936 J instead of 103,896 J), and test_energy_of_a_6s_1300_pack shows 32.76 W h instead of 28.86: every energy is times too big. (d) Step 6 fails, and check.py names test_bounding_box: the arm is 0.114 long where 114 belongs. Every length is 1000 times too small, so the volume is times too small. (e) The surprise: every smooth-lift test still passes. A lift that starts and ends at rest has zero average acceleration, so the average over the whole record happens to equal plus the bias. Two other tests fail. test_bias_is_the_average_of_the_whole_still_window, named first, feeds a still record with one spike at its start; averaging all 200 samples instead of the window’s 100 halves the spike’s pull on the bias, so every corrected sample is off by 0.005 m/s. test_bias_must_come_from_the_still_window ends its record while the phone is still speeding up, so the whole-record average is no longer plus the bias, and the final speed comes out near 0 instead of about 1 m/s. Test at the edges of a method’s assumptions, not only in its comfortable middle.
EL01.18Show solution
Newton’s second law, normalized
Section 2.5 gives , with the total thrust in newtons. In a hover , so and . Dividing the top and bottom of that ratio by the maximum thrust turns it into a ratio of fractions: , which is line 104 exactly. is the commanded thrust as a fraction of maximum; is the fraction that would hover. A positive result means the drone is being pushed upward faster: the command exceeds the hover thrust. (The model assumes the actual thrust is proportional to the command; section 2.7’s thrust table shows why that needs care, and EL04 returns to it.)
Back to EL01.18EL01.19Show solution
A negative exponent in flight code
Parentheses Rewrite first: . Freeze and differentiate in : , and line 86 computes exactly that: -CONSTANTS_ONE_G * thrust / (_hover_thr * _hover_thr). It is negative because a drone that needs a larger share of its thrust just to hover (a heavier one) gets less upward acceleration from the same command. The exponent went from to : the same bookkeeping as section 2.2’s .
EL01.20Show solution
Microseconds and a frame that points down
(a) * 1e-6f converts to seconds, so the timestamps are in microseconds (PX4 stamps every message in microseconds since boot). (b) In north-east-down coordinates, points down, so local_pos.az is positive when the drone accelerates downward, and the thrust vector’s component is negative when it pushes up. The estimator’s model is written up-positive, as the comment on lines 183–184 says, so both are negated. A sign convention chosen once and converted at the boundary is the free-body-diagram rule of section 2.5, applied to software.
EL01.21Show solution
Hover thrust is
Since : the initial guess 0.5 means ; the bounds mean and . A multicopter with near 1.1 has almost no control headroom, and one above 10 is rare outside racing, so estimates outside the range are more likely a disturbance (ground effect, a gust, a transient) than the truth. Clamping keeps one bad estimate from corrupting the controller that uses it.
Back to EL01.21EL01.22Show solution
A dimensional audit
The innovation is an acceleration (m/s), so its variance is in (m/s) . In , is in (m/s) per unit of normalized thrust and in (normalized thrust), so is in m/s, and , _acc_var, must match it. Lines 104 and 109 say m/s: both comments are wrong by one power of seconds. The others check out: _innov in m/s; _state_var in thrust; and _process_var in thrust/s, consistent with line 48’s _process_var * dt * dt. The code is right; only the documentation of units is not.
EL01.23Show solution
EL01.24Show solution
Gate: free-body diagram, Newton, accelerometer
Four 4.5 N thrusts up; weight N down. Net: up; up; reading , which is exactly : thrust per unit mass. Hover needs per motor.
Teach-back rubric (gate part 4). A complete explanation (i) draws the thrusts up at the motors and the weight down at the centre of mass, with a sign convention; (ii) says that means a hover uses at most half of the available thrust, and names at least two uses of the rest: differential thrust for control (and what saturation does), tilting to move (), climbing and gusts (), and battery sag; (iii) says a hovering drone’s accelerometer reads , its thrust per unit mass, because an accelerometer measures every force except gravity, and that 0 would mean free fall; (iv) uses “specific force” and “hover thrust fraction” correctly.
Back to EL01.24EL01.25Show solution
Gate: the whole chain
; ; . Each motor hovers at gf and draws W; four draw 173.3 W; with 5 W of electronics, . Energy: W h. Flight time: h h .
Back to EL01.25EL01.26Show solution
Gate: find the bugs
(a) U4, the missing motor count. Smallest exposing input: any drone with more than one motor, such as four 1500 gf motors on 650 g: it returns 2.31 instead of 9.23. Fix: motor_count * max_thrust_per_motor / (mass * g). (b) Cell voltage used as the pack voltage. Any pack with more than one cell exposes it: 6S 1300 mAh gives 4.81 W h instead of 28.86. Fix: (capacity * cells * cell_voltage).to("W*h"). The units cannot catch this one, because a cell’s voltage and a pack’s voltage have the same unit: only a test with known numbers can. (c) U12, gravity and bias never removed. Any record exposes it: a phone lying still for 1 s “reaches” 9.8 m/s and 4.9 m. Fix: accel = remove_bias(t, accel, still_until) before the first integration.
Warm-up answers#
1. and . 2. and . 3. and .
Retention and readiness#
In two days, redo EL01.05 and EL01.10(b) without looking. In seven days, write and solve a fresh thrust-to-weight and flight-time problem in the style of EL01.25, with numbers of your own, and redo EL01.17(e) from memory. Log every miss under its friction family. Move on to EL02 only after the mastery gate in section 4.3 passes.