ContentsFoundations to FrontierBezhig

eng-lab/labs/el01_hover/tests/test_massbudget.py

All lab code. Download this file.

"""EL01 lab tests for hwe/core/massbudget.py (step 5)."""
import unittest

from _impl import load
from hwe.design import Item
from hwe.units import Q_

mb = load("massbudget")

ALLOW = {"estimated": 0.20, "catalog": 0.05, "cad": 0.10, "measured": 0.01}


def items():
    return [
        Item("frame", "structure", Q_(120, "g"), "catalog"),
        Item("arms (x4)", "structure", Q_(53.2, "g"), "cad"),
        Item("motors (x4)", "propulsion", Q_(134, "g"), "catalog"),
        Item("battery", "power", Q_(210, "g"), "measured"),
        Item("flight controller and ESC", "avionics", Q_(20, "g"), "estimated"),
    ]


class Step5MassBudget(unittest.TestCase):
    def test_basic_mass_is_the_plain_sum(self):
        # 120 + 53.2 + 134 + 210 + 20 = 537.2 g
        self.assertAlmostEqual(mb.basic_mass(items()).to("g").m, 537.2, delta=1e-9)

    def test_basic_mass_of_nothing_is_zero_grams(self):
        # sum() needs a start value with units, or this returns the int 0
        empty = mb.basic_mass([])
        self.assertAlmostEqual(empty.to("g").m, 0.0)

    def test_basic_mass_mixed_units(self):
        mixed = [Item("a", "structure", Q_(0.1, "kg"), "catalog"), Item("b", "structure", Q_(50, "g"), "catalog")]
        self.assertAlmostEqual(mb.basic_mass(mixed).to("g").m, 150.0, delta=1e-9)

    def test_predicted_mass_adds_each_allowance(self):
        # 120*1.05 + 53.2*1.10 + 134*1.05 + 210*1.01 + 20*1.20
        # = 126 + 58.52 + 140.7 + 212.1 + 24 = 561.32 g
        self.assertAlmostEqual(mb.predicted_mass(items(), ALLOW).to("g").m, 561.32, delta=1e-9)

    def test_immature_numbers_get_bigger_allowances(self):
        # Moving the 20 g estimate to "measured" must lower the prediction by 20 x (0.20 - 0.01) = 3.8 g
        before = mb.predicted_mass(items(), ALLOW).to("g").m
        better = items()
        better[4] = Item("flight controller and ESC", "avionics", Q_(20, "g"), "measured")
        after = mb.predicted_mass(better, ALLOW).to("g").m
        self.assertAlmostEqual(before - after, 3.8, delta=1e-9)

    def test_unknown_maturity_is_an_error(self):
        bad = items() + [Item("mystery", "payload", Q_(10, "g"), "guessed")]
        with self.assertRaises(KeyError):
            mb.predicted_mass(bad, ALLOW)

    def test_mass_by_subsystem(self):
        # structure 126 + 58.52 = 184.52; propulsion 140.7; power 212.1; avionics 24
        split = mb.mass_by_subsystem(items(), ALLOW)
        self.assertEqual(set(split), {"structure", "propulsion", "power", "avionics"})
        for name, grams in {"structure": 184.52, "propulsion": 140.7, "power": 212.1, "avionics": 24.0}.items():
            self.assertAlmostEqual(split[name].to("g").m, grams, delta=1e-9, msg=name)

    def test_subsystems_add_up_to_the_total(self):
        split = mb.mass_by_subsystem(items(), ALLOW)
        total = sum(v.to("g").m for v in split.values())
        self.assertAlmostEqual(total, mb.predicted_mass(items(), ALLOW).to("g").m, delta=1e-9)

    def test_allowable_mass_from_ttw_requirement(self):
        # 4 motors x 1500 gf = 6000 gf; T/W >= 2 allows at most 3000 gf of weight: 3000 g
        self.assertAlmostEqual(mb.allowable_mass(Q_(1500, "gf"), 4, 2.0).to("g").m, 3000.0, delta=1e-9)

    def test_allowable_mass_in_newtons(self):
        # 4 x 7.371 N = 29.484 N; / (2 x 9.80665) = 1.503265... kg
        self.assertAlmostEqual(mb.allowable_mass(Q_(7.371, "N"), 4, 2.0).to("kg").m,
                               29.484 / (2 * 9.80665), delta=1e-12)

    def test_mass_margin(self):
        # (3000 - 2550) / 3000 = 0.15
        self.assertAlmostEqual(mb.mass_margin(Q_(2550, "g"), Q_(3000, "g")).m, 0.15, delta=1e-12)

    def test_negative_margin_means_too_heavy(self):
        # (3000 - 3300) / 3000 = -0.1
        self.assertAlmostEqual(mb.mass_margin(Q_(3.3, "kg"), Q_(3000, "g")).m, -0.1, delta=1e-12)


if __name__ == "__main__":
    unittest.main()