- 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
11 KiB
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:
- Stored values are always in the project's native system
(
ProjectMeta.units, defaultSI_M_N). The solver, persistence, and undo stack only ever see native values. - 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.
- 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—UnitSystemenum (4 systems), frozenUnitLabelsbundle (length / force / moment / stress / curvature / rotation, rotation alwaysrad),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_editorhardcodesy (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
UnitTagquantity annotations; generated catalog fields are stillstrplaceholders ('500 MPa',# TODO: unit-aware type). The quantity taxonomy in §5 below is meant to become thoseUnitTagstrings verbatim. - Examples use two systems in practice:
SI_M_N(cantilever → M3 peaks 50 kN·m; portal, space frame, concrete04) andUS_IN_KIP(ex1a–ex4 families in inches/kips/ksi,G=386.4 in/s²).SI_MM_NandUS_FT_KIPappear only in the enum, tests, andassign_hingeprefills.basic_truss.pyhand-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,
METRICandIMPERIAL(§3.1 and §3.4 below, renamed).SI_MM_NandUS_FT_KIPare retired — old.osmodelfiles still load via theLEGACY_UNIT_SYSTEMSmap incore/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 throughSetUnitsCommandand refreshes every units-aware label (_sync_units_everywhereonmodelMutated, covering combo, menu, and undo/redo). All values stay native, so every label next to a value uses theNATIVEtable (native_label()), never theDISPLAYprefixes —DISPLAYremains data-only for a future true display-switching phase. Each table lists the stored unit (what the solver and.osmodelsee) 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_LABELStoday) — 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.
rotationfactor 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)withlabels_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 stayss.views/docks/results_panel.py— displacement/force headers take display units; modal table keepsrad²/s²,rad/s,Hz,s.views/docks/response_spectrum.py— Sa axis takes the acceleration display unit; period stayss.- 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/plfsmall-value aliases — formatting rule in shared label helper, not per-view logic.
7. Persistence, examples, conventions
meta.unitskeeps 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
Optionalwith 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_Nand oneUS_FT_KIPexample 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.pyis grandfathered until then.
8. Verification
- Extend
tests/unit/test_unit_labels.py: factor-table coverage per system (identity forUS_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 diffon*.osmodelmust 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
UnitTagfield annotation per ADR-0001 §2.7. Explicitly out of scope until A–C ship.