otko/docs/consistent_units.md
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

11 KiB
Raw Blame History

Consistent Units

Plan for making units explicit and uniform across OTKO: model input, solver boundary, result display, and persistence. This is a plan, not an implementation — no code changes ship with this document.

1. Principle

OpenSees is unit-agnostic: it never converts. The engineer picks one consistent system and sticks to it. OTKO follows the same rule, one step further:

  1. Stored values are always in the project's native system (ProjectMeta.units, default SI_M_N). The solver, persistence, and undo stack only ever see native values.
  2. Views may relabel or rescale for humans, never for the solver. Any display conversion is a pure view-layer factor applied on read; converted values are never written back.
  3. Every number shown to the user carries its unit, or is explicitly dimensionless (strain, drift ratio, damping ratio).

What this document does NOT propose: auto-conversion on input, unit migration of existing models, or any change to OpenSeesPy command emission order/content (docs/architecture.md stays authoritative).

2. Where we are

  • src/otko/core/units.py — UnitSystem enum (4 systems), frozen UnitLabels bundle (length / force / moment / stress / curvature / rotation, rotation always rad), labels_for() helper. Display only; zero conversion factors anywhere in the repo.
  • ProjectMeta.units (src/otko/core/project.py:124) persisted in every .osmodel; SetUnitsCommand (src/otko/commands/project.py) is display-only by contract; Options → Set Display Units (src/otko/views/action_handlers.py) applies it and refreshes docks.
  • Views consume labels via set_units() (src/otko/views/dock_manager.py): pushover curve and material tester do; force diagrams, time-history, results panel, and response-spectrum Sa axis do not. section_editor hardcodes y (m) / z (m) — wrong for kip-in models. DOF labels (views/docks/_labels.py) are unitless by design.
  • ADR-0001 §2.7 deferred per-field UnitTag quantity annotations; generated catalog fields are still str placeholders ('500 MPa', # TODO: unit-aware type). The quantity taxonomy in §5 below is meant to become those UnitTag strings verbatim.
  • Examples use two systems in practice: SI_M_N (cantilever → M3 peaks 50 kN·m; portal, space frame, concrete04) and US_IN_KIP (ex1a–ex4 families in inches/kips/ksi, G=386.4 in/s²). SI_MM_N and US_FT_KIP appear only in the enum, tests, and assign_hinge prefills. basic_truss.py hand-converts (IN_TO_M, KIP_TO_N) to work in SI — the only example that does.
  • Roadmap backlog item 3 already asks for "units-aware labels" in the input-dialog pass; this plan is the spec for that item.

3. Canonical systems

Update (Sept 2026): collapsed to two stored systems, METRIC and IMPERIAL (§3.1 and §3.4 below, renamed). SI_MM_N and US_FT_KIP are retired — old .osmodel files still load via the LEGACY_UNIT_SYSTEMS map in core/units.py, but their stored numbers keep the old mm/ft scale (re-enter values natively). The status bar offers Metric / Imperial only; switching routes through SetUnitsCommand and refreshes every units-aware label (_sync_units_everywhere on modelMutated, covering combo, menu, and undo/redo). All values stay native, so every label next to a value uses the NATIVE table (native_label()), never the DISPLAY prefixes — DISPLAY remains data-only for a future true display-switching phase. Each table lists the stored unit (what the solver and .osmodel see) and the display unit + factor (view layer only, applied on read).

3.1 Metric — SI_M_N (m, N, Pa)

Building/civil scale. Stored = displayed, except where humans expect scaled prefixes (factor applies at the view, values untouched).

Quantity Stored Display Factor
Geometry / length m m 1
Section dims m mm 1000
Displacement m mm 1000
Rotation rad rad 1 (° toggle, §6)
Point load N kN 1e-3
Distributed load N/m kN/m 1e-3
Moment N·m kN·m 1e-3
Stress / modulus Pa MPa 1e-6
Area m² mm² 1e6
Inertia m⁴ mm⁴ / cm⁴ 1e12 / 1e8
Mass kg kg (t for large) 1
Time / period s s 1

3.2 Metric — SI_MM_N (mm, N, MPa)

Detail/component scale (steel connections, lab specimens). Stored = displayed everywhere; no factors.

Quantity Stored = Display
Geometry, section dims, displacement mm
Rotation rad
Point load N
Distributed load N/mm
Moment N·mm
Stress / modulus MPa
Mass t

3.3 Imperial — US_FT_KIP (ft, kip, ksf)

Building scale. The mixed ft/in convention engineers expect (lengths in feet, displacements in inches) requires display factors — this is the case that forces §4 to exist.

Quantity Stored Display Factor
Geometry / length ft ft 1
Section dims ft in 12
Displacement ft in 12
Rotation rad rad 1 (° toggle, §6)
Point load kip kip 1
Distributed load kip/ft kip/ft (plf alt.) 1
Moment kip·ft kip·ft 1
Stress / modulus ksf ksi (materials) 1/144
Area ft² in² 144
Inertia ft⁴ in⁴ 20736
Mass slug slug 1
Time / period s s 1

Small-load alternative: lbf / plf / lb·ft are display aliases (×1000 from kip units), not separate systems. A view showing 0.004 kip should render 4.0 lbf; threshold and format rules are view concerns (§6).

3.4 Imperial — US_IN_KIP (in, kip, ksi)

Component scale (members, sections, the ex1a–ex4 example families). Stored = displayed everywhere; no factors.

Quantity Stored = Display
Geometry, section dims, displacement in
Rotation rad
Point load kip (lbf alias ×1000)
Distributed load kip/in
Moment kip·in
Stress / modulus ksi
Mass kip·s²/in

Gravity for mass derivation is 386.4 in/s² in this system (32.2 ft/s² under US_FT_KIP, 9.81 m/s² metric) — document the constant next to every mass-from-weight computation; never hardcode it in a system-agnostic path.

4. Display-factor mechanism (view layer only)

New pure-data table in core/units.py, e.g. DISPLAY: dict[UnitSystem, dict[str, tuple[str, float]]] mapping quantity → (display label, multiply-by-factor). Rules:

  • Factors live in core/ as data only (like _LABELS today) — no Qt, no application logic, trivially unit-testable.
  • Factors are applied in views/viewmodels on read (axis labels, table cells, diagram annotations). Converted values never flow into commands, services, or persistence.
  • rotation factor is always 1 (rad); a degrees toggle is a formatting option (§6), not a system.
  • Dimensionless quantities (strain, drift, damping ratio, mass participation) never take factors.

This keeps the current "we don't auto-convert" contract intact: the solver boundary is untouched; only human-facing strings change.

5. Quantity taxonomy (shared with ADR-0001)

Each numeric field in core/ and catalog/ eventually gets one of these quantity tags (same strings as the future UnitTag, so the follow-up ADR adopts them unchanged):

length, displacement, rotation, force, moment, distributed_load, stress, area, inertia, mass, time, frequency, temperature

Minimum viable step (no ADR needed): use the taxonomy as the key set for the §4 table and as the vocabulary for dialog hints (§6). Field annotation (UnitTag) stays deferred per ADR-0001 §2.7.

6. UI rollout checklist

File-by-file, each item independently reviewable:

  • views/dialogs/section_editor.py:184-185 — replace hardcoded (m) with labels_for() length unit. (Bug fix, do first.)
  • views/docks/force_diagram.py — append force/moment display units to component labels and min/max annotations.
  • views/docks/time_history.py — y-axis unit per trace kind (displacement / velocity / acceleration); x-axis stays s.
  • views/docks/results_panel.py — displacement/force headers take display units; modal table keeps rad²/s², rad/s, Hz, s.
  • views/docks/response_spectrum.py — Sa axis takes the acceleration display unit; period stays s.
  • Assign dialogs (assign_load, distributed_load, pattern_loads) — unit hints on field labels, e.g. wy (kip/ft), driven by project system (roadmap item 3).
  • Table dock headers — name-style unit suffixes where numeric columns carry units; dimensionless columns stay bare.
  • Rotation display — solver and storage stay rad; add an optional ° formatting toggle in post views only (×180/π on read).
  • lbf/plf small-value aliases — formatting rule in shared label helper, not per-view logic.

7. Persistence, examples, conventions

  • meta.units keeps meaning "native stored system". Display choices (factors applied, ° toggle, lbf alias) are never persisted — reopening a file always shows native-unit defaults.
  • Old files without new fields (if any are added as Optional with defaults) load unchanged; ProjectMeta extra="forbid" still rejects unknown keys — no migration needed for this plan.
  • Document each example family's convention at the top of its script (one comment line: system + key constants). Add one SI_MM_N and one US_FT_KIP example so all four systems have runner-verified coverage; today two systems have none.
  • Rule for new examples: no hand-conversion constants. Either model natively in the declared system or, once §4 exists, use the shared factor table. basic_truss.py is grandfathered until then.

8. Verification

  • Extend tests/unit/test_unit_labels.py: factor-table coverage per system (identity for US_IN_KIP/SI_MM_N; ft→in ×12, N→kN, Pa→MPa, ksf→ksi spots), rotation always factor 1, dimensionless quantities absent from the table.
  • Round-trip invariant: regen all examples, git diff on *.osmodel must show label/metadata changes only — never numeric value changes.
  • GUI smoke (offscreen/xvfb): open one model per system, flip display units, assert axis/table labels change and stored values do not (compare model_dump() before/after).

9. Phases

  • A — label the stored unit everywhere (§6 minus factors): no new mechanism, pure label plumbing. Shippable alone.
  • B — display-factor table + apply in post views (§4, §5 keys): ft→in, N→kN, Pa→MPa and friends appear; solver untouched.
  • C — input-dialog hints + section_editor fix (roadmap item 3): hints only, values still entered in native units.
  • D — optional, needs follow-up ADR: true display-unit switching (type in inches, store feet) and UnitTag field annotation per ADR-0001 §2.7. Explicitly out of scope until A–C ship.