feat: consolidate units to Metric/Imperial with unit-aware dialogs
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

- 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
This commit is contained in:
smillmorel 2026-09-08 16:33:30 -04:00
commit d01a5957b7
99 changed files with 3143 additions and 976 deletions

View file

@ -31,7 +31,7 @@ def test_empty_project_is_valid() -> None:
assert p.schema_version == 1
assert p.ndm == 3 and p.ndf == 6
assert p.nodes == []
assert p.meta.units == UnitSystem.SI_M_N
assert p.meta.units == UnitSystem.METRIC
def test_invalid_ndm_ndf_pair_rejected() -> None:
@ -42,9 +42,9 @@ def test_invalid_ndm_ndf_pair_rejected() -> None:
def test_meta_round_trip() -> None:
p = Project(meta=ProjectMeta(name="Bridge", author="Ozan", units=UnitSystem.SI_MM_N))
p = Project(meta=ProjectMeta(name="Bridge", author="Ozan", units=UnitSystem.IMPERIAL))
assert p.meta.name == "Bridge"
assert p.meta.units == UnitSystem.SI_MM_N
assert p.meta.units == UnitSystem.IMPERIAL
# ────────────────────────── id allocation ──────────────────────────
@ -126,7 +126,8 @@ def test_validate_references_catches_missing_time_series_in_pattern() -> None:
nodes=[Node(id=1, coords=(0, 0, 0))],
load_patterns=[
PlainLoadPattern(
id=1, time_series_id=99,
id=1,
time_series_id=99,
nodal_loads=[NodalLoad(node_id=1, forces=(0, 0, -10, 0, 0, 0))],
)
],
@ -143,8 +144,13 @@ def test_validate_references_catches_missing_material_in_fiber_patch() -> None:
id=1,
patches=[
RectangularPatch(
material_id=99, n_fib_y=2, n_fib_z=2,
y_i=-0.1, z_i=-0.1, y_j=0.1, z_j=0.1,
material_id=99,
n_fib_y=2,
n_fib_z=2,
y_i=-0.1,
z_i=-0.1,
y_j=0.1,
z_j=0.1,
)
],
)
@ -162,8 +168,13 @@ def test_validate_references_catches_missing_material_in_fiber_layer() -> None:
id=1,
layers=[
StraightLayer(
material_id=99, n_bars=3, bar_area=1e-4,
y_start=-0.1, z_start=-0.1, y_end=0.1, z_end=-0.1,
material_id=99,
n_bars=3,
bar_area=1e-4,
y_start=-0.1,
z_start=-0.1,
y_end=0.1,
z_end=-0.1,
)
],
)
@ -179,7 +190,8 @@ def test_validate_references_catches_missing_material_in_aggregator_pairing() ->
sections=[
ElasticSection(id=1, E=200e9, A=0.01, Iz=8.33e-6),
SectionAggregator(
id=2, section_id=1,
id=2,
section_id=1,
pairings=[AggregatorDOF(material_id=99, dof="T")],
),
],
@ -194,7 +206,8 @@ def test_validate_references_catches_missing_base_section_in_aggregator() -> Non
materials=[Steel01(id=1, Fy=420e6, E0=200e9, b=0.01)],
sections=[
SectionAggregator(
id=2, section_id=42,
id=2,
section_id=42,
pairings=[AggregatorDOF(material_id=1, dof="T")],
),
],
@ -214,19 +227,30 @@ def test_validate_references_passes_on_valid_fiber_and_aggregator() -> None:
id=1,
patches=[
RectangularPatch(
material_id=1, n_fib_y=2, n_fib_z=2,
y_i=-0.1, z_i=-0.1, y_j=0.1, z_j=0.1,
material_id=1,
n_fib_y=2,
n_fib_z=2,
y_i=-0.1,
z_i=-0.1,
y_j=0.1,
z_j=0.1,
)
],
layers=[
StraightLayer(
material_id=2, n_bars=3, bar_area=1e-4,
y_start=-0.1, z_start=-0.1, y_end=0.1, z_end=-0.1,
material_id=2,
n_bars=3,
bar_area=1e-4,
y_start=-0.1,
z_start=-0.1,
y_end=0.1,
z_end=-0.1,
)
],
),
SectionAggregator(
id=2, section_id=1,
id=2,
section_id=1,
pairings=[AggregatorDOF(material_id=1, dof="T")],
),
],
@ -237,7 +261,8 @@ def test_validate_references_passes_on_valid_fiber_and_aggregator() -> None:
# ────────────────────────── small smoke build ──────────────────────────
def test_full_truss_project_builds_and_validates() -> None:
p = Project(
ndm=2, ndf=2,
ndm=2,
ndf=2,
nodes=[
Node(id=1, coords=(0, 0, 0), restraint=(True, True, False, False, False, False)),
Node(id=2, coords=(4, 0, 0), restraint=(False, True, False, False, False, False)),
@ -253,7 +278,8 @@ def test_full_truss_project_builds_and_validates() -> None:
time_series=[LinearTimeSeries(id=1)],
load_patterns=[
PlainLoadPattern(
id=1, time_series_id=1,
id=1,
time_series_id=1,
nodal_loads=[NodalLoad(node_id=3, forces=(0, -1000, 0, 0, 0, 0))],
)
],