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
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:
parent
13bb5d1d91
commit
d01a5957b7
99 changed files with 3143 additions and 976 deletions
249
docs/consistent_units.md
Normal file
249
docs/consistent_units.md
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
# 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.
|
||||
Loading…
Reference in a new issue