otko/docs/consistent_units.md

249 lines
11 KiB
Markdown
Raw Permalink Normal View 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.