ContentsFoundations to FrontierBezhig

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

All lab code. Download this file.

"""EL01 lab tests for hwe/core/energy.py (step 4)."""
import unittest

from _impl import load
from hwe.units import Q_

energy = load("energy")


def close(test, quantity, expected, unit, rel=1e-9):
    value = quantity.to(unit).m
    test.assertAlmostEqual(value, expected, delta=abs(expected) * rel + 1e-12,
                           msg=f"expected {expected} {unit}, got {value} {unit}")


class Step4Energy(unittest.TestCase):
    def test_energy_of_a_6s_1300_pack(self):
        # 1.3 A*h x (6 x 3.7 V = 22.2 V) = 28.86 W*h
        close(self, energy.battery_energy(Q_(1300, "mA*h"), 6), 28.86, "W*h")

    def test_energy_of_the_example_drone_pack(self):
        # 4S 5000 mAh: 5 A*h x 14.8 V = 74 W*h, the batteryCapacityWh in params.h
        close(self, energy.battery_energy(Q_(5000, "mA*h"), 4), 74.0, "W*h")

    def test_energy_uses_the_cell_voltage_given(self):
        # At full charge, 4.2 V per cell: 1.3 x 25.2 = 32.76 W*h
        close(self, energy.battery_energy(Q_(1300, "mA*h"), 6, Q_(4.2, "V")), 32.76, "W*h")

    def test_energy_in_joules(self):
        # 28.86 W*h x 3600 s/h = 103896 J
        close(self, energy.battery_energy(Q_(1300, "mA*h"), 6), 103896.0, "J")

    def test_more_mah_can_mean_less_energy(self):
        # 4S 1500 mAh = 1.5 x 14.8 = 22.2 W*h, less than 6S 1300 mAh = 28.86 W*h
        four_s = energy.battery_energy(Q_(1500, "mA*h"), 4)
        six_s = energy.battery_energy(Q_(1300, "mA*h"), 6)
        self.assertLess(four_s, six_s)

    def test_propulsion_power(self):
        # 375 gf / (5 gf/W) = 75 W per motor; x 4 = 300 W
        close(self, energy.propulsion_power(Q_(375, "gf"), Q_(5, "gf/W"), 4), 300.0, "W")

    def test_propulsion_power_in_newtons(self):
        # 3.67749375 N per motor at 5 gf/W: 3.67749375 N = 375 gf, so again 75 W each; 4 motors = 300 W
        close(self, energy.propulsion_power(Q_(3.67749375, "N"), Q_(5, "gf/W"), 4), 300.0, "W")

    def test_flight_time(self):
        # 0.8 x 74 W*h / 300 W = 0.197333 h = 11.84 min
        close(self, energy.flight_time(Q_(74, "W*h"), Q_(300, "W"), 0.8), 11.84, "min")

    def test_flight_time_default_fraction_is_0_8(self):
        close(self, energy.flight_time(Q_(74, "W*h"), Q_(300, "W")), 11.84, "min")

    def test_flight_time_whole_pack(self):
        # 1.0 x 28.86 W*h / 200 W = 0.1443 h = 8.658 min
        close(self, energy.flight_time(Q_(28.86, "W*h"), Q_(200, "W"), 1.0), 8.658, "min")

    def test_flight_time_rejects_impossible_fractions(self):
        for bad in (0.0, -0.2, 1.5):
            with self.assertRaises(ValueError, msg=f"usable_fraction={bad}"):
                energy.flight_time(Q_(74, "W*h"), Q_(300, "W"), bad)

    def test_flight_time_rejects_non_positive_power(self):
        for bad in (0, -300):
            with self.assertRaises(ValueError, msg=f"power={bad} W"):
                energy.flight_time(Q_(74, "W*h"), Q_(bad, "W"), 0.8)

    def test_whole_chain_example_drone(self):
        # 1.5 kg quad hovering at 4 x 375 gf with 5 gf/W, plus 3 W of avionics, on 74 W*h at 80%:
        # motors 300 W + 3 W = 303 W; t = 0.8 x 74 / 303 h = 0.195380 h = 11.7228 min
        p = energy.propulsion_power(Q_(375, "gf"), Q_(5, "gf/W"), 4) + Q_(3, "W")
        t = energy.flight_time(energy.battery_energy(Q_(5000, "mA*h"), 4), p, 0.8)
        close(self, t, 0.8 * 74 / 303 * 60, "min")


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