otko/tests/unit/test_unit_labels.py
smillmorel d01a5957b7
Some checks failed
CI / lint (push) Has been cancelled
CI / test (macos-latest, 3.10) (push) Has been cancelled
CI / test (macos-latest, 3.11) (push) Has been cancelled
CI / test (macos-latest, 3.12) (push) Has been cancelled
CI / test (ubuntu-latest, 3.10) (push) Has been cancelled
CI / test (ubuntu-latest, 3.11) (push) Has been cancelled
CI / test (ubuntu-latest, 3.12) (push) Has been cancelled
CI / test (windows-latest, 3.10) (push) Has been cancelled
CI / test (windows-latest, 3.11) (push) Has been cancelled
CI / test (windows-latest, 3.12) (push) Has been cancelled
feat: consolidate units to Metric/Imperial with unit-aware dialogs
- core/units: two dominant systems (Metric m/kN, Imperial ft/kip) with
  display conversion helpers, legacy 4-system migration in ProjectMeta
- dialogs/docks: unit-aware material, section, case, load, grid and
  results labels; diagram renderer unit labels; render controls update
- docs: add consistent_units.md; regen examples/*.osmodel artifacts
- tests: update persistence/phase8/project/unit-labels for new systems
2026-09-08 16:33:30 -04:00

142 lines
5.2 KiB
Python

"""Unit-system label table tests."""
from __future__ import annotations
import pytest
from otko.core import UnitSystem, labels_for
from otko.core.units import (
QUANTITIES,
display_factor,
display_for,
display_label,
display_with_alias,
format_display,
format_rotation,
native_label,
to_display,
)
def test_metric_labels() -> None:
lab = labels_for(UnitSystem.METRIC)
assert lab.length == "m"
assert lab.force == "N"
assert lab.moment == "N·m"
assert lab.stress == "Pa"
assert lab.curvature == "1/m"
def test_imperial_labels() -> None:
lab = labels_for(UnitSystem.IMPERIAL)
assert lab.length == "in"
assert lab.force == "kip"
assert lab.moment == "kip·in"
assert lab.stress == "ksi"
assert lab.curvature == "1/in"
def test_only_two_unit_systems_remain() -> None:
"""The status-bar dropdown offers Metric and Imperial only."""
assert {u.name for u in UnitSystem} == {"METRIC", "IMPERIAL"}
def test_legacy_unit_strings_migrate_on_load() -> None:
"""Old .osmodel files (4-system era) load onto Metric/Imperial."""
from otko.core import ProjectMeta
assert ProjectMeta.model_validate({"units": "SI (m, N, kg, s, Pa)"}).units is UnitSystem.METRIC
assert ProjectMeta.model_validate({"units": "SI (mm, N, t, s, MPa)"}).units is UnitSystem.METRIC
assert (
ProjectMeta.model_validate({"units": "US (ft, kip, slug, s, ksf)"}).units
is UnitSystem.IMPERIAL
)
assert (
ProjectMeta.model_validate({"units": "US (in, kip, kip·s²/in, s, ksi)"}).units
is UnitSystem.IMPERIAL
)
def test_labels_for_covers_every_unit_system() -> None:
"""Every UnitSystem enum value must have a matching label bundle."""
for us in UnitSystem:
lab = labels_for(us)
# sanity: at least length / force populated.
assert lab.length and lab.force
def test_display_table_covers_taxonomy_per_system() -> None:
"""Every system exposes every taxonomy quantity (§5 key set)."""
for us in UnitSystem:
for q in QUANTITIES:
label, factor = display_for(us, q)
assert label
assert factor > 0
def test_display_identity_for_imperial() -> None:
"""Imperial stores = displays (no factors)."""
for q in QUANTITIES:
assert display_factor(UnitSystem.IMPERIAL, q) == 1.0
def test_display_spot_factors() -> None:
"""Spot-check Metric factors: N→kN, Pa→MPa, m→mm."""
assert display_for(UnitSystem.METRIC, "force") == ("kN", 1e-3)
assert display_for(UnitSystem.METRIC, "stress") == ("MPa", 1e-6)
assert display_for(UnitSystem.METRIC, "displacement") == ("mm", 1000.0)
assert display_for(UnitSystem.METRIC, "section_dim") == ("mm", 1000.0)
assert display_for(UnitSystem.METRIC, "area") == ("mm²", 1e6)
assert display_for(UnitSystem.METRIC, "inertia") == ("mm⁴", 1e12)
assert display_for(UnitSystem.IMPERIAL, "stress") == ("ksi", 1.0)
def test_rotation_always_factor_one() -> None:
"""Rotation stays rad everywhere; ° is formatting only."""
for us in UnitSystem:
assert display_for(us, "rotation") == ("rad", 1.0)
assert format_rotation(3.14159265).startswith("3.142")
assert "°" in format_rotation(3.14159265, use_deg=True)
def test_dimensionless_absent_from_table() -> None:
"""Strain/drift/damping never take factors — KeyError by design."""
for us in UnitSystem:
for q in ("strain", "drift", "damping_ratio", "mass_participation"):
with pytest.raises(KeyError):
display_for(us, q)
def test_to_display_applies_factor_on_read() -> None:
"""1 m → 1000 mm; Imperial identity; values never written back."""
assert to_display(1.0, UnitSystem.METRIC, "displacement") == pytest.approx(1000.0)
assert to_display(1.0, UnitSystem.IMPERIAL, "displacement") == pytest.approx(1.0)
assert to_display(2.0, UnitSystem.METRIC, "force") == pytest.approx(0.002)
assert format_display(1.0, UnitSystem.METRIC, "displacement") == "1000 mm"
assert display_label(UnitSystem.IMPERIAL, "moment") == "kip·in"
def test_native_labels_match_stored_units() -> None:
"""Labels next to native values must be native units, not prefixes."""
assert native_label(UnitSystem.METRIC, "force") == "N"
assert native_label(UnitSystem.METRIC, "stress") == "Pa"
assert native_label(UnitSystem.METRIC, "displacement") == "m"
assert native_label(UnitSystem.METRIC, "distributed_load") == "N/m"
assert native_label(UnitSystem.METRIC, "area") == "m²"
assert native_label(UnitSystem.METRIC, "inertia") == "m⁴"
assert native_label(UnitSystem.IMPERIAL, "force") == "kip"
assert native_label(UnitSystem.IMPERIAL, "stress") == "ksi"
assert native_label(UnitSystem.IMPERIAL, "length") == "in"
for us in UnitSystem:
for q in QUANTITIES:
assert native_label(us, q)
def test_small_value_lbf_alias() -> None:
"""0.004 kip renders as 4.0 lbf; large values stay in kip."""
val, label = display_with_alias(0.004, UnitSystem.IMPERIAL, "force")
assert label == "lbf"
assert val == pytest.approx(4.0)
val_big, label_big = display_with_alias(2.0, UnitSystem.IMPERIAL, "force")
assert label_big == "kip"
assert val_big == pytest.approx(2.0)