ContentsFoundations to FrontierBezhig

eng-lab/hwe/catalog.py

All lab code. Download this file.

"""Component and material data, each number with its unit and its source.

Two kinds of data live in data/catalog/:
- Thrust tables (CSV, units in the headers): a manufacturer's bench test of one
  motor and propeller at one voltage. EL01 uses the maximum thrust and the
  efficiency (thrust per watt) at the hover point, and cross-checks the flight
  time with the measured current there; EL03 checks them against
  momentum theory, and EL15 against your own thrust stand.
- materials.toml: densities (and, from EL07 on, stiffness and strength).

Interpolation is linear between table rows and refuses to extrapolate: outside
the tested range the table says nothing, and the engine will not pretend.
"""
from __future__ import annotations

import tomllib
from dataclasses import dataclass
from pathlib import Path

import numpy as np

from hwe.testdata import Table, read_csv
from hwe.units import Q_, Quantity, parse

ROOT = Path(__file__).resolve().parents[1]
CATALOG = ROOT / "data" / "catalog"


@dataclass
class ThrustTable:
    table: Table
    source: str = ""

    @property
    def thrust(self) -> Quantity:
        return self.table.find("thrust")

    @property
    def efficiency(self) -> Quantity:
        return self.table.find("efficiency")

    @property
    def power(self) -> Quantity:
        return self.table.find("power")

    @property
    def current(self) -> Quantity:
        return self.table.find("current")

    def max_thrust(self) -> Quantity:
        """The largest thrust in the table (normally the 100% throttle row)."""
        return self.thrust.max()

    def efficiency_at(self, thrust: Quantity) -> Quantity:
        """Efficiency (thrust per watt) at a given thrust, by linear interpolation."""
        return self._at(self.efficiency, "gf/W", thrust)

    def current_at(self, thrust: Quantity) -> Quantity:
        """One motor's current at a given thrust, measured at the table's test voltage."""
        return self._at(self.current, "A", thrust)

    def _at(self, column: Quantity, unit: str, thrust: Quantity) -> Quantity:
        x = self.thrust.to("gf").m
        y = column.to(unit).m
        order = np.argsort(x)
        x, y = x[order], y[order]
        want = thrust.to("gf").m
        if not (x[0] <= want <= x[-1]):
            raise ValueError(
                f"thrust {thrust.to('gf'):.0f~P} is outside the tested range "
                f"{x[0]:.0f}-{x[-1]:.0f} gf of {self.table.path.name}")
        return Q_(float(np.interp(want, x, y)), unit)


def thrust_table(relative_path: str, source: str = "") -> ThrustTable:
    path = (ROOT / relative_path) if not Path(relative_path).is_absolute() else Path(relative_path)
    return ThrustTable(read_csv(path), source)


def materials() -> dict:
    """{name: {"density": Quantity, "source": str, ...}} from data/catalog/materials.toml."""
    with open(CATALOG / "materials.toml", "rb") as f:
        raw = tomllib.load(f)
    out = {}
    for name, entry in raw.items():
        out[name] = {k: (parse(v) if k not in ("source", "note", "name") else v)
                     for k, v in entry.items()}
    return out