ContentsFoundations to FrontierBezhig

eng-lab/hwe/core/massbudget.py

All lab code. Download this file.

"""EL01 lab: the mass budget. YOU WRITE THIS FILE (EL01 lab, step 5).

A mass budget lists every component, adds them up, and adds a growth allowance
on top, because early estimates always grow: wires, screws, glue, and heavier
parts than planned (EL01, section 2.8). The less mature a number, the bigger its
allowance: a guess needs more than a datasheet value, which needs more than a
part you have weighed.

Each component arrives as an hwe.design.Item with these fields:
    name       "motor 1", "battery", ...
    subsystem  "structure", "propulsion", "power", "avionics", or "payload"
    mass       a Quantity, for example Q_(33.5, "g")
    maturity   "estimated", "catalog", "cad", or "measured"
    source     where the number came from (a URL, "CAD", "kitchen scale")

`allowances` maps each maturity to a growth fraction, for example
{"estimated": 0.20, "catalog": 0.05, "cad": 0.10, "measured": 0.01}.
The design file (design/drone.toml) sets them; the tests pass their own.

Python traps in this file
- sum() starts from the plain int 0. pint lets 0 + Q_(33.5, "g") through as a
  special case, so a non-empty list sums fine, but an EMPTY list returns the bare
  int 0: no unit, no .to(), and the next line fails. Give sum() a start value
  with units, sum(..., Q_(0, "g")), and even an empty budget is 0 g.
- A maturity missing from `allowances` is a mistake in the design file: raise
  KeyError rather than silently using 0.
"""
from hwe.units import G0, Q_, quantities


def basic_mass(items):
    """The plain sum of every item's mass, with no allowance. Returns grams."""
    raise NotImplementedError("Step 5: basic_mass")


def predicted_mass(items, allowances):
    """The sum of every item's mass times (1 + its maturity's allowance). Returns grams.

    This is the number the rest of the design must work with, not basic_mass.
    """
    raise NotImplementedError("Step 5: predicted_mass")


def mass_by_subsystem(items, allowances):
    """A dict mapping each subsystem name to its predicted mass (with allowances), in grams.

    Every subsystem that appears in items appears once in the dict; the values
    add up to predicted_mass(items, allowances).
    """
    raise NotImplementedError("Step 5: mass_by_subsystem")


@quantities(max_thrust_per_motor="[force]", g="[acceleration]", returns="[mass]")
def allowable_mass(max_thrust_per_motor, motor_count, min_thrust_to_weight, g=G0):
    """The heaviest the drone may be and still meet a thrust-to-weight requirement.

    min_thrust_to_weight is a pure number such as 2.0. Solve T/W = min for the
    mass. Returns grams.
    """
    raise NotImplementedError("Step 5: allowable_mass")


@quantities(predicted="[mass]", allowable="[mass]", returns="[]")
def mass_margin(predicted, allowable):
    """Mass margin as a fraction of the allowable mass: (allowable - predicted) / allowable.

    0.15 means 15% of the allowable mass is still unspent. Negative means the
    design is already too heavy for its requirement.
    """
    raise NotImplementedError("Step 5: mass_margin")