Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d01a5957b7 | |||
| 13bb5d1d91 | |||
| 8f1aae4370 | |||
| af78e564c9 |
108 changed files with 3347 additions and 997 deletions
|
|
@ -24,6 +24,10 @@ Strict one-way MVVM + services: `views → viewmodels → services → core`.
|
||||||
|
|
||||||
Runner emits OpenSeesPy commands in fixed order (`docs/architecture.md`): `wipe → model → node → fix → material → section → geomTransf → element → timeSeries → pattern/load → recorder → system/numberer/... → analyze`. Never reorder.
|
Runner emits OpenSeesPy commands in fixed order (`docs/architecture.md`): `wipe → model → node → fix → material → section → geomTransf → element → timeSeries → pattern/load → recorder → system/numberer/... → analyze`. Never reorder.
|
||||||
|
|
||||||
|
## Branches
|
||||||
|
|
||||||
|
- Do all feature/fix work on `develop` (or short-lived branches off `develop`, merged back into it). `main` is release-only — never commit directly to it.
|
||||||
|
|
||||||
## Verify (in this order)
|
## Verify (in this order)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
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.
|
||||||
|
|
@ -304,7 +304,7 @@ def build_ex4_portal2d_elastic_element() -> Project:
|
||||||
name=ELASTIC_VARIANT.name,
|
name=ELASTIC_VARIANT.name,
|
||||||
author="OpenSees Wiki / Silvia Mazzoni & Frank McKenna",
|
author="OpenSees Wiki / Silvia Mazzoni & Frank McKenna",
|
||||||
description=ELASTIC_VARIANT.description,
|
description=ELASTIC_VARIANT.description,
|
||||||
units=UnitSystem.US_IN_KIP,
|
units=UnitSystem.IMPERIAL,
|
||||||
),
|
),
|
||||||
ndm=2,
|
ndm=2,
|
||||||
ndf=3,
|
ndf=3,
|
||||||
|
|
@ -333,7 +333,7 @@ def build_ex4_portal2d_inelastic_section() -> Project:
|
||||||
name=INELASTIC_SECTION_VARIANT.name,
|
name=INELASTIC_SECTION_VARIANT.name,
|
||||||
author="OpenSees Wiki / Silvia Mazzoni & Frank McKenna",
|
author="OpenSees Wiki / Silvia Mazzoni & Frank McKenna",
|
||||||
description=INELASTIC_SECTION_VARIANT.description,
|
description=INELASTIC_SECTION_VARIANT.description,
|
||||||
units=UnitSystem.US_IN_KIP,
|
units=UnitSystem.IMPERIAL,
|
||||||
),
|
),
|
||||||
ndm=2,
|
ndm=2,
|
||||||
ndf=3,
|
ndf=3,
|
||||||
|
|
@ -383,7 +383,7 @@ def build_ex4_portal2d_inelastic_fiber_section() -> Project:
|
||||||
name=FIBER_VARIANT.name,
|
name=FIBER_VARIANT.name,
|
||||||
author="OpenSees Wiki / Silvia Mazzoni & Frank McKenna",
|
author="OpenSees Wiki / Silvia Mazzoni & Frank McKenna",
|
||||||
description=FIBER_VARIANT.description,
|
description=FIBER_VARIANT.description,
|
||||||
units=UnitSystem.US_IN_KIP,
|
units=UnitSystem.IMPERIAL,
|
||||||
),
|
),
|
||||||
ndm=2,
|
ndm=2,
|
||||||
ndf=3,
|
ndf=3,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"name": "Basic Truss",
|
"name": "Basic Truss",
|
||||||
"description": "3-bar asymmetric planar truss, linear static analysis.",
|
"description": "3-bar asymmetric planar truss, linear static analysis.",
|
||||||
"author": "OpenSees Examples Manual - Example 1",
|
"author": "OpenSees Examples Manual - Example 1",
|
||||||
"units": "SI (m, N, kg, s, Pa)"
|
"units": "Metric (m, N, kg, s, Pa)"
|
||||||
},
|
},
|
||||||
"ndm": 2,
|
"ndm": 2,
|
||||||
"ndf": 2,
|
"ndf": 2,
|
||||||
|
|
@ -245,6 +245,7 @@
|
||||||
"rho": 0.0
|
"rho": 0.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"mp_constraints": [],
|
||||||
"time_series": [
|
"time_series": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
|
@ -284,6 +285,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
1
|
1
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"n_steps": 1,
|
"n_steps": 1,
|
||||||
"load_factor_increment": 1.0,
|
"load_factor_increment": 1.0,
|
||||||
"system": "BandGeneral",
|
"system": "BandGeneral",
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,7 @@ def build_basic_truss() -> Project:
|
||||||
name="Basic Truss",
|
name="Basic Truss",
|
||||||
author="OpenSees Examples Manual - Example 1",
|
author="OpenSees Examples Manual - Example 1",
|
||||||
description="3-bar asymmetric planar truss, linear static analysis.",
|
description="3-bar asymmetric planar truss, linear static analysis.",
|
||||||
units=UnitSystem.SI_M_N,
|
units=UnitSystem.METRIC,
|
||||||
),
|
),
|
||||||
ndm=2, ndf=2,
|
ndm=2, ndf=2,
|
||||||
coord_systems=[
|
coord_systems=[
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"name": "Simply Supported Beam — Quad Elements (OpenSees Ex 6.4)",
|
"name": "Simply Supported Beam — Quad Elements (OpenSees Ex 6.4)",
|
||||||
"description": "16×4 plane-stress quad mesh of a 40×10 deep beam with two midspan point loads, LoadControl 0→10 in 10 steps.",
|
"description": "16×4 plane-stress quad mesh of a 40×10 deep beam with two midspan point loads, LoadControl 0→10 in 10 steps.",
|
||||||
"author": "OpenSees Examples Manual",
|
"author": "OpenSees Examples Manual",
|
||||||
"units": "US (in, kip, kip·s²/in, s, ksi)"
|
"units": "Imperial (in, kip, kip·s²/in, s, ksi)"
|
||||||
},
|
},
|
||||||
"ndm": 2,
|
"ndm": 2,
|
||||||
"ndf": 2,
|
"ndf": 2,
|
||||||
|
|
@ -3391,6 +3391,7 @@
|
||||||
"b2": 0.0
|
"b2": 0.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"mp_constraints": [],
|
||||||
"time_series": [
|
"time_series": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
|
@ -3441,6 +3442,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
1
|
1
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"n_steps": 10,
|
"n_steps": 10,
|
||||||
"load_factor_increment": 1.0,
|
"load_factor_increment": 1.0,
|
||||||
"system": "ProfileSPD",
|
"system": "ProfileSPD",
|
||||||
|
|
@ -3456,6 +3458,7 @@
|
||||||
"name": "FreeVibration",
|
"name": "FreeVibration",
|
||||||
"type": "Transient",
|
"type": "Transient",
|
||||||
"pattern_ids": [],
|
"pattern_ids": [],
|
||||||
|
"pattern_factors": {},
|
||||||
"dt": 0.5,
|
"dt": 0.5,
|
||||||
"n_steps": 1500,
|
"n_steps": 1500,
|
||||||
"system": "BandGeneral",
|
"system": "BandGeneral",
|
||||||
|
|
@ -3471,6 +3474,8 @@
|
||||||
"max_iter": 10,
|
"max_iter": 10,
|
||||||
"rayleigh_alpha_m": 0.0,
|
"rayleigh_alpha_m": 0.0,
|
||||||
"rayleigh_beta_k": 0.0,
|
"rayleigh_beta_k": 0.0,
|
||||||
|
"rayleigh_beta_k_init": 0.0,
|
||||||
|
"rayleigh_beta_k_comm": 0.0,
|
||||||
"rayleigh_mode1_damping": 0.02,
|
"rayleigh_mode1_damping": 0.02,
|
||||||
"preload_case_ids": [
|
"preload_case_ids": [
|
||||||
1
|
1
|
||||||
|
|
|
||||||
|
|
@ -132,7 +132,7 @@ def build_beam_quad_2d() -> Project:
|
||||||
f"{NX}×{NY} plane-stress quad mesh of a {L:.0f}×{H:.0f} deep "
|
f"{NX}×{NY} plane-stress quad mesh of a {L:.0f}×{H:.0f} deep "
|
||||||
"beam with two midspan point loads, LoadControl 0→10 in 10 steps."
|
"beam with two midspan point loads, LoadControl 0→10 in 10 steps."
|
||||||
),
|
),
|
||||||
units=UnitSystem.US_IN_KIP,
|
units=UnitSystem.IMPERIAL,
|
||||||
),
|
),
|
||||||
ndm=2, ndf=2,
|
ndm=2, ndf=2,
|
||||||
nodes=nodes,
|
nodes=nodes,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"name": "Cantilever",
|
"name": "Cantilever",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "Ozan",
|
"author": "Ozan",
|
||||||
"units": "SI (m, N, kg, s, Pa)"
|
"units": "Metric (m, N, kg, s, Pa)"
|
||||||
},
|
},
|
||||||
"ndm": 3,
|
"ndm": 3,
|
||||||
"ndf": 6,
|
"ndf": 6,
|
||||||
|
|
@ -198,7 +198,8 @@
|
||||||
"Iz": 0.0002,
|
"Iz": 0.0002,
|
||||||
"Iy": 4.5e-05,
|
"Iy": 4.5e-05,
|
||||||
"G": 80000000000.0,
|
"G": 80000000000.0,
|
||||||
"J": 8.5e-07
|
"J": 8.5e-07,
|
||||||
|
"shape": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -212,7 +213,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 1,
|
"section_id": 1,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
|
|
@ -224,7 +226,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 1,
|
"section_id": 1,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 3,
|
"id": 3,
|
||||||
|
|
@ -236,7 +239,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 1,
|
"section_id": 1,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 4,
|
"id": 4,
|
||||||
|
|
@ -248,7 +252,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 1,
|
"section_id": 1,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 5,
|
"id": 5,
|
||||||
|
|
@ -260,9 +265,11 @@
|
||||||
],
|
],
|
||||||
"section_id": 1,
|
"section_id": 1,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"mp_constraints": [],
|
||||||
"time_series": [
|
"time_series": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
|
@ -341,6 +348,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
1
|
1
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"n_steps": 1,
|
"n_steps": 1,
|
||||||
"load_factor_increment": 1.0,
|
"load_factor_increment": 1.0,
|
||||||
"system": "BandGeneral",
|
"system": "BandGeneral",
|
||||||
|
|
@ -358,6 +366,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
2
|
2
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"n_steps": 1,
|
"n_steps": 1,
|
||||||
"load_factor_increment": 1.0,
|
"load_factor_increment": 1.0,
|
||||||
"system": "BandGeneral",
|
"system": "BandGeneral",
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ def build_cantilever() -> Project:
|
||||||
]
|
]
|
||||||
|
|
||||||
return Project(
|
return Project(
|
||||||
meta=ProjectMeta(name="Cantilever", author="Ozan", units=UnitSystem.SI_M_N),
|
meta=ProjectMeta(name="Cantilever", author="Ozan", units=UnitSystem.METRIC),
|
||||||
ndm=3, ndf=6,
|
ndm=3, ndf=6,
|
||||||
nodes=nodes,
|
nodes=nodes,
|
||||||
sections=[
|
sections=[
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"name": "Concrete04 Cantilever",
|
"name": "Concrete04 Cantilever",
|
||||||
"description": "Single RC column cantilever using Concrete04 (Popovics) fiber section. Demonstrates the Concrete04 material end-to-end: schema → model → runner → results.",
|
"description": "Single RC column cantilever using Concrete04 (Popovics) fiber section. Demonstrates the Concrete04 material end-to-end: schema → model → runner → results.",
|
||||||
"author": "",
|
"author": "",
|
||||||
"units": "SI (m, N, kg, s, Pa)"
|
"units": "Metric (m, N, kg, s, Pa)"
|
||||||
},
|
},
|
||||||
"ndm": 2,
|
"ndm": 2,
|
||||||
"ndf": 3,
|
"ndf": 3,
|
||||||
|
|
@ -237,6 +237,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
1
|
1
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"n_steps": 10,
|
"n_steps": 10,
|
||||||
"load_factor_increment": 0.1,
|
"load_factor_increment": 0.1,
|
||||||
"system": "BandGeneral",
|
"system": "BandGeneral",
|
||||||
|
|
@ -254,6 +255,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
2
|
2
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"control_node": 2,
|
"control_node": 2,
|
||||||
"control_dof": 1,
|
"control_dof": 1,
|
||||||
"target_disp": 0.15000000000000002,
|
"target_disp": 0.15000000000000002,
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ def build_concrete04_cantilever() -> Project:
|
||||||
"fiber section. Demonstrates the Concrete04 material "
|
"fiber section. Demonstrates the Concrete04 material "
|
||||||
"end-to-end: schema → model → runner → results."
|
"end-to-end: schema → model → runner → results."
|
||||||
),
|
),
|
||||||
units=UnitSystem.SI_M_N,
|
units=UnitSystem.METRIC,
|
||||||
),
|
),
|
||||||
ndm=2,
|
ndm=2,
|
||||||
ndf=3,
|
ndf=3,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"name": "Eigen - Two-Storey One-Bay Frame",
|
"name": "Eigen - Two-Storey One-Bay Frame",
|
||||||
"description": "Two-storey one-bay elastic frame with two-mode eigenvalue analysis.",
|
"description": "Two-storey one-bay elastic frame with two-mode eigenvalue analysis.",
|
||||||
"author": "OpenSees Wiki / Chopra Example 10.5",
|
"author": "OpenSees Wiki / Chopra Example 10.5",
|
||||||
"units": "US (in, kip, kip·s²/in, s, ksi)"
|
"units": "Imperial (in, kip, kip·s²/in, s, ksi)"
|
||||||
},
|
},
|
||||||
"ndm": 2,
|
"ndm": 2,
|
||||||
"ndf": 3,
|
"ndf": 3,
|
||||||
|
|
@ -198,7 +198,8 @@
|
||||||
"Iz": 640.0,
|
"Iz": 640.0,
|
||||||
"Iy": null,
|
"Iy": null,
|
||||||
"G": null,
|
"G": null,
|
||||||
"J": null
|
"J": null,
|
||||||
|
"shape": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
|
|
@ -209,7 +210,8 @@
|
||||||
"Iz": 320.0,
|
"Iz": 320.0,
|
||||||
"Iy": null,
|
"Iy": null,
|
||||||
"G": null,
|
"G": null,
|
||||||
"J": null
|
"J": null,
|
||||||
|
"shape": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -223,7 +225,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 1,
|
"section_id": 1,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
|
|
@ -235,7 +238,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 2,
|
"section_id": 2,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 3,
|
"id": 3,
|
||||||
|
|
@ -247,7 +251,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 1,
|
"section_id": 1,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 4,
|
"id": 4,
|
||||||
|
|
@ -259,7 +264,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 2,
|
"section_id": 2,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 5,
|
"id": 5,
|
||||||
|
|
@ -271,7 +277,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 1,
|
"section_id": 1,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 6,
|
"id": 6,
|
||||||
|
|
@ -283,7 +290,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 2,
|
"section_id": 2,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"mp_constraints": [],
|
"mp_constraints": [],
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ def build_eigen_two_storey_one_bay_frame() -> Project:
|
||||||
description=(
|
description=(
|
||||||
"Two-storey one-bay elastic frame with two-mode eigenvalue analysis."
|
"Two-storey one-bay elastic frame with two-mode eigenvalue analysis."
|
||||||
),
|
),
|
||||||
units=UnitSystem.US_IN_KIP,
|
units=UnitSystem.IMPERIAL,
|
||||||
),
|
),
|
||||||
ndm=2,
|
ndm=2,
|
||||||
ndf=3,
|
ndf=3,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"name": "Eigen - Two-Storey Shear Frame",
|
"name": "Eigen - Two-Storey Shear Frame",
|
||||||
"description": "Two-storey shear frame with equalDOF floor constraints and two-mode eigenvalue analysis.",
|
"description": "Two-storey shear frame with equalDOF floor constraints and two-mode eigenvalue analysis.",
|
||||||
"author": "OpenSees Wiki / Chopra Example 10.4",
|
"author": "OpenSees Wiki / Chopra Example 10.4",
|
||||||
"units": "US (in, kip, kip·s²/in, s, ksi)"
|
"units": "Imperial (in, kip, kip·s²/in, s, ksi)"
|
||||||
},
|
},
|
||||||
"ndm": 2,
|
"ndm": 2,
|
||||||
"ndf": 3,
|
"ndf": 3,
|
||||||
|
|
@ -198,7 +198,8 @@
|
||||||
"Iz": 640.0,
|
"Iz": 640.0,
|
||||||
"Iy": null,
|
"Iy": null,
|
||||||
"G": null,
|
"G": null,
|
||||||
"J": null
|
"J": null,
|
||||||
|
"shape": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
|
|
@ -209,7 +210,8 @@
|
||||||
"Iz": 320.0,
|
"Iz": 320.0,
|
||||||
"Iy": null,
|
"Iy": null,
|
||||||
"G": null,
|
"G": null,
|
||||||
"J": null
|
"J": null,
|
||||||
|
"shape": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 3,
|
"id": 3,
|
||||||
|
|
@ -220,7 +222,8 @@
|
||||||
"Iz": 10000000000000.0,
|
"Iz": 10000000000000.0,
|
||||||
"Iy": null,
|
"Iy": null,
|
||||||
"G": null,
|
"G": null,
|
||||||
"J": null
|
"J": null,
|
||||||
|
"shape": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -234,7 +237,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 1,
|
"section_id": 1,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
|
|
@ -246,7 +250,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 2,
|
"section_id": 2,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 3,
|
"id": 3,
|
||||||
|
|
@ -258,7 +263,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 1,
|
"section_id": 1,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 4,
|
"id": 4,
|
||||||
|
|
@ -270,7 +276,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 2,
|
"section_id": 2,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 5,
|
"id": 5,
|
||||||
|
|
@ -282,7 +289,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 3,
|
"section_id": 3,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 6,
|
"id": 6,
|
||||||
|
|
@ -294,7 +302,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 3,
|
"section_id": 3,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"mp_constraints": [
|
"mp_constraints": [
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ def build_eigen_two_storey_shear_frame() -> Project:
|
||||||
"Two-storey shear frame with equalDOF floor constraints and "
|
"Two-storey shear frame with equalDOF floor constraints and "
|
||||||
"two-mode eigenvalue analysis."
|
"two-mode eigenvalue analysis."
|
||||||
),
|
),
|
||||||
units=UnitSystem.US_IN_KIP,
|
units=UnitSystem.IMPERIAL,
|
||||||
),
|
),
|
||||||
ndm=2,
|
ndm=2,
|
||||||
ndf=3,
|
ndf=3,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"name": "Elastic Frame (OpenSees Ex 4)",
|
"name": "Elastic Frame (OpenSees Ex 4)",
|
||||||
"description": "3-story 3-bay 2D elastic frame, AISC W-shape sections, gravity (distributed) + lateral (point) + 5-mode eigen.",
|
"description": "3-story 3-bay 2D elastic frame, AISC W-shape sections, gravity (distributed) + lateral (point) + 5-mode eigen.",
|
||||||
"author": "OpenSees Examples Manual",
|
"author": "OpenSees Examples Manual",
|
||||||
"units": "US (in, kip, kip·s²/in, s, ksi)"
|
"units": "Imperial (in, kip, kip·s²/in, s, ksi)"
|
||||||
},
|
},
|
||||||
"ndm": 2,
|
"ndm": 2,
|
||||||
"ndf": 3,
|
"ndf": 3,
|
||||||
|
|
@ -523,7 +523,8 @@
|
||||||
"Iz": 3400.0,
|
"Iz": 3400.0,
|
||||||
"Iy": 3400.0,
|
"Iy": 3400.0,
|
||||||
"G": 11200.0,
|
"G": 11200.0,
|
||||||
"J": 1.0
|
"J": 1.0,
|
||||||
|
"shape": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
|
|
@ -534,7 +535,8 @@
|
||||||
"Iz": 4330.0,
|
"Iz": 4330.0,
|
||||||
"Iy": 4330.0,
|
"Iy": 4330.0,
|
||||||
"G": 11200.0,
|
"G": 11200.0,
|
||||||
"J": 1.0
|
"J": 1.0,
|
||||||
|
"shape": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 3,
|
"id": 3,
|
||||||
|
|
@ -545,7 +547,8 @@
|
||||||
"Iz": 5900.0,
|
"Iz": 5900.0,
|
||||||
"Iy": 5900.0,
|
"Iy": 5900.0,
|
||||||
"G": 11200.0,
|
"G": 11200.0,
|
||||||
"J": 1.0
|
"J": 1.0,
|
||||||
|
"shape": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 4,
|
"id": 4,
|
||||||
|
|
@ -556,7 +559,8 @@
|
||||||
"Iz": 4930.0,
|
"Iz": 4930.0,
|
||||||
"Iy": 4930.0,
|
"Iy": 4930.0,
|
||||||
"G": 11200.0,
|
"G": 11200.0,
|
||||||
"J": 1.0
|
"J": 1.0,
|
||||||
|
"shape": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 5,
|
"id": 5,
|
||||||
|
|
@ -567,7 +571,8 @@
|
||||||
"Iz": 1830.0,
|
"Iz": 1830.0,
|
||||||
"Iy": 1830.0,
|
"Iy": 1830.0,
|
||||||
"G": 11200.0,
|
"G": 11200.0,
|
||||||
"J": 1.0
|
"J": 1.0,
|
||||||
|
"shape": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -581,7 +586,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 1,
|
"section_id": 1,
|
||||||
"geom_transf": "PDelta",
|
"geom_transf": "PDelta",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
|
|
@ -593,7 +599,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 2,
|
"section_id": 2,
|
||||||
"geom_transf": "PDelta",
|
"geom_transf": "PDelta",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 3,
|
"id": 3,
|
||||||
|
|
@ -605,7 +612,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 2,
|
"section_id": 2,
|
||||||
"geom_transf": "PDelta",
|
"geom_transf": "PDelta",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 4,
|
"id": 4,
|
||||||
|
|
@ -617,7 +625,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 1,
|
"section_id": 1,
|
||||||
"geom_transf": "PDelta",
|
"geom_transf": "PDelta",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 5,
|
"id": 5,
|
||||||
|
|
@ -629,7 +638,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 1,
|
"section_id": 1,
|
||||||
"geom_transf": "PDelta",
|
"geom_transf": "PDelta",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 6,
|
"id": 6,
|
||||||
|
|
@ -641,7 +651,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 2,
|
"section_id": 2,
|
||||||
"geom_transf": "PDelta",
|
"geom_transf": "PDelta",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 7,
|
"id": 7,
|
||||||
|
|
@ -653,7 +664,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 2,
|
"section_id": 2,
|
||||||
"geom_transf": "PDelta",
|
"geom_transf": "PDelta",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 8,
|
"id": 8,
|
||||||
|
|
@ -665,7 +677,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 1,
|
"section_id": 1,
|
||||||
"geom_transf": "PDelta",
|
"geom_transf": "PDelta",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 9,
|
"id": 9,
|
||||||
|
|
@ -677,7 +690,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 1,
|
"section_id": 1,
|
||||||
"geom_transf": "PDelta",
|
"geom_transf": "PDelta",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 10,
|
"id": 10,
|
||||||
|
|
@ -689,7 +703,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 2,
|
"section_id": 2,
|
||||||
"geom_transf": "PDelta",
|
"geom_transf": "PDelta",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 11,
|
"id": 11,
|
||||||
|
|
@ -701,7 +716,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 2,
|
"section_id": 2,
|
||||||
"geom_transf": "PDelta",
|
"geom_transf": "PDelta",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 12,
|
"id": 12,
|
||||||
|
|
@ -713,7 +729,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 1,
|
"section_id": 1,
|
||||||
"geom_transf": "PDelta",
|
"geom_transf": "PDelta",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 13,
|
"id": 13,
|
||||||
|
|
@ -725,7 +742,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 3,
|
"section_id": 3,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 14,
|
"id": 14,
|
||||||
|
|
@ -737,7 +755,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 3,
|
"section_id": 3,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 15,
|
"id": 15,
|
||||||
|
|
@ -749,7 +768,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 3,
|
"section_id": 3,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 16,
|
"id": 16,
|
||||||
|
|
@ -761,7 +781,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 4,
|
"section_id": 4,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 17,
|
"id": 17,
|
||||||
|
|
@ -773,7 +794,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 4,
|
"section_id": 4,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 18,
|
"id": 18,
|
||||||
|
|
@ -785,7 +807,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 4,
|
"section_id": 4,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 19,
|
"id": 19,
|
||||||
|
|
@ -797,7 +820,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 5,
|
"section_id": 5,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 20,
|
"id": 20,
|
||||||
|
|
@ -809,7 +833,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 5,
|
"section_id": 5,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 21,
|
"id": 21,
|
||||||
|
|
@ -821,9 +846,11 @@
|
||||||
],
|
],
|
||||||
"section_id": 5,
|
"section_id": 5,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"mp_constraints": [],
|
||||||
"time_series": [
|
"time_series": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
|
@ -954,6 +981,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
1
|
1
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"n_steps": 1,
|
"n_steps": 1,
|
||||||
"load_factor_increment": 1.0,
|
"load_factor_increment": 1.0,
|
||||||
"system": "BandGeneral",
|
"system": "BandGeneral",
|
||||||
|
|
@ -972,6 +1000,7 @@
|
||||||
1,
|
1,
|
||||||
2
|
2
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"n_steps": 1,
|
"n_steps": 1,
|
||||||
"load_factor_increment": 1.0,
|
"load_factor_increment": 1.0,
|
||||||
"system": "BandGeneral",
|
"system": "BandGeneral",
|
||||||
|
|
|
||||||
|
|
@ -227,7 +227,7 @@ def build_elastic_frame() -> Project:
|
||||||
"3-story 3-bay 2D elastic frame, AISC W-shape sections, "
|
"3-story 3-bay 2D elastic frame, AISC W-shape sections, "
|
||||||
"gravity (distributed) + lateral (point) + 5-mode eigen."
|
"gravity (distributed) + lateral (point) + 5-mode eigen."
|
||||||
),
|
),
|
||||||
units=UnitSystem.US_IN_KIP,
|
units=UnitSystem.IMPERIAL,
|
||||||
),
|
),
|
||||||
ndm=2, ndf=3,
|
ndm=2, ndf=3,
|
||||||
coord_systems=[
|
coord_systems=[
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"name": "OpenSees Ex 1a - 2D Elastic Cantilever Column",
|
"name": "OpenSees Ex 1a - 2D Elastic Cantilever Column",
|
||||||
"description": "Original Ex 1a elastic cantilever column with shared gravity preload, static pushover, and BM68elc base-excitation cases.",
|
"description": "Original Ex 1a elastic cantilever column with shared gravity preload, static pushover, and BM68elc base-excitation cases.",
|
||||||
"author": "OpenSees Wiki / Silvia Mazzoni & Frank McKenna",
|
"author": "OpenSees Wiki / Silvia Mazzoni & Frank McKenna",
|
||||||
"units": "US (in, kip, kip·s²/in, s, ksi)"
|
"units": "Imperial (in, kip, kip·s²/in, s, ksi)"
|
||||||
},
|
},
|
||||||
"ndm": 2,
|
"ndm": 2,
|
||||||
"ndf": 3,
|
"ndf": 3,
|
||||||
|
|
@ -98,7 +98,8 @@
|
||||||
"Iz": 1080000.0,
|
"Iz": 1080000.0,
|
||||||
"Iy": 1080000.0,
|
"Iy": 1080000.0,
|
||||||
"G": 1.0,
|
"G": 1.0,
|
||||||
"J": 1.0
|
"J": 1.0,
|
||||||
|
"shape": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -112,7 +113,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 1,
|
"section_id": 1,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"mp_constraints": [],
|
"mp_constraints": [],
|
||||||
|
|
@ -4138,7 +4140,8 @@
|
||||||
0.0006658006,
|
0.0006658006,
|
||||||
0.0006636009
|
0.0006636009
|
||||||
],
|
],
|
||||||
"file_path": "BM68elc.acc"
|
"file_path": "BM68elc.acc",
|
||||||
|
"use_last": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"load_patterns": [
|
"load_patterns": [
|
||||||
|
|
@ -4202,6 +4205,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
1
|
1
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"n_steps": 10,
|
"n_steps": 10,
|
||||||
"load_factor_increment": 0.1,
|
"load_factor_increment": 0.1,
|
||||||
"system": "BandGeneral",
|
"system": "BandGeneral",
|
||||||
|
|
@ -4219,6 +4223,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
2
|
2
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"control_node": 2,
|
"control_node": 2,
|
||||||
"control_dof": 1,
|
"control_dof": 1,
|
||||||
"target_disp": 100.0,
|
"target_disp": 100.0,
|
||||||
|
|
@ -4243,6 +4248,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
3
|
3
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"dt": 0.02,
|
"dt": 0.02,
|
||||||
"n_steps": 1000,
|
"n_steps": 1000,
|
||||||
"system": "BandGeneral",
|
"system": "BandGeneral",
|
||||||
|
|
@ -4258,6 +4264,8 @@
|
||||||
"max_iter": 10,
|
"max_iter": 10,
|
||||||
"rayleigh_alpha_m": 0.0,
|
"rayleigh_alpha_m": 0.0,
|
||||||
"rayleigh_beta_k": 0.0,
|
"rayleigh_beta_k": 0.0,
|
||||||
|
"rayleigh_beta_k_init": 0.0,
|
||||||
|
"rayleigh_beta_k_comm": 0.0,
|
||||||
"rayleigh_mode1_damping": 0.02,
|
"rayleigh_mode1_damping": 0.02,
|
||||||
"preload_case_ids": [
|
"preload_case_ids": [
|
||||||
1
|
1
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ def build_ex1a_canti2d() -> Project:
|
||||||
"Original Ex 1a elastic cantilever column with shared gravity "
|
"Original Ex 1a elastic cantilever column with shared gravity "
|
||||||
"preload, static pushover, and BM68elc base-excitation cases."
|
"preload, static pushover, and BM68elc base-excitation cases."
|
||||||
),
|
),
|
||||||
units=UnitSystem.US_IN_KIP,
|
units=UnitSystem.IMPERIAL,
|
||||||
),
|
),
|
||||||
ndm=2,
|
ndm=2,
|
||||||
ndf=3,
|
ndf=3,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"name": "2D Elastic Cantilever EQ (OpenSees Ex 1a)",
|
"name": "2D Elastic Cantilever EQ (OpenSees Ex 1a)",
|
||||||
"description": "Elastic 2D cantilever with gravity preload + horizontal UniformExcitation time history from the A10000 record.",
|
"description": "Elastic 2D cantilever with gravity preload + horizontal UniformExcitation time history from the A10000 record.",
|
||||||
"author": "OpenSees Wiki / Examples Manual",
|
"author": "OpenSees Wiki / Examples Manual",
|
||||||
"units": "US (in, kip, kip·s²/in, s, ksi)"
|
"units": "Imperial (in, kip, kip·s²/in, s, ksi)"
|
||||||
},
|
},
|
||||||
"ndm": 2,
|
"ndm": 2,
|
||||||
"ndf": 3,
|
"ndf": 3,
|
||||||
|
|
@ -98,7 +98,8 @@
|
||||||
"Iz": 1080000.0,
|
"Iz": 1080000.0,
|
||||||
"Iy": 1080000.0,
|
"Iy": 1080000.0,
|
||||||
"G": 1.0,
|
"G": 1.0,
|
||||||
"J": 1.0
|
"J": 1.0,
|
||||||
|
"shape": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -112,9 +113,11 @@
|
||||||
],
|
],
|
||||||
"section_id": 1,
|
"section_id": 1,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"mp_constraints": [],
|
||||||
"time_series": [
|
"time_series": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
|
@ -8121,7 +8124,8 @@
|
||||||
-0.000147416,
|
-0.000147416,
|
||||||
-0.000147326
|
-0.000147326
|
||||||
],
|
],
|
||||||
"file_path": "A10000.txt"
|
"file_path": "A10000.txt",
|
||||||
|
"use_last": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"load_patterns": [
|
"load_patterns": [
|
||||||
|
|
@ -8165,6 +8169,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
1
|
1
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"n_steps": 10,
|
"n_steps": 10,
|
||||||
"load_factor_increment": 0.1,
|
"load_factor_increment": 0.1,
|
||||||
"system": "BandGeneral",
|
"system": "BandGeneral",
|
||||||
|
|
@ -8182,6 +8187,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
2
|
2
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"dt": 0.01,
|
"dt": 0.01,
|
||||||
"n_steps": 3995,
|
"n_steps": 3995,
|
||||||
"system": "BandGeneral",
|
"system": "BandGeneral",
|
||||||
|
|
@ -8197,6 +8203,8 @@
|
||||||
"max_iter": 10,
|
"max_iter": 10,
|
||||||
"rayleigh_alpha_m": 0.0,
|
"rayleigh_alpha_m": 0.0,
|
||||||
"rayleigh_beta_k": 0.0,
|
"rayleigh_beta_k": 0.0,
|
||||||
|
"rayleigh_beta_k_init": 0.0,
|
||||||
|
"rayleigh_beta_k_comm": 0.0,
|
||||||
"rayleigh_mode1_damping": 0.02,
|
"rayleigh_mode1_damping": 0.02,
|
||||||
"preload_case_ids": [
|
"preload_case_ids": [
|
||||||
1
|
1
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ def build_ex1a_canti2d_eq() -> Project:
|
||||||
"Elastic 2D cantilever with gravity preload + horizontal "
|
"Elastic 2D cantilever with gravity preload + horizontal "
|
||||||
"UniformExcitation time history from the A10000 record."
|
"UniformExcitation time history from the A10000 record."
|
||||||
),
|
),
|
||||||
units=UnitSystem.US_IN_KIP,
|
units=UnitSystem.IMPERIAL,
|
||||||
),
|
),
|
||||||
ndm=2,
|
ndm=2,
|
||||||
ndf=3,
|
ndf=3,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"name": "OpenSees Ex 1b - Elastic Portal Frame",
|
"name": "OpenSees Ex 1b - Elastic Portal Frame",
|
||||||
"description": "Original Ex 1b elastic portal frame with shared gravity preload, static pushover, and BM68elc earthquake cases.",
|
"description": "Original Ex 1b elastic portal frame with shared gravity preload, static pushover, and BM68elc earthquake cases.",
|
||||||
"author": "OpenSees Wiki / Silvia Mazzoni & Frank McKenna",
|
"author": "OpenSees Wiki / Silvia Mazzoni & Frank McKenna",
|
||||||
"units": "US (in, kip, kip·s²/in, s, ksi)"
|
"units": "Imperial (in, kip, kip·s²/in, s, ksi)"
|
||||||
},
|
},
|
||||||
"ndm": 2,
|
"ndm": 2,
|
||||||
"ndf": 3,
|
"ndf": 3,
|
||||||
|
|
@ -148,7 +148,8 @@
|
||||||
"Iz": 1080000.0,
|
"Iz": 1080000.0,
|
||||||
"Iy": 1080000.0,
|
"Iy": 1080000.0,
|
||||||
"G": 1.0,
|
"G": 1.0,
|
||||||
"J": 1.0
|
"J": 1.0,
|
||||||
|
"shape": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
|
|
@ -159,7 +160,8 @@
|
||||||
"Iz": 4423680.0,
|
"Iz": 4423680.0,
|
||||||
"Iy": 4423680.0,
|
"Iy": 4423680.0,
|
||||||
"G": 1.0,
|
"G": 1.0,
|
||||||
"J": 1.0
|
"J": 1.0,
|
||||||
|
"shape": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -173,7 +175,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 1,
|
"section_id": 1,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
|
|
@ -185,7 +188,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 1,
|
"section_id": 1,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 3,
|
"id": 3,
|
||||||
|
|
@ -197,7 +201,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 2,
|
"section_id": 2,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"mp_constraints": [],
|
"mp_constraints": [],
|
||||||
|
|
@ -4223,7 +4228,8 @@
|
||||||
0.0006658006,
|
0.0006658006,
|
||||||
0.0006636009
|
0.0006636009
|
||||||
],
|
],
|
||||||
"file_path": "BM68elc.acc"
|
"file_path": "BM68elc.acc",
|
||||||
|
"use_last": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"load_patterns": [
|
"load_patterns": [
|
||||||
|
|
@ -4293,6 +4299,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
1
|
1
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"n_steps": 10,
|
"n_steps": 10,
|
||||||
"load_factor_increment": 0.1,
|
"load_factor_increment": 0.1,
|
||||||
"system": "BandGeneral",
|
"system": "BandGeneral",
|
||||||
|
|
@ -4310,6 +4317,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
2
|
2
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"control_node": 3,
|
"control_node": 3,
|
||||||
"control_dof": 1,
|
"control_dof": 1,
|
||||||
"target_disp": 10.0,
|
"target_disp": 10.0,
|
||||||
|
|
@ -4335,6 +4343,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
3
|
3
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"dt": 0.02,
|
"dt": 0.02,
|
||||||
"n_steps": 1000,
|
"n_steps": 1000,
|
||||||
"system": "BandGeneral",
|
"system": "BandGeneral",
|
||||||
|
|
@ -4350,6 +4359,8 @@
|
||||||
"max_iter": 10,
|
"max_iter": 10,
|
||||||
"rayleigh_alpha_m": 0.0,
|
"rayleigh_alpha_m": 0.0,
|
||||||
"rayleigh_beta_k": 0.0,
|
"rayleigh_beta_k": 0.0,
|
||||||
|
"rayleigh_beta_k_init": 0.0,
|
||||||
|
"rayleigh_beta_k_comm": 0.0,
|
||||||
"rayleigh_mode1_damping": 0.02,
|
"rayleigh_mode1_damping": 0.02,
|
||||||
"preload_case_ids": [
|
"preload_case_ids": [
|
||||||
1
|
1
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ def build_ex1b_portal2d() -> Project:
|
||||||
"Original Ex 1b elastic portal frame with shared gravity "
|
"Original Ex 1b elastic portal frame with shared gravity "
|
||||||
"preload, static pushover, and BM68elc earthquake cases."
|
"preload, static pushover, and BM68elc earthquake cases."
|
||||||
),
|
),
|
||||||
units=UnitSystem.US_IN_KIP,
|
units=UnitSystem.IMPERIAL,
|
||||||
),
|
),
|
||||||
ndm=2,
|
ndm=2,
|
||||||
ndf=3,
|
ndf=3,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"name": "OpenSees Ex 2a - Elastic Cantilever Column with Variables",
|
"name": "OpenSees Ex 2a - Elastic Cantilever Column with Variables",
|
||||||
"description": "Variable-driven elastic cantilever model with derived mass, section, pushover, and earthquake analysis parameters.",
|
"description": "Variable-driven elastic cantilever model with derived mass, section, pushover, and earthquake analysis parameters.",
|
||||||
"author": "OpenSees Wiki / Silvia Mazzoni & Frank McKenna",
|
"author": "OpenSees Wiki / Silvia Mazzoni & Frank McKenna",
|
||||||
"units": "US (in, kip, kip·s²/in, s, ksi)"
|
"units": "Imperial (in, kip, kip·s²/in, s, ksi)"
|
||||||
},
|
},
|
||||||
"ndm": 2,
|
"ndm": 2,
|
||||||
"ndf": 3,
|
"ndf": 3,
|
||||||
|
|
@ -98,7 +98,8 @@
|
||||||
"Iz": 1080000.0,
|
"Iz": 1080000.0,
|
||||||
"Iy": 1080000.0,
|
"Iy": 1080000.0,
|
||||||
"G": 1.0,
|
"G": 1.0,
|
||||||
"J": 1.0
|
"J": 1.0,
|
||||||
|
"shape": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -112,7 +113,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 1,
|
"section_id": 1,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"mp_constraints": [],
|
"mp_constraints": [],
|
||||||
|
|
@ -4138,7 +4140,8 @@
|
||||||
0.0006658006,
|
0.0006658006,
|
||||||
0.0006636009
|
0.0006636009
|
||||||
],
|
],
|
||||||
"file_path": "BM68elc.acc"
|
"file_path": "BM68elc.acc",
|
||||||
|
"use_last": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"load_patterns": [
|
"load_patterns": [
|
||||||
|
|
@ -4202,6 +4205,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
1
|
1
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"n_steps": 10,
|
"n_steps": 10,
|
||||||
"load_factor_increment": 0.1,
|
"load_factor_increment": 0.1,
|
||||||
"system": "BandGeneral",
|
"system": "BandGeneral",
|
||||||
|
|
@ -4219,6 +4223,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
200
|
200
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"control_node": 2,
|
"control_node": 2,
|
||||||
"control_dof": 1,
|
"control_dof": 1,
|
||||||
"target_disp": 4.32,
|
"target_disp": 4.32,
|
||||||
|
|
@ -4243,6 +4248,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
400
|
400
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"dt": 0.01,
|
"dt": 0.01,
|
||||||
"n_steps": 1000,
|
"n_steps": 1000,
|
||||||
"system": "SparseGeneral",
|
"system": "SparseGeneral",
|
||||||
|
|
@ -4258,6 +4264,8 @@
|
||||||
"max_iter": 10,
|
"max_iter": 10,
|
||||||
"rayleigh_alpha_m": 0.0,
|
"rayleigh_alpha_m": 0.0,
|
||||||
"rayleigh_beta_k": 0.0,
|
"rayleigh_beta_k": 0.0,
|
||||||
|
"rayleigh_beta_k_init": 0.0,
|
||||||
|
"rayleigh_beta_k_comm": 0.0,
|
||||||
"rayleigh_mode1_damping": 0.02,
|
"rayleigh_mode1_damping": 0.02,
|
||||||
"preload_case_ids": [
|
"preload_case_ids": [
|
||||||
1
|
1
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ def build_ex2a_canti2d_elastic_element() -> Project:
|
||||||
"Variable-driven elastic cantilever model with derived mass, "
|
"Variable-driven elastic cantilever model with derived mass, "
|
||||||
"section, pushover, and earthquake analysis parameters."
|
"section, pushover, and earthquake analysis parameters."
|
||||||
),
|
),
|
||||||
units=UnitSystem.US_IN_KIP,
|
units=UnitSystem.IMPERIAL,
|
||||||
),
|
),
|
||||||
ndm=2,
|
ndm=2,
|
||||||
ndf=3,
|
ndf=3,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"name": "OpenSees Ex 2b - Nonlinear Cantilever Column",
|
"name": "OpenSees Ex 2b - Nonlinear Cantilever Column",
|
||||||
"description": "Nonlinear cantilever with an aggregated uniaxial section: elastic axial P plus bilinear inelastic Mz in one force-based element.",
|
"description": "Nonlinear cantilever with an aggregated uniaxial section: elastic axial P plus bilinear inelastic Mz in one force-based element.",
|
||||||
"author": "OpenSees Wiki / Silvia Mazzoni & Frank McKenna",
|
"author": "OpenSees Wiki / Silvia Mazzoni & Frank McKenna",
|
||||||
"units": "US (in, kip, kip·s²/in, s, ksi)"
|
"units": "Imperial (in, kip, kip·s²/in, s, ksi)"
|
||||||
},
|
},
|
||||||
"ndm": 2,
|
"ndm": 2,
|
||||||
"ndf": 3,
|
"ndf": 3,
|
||||||
|
|
@ -4166,7 +4166,8 @@
|
||||||
0.0006658006,
|
0.0006658006,
|
||||||
0.0006636009
|
0.0006636009
|
||||||
],
|
],
|
||||||
"file_path": "BM68elc.acc"
|
"file_path": "BM68elc.acc",
|
||||||
|
"use_last": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"load_patterns": [
|
"load_patterns": [
|
||||||
|
|
@ -4230,6 +4231,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
1
|
1
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"n_steps": 10,
|
"n_steps": 10,
|
||||||
"load_factor_increment": 0.1,
|
"load_factor_increment": 0.1,
|
||||||
"system": "BandGeneral",
|
"system": "BandGeneral",
|
||||||
|
|
@ -4247,6 +4249,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
200
|
200
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"control_node": 2,
|
"control_node": 2,
|
||||||
"control_dof": 1,
|
"control_dof": 1,
|
||||||
"target_disp": 21.6,
|
"target_disp": 21.6,
|
||||||
|
|
@ -4271,6 +4274,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
400
|
400
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"dt": 0.01,
|
"dt": 0.01,
|
||||||
"n_steps": 1000,
|
"n_steps": 1000,
|
||||||
"system": "SparseGeneral",
|
"system": "SparseGeneral",
|
||||||
|
|
@ -4286,6 +4290,8 @@
|
||||||
"max_iter": 10,
|
"max_iter": 10,
|
||||||
"rayleigh_alpha_m": 0.0,
|
"rayleigh_alpha_m": 0.0,
|
||||||
"rayleigh_beta_k": 0.0,
|
"rayleigh_beta_k": 0.0,
|
||||||
|
"rayleigh_beta_k_init": 0.0,
|
||||||
|
"rayleigh_beta_k_comm": 0.0,
|
||||||
"rayleigh_mode1_damping": 0.02,
|
"rayleigh_mode1_damping": 0.02,
|
||||||
"preload_case_ids": [
|
"preload_case_ids": [
|
||||||
1
|
1
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ def build_ex2b_canti2d_inelastic_section() -> Project:
|
||||||
"Nonlinear cantilever with an aggregated uniaxial section: "
|
"Nonlinear cantilever with an aggregated uniaxial section: "
|
||||||
"elastic axial P plus bilinear inelastic Mz in one force-based element."
|
"elastic axial P plus bilinear inelastic Mz in one force-based element."
|
||||||
),
|
),
|
||||||
units=UnitSystem.US_IN_KIP,
|
units=UnitSystem.IMPERIAL,
|
||||||
),
|
),
|
||||||
ndm=2,
|
ndm=2,
|
||||||
ndf=3,
|
ndf=3,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"name": "OpenSees Ex 2c - Inelastic Fiber-Section Cantilever",
|
"name": "OpenSees Ex 2c - Inelastic Fiber-Section Cantilever",
|
||||||
"description": "Nonlinear cantilever with a fiber section built from Concrete02 and Steel02 uniaxial materials.",
|
"description": "Nonlinear cantilever with a fiber section built from Concrete02 and Steel02 uniaxial materials.",
|
||||||
"author": "OpenSees Wiki / Silvia Mazzoni & Frank McKenna",
|
"author": "OpenSees Wiki / Silvia Mazzoni & Frank McKenna",
|
||||||
"units": "US (in, kip, kip·s²/in, s, ksi)"
|
"units": "Imperial (in, kip, kip·s²/in, s, ksi)"
|
||||||
},
|
},
|
||||||
"ndm": 2,
|
"ndm": 2,
|
||||||
"ndf": 3,
|
"ndf": 3,
|
||||||
|
|
@ -4194,7 +4194,8 @@
|
||||||
0.0006658006,
|
0.0006658006,
|
||||||
0.0006636009
|
0.0006636009
|
||||||
],
|
],
|
||||||
"file_path": "BM68elc.acc"
|
"file_path": "BM68elc.acc",
|
||||||
|
"use_last": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"load_patterns": [
|
"load_patterns": [
|
||||||
|
|
@ -4258,6 +4259,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
1
|
1
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"n_steps": 10,
|
"n_steps": 10,
|
||||||
"load_factor_increment": 0.1,
|
"load_factor_increment": 0.1,
|
||||||
"system": "BandGeneral",
|
"system": "BandGeneral",
|
||||||
|
|
@ -4275,6 +4277,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
200
|
200
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"control_node": 2,
|
"control_node": 2,
|
||||||
"control_dof": 1,
|
"control_dof": 1,
|
||||||
"target_disp": 4.32,
|
"target_disp": 4.32,
|
||||||
|
|
@ -4299,6 +4302,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
400
|
400
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"dt": 0.01,
|
"dt": 0.01,
|
||||||
"n_steps": 1000,
|
"n_steps": 1000,
|
||||||
"system": "SparseGeneral",
|
"system": "SparseGeneral",
|
||||||
|
|
@ -4314,6 +4318,8 @@
|
||||||
"max_iter": 10,
|
"max_iter": 10,
|
||||||
"rayleigh_alpha_m": 0.0,
|
"rayleigh_alpha_m": 0.0,
|
||||||
"rayleigh_beta_k": 0.0,
|
"rayleigh_beta_k": 0.0,
|
||||||
|
"rayleigh_beta_k_init": 0.0,
|
||||||
|
"rayleigh_beta_k_comm": 0.0,
|
||||||
"rayleigh_mode1_damping": 0.02,
|
"rayleigh_mode1_damping": 0.02,
|
||||||
"preload_case_ids": [
|
"preload_case_ids": [
|
||||||
1
|
1
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,7 @@ def build_ex2c_canti2d_inelastic_fiber_section() -> Project:
|
||||||
"Nonlinear cantilever with a fiber section built from "
|
"Nonlinear cantilever with a fiber section built from "
|
||||||
"Concrete02 and Steel02 uniaxial materials."
|
"Concrete02 and Steel02 uniaxial materials."
|
||||||
),
|
),
|
||||||
units=UnitSystem.US_IN_KIP,
|
units=UnitSystem.IMPERIAL,
|
||||||
),
|
),
|
||||||
ndm=2,
|
ndm=2,
|
||||||
ndf=3,
|
ndf=3,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"name": "OpenSees Ex 3 - Cantilever (Elastic Build)",
|
"name": "OpenSees Ex 3 - Cantilever (Elastic Build)",
|
||||||
"description": "Example 3 elastic cantilever build with unit-scaled geometry and shared push / uniform-EQ analysis files.",
|
"description": "Example 3 elastic cantilever build with unit-scaled geometry and shared push / uniform-EQ analysis files.",
|
||||||
"author": "OpenSees Wiki / Silvia Mazzoni & Frank McKenna",
|
"author": "OpenSees Wiki / Silvia Mazzoni & Frank McKenna",
|
||||||
"units": "US (in, kip, kip·s²/in, s, ksi)"
|
"units": "Imperial (in, kip, kip·s²/in, s, ksi)"
|
||||||
},
|
},
|
||||||
"ndm": 2,
|
"ndm": 2,
|
||||||
"ndf": 3,
|
"ndf": 3,
|
||||||
|
|
@ -98,7 +98,8 @@
|
||||||
"Iz": 1080000.0,
|
"Iz": 1080000.0,
|
||||||
"Iy": 1080000.0,
|
"Iy": 1080000.0,
|
||||||
"G": 1.0,
|
"G": 1.0,
|
||||||
"J": 1.0
|
"J": 1.0,
|
||||||
|
"shape": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -112,7 +113,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 1,
|
"section_id": 1,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"mp_constraints": [],
|
"mp_constraints": [],
|
||||||
|
|
@ -4138,7 +4140,8 @@
|
||||||
0.0006658006,
|
0.0006658006,
|
||||||
0.0006636009
|
0.0006636009
|
||||||
],
|
],
|
||||||
"file_path": "BM68elc.acc"
|
"file_path": "BM68elc.acc",
|
||||||
|
"use_last": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"load_patterns": [
|
"load_patterns": [
|
||||||
|
|
@ -4202,6 +4205,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
1
|
1
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"n_steps": 10,
|
"n_steps": 10,
|
||||||
"load_factor_increment": 0.1,
|
"load_factor_increment": 0.1,
|
||||||
"system": "BandGeneral",
|
"system": "BandGeneral",
|
||||||
|
|
@ -4219,6 +4223,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
200
|
200
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"control_node": 2,
|
"control_node": 2,
|
||||||
"control_dof": 1,
|
"control_dof": 1,
|
||||||
"target_disp": 21.6,
|
"target_disp": 21.6,
|
||||||
|
|
@ -4243,6 +4248,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
400
|
400
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"dt": 0.01,
|
"dt": 0.01,
|
||||||
"n_steps": 1000,
|
"n_steps": 1000,
|
||||||
"system": "SparseGeneral",
|
"system": "SparseGeneral",
|
||||||
|
|
@ -4258,6 +4264,8 @@
|
||||||
"max_iter": 10,
|
"max_iter": 10,
|
||||||
"rayleigh_alpha_m": 0.0,
|
"rayleigh_alpha_m": 0.0,
|
||||||
"rayleigh_beta_k": 0.0,
|
"rayleigh_beta_k": 0.0,
|
||||||
|
"rayleigh_beta_k_init": 0.0,
|
||||||
|
"rayleigh_beta_k_comm": 0.0,
|
||||||
"rayleigh_mode1_damping": 0.02,
|
"rayleigh_mode1_damping": 0.02,
|
||||||
"preload_case_ids": [
|
"preload_case_ids": [
|
||||||
1
|
1
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ def build_ex3_canti2d_elastic_element() -> Project:
|
||||||
"Example 3 elastic cantilever build with unit-scaled geometry "
|
"Example 3 elastic cantilever build with unit-scaled geometry "
|
||||||
"and shared push / uniform-EQ analysis files."
|
"and shared push / uniform-EQ analysis files."
|
||||||
),
|
),
|
||||||
units=UnitSystem.US_IN_KIP,
|
units=UnitSystem.IMPERIAL,
|
||||||
),
|
),
|
||||||
ndm=2,
|
ndm=2,
|
||||||
ndf=3,
|
ndf=3,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"name": "OpenSees Ex 3 - Cantilever (Fiber Section Build)",
|
"name": "OpenSees Ex 3 - Cantilever (Fiber Section Build)",
|
||||||
"description": "Example 3 cantilever with inelastic fiber section and shared analysis files.",
|
"description": "Example 3 cantilever with inelastic fiber section and shared analysis files.",
|
||||||
"author": "OpenSees Wiki / Silvia Mazzoni & Frank McKenna",
|
"author": "OpenSees Wiki / Silvia Mazzoni & Frank McKenna",
|
||||||
"units": "US (in, kip, kip·s²/in, s, ksi)"
|
"units": "Imperial (in, kip, kip·s²/in, s, ksi)"
|
||||||
},
|
},
|
||||||
"ndm": 2,
|
"ndm": 2,
|
||||||
"ndf": 3,
|
"ndf": 3,
|
||||||
|
|
@ -4194,7 +4194,8 @@
|
||||||
0.0006658006,
|
0.0006658006,
|
||||||
0.0006636009
|
0.0006636009
|
||||||
],
|
],
|
||||||
"file_path": "BM68elc.acc"
|
"file_path": "BM68elc.acc",
|
||||||
|
"use_last": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"load_patterns": [
|
"load_patterns": [
|
||||||
|
|
@ -4258,6 +4259,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
1
|
1
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"n_steps": 10,
|
"n_steps": 10,
|
||||||
"load_factor_increment": 0.1,
|
"load_factor_increment": 0.1,
|
||||||
"system": "BandGeneral",
|
"system": "BandGeneral",
|
||||||
|
|
@ -4275,6 +4277,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
200
|
200
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"control_node": 2,
|
"control_node": 2,
|
||||||
"control_dof": 1,
|
"control_dof": 1,
|
||||||
"target_disp": 21.6,
|
"target_disp": 21.6,
|
||||||
|
|
@ -4299,6 +4302,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
400
|
400
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"dt": 0.01,
|
"dt": 0.01,
|
||||||
"n_steps": 1000,
|
"n_steps": 1000,
|
||||||
"system": "SparseGeneral",
|
"system": "SparseGeneral",
|
||||||
|
|
@ -4314,6 +4318,8 @@
|
||||||
"max_iter": 10,
|
"max_iter": 10,
|
||||||
"rayleigh_alpha_m": 0.0,
|
"rayleigh_alpha_m": 0.0,
|
||||||
"rayleigh_beta_k": 0.0,
|
"rayleigh_beta_k": 0.0,
|
||||||
|
"rayleigh_beta_k_init": 0.0,
|
||||||
|
"rayleigh_beta_k_comm": 0.0,
|
||||||
"rayleigh_mode1_damping": 0.02,
|
"rayleigh_mode1_damping": 0.02,
|
||||||
"preload_case_ids": [
|
"preload_case_ids": [
|
||||||
1
|
1
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,7 @@ def build_ex3_canti2d_inelastic_fiber_section() -> Project:
|
||||||
name="OpenSees Ex 3 - Cantilever (Fiber Section Build)",
|
name="OpenSees Ex 3 - Cantilever (Fiber Section Build)",
|
||||||
author="OpenSees Wiki / Silvia Mazzoni & Frank McKenna",
|
author="OpenSees Wiki / Silvia Mazzoni & Frank McKenna",
|
||||||
description="Example 3 cantilever with inelastic fiber section and shared analysis files.",
|
description="Example 3 cantilever with inelastic fiber section and shared analysis files.",
|
||||||
units=UnitSystem.US_IN_KIP,
|
units=UnitSystem.IMPERIAL,
|
||||||
),
|
),
|
||||||
ndm=2, ndf=3,
|
ndm=2, ndf=3,
|
||||||
nodes=[
|
nodes=[
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"name": "OpenSees Ex 3 - Cantilever (Inelastic Section Build)",
|
"name": "OpenSees Ex 3 - Cantilever (Inelastic Section Build)",
|
||||||
"description": "Example 3 cantilever with aggregated uniaxial inelastic section and shared analysis files.",
|
"description": "Example 3 cantilever with aggregated uniaxial inelastic section and shared analysis files.",
|
||||||
"author": "OpenSees Wiki / Silvia Mazzoni & Frank McKenna",
|
"author": "OpenSees Wiki / Silvia Mazzoni & Frank McKenna",
|
||||||
"units": "US (in, kip, kip·s²/in, s, ksi)"
|
"units": "Imperial (in, kip, kip·s²/in, s, ksi)"
|
||||||
},
|
},
|
||||||
"ndm": 2,
|
"ndm": 2,
|
||||||
"ndf": 3,
|
"ndf": 3,
|
||||||
|
|
@ -4166,7 +4166,8 @@
|
||||||
0.0006658006,
|
0.0006658006,
|
||||||
0.0006636009
|
0.0006636009
|
||||||
],
|
],
|
||||||
"file_path": "BM68elc.acc"
|
"file_path": "BM68elc.acc",
|
||||||
|
"use_last": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"load_patterns": [
|
"load_patterns": [
|
||||||
|
|
@ -4230,6 +4231,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
1
|
1
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"n_steps": 10,
|
"n_steps": 10,
|
||||||
"load_factor_increment": 0.1,
|
"load_factor_increment": 0.1,
|
||||||
"system": "BandGeneral",
|
"system": "BandGeneral",
|
||||||
|
|
@ -4247,6 +4249,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
200
|
200
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"control_node": 2,
|
"control_node": 2,
|
||||||
"control_dof": 1,
|
"control_dof": 1,
|
||||||
"target_disp": 21.6,
|
"target_disp": 21.6,
|
||||||
|
|
@ -4271,6 +4274,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
400
|
400
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"dt": 0.01,
|
"dt": 0.01,
|
||||||
"n_steps": 1000,
|
"n_steps": 1000,
|
||||||
"system": "SparseGeneral",
|
"system": "SparseGeneral",
|
||||||
|
|
@ -4286,6 +4290,8 @@
|
||||||
"max_iter": 10,
|
"max_iter": 10,
|
||||||
"rayleigh_alpha_m": 0.0,
|
"rayleigh_alpha_m": 0.0,
|
||||||
"rayleigh_beta_k": 0.0,
|
"rayleigh_beta_k": 0.0,
|
||||||
|
"rayleigh_beta_k_init": 0.0,
|
||||||
|
"rayleigh_beta_k_comm": 0.0,
|
||||||
"rayleigh_mode1_damping": 0.02,
|
"rayleigh_mode1_damping": 0.02,
|
||||||
"preload_case_ids": [
|
"preload_case_ids": [
|
||||||
1
|
1
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ def build_ex3_canti2d_inelastic_section() -> Project:
|
||||||
name="OpenSees Ex 3 - Cantilever (Inelastic Section Build)",
|
name="OpenSees Ex 3 - Cantilever (Inelastic Section Build)",
|
||||||
author="OpenSees Wiki / Silvia Mazzoni & Frank McKenna",
|
author="OpenSees Wiki / Silvia Mazzoni & Frank McKenna",
|
||||||
description="Example 3 cantilever with aggregated uniaxial inelastic section and shared analysis files.",
|
description="Example 3 cantilever with aggregated uniaxial inelastic section and shared analysis files.",
|
||||||
units=UnitSystem.US_IN_KIP,
|
units=UnitSystem.IMPERIAL,
|
||||||
),
|
),
|
||||||
ndm=2, ndf=3,
|
ndm=2, ndf=3,
|
||||||
nodes=[
|
nodes=[
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"name": "OpenSees Ex 4 - Portal Frame (Elastic Build)",
|
"name": "OpenSees Ex 4 - Portal Frame (Elastic Build)",
|
||||||
"description": "Example 4 elastic portal frame with shared gravity, push, and sine-wave support motion.",
|
"description": "Example 4 elastic portal frame with shared gravity, push, and sine-wave support motion.",
|
||||||
"author": "OpenSees Wiki / Silvia Mazzoni & Frank McKenna",
|
"author": "OpenSees Wiki / Silvia Mazzoni & Frank McKenna",
|
||||||
"units": "US (in, kip, kip·s²/in, s, ksi)"
|
"units": "Imperial (in, kip, kip·s²/in, s, ksi)"
|
||||||
},
|
},
|
||||||
"ndm": 2,
|
"ndm": 2,
|
||||||
"ndf": 3,
|
"ndf": 3,
|
||||||
|
|
@ -148,7 +148,8 @@
|
||||||
"Iz": 864000.0,
|
"Iz": 864000.0,
|
||||||
"Iy": 864000.0,
|
"Iy": 864000.0,
|
||||||
"G": 1.0,
|
"G": 1.0,
|
||||||
"J": 1.0
|
"J": 1.0,
|
||||||
|
"shape": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
|
|
@ -159,7 +160,8 @@
|
||||||
"Iz": 4423680.0,
|
"Iz": 4423680.0,
|
||||||
"Iy": 4423680.0,
|
"Iy": 4423680.0,
|
||||||
"G": 1.0,
|
"G": 1.0,
|
||||||
"J": 1.0
|
"J": 1.0,
|
||||||
|
"shape": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -173,7 +175,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 1,
|
"section_id": 1,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
|
|
@ -185,7 +188,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 1,
|
"section_id": 1,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 3,
|
"id": 3,
|
||||||
|
|
@ -197,7 +201,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 2,
|
"section_id": 2,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"mp_constraints": [],
|
"mp_constraints": [],
|
||||||
|
|
@ -335,7 +340,7 @@
|
||||||
-83.82633839751217,
|
-83.82633839751217,
|
||||||
-99.09214039482102,
|
-99.09214039482102,
|
||||||
-113.56011074290575,
|
-113.56011074290575,
|
||||||
-127.11376185155243,
|
-127.11376185155244,
|
||||||
-139.643967691452,
|
-139.643967691452,
|
||||||
-151.04984241282355,
|
-151.04984241282355,
|
||||||
-161.23955261895566,
|
-161.23955261895566,
|
||||||
|
|
@ -428,7 +433,7 @@
|
||||||
-113.56011074290593,
|
-113.56011074290593,
|
||||||
-99.0921403948212,
|
-99.0921403948212,
|
||||||
-83.82633839751236,
|
-83.82633839751236,
|
||||||
-67.88561601251554,
|
-67.88561601251556,
|
||||||
-51.398318563481766,
|
-51.398318563481766,
|
||||||
-34.49719207509674,
|
-34.49719207509674,
|
||||||
-17.318314480143496,
|
-17.318314480143496,
|
||||||
|
|
@ -511,7 +516,7 @@
|
||||||
99.0921403948209,
|
99.0921403948209,
|
||||||
113.56011074290566,
|
113.56011074290566,
|
||||||
127.11376185155234,
|
127.11376185155234,
|
||||||
139.64396769145193,
|
139.6439676914519,
|
||||||
151.04984241282366,
|
151.04984241282366,
|
||||||
161.23955261895557,
|
161.23955261895557,
|
||||||
170.1310567547217,
|
170.1310567547217,
|
||||||
|
|
@ -540,12 +545,12 @@
|
||||||
2.129415854357418e-13,
|
2.129415854357418e-13,
|
||||||
-17.31831448014314,
|
-17.31831448014314,
|
||||||
-34.49719207509707,
|
-34.49719207509707,
|
||||||
-51.398318563481425,
|
-51.39831856348143,
|
||||||
-67.88561601251521,
|
-67.88561601251521,
|
||||||
-83.82633839751205,
|
-83.82633839751205,
|
||||||
-99.09214039482087,
|
-99.09214039482087,
|
||||||
-113.5601107429062,
|
-113.5601107429062,
|
||||||
-127.11376185155231,
|
-127.11376185155234,
|
||||||
-139.6439676914519,
|
-139.6439676914519,
|
||||||
-151.04984241282324,
|
-151.04984241282324,
|
||||||
-161.23955261895594,
|
-161.23955261895594,
|
||||||
|
|
@ -578,7 +583,7 @@
|
||||||
51.3983185634814,
|
51.3983185634814,
|
||||||
67.88561601251583,
|
67.88561601251583,
|
||||||
83.82633839751202,
|
83.82633839751202,
|
||||||
99.09214039482086,
|
99.09214039482087,
|
||||||
113.56011074290619,
|
113.56011074290619,
|
||||||
127.11376185155231,
|
127.11376185155231,
|
||||||
139.64396769145142,
|
139.64396769145142,
|
||||||
|
|
@ -635,7 +640,7 @@
|
||||||
-151.04984241282355,
|
-151.04984241282355,
|
||||||
-139.6439676914518,
|
-139.6439676914518,
|
||||||
-127.1137618515527,
|
-127.1137618515527,
|
||||||
-113.56011074290548,
|
-113.5601107429055,
|
||||||
-99.09214039482131,
|
-99.09214039482131,
|
||||||
-83.82633839751311,
|
-83.82633839751311,
|
||||||
-67.88561601251568,
|
-67.88561601251568,
|
||||||
|
|
@ -654,7 +659,7 @@
|
||||||
139.64396769145233,
|
139.64396769145233,
|
||||||
151.0498424128232,
|
151.0498424128232,
|
||||||
161.2395526189559,
|
161.2395526189559,
|
||||||
170.13105675472167,
|
170.13105675472164,
|
||||||
177.6527656569399,
|
177.6527656569399,
|
||||||
183.7441189482236,
|
183.7441189482236,
|
||||||
188.3560726335284,
|
188.3560726335284,
|
||||||
|
|
@ -668,7 +673,7 @@
|
||||||
170.1310567547216,
|
170.1310567547216,
|
||||||
161.23955261895586,
|
161.23955261895586,
|
||||||
151.04984241282312,
|
151.04984241282312,
|
||||||
139.64396769145227,
|
139.64396769145225,
|
||||||
127.11376185155324,
|
127.11376185155324,
|
||||||
113.56011074290606,
|
113.56011074290606,
|
||||||
99.09214039482075,
|
99.09214039482075,
|
||||||
|
|
@ -805,14 +810,14 @@
|
||||||
188.35607263352813,
|
188.35607263352813,
|
||||||
183.7441189482236,
|
183.7441189482236,
|
||||||
177.65276565694043,
|
177.65276565694043,
|
||||||
170.13105675472167,
|
170.13105675472164,
|
||||||
161.2395526189559,
|
161.2395526189559,
|
||||||
151.0498424128232,
|
151.0498424128232,
|
||||||
139.64396769145233,
|
139.64396769145233,
|
||||||
127.11376185155329,
|
127.11376185155329,
|
||||||
113.56011074290502,
|
113.56011074290502,
|
||||||
99.09214039482083,
|
99.09214039482083,
|
||||||
83.82633839751259,
|
83.8263383975126,
|
||||||
67.88561601251514,
|
67.88561601251514,
|
||||||
51.39831856348202,
|
51.39831856348202,
|
||||||
34.49719207509632,
|
34.49719207509632,
|
||||||
|
|
@ -824,7 +829,8 @@
|
||||||
-67.88561601251568,
|
-67.88561601251568,
|
||||||
-83.82633839751188
|
-83.82633839751188
|
||||||
],
|
],
|
||||||
"file_path": "generated:sine-wave"
|
"file_path": "generated:sine-wave",
|
||||||
|
"use_last": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 401,
|
"id": 401,
|
||||||
|
|
@ -860,7 +866,7 @@
|
||||||
5.09980167540335,
|
5.09980167540335,
|
||||||
5.928784854272076,
|
5.928784854272076,
|
||||||
6.7100329435632435,
|
6.7100329435632435,
|
||||||
7.437255793028317,
|
7.437255793028318,
|
||||||
8.104598231883518,
|
8.104598231883518,
|
||||||
8.70668721120218,
|
8.70668721120218,
|
||||||
9.238675064545419,
|
9.238675064545419,
|
||||||
|
|
@ -911,7 +917,7 @@
|
||||||
-10.075813279025667,
|
-10.075813279025667,
|
||||||
-9.696278538521907,
|
-9.696278538521907,
|
||||||
-9.238675064545415,
|
-9.238675064545415,
|
||||||
-8.706687211202178,
|
-8.706687211202176,
|
||||||
-8.10459823188352,
|
-8.10459823188352,
|
||||||
-7.437255793028321,
|
-7.437255793028321,
|
||||||
-6.710032943563246,
|
-6.710032943563246,
|
||||||
|
|
@ -941,7 +947,7 @@
|
||||||
10.718732373371513,
|
10.718732373371513,
|
||||||
10.762057251873964,
|
10.762057251873964,
|
||||||
10.718732373371514,
|
10.718732373371514,
|
||||||
10.58910656431125,
|
10.589106564311248,
|
||||||
10.37422349548898,
|
10.37422349548898,
|
||||||
10.075813279025667,
|
10.075813279025667,
|
||||||
9.696278538521907,
|
9.696278538521907,
|
||||||
|
|
@ -1017,7 +1023,7 @@
|
||||||
9.696278538521907,
|
9.696278538521907,
|
||||||
9.238675064545422,
|
9.238675064545422,
|
||||||
8.706687211202185,
|
8.706687211202185,
|
||||||
8.104598231883523,
|
8.104598231883521,
|
||||||
7.437255793028323,
|
7.437255793028323,
|
||||||
6.71003294356325,
|
6.71003294356325,
|
||||||
5.928784854272083,
|
5.928784854272083,
|
||||||
|
|
@ -1158,8 +1164,8 @@
|
||||||
9.238675064545424,
|
9.238675064545424,
|
||||||
8.706687211202166,
|
8.706687211202166,
|
||||||
8.104598231883525,
|
8.104598231883525,
|
||||||
7.437255793028327,
|
7.437255793028328,
|
||||||
6.710032943563254,
|
6.710032943563253,
|
||||||
5.928784854272055,
|
5.928784854272055,
|
||||||
5.099801675403362,
|
5.099801675403362,
|
||||||
4.229757892079114,
|
4.229757892079114,
|
||||||
|
|
@ -1193,7 +1199,7 @@
|
||||||
-9.238675064545426,
|
-9.238675064545426,
|
||||||
-8.706687211202167,
|
-8.706687211202167,
|
||||||
-8.104598231883527,
|
-8.104598231883527,
|
||||||
-7.437255793028355,
|
-7.437255793028356,
|
||||||
-6.710032943563254,
|
-6.710032943563254,
|
||||||
-5.928784854272056,
|
-5.928784854272056,
|
||||||
-5.099801675403363,
|
-5.099801675403363,
|
||||||
|
|
@ -1265,7 +1271,7 @@
|
||||||
-8.104598231883529,
|
-8.104598231883529,
|
||||||
-7.437255793028303,
|
-7.437255793028303,
|
||||||
-6.710032943563257,
|
-6.710032943563257,
|
||||||
-5.928784854272058,
|
-5.928784854272059,
|
||||||
-5.0998016754033655,
|
-5.0998016754033655,
|
||||||
-4.229757892079153,
|
-4.229757892079153,
|
||||||
-3.3256585852652147,
|
-3.3256585852652147,
|
||||||
|
|
@ -1279,7 +1285,7 @@
|
||||||
4.229757892079088,
|
4.229757892079088,
|
||||||
5.09980167540337,
|
5.09980167540337,
|
||||||
5.928784854272062,
|
5.928784854272062,
|
||||||
6.710032943563261,
|
6.71003294356326,
|
||||||
7.4372557930283065,
|
7.4372557930283065,
|
||||||
8.104598231883482,
|
8.104598231883482,
|
||||||
8.706687211202171,
|
8.706687211202171,
|
||||||
|
|
@ -1316,7 +1322,7 @@
|
||||||
-5.92878485427203,
|
-5.92878485427203,
|
||||||
-6.71003294356329,
|
-6.71003294356329,
|
||||||
-7.437255793028332,
|
-7.437255793028332,
|
||||||
-8.104598231883505,
|
-8.104598231883507,
|
||||||
-8.706687211202192,
|
-8.706687211202192,
|
||||||
-9.23867506454541,
|
-9.23867506454541,
|
||||||
-9.69627853852188,
|
-9.69627853852188,
|
||||||
|
|
@ -1328,7 +1334,7 @@
|
||||||
-10.71873237337151,
|
-10.71873237337151,
|
||||||
-10.589106564311251,
|
-10.589106564311251,
|
||||||
-10.374223495488975,
|
-10.374223495488975,
|
||||||
-10.075813279025672,
|
-10.075813279025674,
|
||||||
-9.696278538521929,
|
-9.696278538521929,
|
||||||
-9.238675064545427,
|
-9.238675064545427,
|
||||||
-8.706687211202215,
|
-8.706687211202215,
|
||||||
|
|
@ -1355,7 +1361,7 @@
|
||||||
8.70668721120217,
|
8.70668721120217,
|
||||||
9.238675064545427,
|
9.238675064545427,
|
||||||
9.696278538521897,
|
9.696278538521897,
|
||||||
10.075813279025672,
|
10.075813279025674,
|
||||||
10.374223495488975,
|
10.374223495488975,
|
||||||
10.589106564311237,
|
10.589106564311237,
|
||||||
10.718732373371518,
|
10.718732373371518,
|
||||||
|
|
@ -1402,7 +1408,7 @@
|
||||||
-9.696278538521899,
|
-9.696278538521899,
|
||||||
-9.23867506454543,
|
-9.23867506454543,
|
||||||
-8.706687211202215,
|
-8.706687211202215,
|
||||||
-8.104598231883534,
|
-8.104598231883532,
|
||||||
-7.437255793028362,
|
-7.437255793028362,
|
||||||
-6.710032943563201,
|
-6.710032943563201,
|
||||||
-5.928784854272062,
|
-5.928784854272062,
|
||||||
|
|
@ -1418,11 +1424,11 @@
|
||||||
3.3256585852652147,
|
3.3256585852652147,
|
||||||
4.229757892079083,
|
4.229757892079083,
|
||||||
5.0998016754033655,
|
5.0998016754033655,
|
||||||
5.928784854272058,
|
5.928784854272059,
|
||||||
6.710032943563257,
|
6.710032943563257,
|
||||||
7.437255793028303,
|
7.437255793028303,
|
||||||
8.104598231883479,
|
8.104598231883479,
|
||||||
8.706687211202214,
|
8.706687211202212,
|
||||||
9.238675064545427,
|
9.238675064545427,
|
||||||
9.696278538521895,
|
9.696278538521895,
|
||||||
10.075813279025672,
|
10.075813279025672,
|
||||||
|
|
@ -1436,7 +1442,8 @@
|
||||||
10.07581327902566,
|
10.07581327902566,
|
||||||
9.696278538521915
|
9.696278538521915
|
||||||
],
|
],
|
||||||
"file_path": "generated:sine-wave-velocity"
|
"file_path": "generated:sine-wave-velocity",
|
||||||
|
"use_last": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"load_patterns": [
|
"load_patterns": [
|
||||||
|
|
@ -1506,6 +1513,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
1
|
1
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"n_steps": 10,
|
"n_steps": 10,
|
||||||
"load_factor_increment": 0.1,
|
"load_factor_increment": 0.1,
|
||||||
"system": "BandGeneral",
|
"system": "BandGeneral",
|
||||||
|
|
@ -1523,6 +1531,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
200
|
200
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"control_node": 3,
|
"control_node": 3,
|
||||||
"control_dof": 1,
|
"control_dof": 1,
|
||||||
"target_disp": 43.2,
|
"target_disp": 43.2,
|
||||||
|
|
@ -1548,6 +1557,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
400
|
400
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"dt": 0.01,
|
"dt": 0.01,
|
||||||
"n_steps": 1000,
|
"n_steps": 1000,
|
||||||
"system": "BandGeneral",
|
"system": "BandGeneral",
|
||||||
|
|
@ -1563,6 +1573,8 @@
|
||||||
"max_iter": 10,
|
"max_iter": 10,
|
||||||
"rayleigh_alpha_m": 0.0,
|
"rayleigh_alpha_m": 0.0,
|
||||||
"rayleigh_beta_k": 0.0,
|
"rayleigh_beta_k": 0.0,
|
||||||
|
"rayleigh_beta_k_init": 0.0,
|
||||||
|
"rayleigh_beta_k_comm": 0.0,
|
||||||
"rayleigh_mode1_damping": 0.02,
|
"rayleigh_mode1_damping": 0.02,
|
||||||
"preload_case_ids": [
|
"preload_case_ids": [
|
||||||
1
|
1
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"name": "OpenSees Ex 4 - Portal Frame (Fiber Section Build)",
|
"name": "OpenSees Ex 4 - Portal Frame (Fiber Section Build)",
|
||||||
"description": "Example 4 portal frame with fiber-section columns and shared pushover / sine-wave analyses.",
|
"description": "Example 4 portal frame with fiber-section columns and shared pushover / sine-wave analyses.",
|
||||||
"author": "OpenSees Wiki / Silvia Mazzoni & Frank McKenna",
|
"author": "OpenSees Wiki / Silvia Mazzoni & Frank McKenna",
|
||||||
"units": "US (in, kip, kip·s²/in, s, ksi)"
|
"units": "Imperial (in, kip, kip·s²/in, s, ksi)"
|
||||||
},
|
},
|
||||||
"ndm": 2,
|
"ndm": 2,
|
||||||
"ndf": 3,
|
"ndf": 3,
|
||||||
|
|
@ -213,7 +213,8 @@
|
||||||
"Iz": 4423680.0,
|
"Iz": 4423680.0,
|
||||||
"Iy": 4423680.0,
|
"Iy": 4423680.0,
|
||||||
"G": 1.0,
|
"G": 1.0,
|
||||||
"J": 1.0
|
"J": 1.0,
|
||||||
|
"shape": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -395,7 +396,7 @@
|
||||||
-83.82633839751217,
|
-83.82633839751217,
|
||||||
-99.09214039482102,
|
-99.09214039482102,
|
||||||
-113.56011074290575,
|
-113.56011074290575,
|
||||||
-127.11376185155243,
|
-127.11376185155244,
|
||||||
-139.643967691452,
|
-139.643967691452,
|
||||||
-151.04984241282355,
|
-151.04984241282355,
|
||||||
-161.23955261895566,
|
-161.23955261895566,
|
||||||
|
|
@ -488,7 +489,7 @@
|
||||||
-113.56011074290593,
|
-113.56011074290593,
|
||||||
-99.0921403948212,
|
-99.0921403948212,
|
||||||
-83.82633839751236,
|
-83.82633839751236,
|
||||||
-67.88561601251554,
|
-67.88561601251556,
|
||||||
-51.398318563481766,
|
-51.398318563481766,
|
||||||
-34.49719207509674,
|
-34.49719207509674,
|
||||||
-17.318314480143496,
|
-17.318314480143496,
|
||||||
|
|
@ -571,7 +572,7 @@
|
||||||
99.0921403948209,
|
99.0921403948209,
|
||||||
113.56011074290566,
|
113.56011074290566,
|
||||||
127.11376185155234,
|
127.11376185155234,
|
||||||
139.64396769145193,
|
139.6439676914519,
|
||||||
151.04984241282366,
|
151.04984241282366,
|
||||||
161.23955261895557,
|
161.23955261895557,
|
||||||
170.1310567547217,
|
170.1310567547217,
|
||||||
|
|
@ -600,12 +601,12 @@
|
||||||
2.129415854357418e-13,
|
2.129415854357418e-13,
|
||||||
-17.31831448014314,
|
-17.31831448014314,
|
||||||
-34.49719207509707,
|
-34.49719207509707,
|
||||||
-51.398318563481425,
|
-51.39831856348143,
|
||||||
-67.88561601251521,
|
-67.88561601251521,
|
||||||
-83.82633839751205,
|
-83.82633839751205,
|
||||||
-99.09214039482087,
|
-99.09214039482087,
|
||||||
-113.5601107429062,
|
-113.5601107429062,
|
||||||
-127.11376185155231,
|
-127.11376185155234,
|
||||||
-139.6439676914519,
|
-139.6439676914519,
|
||||||
-151.04984241282324,
|
-151.04984241282324,
|
||||||
-161.23955261895594,
|
-161.23955261895594,
|
||||||
|
|
@ -638,7 +639,7 @@
|
||||||
51.3983185634814,
|
51.3983185634814,
|
||||||
67.88561601251583,
|
67.88561601251583,
|
||||||
83.82633839751202,
|
83.82633839751202,
|
||||||
99.09214039482086,
|
99.09214039482087,
|
||||||
113.56011074290619,
|
113.56011074290619,
|
||||||
127.11376185155231,
|
127.11376185155231,
|
||||||
139.64396769145142,
|
139.64396769145142,
|
||||||
|
|
@ -695,7 +696,7 @@
|
||||||
-151.04984241282355,
|
-151.04984241282355,
|
||||||
-139.6439676914518,
|
-139.6439676914518,
|
||||||
-127.1137618515527,
|
-127.1137618515527,
|
||||||
-113.56011074290548,
|
-113.5601107429055,
|
||||||
-99.09214039482131,
|
-99.09214039482131,
|
||||||
-83.82633839751311,
|
-83.82633839751311,
|
||||||
-67.88561601251568,
|
-67.88561601251568,
|
||||||
|
|
@ -714,7 +715,7 @@
|
||||||
139.64396769145233,
|
139.64396769145233,
|
||||||
151.0498424128232,
|
151.0498424128232,
|
||||||
161.2395526189559,
|
161.2395526189559,
|
||||||
170.13105675472167,
|
170.13105675472164,
|
||||||
177.6527656569399,
|
177.6527656569399,
|
||||||
183.7441189482236,
|
183.7441189482236,
|
||||||
188.3560726335284,
|
188.3560726335284,
|
||||||
|
|
@ -728,7 +729,7 @@
|
||||||
170.1310567547216,
|
170.1310567547216,
|
||||||
161.23955261895586,
|
161.23955261895586,
|
||||||
151.04984241282312,
|
151.04984241282312,
|
||||||
139.64396769145227,
|
139.64396769145225,
|
||||||
127.11376185155324,
|
127.11376185155324,
|
||||||
113.56011074290606,
|
113.56011074290606,
|
||||||
99.09214039482075,
|
99.09214039482075,
|
||||||
|
|
@ -865,14 +866,14 @@
|
||||||
188.35607263352813,
|
188.35607263352813,
|
||||||
183.7441189482236,
|
183.7441189482236,
|
||||||
177.65276565694043,
|
177.65276565694043,
|
||||||
170.13105675472167,
|
170.13105675472164,
|
||||||
161.2395526189559,
|
161.2395526189559,
|
||||||
151.0498424128232,
|
151.0498424128232,
|
||||||
139.64396769145233,
|
139.64396769145233,
|
||||||
127.11376185155329,
|
127.11376185155329,
|
||||||
113.56011074290502,
|
113.56011074290502,
|
||||||
99.09214039482083,
|
99.09214039482083,
|
||||||
83.82633839751259,
|
83.8263383975126,
|
||||||
67.88561601251514,
|
67.88561601251514,
|
||||||
51.39831856348202,
|
51.39831856348202,
|
||||||
34.49719207509632,
|
34.49719207509632,
|
||||||
|
|
@ -884,7 +885,8 @@
|
||||||
-67.88561601251568,
|
-67.88561601251568,
|
||||||
-83.82633839751188
|
-83.82633839751188
|
||||||
],
|
],
|
||||||
"file_path": "generated:sine-wave"
|
"file_path": "generated:sine-wave",
|
||||||
|
"use_last": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 401,
|
"id": 401,
|
||||||
|
|
@ -920,7 +922,7 @@
|
||||||
5.09980167540335,
|
5.09980167540335,
|
||||||
5.928784854272076,
|
5.928784854272076,
|
||||||
6.7100329435632435,
|
6.7100329435632435,
|
||||||
7.437255793028317,
|
7.437255793028318,
|
||||||
8.104598231883518,
|
8.104598231883518,
|
||||||
8.70668721120218,
|
8.70668721120218,
|
||||||
9.238675064545419,
|
9.238675064545419,
|
||||||
|
|
@ -971,7 +973,7 @@
|
||||||
-10.075813279025667,
|
-10.075813279025667,
|
||||||
-9.696278538521907,
|
-9.696278538521907,
|
||||||
-9.238675064545415,
|
-9.238675064545415,
|
||||||
-8.706687211202178,
|
-8.706687211202176,
|
||||||
-8.10459823188352,
|
-8.10459823188352,
|
||||||
-7.437255793028321,
|
-7.437255793028321,
|
||||||
-6.710032943563246,
|
-6.710032943563246,
|
||||||
|
|
@ -1001,7 +1003,7 @@
|
||||||
10.718732373371513,
|
10.718732373371513,
|
||||||
10.762057251873964,
|
10.762057251873964,
|
||||||
10.718732373371514,
|
10.718732373371514,
|
||||||
10.58910656431125,
|
10.589106564311248,
|
||||||
10.37422349548898,
|
10.37422349548898,
|
||||||
10.075813279025667,
|
10.075813279025667,
|
||||||
9.696278538521907,
|
9.696278538521907,
|
||||||
|
|
@ -1077,7 +1079,7 @@
|
||||||
9.696278538521907,
|
9.696278538521907,
|
||||||
9.238675064545422,
|
9.238675064545422,
|
||||||
8.706687211202185,
|
8.706687211202185,
|
||||||
8.104598231883523,
|
8.104598231883521,
|
||||||
7.437255793028323,
|
7.437255793028323,
|
||||||
6.71003294356325,
|
6.71003294356325,
|
||||||
5.928784854272083,
|
5.928784854272083,
|
||||||
|
|
@ -1218,8 +1220,8 @@
|
||||||
9.238675064545424,
|
9.238675064545424,
|
||||||
8.706687211202166,
|
8.706687211202166,
|
||||||
8.104598231883525,
|
8.104598231883525,
|
||||||
7.437255793028327,
|
7.437255793028328,
|
||||||
6.710032943563254,
|
6.710032943563253,
|
||||||
5.928784854272055,
|
5.928784854272055,
|
||||||
5.099801675403362,
|
5.099801675403362,
|
||||||
4.229757892079114,
|
4.229757892079114,
|
||||||
|
|
@ -1253,7 +1255,7 @@
|
||||||
-9.238675064545426,
|
-9.238675064545426,
|
||||||
-8.706687211202167,
|
-8.706687211202167,
|
||||||
-8.104598231883527,
|
-8.104598231883527,
|
||||||
-7.437255793028355,
|
-7.437255793028356,
|
||||||
-6.710032943563254,
|
-6.710032943563254,
|
||||||
-5.928784854272056,
|
-5.928784854272056,
|
||||||
-5.099801675403363,
|
-5.099801675403363,
|
||||||
|
|
@ -1325,7 +1327,7 @@
|
||||||
-8.104598231883529,
|
-8.104598231883529,
|
||||||
-7.437255793028303,
|
-7.437255793028303,
|
||||||
-6.710032943563257,
|
-6.710032943563257,
|
||||||
-5.928784854272058,
|
-5.928784854272059,
|
||||||
-5.0998016754033655,
|
-5.0998016754033655,
|
||||||
-4.229757892079153,
|
-4.229757892079153,
|
||||||
-3.3256585852652147,
|
-3.3256585852652147,
|
||||||
|
|
@ -1339,7 +1341,7 @@
|
||||||
4.229757892079088,
|
4.229757892079088,
|
||||||
5.09980167540337,
|
5.09980167540337,
|
||||||
5.928784854272062,
|
5.928784854272062,
|
||||||
6.710032943563261,
|
6.71003294356326,
|
||||||
7.4372557930283065,
|
7.4372557930283065,
|
||||||
8.104598231883482,
|
8.104598231883482,
|
||||||
8.706687211202171,
|
8.706687211202171,
|
||||||
|
|
@ -1376,7 +1378,7 @@
|
||||||
-5.92878485427203,
|
-5.92878485427203,
|
||||||
-6.71003294356329,
|
-6.71003294356329,
|
||||||
-7.437255793028332,
|
-7.437255793028332,
|
||||||
-8.104598231883505,
|
-8.104598231883507,
|
||||||
-8.706687211202192,
|
-8.706687211202192,
|
||||||
-9.23867506454541,
|
-9.23867506454541,
|
||||||
-9.69627853852188,
|
-9.69627853852188,
|
||||||
|
|
@ -1388,7 +1390,7 @@
|
||||||
-10.71873237337151,
|
-10.71873237337151,
|
||||||
-10.589106564311251,
|
-10.589106564311251,
|
||||||
-10.374223495488975,
|
-10.374223495488975,
|
||||||
-10.075813279025672,
|
-10.075813279025674,
|
||||||
-9.696278538521929,
|
-9.696278538521929,
|
||||||
-9.238675064545427,
|
-9.238675064545427,
|
||||||
-8.706687211202215,
|
-8.706687211202215,
|
||||||
|
|
@ -1415,7 +1417,7 @@
|
||||||
8.70668721120217,
|
8.70668721120217,
|
||||||
9.238675064545427,
|
9.238675064545427,
|
||||||
9.696278538521897,
|
9.696278538521897,
|
||||||
10.075813279025672,
|
10.075813279025674,
|
||||||
10.374223495488975,
|
10.374223495488975,
|
||||||
10.589106564311237,
|
10.589106564311237,
|
||||||
10.718732373371518,
|
10.718732373371518,
|
||||||
|
|
@ -1462,7 +1464,7 @@
|
||||||
-9.696278538521899,
|
-9.696278538521899,
|
||||||
-9.23867506454543,
|
-9.23867506454543,
|
||||||
-8.706687211202215,
|
-8.706687211202215,
|
||||||
-8.104598231883534,
|
-8.104598231883532,
|
||||||
-7.437255793028362,
|
-7.437255793028362,
|
||||||
-6.710032943563201,
|
-6.710032943563201,
|
||||||
-5.928784854272062,
|
-5.928784854272062,
|
||||||
|
|
@ -1478,11 +1480,11 @@
|
||||||
3.3256585852652147,
|
3.3256585852652147,
|
||||||
4.229757892079083,
|
4.229757892079083,
|
||||||
5.0998016754033655,
|
5.0998016754033655,
|
||||||
5.928784854272058,
|
5.928784854272059,
|
||||||
6.710032943563257,
|
6.710032943563257,
|
||||||
7.437255793028303,
|
7.437255793028303,
|
||||||
8.104598231883479,
|
8.104598231883479,
|
||||||
8.706687211202214,
|
8.706687211202212,
|
||||||
9.238675064545427,
|
9.238675064545427,
|
||||||
9.696278538521895,
|
9.696278538521895,
|
||||||
10.075813279025672,
|
10.075813279025672,
|
||||||
|
|
@ -1496,7 +1498,8 @@
|
||||||
10.07581327902566,
|
10.07581327902566,
|
||||||
9.696278538521915
|
9.696278538521915
|
||||||
],
|
],
|
||||||
"file_path": "generated:sine-wave-velocity"
|
"file_path": "generated:sine-wave-velocity",
|
||||||
|
"use_last": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"load_patterns": [
|
"load_patterns": [
|
||||||
|
|
@ -1566,6 +1569,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
1
|
1
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"n_steps": 10,
|
"n_steps": 10,
|
||||||
"load_factor_increment": 0.1,
|
"load_factor_increment": 0.1,
|
||||||
"system": "BandGeneral",
|
"system": "BandGeneral",
|
||||||
|
|
@ -1583,6 +1587,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
200
|
200
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"control_node": 3,
|
"control_node": 3,
|
||||||
"control_dof": 1,
|
"control_dof": 1,
|
||||||
"target_disp": 43.2,
|
"target_disp": 43.2,
|
||||||
|
|
@ -1608,6 +1613,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
400
|
400
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"dt": 0.01,
|
"dt": 0.01,
|
||||||
"n_steps": 1000,
|
"n_steps": 1000,
|
||||||
"system": "BandGeneral",
|
"system": "BandGeneral",
|
||||||
|
|
@ -1623,6 +1629,8 @@
|
||||||
"max_iter": 10,
|
"max_iter": 10,
|
||||||
"rayleigh_alpha_m": 0.0,
|
"rayleigh_alpha_m": 0.0,
|
||||||
"rayleigh_beta_k": 0.0,
|
"rayleigh_beta_k": 0.0,
|
||||||
|
"rayleigh_beta_k_init": 0.0,
|
||||||
|
"rayleigh_beta_k_comm": 0.0,
|
||||||
"rayleigh_mode1_damping": 0.02,
|
"rayleigh_mode1_damping": 0.02,
|
||||||
"preload_case_ids": [
|
"preload_case_ids": [
|
||||||
1
|
1
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"name": "OpenSees Ex 4 - Portal Frame (Inelastic Section Build)",
|
"name": "OpenSees Ex 4 - Portal Frame (Inelastic Section Build)",
|
||||||
"description": "Example 4 portal frame with aggregated uniaxial inelastic column sections and shared analyses.",
|
"description": "Example 4 portal frame with aggregated uniaxial inelastic column sections and shared analyses.",
|
||||||
"author": "OpenSees Wiki / Silvia Mazzoni & Frank McKenna",
|
"author": "OpenSees Wiki / Silvia Mazzoni & Frank McKenna",
|
||||||
"units": "US (in, kip, kip·s²/in, s, ksi)"
|
"units": "Imperial (in, kip, kip·s²/in, s, ksi)"
|
||||||
},
|
},
|
||||||
"ndm": 2,
|
"ndm": 2,
|
||||||
"ndf": 3,
|
"ndf": 3,
|
||||||
|
|
@ -185,7 +185,8 @@
|
||||||
"Iz": 4423680.0,
|
"Iz": 4423680.0,
|
||||||
"Iy": 4423680.0,
|
"Iy": 4423680.0,
|
||||||
"G": 1.0,
|
"G": 1.0,
|
||||||
"J": 1.0
|
"J": 1.0,
|
||||||
|
"shape": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -367,7 +368,7 @@
|
||||||
-83.82633839751217,
|
-83.82633839751217,
|
||||||
-99.09214039482102,
|
-99.09214039482102,
|
||||||
-113.56011074290575,
|
-113.56011074290575,
|
||||||
-127.11376185155243,
|
-127.11376185155244,
|
||||||
-139.643967691452,
|
-139.643967691452,
|
||||||
-151.04984241282355,
|
-151.04984241282355,
|
||||||
-161.23955261895566,
|
-161.23955261895566,
|
||||||
|
|
@ -460,7 +461,7 @@
|
||||||
-113.56011074290593,
|
-113.56011074290593,
|
||||||
-99.0921403948212,
|
-99.0921403948212,
|
||||||
-83.82633839751236,
|
-83.82633839751236,
|
||||||
-67.88561601251554,
|
-67.88561601251556,
|
||||||
-51.398318563481766,
|
-51.398318563481766,
|
||||||
-34.49719207509674,
|
-34.49719207509674,
|
||||||
-17.318314480143496,
|
-17.318314480143496,
|
||||||
|
|
@ -543,7 +544,7 @@
|
||||||
99.0921403948209,
|
99.0921403948209,
|
||||||
113.56011074290566,
|
113.56011074290566,
|
||||||
127.11376185155234,
|
127.11376185155234,
|
||||||
139.64396769145193,
|
139.6439676914519,
|
||||||
151.04984241282366,
|
151.04984241282366,
|
||||||
161.23955261895557,
|
161.23955261895557,
|
||||||
170.1310567547217,
|
170.1310567547217,
|
||||||
|
|
@ -572,12 +573,12 @@
|
||||||
2.129415854357418e-13,
|
2.129415854357418e-13,
|
||||||
-17.31831448014314,
|
-17.31831448014314,
|
||||||
-34.49719207509707,
|
-34.49719207509707,
|
||||||
-51.398318563481425,
|
-51.39831856348143,
|
||||||
-67.88561601251521,
|
-67.88561601251521,
|
||||||
-83.82633839751205,
|
-83.82633839751205,
|
||||||
-99.09214039482087,
|
-99.09214039482087,
|
||||||
-113.5601107429062,
|
-113.5601107429062,
|
||||||
-127.11376185155231,
|
-127.11376185155234,
|
||||||
-139.6439676914519,
|
-139.6439676914519,
|
||||||
-151.04984241282324,
|
-151.04984241282324,
|
||||||
-161.23955261895594,
|
-161.23955261895594,
|
||||||
|
|
@ -610,7 +611,7 @@
|
||||||
51.3983185634814,
|
51.3983185634814,
|
||||||
67.88561601251583,
|
67.88561601251583,
|
||||||
83.82633839751202,
|
83.82633839751202,
|
||||||
99.09214039482086,
|
99.09214039482087,
|
||||||
113.56011074290619,
|
113.56011074290619,
|
||||||
127.11376185155231,
|
127.11376185155231,
|
||||||
139.64396769145142,
|
139.64396769145142,
|
||||||
|
|
@ -667,7 +668,7 @@
|
||||||
-151.04984241282355,
|
-151.04984241282355,
|
||||||
-139.6439676914518,
|
-139.6439676914518,
|
||||||
-127.1137618515527,
|
-127.1137618515527,
|
||||||
-113.56011074290548,
|
-113.5601107429055,
|
||||||
-99.09214039482131,
|
-99.09214039482131,
|
||||||
-83.82633839751311,
|
-83.82633839751311,
|
||||||
-67.88561601251568,
|
-67.88561601251568,
|
||||||
|
|
@ -686,7 +687,7 @@
|
||||||
139.64396769145233,
|
139.64396769145233,
|
||||||
151.0498424128232,
|
151.0498424128232,
|
||||||
161.2395526189559,
|
161.2395526189559,
|
||||||
170.13105675472167,
|
170.13105675472164,
|
||||||
177.6527656569399,
|
177.6527656569399,
|
||||||
183.7441189482236,
|
183.7441189482236,
|
||||||
188.3560726335284,
|
188.3560726335284,
|
||||||
|
|
@ -700,7 +701,7 @@
|
||||||
170.1310567547216,
|
170.1310567547216,
|
||||||
161.23955261895586,
|
161.23955261895586,
|
||||||
151.04984241282312,
|
151.04984241282312,
|
||||||
139.64396769145227,
|
139.64396769145225,
|
||||||
127.11376185155324,
|
127.11376185155324,
|
||||||
113.56011074290606,
|
113.56011074290606,
|
||||||
99.09214039482075,
|
99.09214039482075,
|
||||||
|
|
@ -837,14 +838,14 @@
|
||||||
188.35607263352813,
|
188.35607263352813,
|
||||||
183.7441189482236,
|
183.7441189482236,
|
||||||
177.65276565694043,
|
177.65276565694043,
|
||||||
170.13105675472167,
|
170.13105675472164,
|
||||||
161.2395526189559,
|
161.2395526189559,
|
||||||
151.0498424128232,
|
151.0498424128232,
|
||||||
139.64396769145233,
|
139.64396769145233,
|
||||||
127.11376185155329,
|
127.11376185155329,
|
||||||
113.56011074290502,
|
113.56011074290502,
|
||||||
99.09214039482083,
|
99.09214039482083,
|
||||||
83.82633839751259,
|
83.8263383975126,
|
||||||
67.88561601251514,
|
67.88561601251514,
|
||||||
51.39831856348202,
|
51.39831856348202,
|
||||||
34.49719207509632,
|
34.49719207509632,
|
||||||
|
|
@ -856,7 +857,8 @@
|
||||||
-67.88561601251568,
|
-67.88561601251568,
|
||||||
-83.82633839751188
|
-83.82633839751188
|
||||||
],
|
],
|
||||||
"file_path": "generated:sine-wave"
|
"file_path": "generated:sine-wave",
|
||||||
|
"use_last": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 401,
|
"id": 401,
|
||||||
|
|
@ -892,7 +894,7 @@
|
||||||
5.09980167540335,
|
5.09980167540335,
|
||||||
5.928784854272076,
|
5.928784854272076,
|
||||||
6.7100329435632435,
|
6.7100329435632435,
|
||||||
7.437255793028317,
|
7.437255793028318,
|
||||||
8.104598231883518,
|
8.104598231883518,
|
||||||
8.70668721120218,
|
8.70668721120218,
|
||||||
9.238675064545419,
|
9.238675064545419,
|
||||||
|
|
@ -943,7 +945,7 @@
|
||||||
-10.075813279025667,
|
-10.075813279025667,
|
||||||
-9.696278538521907,
|
-9.696278538521907,
|
||||||
-9.238675064545415,
|
-9.238675064545415,
|
||||||
-8.706687211202178,
|
-8.706687211202176,
|
||||||
-8.10459823188352,
|
-8.10459823188352,
|
||||||
-7.437255793028321,
|
-7.437255793028321,
|
||||||
-6.710032943563246,
|
-6.710032943563246,
|
||||||
|
|
@ -973,7 +975,7 @@
|
||||||
10.718732373371513,
|
10.718732373371513,
|
||||||
10.762057251873964,
|
10.762057251873964,
|
||||||
10.718732373371514,
|
10.718732373371514,
|
||||||
10.58910656431125,
|
10.589106564311248,
|
||||||
10.37422349548898,
|
10.37422349548898,
|
||||||
10.075813279025667,
|
10.075813279025667,
|
||||||
9.696278538521907,
|
9.696278538521907,
|
||||||
|
|
@ -1049,7 +1051,7 @@
|
||||||
9.696278538521907,
|
9.696278538521907,
|
||||||
9.238675064545422,
|
9.238675064545422,
|
||||||
8.706687211202185,
|
8.706687211202185,
|
||||||
8.104598231883523,
|
8.104598231883521,
|
||||||
7.437255793028323,
|
7.437255793028323,
|
||||||
6.71003294356325,
|
6.71003294356325,
|
||||||
5.928784854272083,
|
5.928784854272083,
|
||||||
|
|
@ -1190,8 +1192,8 @@
|
||||||
9.238675064545424,
|
9.238675064545424,
|
||||||
8.706687211202166,
|
8.706687211202166,
|
||||||
8.104598231883525,
|
8.104598231883525,
|
||||||
7.437255793028327,
|
7.437255793028328,
|
||||||
6.710032943563254,
|
6.710032943563253,
|
||||||
5.928784854272055,
|
5.928784854272055,
|
||||||
5.099801675403362,
|
5.099801675403362,
|
||||||
4.229757892079114,
|
4.229757892079114,
|
||||||
|
|
@ -1225,7 +1227,7 @@
|
||||||
-9.238675064545426,
|
-9.238675064545426,
|
||||||
-8.706687211202167,
|
-8.706687211202167,
|
||||||
-8.104598231883527,
|
-8.104598231883527,
|
||||||
-7.437255793028355,
|
-7.437255793028356,
|
||||||
-6.710032943563254,
|
-6.710032943563254,
|
||||||
-5.928784854272056,
|
-5.928784854272056,
|
||||||
-5.099801675403363,
|
-5.099801675403363,
|
||||||
|
|
@ -1297,7 +1299,7 @@
|
||||||
-8.104598231883529,
|
-8.104598231883529,
|
||||||
-7.437255793028303,
|
-7.437255793028303,
|
||||||
-6.710032943563257,
|
-6.710032943563257,
|
||||||
-5.928784854272058,
|
-5.928784854272059,
|
||||||
-5.0998016754033655,
|
-5.0998016754033655,
|
||||||
-4.229757892079153,
|
-4.229757892079153,
|
||||||
-3.3256585852652147,
|
-3.3256585852652147,
|
||||||
|
|
@ -1311,7 +1313,7 @@
|
||||||
4.229757892079088,
|
4.229757892079088,
|
||||||
5.09980167540337,
|
5.09980167540337,
|
||||||
5.928784854272062,
|
5.928784854272062,
|
||||||
6.710032943563261,
|
6.71003294356326,
|
||||||
7.4372557930283065,
|
7.4372557930283065,
|
||||||
8.104598231883482,
|
8.104598231883482,
|
||||||
8.706687211202171,
|
8.706687211202171,
|
||||||
|
|
@ -1348,7 +1350,7 @@
|
||||||
-5.92878485427203,
|
-5.92878485427203,
|
||||||
-6.71003294356329,
|
-6.71003294356329,
|
||||||
-7.437255793028332,
|
-7.437255793028332,
|
||||||
-8.104598231883505,
|
-8.104598231883507,
|
||||||
-8.706687211202192,
|
-8.706687211202192,
|
||||||
-9.23867506454541,
|
-9.23867506454541,
|
||||||
-9.69627853852188,
|
-9.69627853852188,
|
||||||
|
|
@ -1360,7 +1362,7 @@
|
||||||
-10.71873237337151,
|
-10.71873237337151,
|
||||||
-10.589106564311251,
|
-10.589106564311251,
|
||||||
-10.374223495488975,
|
-10.374223495488975,
|
||||||
-10.075813279025672,
|
-10.075813279025674,
|
||||||
-9.696278538521929,
|
-9.696278538521929,
|
||||||
-9.238675064545427,
|
-9.238675064545427,
|
||||||
-8.706687211202215,
|
-8.706687211202215,
|
||||||
|
|
@ -1387,7 +1389,7 @@
|
||||||
8.70668721120217,
|
8.70668721120217,
|
||||||
9.238675064545427,
|
9.238675064545427,
|
||||||
9.696278538521897,
|
9.696278538521897,
|
||||||
10.075813279025672,
|
10.075813279025674,
|
||||||
10.374223495488975,
|
10.374223495488975,
|
||||||
10.589106564311237,
|
10.589106564311237,
|
||||||
10.718732373371518,
|
10.718732373371518,
|
||||||
|
|
@ -1434,7 +1436,7 @@
|
||||||
-9.696278538521899,
|
-9.696278538521899,
|
||||||
-9.23867506454543,
|
-9.23867506454543,
|
||||||
-8.706687211202215,
|
-8.706687211202215,
|
||||||
-8.104598231883534,
|
-8.104598231883532,
|
||||||
-7.437255793028362,
|
-7.437255793028362,
|
||||||
-6.710032943563201,
|
-6.710032943563201,
|
||||||
-5.928784854272062,
|
-5.928784854272062,
|
||||||
|
|
@ -1450,11 +1452,11 @@
|
||||||
3.3256585852652147,
|
3.3256585852652147,
|
||||||
4.229757892079083,
|
4.229757892079083,
|
||||||
5.0998016754033655,
|
5.0998016754033655,
|
||||||
5.928784854272058,
|
5.928784854272059,
|
||||||
6.710032943563257,
|
6.710032943563257,
|
||||||
7.437255793028303,
|
7.437255793028303,
|
||||||
8.104598231883479,
|
8.104598231883479,
|
||||||
8.706687211202214,
|
8.706687211202212,
|
||||||
9.238675064545427,
|
9.238675064545427,
|
||||||
9.696278538521895,
|
9.696278538521895,
|
||||||
10.075813279025672,
|
10.075813279025672,
|
||||||
|
|
@ -1468,7 +1470,8 @@
|
||||||
10.07581327902566,
|
10.07581327902566,
|
||||||
9.696278538521915
|
9.696278538521915
|
||||||
],
|
],
|
||||||
"file_path": "generated:sine-wave-velocity"
|
"file_path": "generated:sine-wave-velocity",
|
||||||
|
"use_last": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"load_patterns": [
|
"load_patterns": [
|
||||||
|
|
@ -1538,6 +1541,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
1
|
1
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"n_steps": 10,
|
"n_steps": 10,
|
||||||
"load_factor_increment": 0.1,
|
"load_factor_increment": 0.1,
|
||||||
"system": "BandGeneral",
|
"system": "BandGeneral",
|
||||||
|
|
@ -1555,6 +1559,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
200
|
200
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"control_node": 3,
|
"control_node": 3,
|
||||||
"control_dof": 1,
|
"control_dof": 1,
|
||||||
"target_disp": 43.2,
|
"target_disp": 43.2,
|
||||||
|
|
@ -1580,6 +1585,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
400
|
400
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"dt": 0.01,
|
"dt": 0.01,
|
||||||
"n_steps": 1000,
|
"n_steps": 1000,
|
||||||
"system": "BandGeneral",
|
"system": "BandGeneral",
|
||||||
|
|
@ -1595,6 +1601,8 @@
|
||||||
"max_iter": 10,
|
"max_iter": 10,
|
||||||
"rayleigh_alpha_m": 0.0,
|
"rayleigh_alpha_m": 0.0,
|
||||||
"rayleigh_beta_k": 0.0,
|
"rayleigh_beta_k": 0.0,
|
||||||
|
"rayleigh_beta_k_init": 0.0,
|
||||||
|
"rayleigh_beta_k_comm": 0.0,
|
||||||
"rayleigh_mode1_damping": 0.02,
|
"rayleigh_mode1_damping": 0.02,
|
||||||
"preload_case_ids": [
|
"preload_case_ids": [
|
||||||
1
|
1
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"name": "Moment-Curvature (OpenSees Ex 2)",
|
"name": "Moment-Curvature (OpenSees Ex 2)",
|
||||||
"description": "RC column fibre section — constant axial P + DisplacementControl curvature pushover. Kip-in-ksi units throughout.",
|
"description": "RC column fibre section — constant axial P + DisplacementControl curvature pushover. Kip-in-ksi units throughout.",
|
||||||
"author": "OpenSees Examples Manual",
|
"author": "OpenSees Examples Manual",
|
||||||
"units": "US (in, kip, kip·s²/in, s, ksi)"
|
"units": "Imperial (in, kip, kip·s²/in, s, ksi)"
|
||||||
},
|
},
|
||||||
"ndm": 2,
|
"ndm": 2,
|
||||||
"ndf": 3,
|
"ndf": 3,
|
||||||
|
|
@ -251,6 +251,7 @@
|
||||||
"section_id": 1
|
"section_id": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"mp_constraints": [],
|
||||||
"time_series": [
|
"time_series": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
|
@ -317,6 +318,7 @@
|
||||||
1,
|
1,
|
||||||
2
|
2
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"control_node": 2,
|
"control_node": 2,
|
||||||
"control_dof": 3,
|
"control_dof": 3,
|
||||||
"target_disp": 0.0019047619047619052,
|
"target_disp": 0.0019047619047619052,
|
||||||
|
|
@ -329,7 +331,8 @@
|
||||||
"algorithm": "Newton",
|
"algorithm": "Newton",
|
||||||
"test": "NormUnbalance",
|
"test": "NormUnbalance",
|
||||||
"tolerance": 1e-09,
|
"tolerance": 1e-09,
|
||||||
"max_iter": 25
|
"max_iter": 25,
|
||||||
|
"preload_case_ids": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -14,7 +14,7 @@ linear reference moment pattern (Mz = 1 kip·in) is added and
|
||||||
DisplacementControl on DOF 3 ramps the curvature to μ·Ky where
|
DisplacementControl on DOF 3 ramps the curvature to μ·Ky where
|
||||||
μ = 15 and Ky is the elastic yield curvature estimate.
|
μ = 15 and Ky is the elastic yield curvature estimate.
|
||||||
|
|
||||||
Units: kip, in, ksi (UnitSystem.US_IN_KIP).
|
Units: kip, in, ksi (UnitSystem.IMPERIAL).
|
||||||
|
|
||||||
GUI walkthrough: File → Open → moment_curvature.osmodel, Options →
|
GUI walkthrough: File → Open → moment_curvature.osmodel, Options →
|
||||||
Set Display Units → US (in, kip, kip·s²/in, s, ksi), Analyze → Run →
|
Set Display Units → US (in, kip, kip·s²/in, s, ksi), Analyze → Run →
|
||||||
|
|
@ -83,7 +83,7 @@ def build_moment_curvature() -> Project:
|
||||||
"RC column fibre section — constant axial P + DisplacementControl "
|
"RC column fibre section — constant axial P + DisplacementControl "
|
||||||
"curvature pushover. Kip-in-ksi units throughout."
|
"curvature pushover. Kip-in-ksi units throughout."
|
||||||
),
|
),
|
||||||
units=UnitSystem.US_IN_KIP,
|
units=UnitSystem.IMPERIAL,
|
||||||
),
|
),
|
||||||
ndm=2, ndf=3,
|
ndm=2, ndf=3,
|
||||||
coord_systems=[
|
coord_systems=[
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"name": "Portal Frame",
|
"name": "Portal Frame",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "Ozan",
|
"author": "Ozan",
|
||||||
"units": "SI (m, N, kg, s, Pa)"
|
"units": "Metric (m, N, kg, s, Pa)"
|
||||||
},
|
},
|
||||||
"ndm": 3,
|
"ndm": 3,
|
||||||
"ndf": 6,
|
"ndf": 6,
|
||||||
|
|
@ -161,7 +161,8 @@
|
||||||
"Iz": 0.000416,
|
"Iz": 0.000416,
|
||||||
"Iy": 0.000129,
|
"Iy": 0.000129,
|
||||||
"G": 80000000000.0,
|
"G": 80000000000.0,
|
||||||
"J": 2.04e-06
|
"J": 2.04e-06,
|
||||||
|
"shape": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -175,7 +176,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 1,
|
"section_id": 1,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
|
|
@ -187,7 +189,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 1,
|
"section_id": 1,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 3,
|
"id": 3,
|
||||||
|
|
@ -199,9 +202,11 @@
|
||||||
],
|
],
|
||||||
"section_id": 1,
|
"section_id": 1,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"mp_constraints": [],
|
||||||
"time_series": [
|
"time_series": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
|
@ -418,7 +423,8 @@
|
||||||
0.0,
|
0.0,
|
||||||
0.0
|
0.0
|
||||||
],
|
],
|
||||||
"file_path": null
|
"file_path": null,
|
||||||
|
"use_last": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"load_patterns": [
|
"load_patterns": [
|
||||||
|
|
@ -472,6 +478,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
1
|
1
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"n_steps": 1,
|
"n_steps": 1,
|
||||||
"load_factor_increment": 1.0,
|
"load_factor_increment": 1.0,
|
||||||
"system": "BandGeneral",
|
"system": "BandGeneral",
|
||||||
|
|
@ -496,6 +503,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
2
|
2
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"dt": 0.01,
|
"dt": 0.01,
|
||||||
"n_steps": 200,
|
"n_steps": 200,
|
||||||
"system": "BandGeneral",
|
"system": "BandGeneral",
|
||||||
|
|
@ -510,7 +518,12 @@
|
||||||
"tolerance": 1e-06,
|
"tolerance": 1e-06,
|
||||||
"max_iter": 25,
|
"max_iter": 25,
|
||||||
"rayleigh_alpha_m": 0.0,
|
"rayleigh_alpha_m": 0.0,
|
||||||
"rayleigh_beta_k": 0.0
|
"rayleigh_beta_k": 0.0,
|
||||||
|
"rayleigh_beta_k_init": 0.0,
|
||||||
|
"rayleigh_beta_k_comm": 0.0,
|
||||||
|
"rayleigh_mode1_damping": null,
|
||||||
|
"preload_case_ids": [],
|
||||||
|
"remove_patterns": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -59,7 +59,7 @@ def _sine_pulse_factors() -> list[float]:
|
||||||
def build_portal_frame() -> Project:
|
def build_portal_frame() -> Project:
|
||||||
"""A two-column, one-beam steel portal frame with static + dynamic cases."""
|
"""A two-column, one-beam steel portal frame with static + dynamic cases."""
|
||||||
return Project(
|
return Project(
|
||||||
meta=ProjectMeta(name="Portal Frame", author="Ozan", units=UnitSystem.SI_M_N),
|
meta=ProjectMeta(name="Portal Frame", author="Ozan", units=UnitSystem.METRIC),
|
||||||
ndm=3,
|
ndm=3,
|
||||||
ndf=6,
|
ndf=6,
|
||||||
nodes=[
|
nodes=[
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"name": "Portal Pushover",
|
"name": "Portal Pushover",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "Ozan",
|
"author": "Ozan",
|
||||||
"units": "SI (m, N, kg, s, Pa)"
|
"units": "Metric (m, N, kg, s, Pa)"
|
||||||
},
|
},
|
||||||
"ndm": 3,
|
"ndm": 3,
|
||||||
"ndf": 6,
|
"ndf": 6,
|
||||||
|
|
@ -231,7 +231,8 @@
|
||||||
"Iz": 0.000416,
|
"Iz": 0.000416,
|
||||||
"Iy": 0.000129,
|
"Iy": 0.000129,
|
||||||
"G": 80000000000.0,
|
"G": 80000000000.0,
|
||||||
"J": 2.04e-06
|
"J": 2.04e-06,
|
||||||
|
"shape": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -285,9 +286,11 @@
|
||||||
],
|
],
|
||||||
"section_id": 3,
|
"section_id": 3,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"mp_constraints": [],
|
||||||
"time_series": [
|
"time_series": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
|
@ -327,6 +330,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
1
|
1
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"control_node": 3,
|
"control_node": 3,
|
||||||
"control_dof": 1,
|
"control_dof": 1,
|
||||||
"target_disp": 0.15,
|
"target_disp": 0.15,
|
||||||
|
|
@ -340,7 +344,8 @@
|
||||||
"algorithm": "Newton",
|
"algorithm": "Newton",
|
||||||
"test": "NormDispIncr",
|
"test": "NormDispIncr",
|
||||||
"tolerance": 1e-06,
|
"tolerance": 1e-06,
|
||||||
"max_iter": 25
|
"max_iter": 25,
|
||||||
|
"preload_case_ids": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ def build_portal_pushover() -> Project:
|
||||||
|
|
||||||
return Project(
|
return Project(
|
||||||
meta=ProjectMeta(name="Portal Pushover", author="Ozan",
|
meta=ProjectMeta(name="Portal Pushover", author="Ozan",
|
||||||
units=UnitSystem.SI_M_N),
|
units=UnitSystem.METRIC),
|
||||||
ndm=3, ndf=6,
|
ndm=3, ndf=6,
|
||||||
nodes=[
|
nodes=[
|
||||||
Node(id=1, name="Base-L", coords=(0.0, 0.0, 0.0),
|
Node(id=1, name="Base-L", coords=(0.0, 0.0, 0.0),
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"name": "RC Frame Earthquake (OpenSees Ex 3.3)",
|
"name": "RC Frame Earthquake (OpenSees Ex 3.3)",
|
||||||
"description": "Ex 3 gravity + uniform base excitation (horizontal, 4-s synthetic record peaking at ~0.35 g) + Rayleigh beta_k",
|
"description": "Ex 3 gravity + uniform base excitation (horizontal, 4-s synthetic record peaking at ~0.35 g) + Rayleigh beta_k",
|
||||||
"author": "OpenSees Examples Manual",
|
"author": "OpenSees Examples Manual",
|
||||||
"units": "US (in, kip, kip·s²/in, s, ksi)"
|
"units": "Imperial (in, kip, kip·s²/in, s, ksi)"
|
||||||
},
|
},
|
||||||
"ndm": 2,
|
"ndm": 2,
|
||||||
"ndf": 3,
|
"ndf": 3,
|
||||||
|
|
@ -313,7 +313,8 @@
|
||||||
"Iz": 8640.0,
|
"Iz": 8640.0,
|
||||||
"Iy": 8640.0,
|
"Iy": 8640.0,
|
||||||
"G": 1500.0,
|
"G": 1500.0,
|
||||||
"J": 1.0
|
"J": 1.0,
|
||||||
|
"shape": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -355,9 +356,11 @@
|
||||||
],
|
],
|
||||||
"section_id": 2,
|
"section_id": 2,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"mp_constraints": [],
|
||||||
"time_series": [
|
"time_series": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
|
@ -455,7 +458,7 @@
|
||||||
-0.07226305111525733,
|
-0.07226305111525733,
|
||||||
-0.08816778784387092,
|
-0.08816778784387092,
|
||||||
-0.10268206588930337,
|
-0.10268206588930337,
|
||||||
-0.11557698641636836,
|
-0.11557698641636838,
|
||||||
-0.12664918882530235,
|
-0.12664918882530235,
|
||||||
-0.13572405786990283,
|
-0.13572405786990283,
|
||||||
-0.142658477444273,
|
-0.142658477444273,
|
||||||
|
|
@ -489,7 +492,7 @@
|
||||||
0.14970400926424074,
|
0.14970400926424074,
|
||||||
0.14734308760930329,
|
0.14734308760930329,
|
||||||
0.14265847744427299,
|
0.14265847744427299,
|
||||||
0.13572405786990302,
|
0.135724057869903,
|
||||||
0.12664918882530243,
|
0.12664918882530243,
|
||||||
0.11557698641636846,
|
0.11557698641636846,
|
||||||
0.10268206588930347,
|
0.10268206588930347,
|
||||||
|
|
@ -504,7 +507,7 @@
|
||||||
-0.0552186829027018,
|
-0.0552186829027018,
|
||||||
-0.0722630511152573,
|
-0.0722630511152573,
|
||||||
-0.08816778784387089,
|
-0.08816778784387089,
|
||||||
-0.10268206588930313,
|
-0.10268206588930316,
|
||||||
-0.11557698641636852,
|
-0.11557698641636852,
|
||||||
-0.12664918882530232,
|
-0.12664918882530232,
|
||||||
-0.13572405786990294,
|
-0.13572405786990294,
|
||||||
|
|
@ -566,7 +569,7 @@
|
||||||
-0.14265847744427307,
|
-0.14265847744427307,
|
||||||
-0.1357240578699028,
|
-0.1357240578699028,
|
||||||
-0.12664918882530243,
|
-0.12664918882530243,
|
||||||
-0.11557698641636868,
|
-0.11557698641636867,
|
||||||
-0.10268206588930333,
|
-0.10268206588930333,
|
||||||
-0.08816778784387108,
|
-0.08816778784387108,
|
||||||
-0.0722630511152575,
|
-0.0722630511152575,
|
||||||
|
|
@ -616,12 +619,12 @@
|
||||||
-0.08652674043787481,
|
-0.08652674043787481,
|
||||||
-0.08129819692830416,
|
-0.08129819692830416,
|
||||||
-0.07492000704231337,
|
-0.07492000704231337,
|
||||||
-0.06752087821731563,
|
-0.06752087821731562,
|
||||||
-0.05924239844431021,
|
-0.059242398444310215,
|
||||||
-0.05023649120795737,
|
-0.05023649120795737,
|
||||||
-0.04066277069740133,
|
-0.04066277069740133,
|
||||||
-0.030685841350052333,
|
-0.030685841350052333,
|
||||||
-0.020472585578247403,
|
-0.0204725855782474,
|
||||||
-0.010189482631897374,
|
-0.010189482631897374,
|
||||||
-9.832592927142318e-17,
|
-9.832592927142318e-17,
|
||||||
0.009937903409330418,
|
0.009937903409330418,
|
||||||
|
|
@ -642,7 +645,7 @@
|
||||||
0.05947903147792919,
|
0.05947903147792919,
|
||||||
0.05481264807294881,
|
0.05481264807294881,
|
||||||
0.04939932978399098,
|
0.04939932978399098,
|
||||||
0.043342664598141574,
|
0.04334266459814158,
|
||||||
0.03675380211118236,
|
0.03675380211118236,
|
||||||
0.029749518558492356,
|
0.029749518558492356,
|
||||||
0.02245024111907521,
|
0.02245024111907521,
|
||||||
|
|
@ -666,9 +669,9 @@
|
||||||
-0.04631442669186875,
|
-0.04631442669186875,
|
||||||
-0.04351578902386234,
|
-0.04351578902386234,
|
||||||
-0.04010178999412147,
|
-0.04010178999412147,
|
||||||
-0.03614132172945704,
|
-0.036141321729457034,
|
||||||
-0.03171017082019282,
|
-0.03171017082019282,
|
||||||
-0.02688965604775296,
|
-0.026889656047752963,
|
||||||
-0.021765212731031044,
|
-0.021765212731031044,
|
||||||
-0.016424947276336123,
|
-0.016424947276336123,
|
||||||
-0.010958185402089994,
|
-0.010958185402089994,
|
||||||
|
|
@ -682,7 +685,7 @@
|
||||||
0.027293196958740988,
|
0.027293196958740988,
|
||||||
0.03033908626567152,
|
0.03033908626567152,
|
||||||
0.03283256872166209,
|
0.03283256872166209,
|
||||||
0.034748063307665326,
|
0.03474806330766532,
|
||||||
0.03606971177513051,
|
0.03606971177513051,
|
||||||
0.03679138802438222,
|
0.03679138802438222,
|
||||||
0.03691655417444788,
|
0.03691655417444788,
|
||||||
|
|
@ -720,7 +723,7 @@
|
||||||
-0.016973231331797612,
|
-0.016973231331797612,
|
||||||
-0.014392995708504558,
|
-0.014392995708504558,
|
||||||
-0.011650078858431393,
|
-0.011650078858431393,
|
||||||
-0.008791640742480778,
|
-0.008791640742480776,
|
||||||
-0.005865493972298641,
|
-0.005865493972298641,
|
||||||
-0.002919335651562286,
|
-0.002919335651562286,
|
||||||
-3.943919054879792e-17,
|
-3.943919054879792e-17,
|
||||||
|
|
@ -729,7 +732,7 @@
|
||||||
0.008156387432977339,
|
0.008156387432977339,
|
||||||
0.010541427274178315,
|
0.010541427274178315,
|
||||||
0.012701774131668711,
|
0.012701774131668711,
|
||||||
0.014608995592985858,
|
0.01460899559298586,
|
||||||
0.016239342654524213,
|
0.016239342654524213,
|
||||||
0.01757400763590476,
|
0.01757400763590476,
|
||||||
0.01859929800432925,
|
0.01859929800432925,
|
||||||
|
|
@ -742,7 +745,7 @@
|
||||||
0.01704102783102496,
|
0.01704102783102496,
|
||||||
0.015704086601509296,
|
0.015704086601509296,
|
||||||
0.014153144944791883,
|
0.014153144944791883,
|
||||||
0.012417881316069912,
|
0.012417881316069914,
|
||||||
0.010530140607703934,
|
0.010530140607703934,
|
||||||
0.008523379771289316,
|
0.008523379771289316,
|
||||||
0.00643210177128294,
|
0.00643210177128294,
|
||||||
|
|
@ -754,7 +757,7 @@
|
||||||
-0.005967340521710147,
|
-0.005967340521710147,
|
||||||
-0.007712272945193301,
|
-0.007712272945193301,
|
||||||
-0.009292816470078945,
|
-0.009292816470078945,
|
||||||
-0.010688169499041026,
|
-0.010688169499041028,
|
||||||
-0.011880956889869792,
|
-0.011880956889869792,
|
||||||
-0.012857418649655518,
|
-0.012857418649655518,
|
||||||
-0.013607537107403339,
|
-0.013607537107403339,
|
||||||
|
|
@ -774,7 +777,8 @@
|
||||||
-0.003139572682582101,
|
-0.003139572682582101,
|
||||||
-0.0015626077711816495
|
-0.0015626077711816495
|
||||||
],
|
],
|
||||||
"file_path": null
|
"file_path": null,
|
||||||
|
"use_last": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"load_patterns": [
|
"load_patterns": [
|
||||||
|
|
@ -830,6 +834,7 @@
|
||||||
1,
|
1,
|
||||||
2
|
2
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"dt": 0.01,
|
"dt": 0.01,
|
||||||
"n_steps": 400,
|
"n_steps": 400,
|
||||||
"system": "BandGeneral",
|
"system": "BandGeneral",
|
||||||
|
|
@ -844,7 +849,12 @@
|
||||||
"tolerance": 1e-12,
|
"tolerance": 1e-12,
|
||||||
"max_iter": 10,
|
"max_iter": 10,
|
||||||
"rayleigh_alpha_m": 0.0,
|
"rayleigh_alpha_m": 0.0,
|
||||||
"rayleigh_beta_k": 0.000625
|
"rayleigh_beta_k": 0.000625,
|
||||||
|
"rayleigh_beta_k_init": 0.0,
|
||||||
|
"rayleigh_beta_k_comm": 0.0,
|
||||||
|
"rayleigh_mode1_damping": null,
|
||||||
|
"preload_case_ids": [],
|
||||||
|
"remove_patterns": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"name": "RC Frame Gravity (OpenSees Ex 3)",
|
"name": "RC Frame Gravity (OpenSees Ex 3)",
|
||||||
"description": "1-bay 1-storey portal frame, nonlinear fibre columns + elastic beam, 10-step LoadControl gravity pushdown.",
|
"description": "1-bay 1-storey portal frame, nonlinear fibre columns + elastic beam, 10-step LoadControl gravity pushdown.",
|
||||||
"author": "OpenSees Examples Manual",
|
"author": "OpenSees Examples Manual",
|
||||||
"units": "US (in, kip, kip·s²/in, s, ksi)"
|
"units": "Imperial (in, kip, kip·s²/in, s, ksi)"
|
||||||
},
|
},
|
||||||
"ndm": 2,
|
"ndm": 2,
|
||||||
"ndf": 3,
|
"ndf": 3,
|
||||||
|
|
@ -313,7 +313,8 @@
|
||||||
"Iz": 8640.0,
|
"Iz": 8640.0,
|
||||||
"Iy": 8640.0,
|
"Iy": 8640.0,
|
||||||
"G": 1500.0,
|
"G": 1500.0,
|
||||||
"J": 1.0
|
"J": 1.0,
|
||||||
|
"shape": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -355,9 +356,11 @@
|
||||||
],
|
],
|
||||||
"section_id": 2,
|
"section_id": 2,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"mp_constraints": [],
|
||||||
"time_series": [
|
"time_series": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
|
@ -408,6 +411,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
1
|
1
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"n_steps": 10,
|
"n_steps": 10,
|
||||||
"load_factor_increment": 0.1,
|
"load_factor_increment": 0.1,
|
||||||
"system": "BandGeneral",
|
"system": "BandGeneral",
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ def build_rc_frame_gravity() -> Project:
|
||||||
"1-bay 1-storey portal frame, nonlinear fibre columns + "
|
"1-bay 1-storey portal frame, nonlinear fibre columns + "
|
||||||
"elastic beam, 10-step LoadControl gravity pushdown."
|
"elastic beam, 10-step LoadControl gravity pushdown."
|
||||||
),
|
),
|
||||||
units=UnitSystem.US_IN_KIP,
|
units=UnitSystem.IMPERIAL,
|
||||||
),
|
),
|
||||||
ndm=2, ndf=3,
|
ndm=2, ndf=3,
|
||||||
coord_systems=[
|
coord_systems=[
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"name": "RC Frame Pushover (OpenSees Ex 3.2)",
|
"name": "RC Frame Pushover (OpenSees Ex 3.2)",
|
||||||
"description": "Ex 3 gravity preload (StaticCase) + lateral reference load + DisplacementControl pushover on node 3 (DOF 1) to 15 in, chained via preload_case_ids.",
|
"description": "Ex 3 gravity preload (StaticCase) + lateral reference load + DisplacementControl pushover on node 3 (DOF 1) to 15 in, chained via preload_case_ids.",
|
||||||
"author": "OpenSees Examples Manual",
|
"author": "OpenSees Examples Manual",
|
||||||
"units": "US (in, kip, kip·s²/in, s, ksi)"
|
"units": "Imperial (in, kip, kip·s²/in, s, ksi)"
|
||||||
},
|
},
|
||||||
"ndm": 2,
|
"ndm": 2,
|
||||||
"ndf": 3,
|
"ndf": 3,
|
||||||
|
|
@ -313,7 +313,8 @@
|
||||||
"Iz": 8640.0,
|
"Iz": 8640.0,
|
||||||
"Iy": 8640.0,
|
"Iy": 8640.0,
|
||||||
"G": 1500.0,
|
"G": 1500.0,
|
||||||
"J": 1.0
|
"J": 1.0,
|
||||||
|
"shape": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -355,9 +356,11 @@
|
||||||
],
|
],
|
||||||
"section_id": 2,
|
"section_id": 2,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"mp_constraints": [],
|
||||||
"time_series": [
|
"time_series": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
|
@ -445,6 +448,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
1
|
1
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"n_steps": 10,
|
"n_steps": 10,
|
||||||
"load_factor_increment": 0.1,
|
"load_factor_increment": 0.1,
|
||||||
"system": "BandGeneral",
|
"system": "BandGeneral",
|
||||||
|
|
@ -462,6 +466,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
2
|
2
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"control_node": 3,
|
"control_node": 3,
|
||||||
"control_dof": 1,
|
"control_dof": 1,
|
||||||
"target_disp": 15.0,
|
"target_disp": 15.0,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"name": "SDOF Pushover",
|
"name": "SDOF Pushover",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "Ozan",
|
"author": "Ozan",
|
||||||
"units": "SI (m, N, kg, s, Pa)"
|
"units": "Metric (m, N, kg, s, Pa)"
|
||||||
},
|
},
|
||||||
"ndm": 3,
|
"ndm": 3,
|
||||||
"ndf": 6,
|
"ndf": 6,
|
||||||
|
|
@ -121,7 +121,8 @@
|
||||||
"Iz": 0.0002,
|
"Iz": 0.0002,
|
||||||
"Iy": 4.5e-05,
|
"Iy": 4.5e-05,
|
||||||
"G": 80000000000.0,
|
"G": 80000000000.0,
|
||||||
"J": 8.5e-07
|
"J": 8.5e-07,
|
||||||
|
"shape": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -135,9 +136,11 @@
|
||||||
],
|
],
|
||||||
"section_id": 1,
|
"section_id": 1,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"mp_constraints": [],
|
||||||
"time_series": [
|
"time_series": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
|
@ -177,6 +180,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
1
|
1
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"control_node": 2,
|
"control_node": 2,
|
||||||
"control_dof": 1,
|
"control_dof": 1,
|
||||||
"target_disp": 0.1,
|
"target_disp": 0.1,
|
||||||
|
|
@ -189,7 +193,8 @@
|
||||||
"algorithm": "Newton",
|
"algorithm": "Newton",
|
||||||
"test": "NormDispIncr",
|
"test": "NormDispIncr",
|
||||||
"tolerance": 1e-06,
|
"tolerance": 1e-06,
|
||||||
"max_iter": 25
|
"max_iter": 25,
|
||||||
|
"preload_case_ids": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ from otko.services import load_project, save_project
|
||||||
def build_sdof() -> Project:
|
def build_sdof() -> Project:
|
||||||
return Project(
|
return Project(
|
||||||
meta=ProjectMeta(name="SDOF Pushover", author="Ozan",
|
meta=ProjectMeta(name="SDOF Pushover", author="Ozan",
|
||||||
units=UnitSystem.SI_M_N),
|
units=UnitSystem.METRIC),
|
||||||
ndm=3, ndf=6,
|
ndm=3, ndf=6,
|
||||||
nodes=[
|
nodes=[
|
||||||
Node(id=1, name="Base", coords=(0.0, 0.0, 0.0),
|
Node(id=1, name="Base", coords=(0.0, 0.0, 0.0),
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"name": "Space Frame 3D",
|
"name": "Space Frame 3D",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "Ozan",
|
"author": "Ozan",
|
||||||
"units": "SI (m, N, kg, s, Pa)"
|
"units": "Metric (m, N, kg, s, Pa)"
|
||||||
},
|
},
|
||||||
"ndm": 3,
|
"ndm": 3,
|
||||||
"ndf": 6,
|
"ndf": 6,
|
||||||
|
|
@ -348,7 +348,8 @@
|
||||||
"Iz": 0.00025,
|
"Iz": 0.00025,
|
||||||
"Iy": 0.00025,
|
"Iy": 0.00025,
|
||||||
"G": 80000000000.0,
|
"G": 80000000000.0,
|
||||||
"J": 0.0004
|
"J": 0.0004,
|
||||||
|
"shape": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
|
|
@ -359,7 +360,8 @@
|
||||||
"Iz": 0.0003,
|
"Iz": 0.0003,
|
||||||
"Iy": 8e-05,
|
"Iy": 8e-05,
|
||||||
"G": 80000000000.0,
|
"G": 80000000000.0,
|
||||||
"J": 1e-06
|
"J": 1e-06,
|
||||||
|
"shape": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -373,7 +375,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 1,
|
"section_id": 1,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
|
|
@ -385,7 +388,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 1,
|
"section_id": 1,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 3,
|
"id": 3,
|
||||||
|
|
@ -397,7 +401,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 1,
|
"section_id": 1,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 4,
|
"id": 4,
|
||||||
|
|
@ -409,7 +414,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 1,
|
"section_id": 1,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 5,
|
"id": 5,
|
||||||
|
|
@ -421,7 +427,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 1,
|
"section_id": 1,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 6,
|
"id": 6,
|
||||||
|
|
@ -433,7 +440,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 1,
|
"section_id": 1,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 7,
|
"id": 7,
|
||||||
|
|
@ -445,7 +453,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 1,
|
"section_id": 1,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 8,
|
"id": 8,
|
||||||
|
|
@ -457,7 +466,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 1,
|
"section_id": 1,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 9,
|
"id": 9,
|
||||||
|
|
@ -469,7 +479,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 2,
|
"section_id": 2,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 10,
|
"id": 10,
|
||||||
|
|
@ -481,7 +492,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 2,
|
"section_id": 2,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 11,
|
"id": 11,
|
||||||
|
|
@ -493,7 +505,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 2,
|
"section_id": 2,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 12,
|
"id": 12,
|
||||||
|
|
@ -505,7 +518,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 2,
|
"section_id": 2,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 13,
|
"id": 13,
|
||||||
|
|
@ -517,7 +531,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 2,
|
"section_id": 2,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 14,
|
"id": 14,
|
||||||
|
|
@ -529,7 +544,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 2,
|
"section_id": 2,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 15,
|
"id": 15,
|
||||||
|
|
@ -541,7 +557,8 @@
|
||||||
],
|
],
|
||||||
"section_id": 2,
|
"section_id": 2,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 16,
|
"id": 16,
|
||||||
|
|
@ -553,9 +570,11 @@
|
||||||
],
|
],
|
||||||
"section_id": 2,
|
"section_id": 2,
|
||||||
"geom_transf": "Linear",
|
"geom_transf": "Linear",
|
||||||
"rho": 0.0
|
"rho": 0.0,
|
||||||
|
"consistent_mass": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"mp_constraints": [],
|
||||||
"time_series": [
|
"time_series": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
|
@ -653,7 +672,7 @@
|
||||||
-0.29326050060188424,
|
-0.29326050060188424,
|
||||||
-0.3555645611718062,
|
-0.3555645611718062,
|
||||||
-0.4115042921209622,
|
-0.4115042921209622,
|
||||||
-0.46028029767148104,
|
-0.4602802976714811,
|
||||||
-0.5012157143407345,
|
-0.5012157143407345,
|
||||||
-0.5337653150252811,
|
-0.5337653150252811,
|
||||||
-0.557522431249662,
|
-0.557522431249662,
|
||||||
|
|
@ -663,7 +682,7 @@
|
||||||
-0.5615384725351046,
|
-0.5615384725351046,
|
||||||
-0.5402796151704672,
|
-0.5402796151704672,
|
||||||
-0.5107978730732473,
|
-0.5107978730732473,
|
||||||
-0.473659120634178,
|
-0.47365912063417787,
|
||||||
-0.42954246541985186,
|
-0.42954246541985186,
|
||||||
-0.37922816465313647,
|
-0.37922816465313647,
|
||||||
-0.3235841034221959,
|
-0.3235841034221959,
|
||||||
|
|
@ -687,7 +706,7 @@
|
||||||
0.4906740795248149,
|
0.4906740795248149,
|
||||||
0.4799109935380196,
|
0.4799109935380196,
|
||||||
0.46174240873333633,
|
0.46174240873333633,
|
||||||
0.43654625061930163,
|
0.4365462506193016,
|
||||||
0.40480613582123315,
|
0.40480613582123315,
|
||||||
0.367102454112839,
|
0.367102454112839,
|
||||||
0.32410204140538085,
|
0.32410204140538085,
|
||||||
|
|
@ -702,7 +721,7 @@
|
||||||
-0.16470790295421026,
|
-0.16470790295421026,
|
||||||
-0.21419825881788915,
|
-0.21419825881788915,
|
||||||
-0.2597053123214179,
|
-0.2597053123214179,
|
||||||
-0.3005638423432181,
|
-0.30056384234321815,
|
||||||
-0.33618996805592183,
|
-0.33618996805592183,
|
||||||
-0.3660893065503416,
|
-0.3660893065503416,
|
||||||
-0.3898636224828937,
|
-0.3898636224828937,
|
||||||
|
|
@ -764,10 +783,10 @@
|
||||||
-0.2882327405527284,
|
-0.2882327405527284,
|
||||||
-0.2725045822392421,
|
-0.2725045822392421,
|
||||||
-0.25269150009501856,
|
-0.25269150009501856,
|
||||||
-0.2291557899194038,
|
-0.22915578991940377,
|
||||||
-0.20231370964878448,
|
-0.20231370964878448,
|
||||||
-0.17262826564213357,
|
-0.17262826564213357,
|
||||||
-0.14060135458277695,
|
-0.14060135458277698,
|
||||||
-0.10676539250226193,
|
-0.10676539250226193,
|
||||||
-0.07167456884782708,
|
-0.07167456884782708,
|
||||||
-0.03589586768362362,
|
-0.03589586768362362,
|
||||||
|
|
@ -814,8 +833,8 @@
|
||||||
-0.210525969348039,
|
-0.210525969348039,
|
||||||
-0.1990380801906294,
|
-0.1990380801906294,
|
||||||
-0.18456655167452068,
|
-0.18456655167452068,
|
||||||
-0.1673760056265102,
|
-0.16737600562651017,
|
||||||
-0.14777047796350654,
|
-0.14777047796350656,
|
||||||
-0.12608814977607422,
|
-0.12608814977607422,
|
||||||
-0.10269560775234517,
|
-0.10269560775234517,
|
||||||
-0.07798172999451634,
|
-0.07798172999451634,
|
||||||
|
|
@ -840,7 +859,7 @@
|
||||||
0.17010510853326,
|
0.17010510853326,
|
||||||
0.15773721930062054,
|
0.15773721930062054,
|
||||||
0.14304555980288974,
|
0.14304555980288974,
|
||||||
0.1262899700796923,
|
0.12628997007969234,
|
||||||
0.10775947186525829,
|
0.10775947186525829,
|
||||||
0.08776736334007407,
|
0.08776736334007407,
|
||||||
0.06664599372956057,
|
0.06664599372956057,
|
||||||
|
|
@ -864,9 +883,9 @@
|
||||||
-0.15376873454743234,
|
-0.15376873454743234,
|
||||||
-0.14537794939239213,
|
-0.14537794939239213,
|
||||||
-0.13480790602063883,
|
-0.13480790602063883,
|
||||||
-0.12225188492657595,
|
-0.12225188492657593,
|
||||||
-0.10793195476209022,
|
-0.10793195476209022,
|
||||||
-0.09209512390578932,
|
-0.09209512390578933,
|
||||||
-0.07500914826118861,
|
-0.07500914826118861,
|
||||||
-0.05695806544062365,
|
-0.05695806544062365,
|
||||||
-0.0382375289144052,
|
-0.0382375289144052,
|
||||||
|
|
@ -875,7 +894,7 @@
|
||||||
0.018910877417136193,
|
0.018910877417136193,
|
||||||
0.037288490913057575,
|
0.037288490913057575,
|
||||||
0.054850768151679684,
|
0.054850768151679684,
|
||||||
0.07133196903235188,
|
0.07133196903235187,
|
||||||
0.08648665679303691,
|
0.08648665679303691,
|
||||||
0.1000933005365811,
|
0.1000933005365811,
|
||||||
0.11195745718335284,
|
0.11195745718335284,
|
||||||
|
|
@ -904,7 +923,7 @@
|
||||||
-0.0739146103417395,
|
-0.0739146103417395,
|
||||||
-0.08554333791262553,
|
-0.08554333791262553,
|
||||||
-0.09568287328255036,
|
-0.09568287328255036,
|
||||||
-0.10419251035749608,
|
-0.10419251035749609,
|
||||||
-0.11095890755818626,
|
-0.11095890755818626,
|
||||||
-0.11589752681422662,
|
-0.11589752681422662,
|
||||||
-0.11895360205510225,
|
-0.11895360205510225,
|
||||||
|
|
@ -918,7 +937,7 @@
|
||||||
-0.07883379020837138,
|
-0.07883379020837138,
|
||||||
-0.06726652633324699,
|
-0.06726652633324699,
|
||||||
-0.054786883743239034,
|
-0.054786883743239034,
|
||||||
-0.0416023242747564,
|
-0.041602324274756394,
|
||||||
-0.027928794018131982,
|
-0.027928794018131982,
|
||||||
-0.013987224628675814,
|
-0.013987224628675814,
|
||||||
-1.9014085379808582e-16,
|
-1.9014085379808582e-16,
|
||||||
|
|
@ -927,7 +946,7 @@
|
||||||
0.040063148664072165,
|
0.040063148664072165,
|
||||||
0.05210106213902843,
|
0.05210106213902843,
|
||||||
0.06317008686143476,
|
0.06317008686143476,
|
||||||
0.07310841606785966,
|
0.07310841606785967,
|
||||||
0.0817740280096844,
|
0.0817740280096844,
|
||||||
0.08904667019366247,
|
0.08904667019366247,
|
||||||
0.09482947682594194,
|
0.09482947682594194,
|
||||||
|
|
@ -945,14 +964,14 @@
|
||||||
0.04682284312839545,
|
0.04682284312839545,
|
||||||
0.035554843973652946,
|
0.035554843973652946,
|
||||||
0.023868952780831416,
|
0.023868952780831416,
|
||||||
0.011953985695908748,
|
0.011953985695908746,
|
||||||
5.108354497396276e-16,
|
5.108354497396276e-16,
|
||||||
-0.011804707389018669,
|
-0.011804707389018669,
|
||||||
-0.023276536275777145,
|
-0.023276536275777145,
|
||||||
-0.034239409087743526,
|
-0.034239409087743526,
|
||||||
-0.04452744329813354,
|
-0.04452744329813354,
|
||||||
-0.05398743030141857,
|
-0.05398743030141857,
|
||||||
-0.0624810842126653,
|
-0.06248108421266531,
|
||||||
-0.06988702813284133,
|
-0.06988702813284133,
|
||||||
-0.076102489952229,
|
-0.076102489952229,
|
||||||
-0.08104468467631713,
|
-0.08104468467631713,
|
||||||
|
|
@ -972,7 +991,8 @@
|
||||||
-0.020399266308586933,
|
-0.020399266308586933,
|
||||||
-0.010216306509086727
|
-0.010216306509086727
|
||||||
],
|
],
|
||||||
"file_path": null
|
"file_path": null,
|
||||||
|
"use_last": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"load_patterns": [
|
"load_patterns": [
|
||||||
|
|
@ -1082,6 +1102,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
1
|
1
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"n_steps": 1,
|
"n_steps": 1,
|
||||||
"load_factor_increment": 1.0,
|
"load_factor_increment": 1.0,
|
||||||
"system": "BandGeneral",
|
"system": "BandGeneral",
|
||||||
|
|
@ -1106,6 +1127,7 @@
|
||||||
"pattern_ids": [
|
"pattern_ids": [
|
||||||
2
|
2
|
||||||
],
|
],
|
||||||
|
"pattern_factors": {},
|
||||||
"dt": 0.01,
|
"dt": 0.01,
|
||||||
"n_steps": 400,
|
"n_steps": 400,
|
||||||
"system": "BandGeneral",
|
"system": "BandGeneral",
|
||||||
|
|
@ -1120,7 +1142,12 @@
|
||||||
"tolerance": 1e-06,
|
"tolerance": 1e-06,
|
||||||
"max_iter": 25,
|
"max_iter": 25,
|
||||||
"rayleigh_alpha_m": 0.524,
|
"rayleigh_alpha_m": 0.524,
|
||||||
"rayleigh_beta_k": 0.00106
|
"rayleigh_beta_k": 0.00106,
|
||||||
|
"rayleigh_beta_k_init": 0.0,
|
||||||
|
"rayleigh_beta_k_comm": 0.0,
|
||||||
|
"rayleigh_mode1_damping": null,
|
||||||
|
"preload_case_ids": [],
|
||||||
|
"remove_patterns": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 4,
|
"id": 4,
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,7 @@ def build_space_frame() -> Project:
|
||||||
add_el(b, d, sec=2, name=f"Beam-{story_base}-Y2")
|
add_el(b, d, sec=2, name=f"Beam-{story_base}-Y2")
|
||||||
|
|
||||||
return Project(
|
return Project(
|
||||||
meta=ProjectMeta(name="Space Frame 3D", author="Ozan", units=UnitSystem.SI_M_N),
|
meta=ProjectMeta(name="Space Frame 3D", author="Ozan", units=UnitSystem.METRIC),
|
||||||
ndm=3, ndf=6,
|
ndm=3, ndf=6,
|
||||||
nodes=nodes,
|
nodes=nodes,
|
||||||
sections=[
|
sections=[
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ from otko.core.geometry import (
|
||||||
DispBeamColumn,
|
DispBeamColumn,
|
||||||
ElasticBeamColumn,
|
ElasticBeamColumn,
|
||||||
Element,
|
Element,
|
||||||
|
ElementBase,
|
||||||
ForceBeamColumn,
|
ForceBeamColumn,
|
||||||
GridLine,
|
GridLine,
|
||||||
GridSystem,
|
GridSystem,
|
||||||
|
|
@ -88,7 +89,24 @@ from otko.core.sections import (
|
||||||
SectionShape,
|
SectionShape,
|
||||||
StraightLayer,
|
StraightLayer,
|
||||||
)
|
)
|
||||||
from otko.core.units import UnitLabels, UnitSystem, labels_for
|
from otko.core.units import (
|
||||||
|
DISPLAY,
|
||||||
|
LEGACY_UNIT_SYSTEMS,
|
||||||
|
NATIVE,
|
||||||
|
QUANTITIES,
|
||||||
|
RAD_TO_DEG,
|
||||||
|
UnitLabels,
|
||||||
|
UnitSystem,
|
||||||
|
display_factor,
|
||||||
|
display_for,
|
||||||
|
display_label,
|
||||||
|
display_with_alias,
|
||||||
|
format_display,
|
||||||
|
format_rotation,
|
||||||
|
labels_for,
|
||||||
|
native_label,
|
||||||
|
to_display,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
# Project + meta
|
# Project + meta
|
||||||
|
|
@ -97,6 +115,19 @@ __all__ = [
|
||||||
"UnitSystem",
|
"UnitSystem",
|
||||||
"UnitLabels",
|
"UnitLabels",
|
||||||
"labels_for",
|
"labels_for",
|
||||||
|
"DISPLAY",
|
||||||
|
"LEGACY_UNIT_SYSTEMS",
|
||||||
|
"NATIVE",
|
||||||
|
"QUANTITIES",
|
||||||
|
"RAD_TO_DEG",
|
||||||
|
"display_for",
|
||||||
|
"display_label",
|
||||||
|
"display_factor",
|
||||||
|
"display_with_alias",
|
||||||
|
"format_display",
|
||||||
|
"format_rotation",
|
||||||
|
"native_label",
|
||||||
|
"to_display",
|
||||||
# Geometry
|
# Geometry
|
||||||
"Node",
|
"Node",
|
||||||
"EqualDOFConstraint",
|
"EqualDOFConstraint",
|
||||||
|
|
@ -114,6 +145,7 @@ __all__ = [
|
||||||
"ensure_default_truss_material",
|
"ensure_default_truss_material",
|
||||||
"ensure_default_timeseries_and_pattern",
|
"ensure_default_timeseries_and_pattern",
|
||||||
"Element",
|
"Element",
|
||||||
|
"ElementBase",
|
||||||
"TrussElement",
|
"TrussElement",
|
||||||
"CorotTrussElement",
|
"CorotTrussElement",
|
||||||
"ElasticBeamColumn",
|
"ElasticBeamColumn",
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,31 @@ class Entity(BaseModel):
|
||||||
"""Base class for every persisted domain object."""
|
"""Base class for every persisted domain object."""
|
||||||
|
|
||||||
model_config = ConfigDict(
|
model_config = ConfigDict(
|
||||||
frozen=False, # individual setters allowed; we lock at the Project boundary
|
frozen=False, # individual setters allowed; we lock at the Project boundary
|
||||||
extra="forbid", # unknown JSON keys are an error, not a silent ignore
|
extra="forbid", # unknown JSON keys are an error, not a silent ignore
|
||||||
validate_assignment=True,
|
validate_assignment=True,
|
||||||
populate_by_name=True,
|
populate_by_name=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
id: PositiveInt = Field(..., description="Unique tag within its kind. Used as the OpenSees tag.")
|
id: PositiveInt = Field(
|
||||||
|
..., description="Unique tag within its kind. Used as the OpenSees tag."
|
||||||
|
)
|
||||||
name: str = Field(default="", description="Optional human-readable label.")
|
name: str = Field(default="", description="Optional human-readable label.")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default_name(self) -> str:
|
||||||
|
"""Fallback label used when ``name`` is blank.
|
||||||
|
|
||||||
|
Display-only — reading it never writes back into ``name`` and it
|
||||||
|
is never persisted. Subclasses override this with their prefix
|
||||||
|
(e.g. ``N<id>`` for nodes, ``E<id>`` for elements).
|
||||||
|
"""
|
||||||
|
return ""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def display_name(self) -> str:
|
||||||
|
"""Explicit ``name`` stripped, or ``default_name`` when blank.
|
||||||
|
|
||||||
|
Display-only fallback — reading this never mutates ``name``.
|
||||||
|
"""
|
||||||
|
return self.name.strip() or self.default_name
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ from otko.core.geometry.elements import (
|
||||||
DispBeamColumn,
|
DispBeamColumn,
|
||||||
ElasticBeamColumn,
|
ElasticBeamColumn,
|
||||||
Element,
|
Element,
|
||||||
|
ElementBase,
|
||||||
ForceBeamColumn,
|
ForceBeamColumn,
|
||||||
QuadElement,
|
QuadElement,
|
||||||
TrussElement,
|
TrussElement,
|
||||||
|
|
@ -25,6 +26,7 @@ from otko.core.geometry.node import Node
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Node",
|
"Node",
|
||||||
"Element",
|
"Element",
|
||||||
|
"ElementBase",
|
||||||
"TrussElement",
|
"TrussElement",
|
||||||
"CorotTrussElement",
|
"CorotTrussElement",
|
||||||
"ElasticBeamColumn",
|
"ElasticBeamColumn",
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,21 @@ from pydantic import Field, PositiveFloat, PositiveInt
|
||||||
from otko.core._base import Entity
|
from otko.core._base import Entity
|
||||||
|
|
||||||
|
|
||||||
class TrussElement(Entity):
|
class ElementBase(Entity):
|
||||||
|
"""Shared base for all structural elements.
|
||||||
|
|
||||||
|
Carries the display-only ``E<id>`` fallback label so every element
|
||||||
|
type (including Quad) shows the same default name on the canvas
|
||||||
|
and in the table without persisting it into ``name``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default_name(self) -> str:
|
||||||
|
"""Fallback label for unnamed elements (``E<id>``)."""
|
||||||
|
return f"E{self.id}"
|
||||||
|
|
||||||
|
|
||||||
|
class TrussElement(ElementBase):
|
||||||
"""Two-node truss with axial stiffness only — ``element truss``."""
|
"""Two-node truss with axial stiffness only — ``element truss``."""
|
||||||
|
|
||||||
type: Literal["Truss"] = "Truss"
|
type: Literal["Truss"] = "Truss"
|
||||||
|
|
@ -27,7 +41,7 @@ class TrussElement(Entity):
|
||||||
rho: float = Field(default=0.0, ge=0.0, description="Mass per unit length.")
|
rho: float = Field(default=0.0, ge=0.0, description="Mass per unit length.")
|
||||||
|
|
||||||
|
|
||||||
class CorotTrussElement(Entity):
|
class CorotTrussElement(ElementBase):
|
||||||
"""Co-rotational truss for large displacements — ``element corotTruss``."""
|
"""Co-rotational truss for large displacements — ``element corotTruss``."""
|
||||||
|
|
||||||
type: Literal["CorotTruss"] = "CorotTruss"
|
type: Literal["CorotTruss"] = "CorotTruss"
|
||||||
|
|
@ -37,7 +51,7 @@ class CorotTrussElement(Entity):
|
||||||
rho: float = Field(default=0.0, ge=0.0)
|
rho: float = Field(default=0.0, ge=0.0)
|
||||||
|
|
||||||
|
|
||||||
class ElasticBeamColumn(Entity):
|
class ElasticBeamColumn(ElementBase):
|
||||||
"""Linear-elastic frame element — ``element elasticBeamColumn``.
|
"""Linear-elastic frame element — ``element elasticBeamColumn``.
|
||||||
|
|
||||||
Pure-section style: provide section_id; OpenSees pulls EA, EI from
|
Pure-section style: provide section_id; OpenSees pulls EA, EI from
|
||||||
|
|
@ -60,7 +74,7 @@ class ElasticBeamColumn(Entity):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ForceBeamColumn(Entity):
|
class ForceBeamColumn(ElementBase):
|
||||||
"""Force-based fibre frame element — ``element forceBeamColumn``."""
|
"""Force-based fibre frame element — ``element forceBeamColumn``."""
|
||||||
|
|
||||||
type: Literal["ForceBeamColumn"] = "ForceBeamColumn"
|
type: Literal["ForceBeamColumn"] = "ForceBeamColumn"
|
||||||
|
|
@ -72,7 +86,7 @@ class ForceBeamColumn(Entity):
|
||||||
tolerance: float = Field(default=1e-12, gt=0.0)
|
tolerance: float = Field(default=1e-12, gt=0.0)
|
||||||
|
|
||||||
|
|
||||||
class DispBeamColumn(Entity):
|
class DispBeamColumn(ElementBase):
|
||||||
"""Displacement-based fibre frame element — ``element dispBeamColumn``."""
|
"""Displacement-based fibre frame element — ``element dispBeamColumn``."""
|
||||||
|
|
||||||
type: Literal["DispBeamColumn"] = "DispBeamColumn"
|
type: Literal["DispBeamColumn"] = "DispBeamColumn"
|
||||||
|
|
@ -82,7 +96,7 @@ class DispBeamColumn(Entity):
|
||||||
geom_transf: Literal["Linear", "PDelta", "Corotational"] = "Linear"
|
geom_transf: Literal["Linear", "PDelta", "Corotational"] = "Linear"
|
||||||
|
|
||||||
|
|
||||||
class ZeroLengthElement(Entity):
|
class ZeroLengthElement(ElementBase):
|
||||||
"""Two coincident nodes connected by uniaxial materials per DOF.
|
"""Two coincident nodes connected by uniaxial materials per DOF.
|
||||||
|
|
||||||
Foundation building block for plastic hinges and isolators.
|
Foundation building block for plastic hinges and isolators.
|
||||||
|
|
@ -91,7 +105,9 @@ class ZeroLengthElement(Entity):
|
||||||
type: Literal["ZeroLength"] = "ZeroLength"
|
type: Literal["ZeroLength"] = "ZeroLength"
|
||||||
nodes: tuple[PositiveInt, PositiveInt]
|
nodes: tuple[PositiveInt, PositiveInt]
|
||||||
material_ids: tuple[PositiveInt, ...] = Field(..., min_length=1)
|
material_ids: tuple[PositiveInt, ...] = Field(..., min_length=1)
|
||||||
dofs: tuple[int, ...] = Field(..., min_length=1, description="DOF directions, 1-indexed (1..6).")
|
dofs: tuple[int, ...] = Field(
|
||||||
|
..., min_length=1, description="DOF directions, 1-indexed (1..6)."
|
||||||
|
)
|
||||||
do_rayleigh: bool = Field(
|
do_rayleigh: bool = Field(
|
||||||
default=False,
|
default=False,
|
||||||
description=(
|
description=(
|
||||||
|
|
@ -104,7 +120,7 @@ class ZeroLengthElement(Entity):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ZeroLengthSectionElement(Entity):
|
class ZeroLengthSectionElement(ElementBase):
|
||||||
"""Two coincident nodes connected by a full :class:`Section` —
|
"""Two coincident nodes connected by a full :class:`Section` —
|
||||||
OpenSees ``element zeroLengthSection``.
|
OpenSees ``element zeroLengthSection``.
|
||||||
|
|
||||||
|
|
@ -121,10 +137,12 @@ class ZeroLengthSectionElement(Entity):
|
||||||
|
|
||||||
type: Literal["ZeroLengthSection"] = "ZeroLengthSection"
|
type: Literal["ZeroLengthSection"] = "ZeroLengthSection"
|
||||||
nodes: tuple[PositiveInt, PositiveInt]
|
nodes: tuple[PositiveInt, PositiveInt]
|
||||||
section_id: PositiveInt = Field(..., description="Section attached to the two coincident nodes.")
|
section_id: PositiveInt = Field(
|
||||||
|
..., description="Section attached to the two coincident nodes."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BeamWithHingesElement(Entity):
|
class BeamWithHingesElement(ElementBase):
|
||||||
"""Force-based beam with lumped plasticity at both ends —
|
"""Force-based beam with lumped plasticity at both ends —
|
||||||
``element beamWithHinges``.
|
``element beamWithHinges``.
|
||||||
|
|
||||||
|
|
@ -156,7 +174,7 @@ class BeamWithHingesElement(Entity):
|
||||||
geom_transf: Literal["Linear", "PDelta", "Corotational"] = "Linear"
|
geom_transf: Literal["Linear", "PDelta", "Corotational"] = "Linear"
|
||||||
|
|
||||||
|
|
||||||
class QuadElement(Entity):
|
class QuadElement(ElementBase):
|
||||||
"""Four-node 2D quadrilateral (plane-stress / plane-strain) —
|
"""Four-node 2D quadrilateral (plane-stress / plane-strain) —
|
||||||
``element quad`` / ``bbarQuad`` / ``enhancedQuad``.
|
``element quad`` / ``bbarQuad`` / ``enhancedQuad``.
|
||||||
|
|
||||||
|
|
@ -179,7 +197,8 @@ class QuadElement(Entity):
|
||||||
description="Surface pressure applied over the element (force / area).",
|
description="Surface pressure applied over the element (force / area).",
|
||||||
)
|
)
|
||||||
rho: float = Field(
|
rho: float = Field(
|
||||||
default=0.0, ge=0.0,
|
default=0.0,
|
||||||
|
ge=0.0,
|
||||||
description="Mass density override (kip·s²/in⁴). Leave 0 to use material rho.",
|
description="Mass density override (kip·s²/in⁴). Leave 0 to use material rho.",
|
||||||
)
|
)
|
||||||
b1: float = Field(
|
b1: float = Field(
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,11 @@ class Node(Entity):
|
||||||
mass: Mass6 = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
|
mass: Mass6 = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
|
||||||
restraint: Restraint6 = (False, False, False, False, False, False)
|
restraint: Restraint6 = (False, False, False, False, False, False)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default_name(self) -> str:
|
||||||
|
"""Fallback label for unnamed nodes (``N<id>``)."""
|
||||||
|
return f"N{self.id}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_restrained(self) -> bool:
|
def is_restrained(self) -> bool:
|
||||||
"""True if any DOF is fixed."""
|
"""True if any DOF is fixed."""
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,15 @@ from __future__ import annotations
|
||||||
from collections.abc import Iterable
|
from collections.abc import Iterable
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field, PositiveInt, PrivateAttr, model_validator
|
from pydantic import (
|
||||||
|
BaseModel,
|
||||||
|
ConfigDict,
|
||||||
|
Field,
|
||||||
|
PositiveInt,
|
||||||
|
PrivateAttr,
|
||||||
|
field_validator,
|
||||||
|
model_validator,
|
||||||
|
)
|
||||||
|
|
||||||
from otko.core.analysis import AnalysisCase
|
from otko.core.analysis import AnalysisCase
|
||||||
from otko.core.constraints import EqualDOFConstraint
|
from otko.core.constraints import EqualDOFConstraint
|
||||||
|
|
@ -121,7 +129,22 @@ class ProjectMeta(BaseModel):
|
||||||
name: str = "Untitled"
|
name: str = "Untitled"
|
||||||
description: str = ""
|
description: str = ""
|
||||||
author: str = ""
|
author: str = ""
|
||||||
units: UnitSystem = UnitSystem.SI_M_N
|
units: UnitSystem = UnitSystem.METRIC
|
||||||
|
|
||||||
|
@field_validator("units", mode="before")
|
||||||
|
@classmethod
|
||||||
|
def _migrate_legacy_units(cls, value: object) -> object:
|
||||||
|
"""Map retired 4-system value strings onto Metric/Imperial.
|
||||||
|
|
||||||
|
Files saved under the dominant systems map exactly; files saved
|
||||||
|
under the retired detail systems (mm / ft) still load, but their
|
||||||
|
stored numbers keep the old scale — see ``LEGACY_UNIT_SYSTEMS``.
|
||||||
|
"""
|
||||||
|
from otko.core.units import LEGACY_UNIT_SYSTEMS
|
||||||
|
|
||||||
|
if isinstance(value, str) and value in LEGACY_UNIT_SYSTEMS:
|
||||||
|
return LEGACY_UNIT_SYSTEMS[value]
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
class Project(BaseModel):
|
class Project(BaseModel):
|
||||||
|
|
@ -155,11 +178,13 @@ class Project(BaseModel):
|
||||||
"""
|
"""
|
||||||
if isinstance(data, dict) and "grid_system" in data and "coord_systems" not in data:
|
if isinstance(data, dict) and "grid_system" in data and "coord_systems" not in data:
|
||||||
legacy = data.pop("grid_system")
|
legacy = data.pop("grid_system")
|
||||||
data["coord_systems"] = [{
|
data["coord_systems"] = [
|
||||||
"name": "Global",
|
{
|
||||||
"coord": {},
|
"name": "Global",
|
||||||
"grid": legacy,
|
"coord": {},
|
||||||
}]
|
"grid": legacy,
|
||||||
|
}
|
||||||
|
]
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
|
|
@ -203,10 +228,12 @@ class Project(BaseModel):
|
||||||
return
|
return
|
||||||
# No Global system yet — create one with this grid.
|
# No Global system yet — create one with this grid.
|
||||||
from otko.core.geometry import CoordinateGridSystem
|
from otko.core.geometry import CoordinateGridSystem
|
||||||
|
|
||||||
self.coord_systems.insert(
|
self.coord_systems.insert(
|
||||||
0,
|
0,
|
||||||
CoordinateGridSystem(name="Global", grid=new_grid),
|
CoordinateGridSystem(name="Global", grid=new_grid),
|
||||||
)
|
)
|
||||||
|
|
||||||
sections: list[Section] = Field(default_factory=list)
|
sections: list[Section] = Field(default_factory=list)
|
||||||
elements: list[Element] = Field(default_factory=list)
|
elements: list[Element] = Field(default_factory=list)
|
||||||
mp_constraints: list[EqualDOFConstraint] = Field(default_factory=list)
|
mp_constraints: list[EqualDOFConstraint] = Field(default_factory=list)
|
||||||
|
|
|
||||||
|
|
@ -10,30 +10,52 @@ The :func:`labels_for` helper returns an :class:`UnitLabels` bundle
|
||||||
matching SAP2000's "Set Program Default Display Units" semantics:
|
matching SAP2000's "Set Program Default Display Units" semantics:
|
||||||
length / force / moment / stress / curvature / rotation labels, all
|
length / force / moment / stress / curvature / rotation labels, all
|
||||||
driven by :class:`UnitSystem` enum.
|
driven by :class:`UnitSystem` enum.
|
||||||
|
|
||||||
|
The ``DISPLAY`` table implements ``docs/consistent_units.md`` §4: pure
|
||||||
|
data mapping each quantity to a ``(display label, multiply-by-factor)``
|
||||||
|
pair. Factors are applied in views/viewmodels on read only — converted
|
||||||
|
values never flow into commands, services, or persistence. Stored
|
||||||
|
values are always in the project's native system.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import math
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
class UnitSystem(str, Enum):
|
class UnitSystem(str, Enum):
|
||||||
"""Consistent unit systems supported by the application."""
|
"""Consistent unit systems supported by the application.
|
||||||
|
|
||||||
SI_M_N = "SI (m, N, kg, s, Pa)"
|
Two systems only: Metric (metre/newton) and Imperial (inch/kip).
|
||||||
|
OpenSees never converts, and neither do we — stored values are
|
||||||
|
always native to the project's system; views only relabel/rescale
|
||||||
|
for humans on read.
|
||||||
|
"""
|
||||||
|
|
||||||
|
METRIC = "Metric (m, N, kg, s, Pa)"
|
||||||
"""Length: m, Force: N, Mass: kg, Time: s, Stress: Pa."""
|
"""Length: m, Force: N, Mass: kg, Time: s, Stress: Pa."""
|
||||||
|
|
||||||
SI_MM_N = "SI (mm, N, t, s, MPa)"
|
IMPERIAL = "Imperial (in, kip, kip·s²/in, s, ksi)"
|
||||||
"""Length: mm, Force: N, Mass: t, Time: s, Stress: MPa."""
|
|
||||||
|
|
||||||
US_FT_KIP = "US (ft, kip, slug, s, ksf)"
|
|
||||||
"""Length: ft, Force: kip, Mass: slug, Time: s, Stress: ksf."""
|
|
||||||
|
|
||||||
US_IN_KIP = "US (in, kip, kip·s²/in, s, ksi)"
|
|
||||||
"""Length: in, Force: kip, Mass: kip·s²/in, Time: s, Stress: ksi."""
|
"""Length: in, Force: kip, Mass: kip·s²/in, Time: s, Stress: ksi."""
|
||||||
|
|
||||||
|
|
||||||
|
#: Legacy value strings from the retired 4-system era, mapped to their
|
||||||
|
#: surviving system on load. Files saved under the two dominant systems
|
||||||
|
#: (``SI_M_N``/``US_IN_KIP``) map exactly. Files saved under the detail
|
||||||
|
#: systems (``SI_MM_N`` in mm, ``US_FT_KIP`` in ft) still load, but their
|
||||||
|
#: stored numbers keep their old scale — a mm model opened as Metric
|
||||||
|
#: will read 1000× too large until the engineer re-enters values in the
|
||||||
|
#: native m/in system. The scale caveat is logged by the loader path.
|
||||||
|
LEGACY_UNIT_SYSTEMS: dict[str, UnitSystem] = {
|
||||||
|
"SI (m, N, kg, s, Pa)": UnitSystem.METRIC,
|
||||||
|
"SI (mm, N, t, s, MPa)": UnitSystem.METRIC,
|
||||||
|
"US (ft, kip, slug, s, ksf)": UnitSystem.IMPERIAL,
|
||||||
|
"US (in, kip, kip·s²/in, s, ksi)": UnitSystem.IMPERIAL,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class UnitLabels:
|
class UnitLabels:
|
||||||
"""Display strings for each basic quantity in a unit system.
|
"""Display strings for each basic quantity in a unit system.
|
||||||
|
|
@ -42,30 +64,30 @@ class UnitLabels:
|
||||||
label axes without hard-coding any particular set of units.
|
label axes without hard-coding any particular set of units.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
length: str # "m", "mm", "in", "ft"
|
length: str # "m", "mm", "in", "ft"
|
||||||
force: str # "N", "kip"
|
force: str # "N", "kip"
|
||||||
moment: str # "N·m", "kip·in"
|
moment: str # "N·m", "kip·in"
|
||||||
stress: str # "Pa", "MPa", "ksi", "ksf"
|
stress: str # "Pa", "MPa", "ksi", "ksf"
|
||||||
curvature: str # "1/m", "1/in", …
|
curvature: str # "1/m", "1/in", …
|
||||||
rotation: str # "rad" (always, no unit variants in practice)
|
rotation: str # "rad" (always, no unit variants in practice)
|
||||||
|
|
||||||
|
|
||||||
_LABELS: dict[UnitSystem, UnitLabels] = {
|
_LABELS: dict[UnitSystem, UnitLabels] = {
|
||||||
UnitSystem.SI_M_N: UnitLabels(
|
UnitSystem.METRIC: UnitLabels(
|
||||||
length="m", force="N", moment="N·m",
|
length="m",
|
||||||
stress="Pa", curvature="1/m", rotation="rad",
|
force="N",
|
||||||
|
moment="N·m",
|
||||||
|
stress="Pa",
|
||||||
|
curvature="1/m",
|
||||||
|
rotation="rad",
|
||||||
),
|
),
|
||||||
UnitSystem.SI_MM_N: UnitLabels(
|
UnitSystem.IMPERIAL: UnitLabels(
|
||||||
length="mm", force="N", moment="N·mm",
|
length="in",
|
||||||
stress="MPa", curvature="1/mm", rotation="rad",
|
force="kip",
|
||||||
),
|
moment="kip·in",
|
||||||
UnitSystem.US_FT_KIP: UnitLabels(
|
stress="ksi",
|
||||||
length="ft", force="kip", moment="kip·ft",
|
curvature="1/in",
|
||||||
stress="ksf", curvature="1/ft", rotation="rad",
|
rotation="rad",
|
||||||
),
|
|
||||||
UnitSystem.US_IN_KIP: UnitLabels(
|
|
||||||
length="in", force="kip", moment="kip·in",
|
|
||||||
stress="ksi", curvature="1/in", rotation="rad",
|
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -73,3 +95,255 @@ _LABELS: dict[UnitSystem, UnitLabels] = {
|
||||||
def labels_for(units: UnitSystem) -> UnitLabels:
|
def labels_for(units: UnitSystem) -> UnitLabels:
|
||||||
"""Return the label bundle for the given unit system."""
|
"""Return the label bundle for the given unit system."""
|
||||||
return _LABELS[units]
|
return _LABELS[units]
|
||||||
|
|
||||||
|
|
||||||
|
# ── Consistent-units plan (§5 quantity taxonomy + §4 display factors) ──
|
||||||
|
|
||||||
|
#: Canonical quantity keys. Same strings as the future ``UnitTag`` so a
|
||||||
|
#: follow-up ADR can adopt them unchanged. ``section_dim`` is an extra
|
||||||
|
#: display-only key (geometry vs section-size prefix split in §3).
|
||||||
|
QUANTITIES: tuple[str, ...] = (
|
||||||
|
"length",
|
||||||
|
"section_dim",
|
||||||
|
"displacement",
|
||||||
|
"rotation",
|
||||||
|
"force",
|
||||||
|
"moment",
|
||||||
|
"distributed_load",
|
||||||
|
"stress",
|
||||||
|
"area",
|
||||||
|
"inertia",
|
||||||
|
"mass",
|
||||||
|
"time",
|
||||||
|
"velocity",
|
||||||
|
"acceleration",
|
||||||
|
"frequency",
|
||||||
|
"curvature",
|
||||||
|
"temperature",
|
||||||
|
)
|
||||||
|
|
||||||
|
#: Dimensionless quantities never take factors and are absent from
|
||||||
|
#: ``DISPLAY`` by design (strain, drift ratio, damping ratio, mass
|
||||||
|
#: participation, etc.).
|
||||||
|
DIMENSIONLESS: tuple[str, ...] = (
|
||||||
|
"strain",
|
||||||
|
"drift",
|
||||||
|
"damping_ratio",
|
||||||
|
"mass_participation",
|
||||||
|
)
|
||||||
|
|
||||||
|
#: Gravity constants for mass-from-weight derivations. Document the
|
||||||
|
#: constant next to every such computation; never hardcode it in a
|
||||||
|
#: system-agnostic path.
|
||||||
|
GRAVITY: dict[UnitSystem, float] = {
|
||||||
|
UnitSystem.METRIC: 9.81, # m/s²
|
||||||
|
UnitSystem.IMPERIAL: 386.4, # in/s²
|
||||||
|
}
|
||||||
|
|
||||||
|
#: Degrees per radian — formatting option for post views only (§6).
|
||||||
|
#: Solver and storage stay in radians.
|
||||||
|
RAD_TO_DEG: float = 180.0 / math.pi
|
||||||
|
|
||||||
|
#: Stored (native) unit labels per quantity — what the solver and the
|
||||||
|
#: ``.osmodel`` see. Views show native values everywhere (we never
|
||||||
|
#: auto-convert), so input hints, table headers, and axis labels must
|
||||||
|
#: use these, NOT the ``DISPLAY`` (human-prefix) labels. E.g. Metric
|
||||||
|
#: forces are stored in N ("Fx (N)"), even though post views may one
|
||||||
|
#: day render them as kN.
|
||||||
|
NATIVE: dict[UnitSystem, dict[str, str]] = {
|
||||||
|
UnitSystem.METRIC: {
|
||||||
|
"length": "m",
|
||||||
|
"section_dim": "m",
|
||||||
|
"displacement": "m",
|
||||||
|
"rotation": "rad",
|
||||||
|
"force": "N",
|
||||||
|
"moment": "N·m",
|
||||||
|
"distributed_load": "N/m",
|
||||||
|
"stress": "Pa",
|
||||||
|
"area": "m²",
|
||||||
|
"inertia": "m⁴",
|
||||||
|
"mass": "kg",
|
||||||
|
"time": "s",
|
||||||
|
"velocity": "m/s",
|
||||||
|
"acceleration": "m/s²",
|
||||||
|
"frequency": "Hz",
|
||||||
|
"curvature": "1/m",
|
||||||
|
"temperature": "°C",
|
||||||
|
},
|
||||||
|
UnitSystem.IMPERIAL: {
|
||||||
|
"length": "in",
|
||||||
|
"section_dim": "in",
|
||||||
|
"displacement": "in",
|
||||||
|
"rotation": "rad",
|
||||||
|
"force": "kip",
|
||||||
|
"moment": "kip·in",
|
||||||
|
"distributed_load": "kip/in",
|
||||||
|
"stress": "ksi",
|
||||||
|
"area": "in²",
|
||||||
|
"inertia": "in⁴",
|
||||||
|
"mass": "kip·s²/in",
|
||||||
|
"time": "s",
|
||||||
|
"velocity": "in/s",
|
||||||
|
"acceleration": "in/s²",
|
||||||
|
"frequency": "Hz",
|
||||||
|
"curvature": "1/in",
|
||||||
|
"temperature": "°F",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def native_label(units: UnitSystem, quantity: str) -> str:
|
||||||
|
"""Return the stored-unit label for a quantity (e.g. ``"N"``).
|
||||||
|
|
||||||
|
Use this for every label placed next to a native (unconverted)
|
||||||
|
value: input hints, table headers, axis titles.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return NATIVE[units][quantity]
|
||||||
|
except KeyError:
|
||||||
|
raise KeyError(f"No native unit for quantity '{quantity}' in {units.value}.") from None
|
||||||
|
|
||||||
|
|
||||||
|
#: View-layer display table: quantity → (display label, multiply factor).
|
||||||
|
#: Stored = native solver unit; display = what humans read. Factors are
|
||||||
|
#: applied on read; converted values are never written back.
|
||||||
|
DISPLAY: dict[UnitSystem, dict[str, tuple[str, float]]] = {
|
||||||
|
UnitSystem.METRIC: {
|
||||||
|
"length": ("m", 1.0),
|
||||||
|
"section_dim": ("mm", 1000.0),
|
||||||
|
"displacement": ("mm", 1000.0),
|
||||||
|
"rotation": ("rad", 1.0),
|
||||||
|
"force": ("kN", 1e-3),
|
||||||
|
"moment": ("kN·m", 1e-3),
|
||||||
|
"distributed_load": ("kN/m", 1e-3),
|
||||||
|
"stress": ("MPa", 1e-6),
|
||||||
|
"area": ("mm²", 1e6),
|
||||||
|
"inertia": ("mm⁴", 1e12),
|
||||||
|
"mass": ("kg", 1.0),
|
||||||
|
"time": ("s", 1.0),
|
||||||
|
"velocity": ("m/s", 1.0),
|
||||||
|
"acceleration": ("m/s²", 1.0),
|
||||||
|
"frequency": ("Hz", 1.0),
|
||||||
|
"curvature": ("1/m", 1.0),
|
||||||
|
"temperature": ("°C", 1.0),
|
||||||
|
},
|
||||||
|
UnitSystem.IMPERIAL: {
|
||||||
|
"length": ("in", 1.0),
|
||||||
|
"section_dim": ("in", 1.0),
|
||||||
|
"displacement": ("in", 1.0),
|
||||||
|
"rotation": ("rad", 1.0),
|
||||||
|
"force": ("kip", 1.0),
|
||||||
|
"moment": ("kip·in", 1.0),
|
||||||
|
"distributed_load": ("kip/in", 1.0),
|
||||||
|
"stress": ("ksi", 1.0),
|
||||||
|
"area": ("in²", 1.0),
|
||||||
|
"inertia": ("in⁴", 1.0),
|
||||||
|
"mass": ("kip·s²/in", 1.0),
|
||||||
|
"time": ("s", 1.0),
|
||||||
|
"velocity": ("in/s", 1.0),
|
||||||
|
"acceleration": ("in/s²", 1.0),
|
||||||
|
"frequency": ("Hz", 1.0),
|
||||||
|
"curvature": ("1/in", 1.0),
|
||||||
|
"temperature": ("°F", 1.0),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#: Small-value display aliases (×1000 from kip units, view formatting
|
||||||
|
#: only — never separate systems). Applied when |display value| is
|
||||||
|
#: small enough that kip units would read as ~0.
|
||||||
|
_SMALL_ALIASES: dict[tuple[UnitSystem, str], tuple[str, float]] = {
|
||||||
|
(UnitSystem.IMPERIAL, "force"): ("lbf", 1000.0),
|
||||||
|
(UnitSystem.IMPERIAL, "distributed_load"): ("lbf/in", 1000.0),
|
||||||
|
(UnitSystem.IMPERIAL, "moment"): ("lb·in", 1000.0),
|
||||||
|
}
|
||||||
|
|
||||||
|
#: Below this display magnitude (in base display units) the small-value
|
||||||
|
#: alias kicks in. Zero never aliases.
|
||||||
|
SMALL_ALIAS_THRESHOLD: float = 0.1
|
||||||
|
|
||||||
|
|
||||||
|
def display_for(units: UnitSystem, quantity: str) -> tuple[str, float]:
|
||||||
|
"""Return ``(display label, multiply-by-factor)`` for a quantity.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
units: Project's native stored system.
|
||||||
|
quantity: One of :data:`QUANTITIES`.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
KeyError: For unknown or dimensionless quantities (dimensionless
|
||||||
|
values never take factors by design).
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return DISPLAY[units][quantity]
|
||||||
|
except KeyError:
|
||||||
|
raise KeyError(
|
||||||
|
f"No display entry for quantity '{quantity}' in {units.value}. "
|
||||||
|
"Dimensionless quantities (strain, drift, damping, …) "
|
||||||
|
"never take factors."
|
||||||
|
) from None
|
||||||
|
|
||||||
|
|
||||||
|
def display_label(units: UnitSystem, quantity: str) -> str:
|
||||||
|
"""Return the display label for a quantity (e.g. ``"mm"``)."""
|
||||||
|
label, _factor = display_for(units, quantity)
|
||||||
|
return label
|
||||||
|
|
||||||
|
|
||||||
|
def display_factor(units: UnitSystem, quantity: str) -> float:
|
||||||
|
"""Return the multiply-by-factor for a quantity (e.g. ``1000.0``)."""
|
||||||
|
_label, factor = display_for(units, quantity)
|
||||||
|
return factor
|
||||||
|
|
||||||
|
|
||||||
|
def to_display(value: float, units: UnitSystem, quantity: str) -> float:
|
||||||
|
"""Convert a native stored value to its display value (× factor).
|
||||||
|
|
||||||
|
View-layer only — the result must never be written back to the
|
||||||
|
model, commands, services, or persistence.
|
||||||
|
"""
|
||||||
|
return float(value) * display_factor(units, quantity)
|
||||||
|
|
||||||
|
|
||||||
|
def format_display(
|
||||||
|
value: float,
|
||||||
|
units: UnitSystem,
|
||||||
|
quantity: str,
|
||||||
|
*,
|
||||||
|
precision: str = ".4g",
|
||||||
|
) -> str:
|
||||||
|
"""Format ``"12.3 mm"`` for a native value in display units."""
|
||||||
|
label, factor = display_for(units, quantity)
|
||||||
|
return f"{float(value) * factor:{precision}} {label}"
|
||||||
|
|
||||||
|
|
||||||
|
def display_with_alias(
|
||||||
|
value: float,
|
||||||
|
units: UnitSystem,
|
||||||
|
quantity: str,
|
||||||
|
) -> tuple[float, str]:
|
||||||
|
"""Return ``(display value, label)`` applying the lbf/plf alias rule.
|
||||||
|
|
||||||
|
When the base display magnitude is non-zero but below
|
||||||
|
:data:`SMALL_ALIAS_THRESHOLD`, imperial force-like quantities are
|
||||||
|
scaled ×1000 into ``lbf``/``plf``/``lb·ft`` so ``0.004 kip``
|
||||||
|
renders as ``4.0 lbf``. All other cases return the base display
|
||||||
|
pair unchanged. Pure formatting helper — no model impact.
|
||||||
|
"""
|
||||||
|
label, factor = display_for(units, quantity)
|
||||||
|
disp = float(value) * factor
|
||||||
|
alias = _SMALL_ALIASES.get((units, quantity))
|
||||||
|
if alias is not None and disp != 0.0 and abs(disp) < SMALL_ALIAS_THRESHOLD:
|
||||||
|
alias_label, alias_scale = alias
|
||||||
|
return disp * alias_scale, alias_label
|
||||||
|
return disp, label
|
||||||
|
|
||||||
|
|
||||||
|
def format_rotation(value_rad: float, *, use_deg: bool = False) -> str:
|
||||||
|
"""Format a rotation stored in radians.
|
||||||
|
|
||||||
|
Solver and storage stay in radians; ``use_deg=True`` is a post-view
|
||||||
|
formatting option only (×180/π on read).
|
||||||
|
"""
|
||||||
|
if use_deg:
|
||||||
|
return f"{float(value_rad) * RAD_TO_DEG:.4g} °"
|
||||||
|
return f"{float(value_rad):.4g} rad"
|
||||||
|
|
|
||||||
11
src/otko/resources/icons-dark/assign-distributed.svg
Normal file
11
src/otko/resources/icons-dark/assign-distributed.svg
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<title>Assign distributed load</title>
|
||||||
|
<path d="M5 4.5h14" stroke="#F2A93B" stroke-width="1.6"/>
|
||||||
|
<path d="M7 4.5V14M12 4.5V14M17 4.5V14" stroke="#F2A93B" stroke-width="2"/>
|
||||||
|
<path d="M5 11.8l2 2.2 2-2.2M10 11.8l2 2.2 2-2.2M15 11.8l2 2.2 2-2.2" stroke="#F2A93B" stroke-width="2"/>
|
||||||
|
<path d="M4 18h16" stroke="#4F92E8" stroke-width="2"/>
|
||||||
|
<circle cx="7" cy="18" r="1.3" fill="#F2A93B"/>
|
||||||
|
<circle cx="12" cy="18" r="1.3" fill="#F2A93B"/>
|
||||||
|
<circle cx="17" cy="18" r="1.3" fill="#F2A93B"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 662 B |
11
src/otko/resources/icons/assign-distributed.svg
Normal file
11
src/otko/resources/icons/assign-distributed.svg
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<title>Assign distributed load</title>
|
||||||
|
<path d="M5 4.5h14" stroke="#F2A93B" stroke-width="1.6"/>
|
||||||
|
<path d="M7 4.5V14M12 4.5V14M17 4.5V14" stroke="#F2A93B" stroke-width="2"/>
|
||||||
|
<path d="M5 11.8l2 2.2 2-2.2M10 11.8l2 2.2 2-2.2M15 11.8l2 2.2 2-2.2" stroke="#F2A93B" stroke-width="2"/>
|
||||||
|
<path d="M4 18h16" stroke="#4F92E8" stroke-width="2"/>
|
||||||
|
<circle cx="7" cy="18" r="1.3" fill="#F2A93B"/>
|
||||||
|
<circle cx="12" cy="18" r="1.3" fill="#F2A93B"/>
|
||||||
|
<circle cx="17" cy="18" r="1.3" fill="#F2A93B"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 662 B |
|
|
@ -34,6 +34,7 @@ from otko.commands import (
|
||||||
SetRestraintCommand,
|
SetRestraintCommand,
|
||||||
SetUnitsCommand,
|
SetUnitsCommand,
|
||||||
)
|
)
|
||||||
|
from otko.core.units import UnitSystem
|
||||||
from otko.services import PROJECT_FILE_SUFFIX
|
from otko.services import PROJECT_FILE_SUFFIX
|
||||||
from otko.views.dialogs import (
|
from otko.views.dialogs import (
|
||||||
AddNodeDialog,
|
AddNodeDialog,
|
||||||
|
|
@ -253,7 +254,12 @@ class ActionHandlers:
|
||||||
if not sel_nodes:
|
if not sel_nodes:
|
||||||
QMessageBox.information(self, "Move", "Select one or more nodes first.")
|
QMessageBox.information(self, "Move", "Select one or more nodes first.")
|
||||||
return
|
return
|
||||||
dlg = MoveDialog(len(sel_nodes), self)
|
proj = self._vm.project
|
||||||
|
dlg = MoveDialog(
|
||||||
|
len(sel_nodes),
|
||||||
|
self,
|
||||||
|
units=proj.meta.units if proj is not None else UnitSystem.METRIC,
|
||||||
|
)
|
||||||
if dlg.exec() != QDialog.DialogCode.Accepted:
|
if dlg.exec() != QDialog.DialogCode.Accepted:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
|
|
@ -275,7 +281,14 @@ class ActionHandlers:
|
||||||
for el in self._vm.project.elements
|
for el in self._vm.project.elements
|
||||||
if all(nid in sel_nodes for nid in el.nodes)
|
if all(nid in sel_nodes for nid in el.nodes)
|
||||||
}
|
}
|
||||||
dlg = ReplicateDialog(len(sel_nodes), len(sel_elements), self)
|
dlg = ReplicateDialog(
|
||||||
|
len(sel_nodes),
|
||||||
|
len(sel_elements),
|
||||||
|
self,
|
||||||
|
units=self._vm.project.meta.units
|
||||||
|
if self._vm.project is not None
|
||||||
|
else UnitSystem.METRIC,
|
||||||
|
)
|
||||||
if dlg.exec() != QDialog.DialogCode.Accepted:
|
if dlg.exec() != QDialog.DialogCode.Accepted:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
|
|
@ -369,6 +382,7 @@ class ActionHandlers:
|
||||||
grid=proj.grid_system,
|
grid=proj.grid_system,
|
||||||
ndm=proj.ndm,
|
ndm=proj.ndm,
|
||||||
parent=self,
|
parent=self,
|
||||||
|
units=proj.meta.units,
|
||||||
)
|
)
|
||||||
if dlg.exec() != QDialog.DialogCode.Accepted:
|
if dlg.exec() != QDialog.DialogCode.Accepted:
|
||||||
return
|
return
|
||||||
|
|
@ -552,7 +566,12 @@ class ActionHandlers:
|
||||||
for p in self._vm.project.load_patterns
|
for p in self._vm.project.load_patterns
|
||||||
if isinstance(p, PlainLoadPattern)
|
if isinstance(p, PlainLoadPattern)
|
||||||
]
|
]
|
||||||
dlg = AssignLoadDialog(len(sel_nodes), existing_patterns=existing, parent=self)
|
dlg = AssignLoadDialog(
|
||||||
|
len(sel_nodes),
|
||||||
|
existing_patterns=existing,
|
||||||
|
parent=self,
|
||||||
|
units=self._vm.project.meta.units,
|
||||||
|
)
|
||||||
if dlg.exec() != QDialog.DialogCode.Accepted:
|
if dlg.exec() != QDialog.DialogCode.Accepted:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
|
|
@ -718,7 +737,12 @@ class ActionHandlers:
|
||||||
"Select one or more nodes first.",
|
"Select one or more nodes first.",
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
dlg = AssignMassesDialog(len(sel_nodes), ndf=self._vm.project.ndf, parent=self)
|
dlg = AssignMassesDialog(
|
||||||
|
len(sel_nodes),
|
||||||
|
ndf=self._vm.project.ndf,
|
||||||
|
parent=self,
|
||||||
|
units=self._vm.project.meta.units,
|
||||||
|
)
|
||||||
if dlg.exec() != QDialog.DialogCode.Accepted:
|
if dlg.exec() != QDialog.DialogCode.Accepted:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
|
|
@ -743,7 +767,10 @@ class ActionHandlers:
|
||||||
(p.id, p.name) for p in project.load_patterns if isinstance(p, PlainLoadPattern)
|
(p.id, p.name) for p in project.load_patterns if isinstance(p, PlainLoadPattern)
|
||||||
]
|
]
|
||||||
dlg = AssignDistributedLoadDialog(
|
dlg = AssignDistributedLoadDialog(
|
||||||
len(sel_elements), existing_patterns=existing, parent=self
|
len(sel_elements),
|
||||||
|
existing_patterns=existing,
|
||||||
|
parent=self,
|
||||||
|
units=project.meta.units,
|
||||||
)
|
)
|
||||||
if dlg.exec() != QDialog.DialogCode.Accepted:
|
if dlg.exec() != QDialog.DialogCode.Accepted:
|
||||||
return
|
return
|
||||||
|
|
@ -938,6 +965,10 @@ class ActionHandlers:
|
||||||
self._latest_results = results
|
self._latest_results = results
|
||||||
project = self._vm.project
|
project = self._vm.project
|
||||||
if project is not None:
|
if project is not None:
|
||||||
|
try:
|
||||||
|
self._results_panel.set_units(project.meta.units, project.ndf)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
self._results_panel.show_results(results, ndm=project.ndm, ndf=project.ndf)
|
self._results_panel.show_results(results, ndm=project.ndm, ndf=project.ndf)
|
||||||
else:
|
else:
|
||||||
self._results_panel.show_results(results)
|
self._results_panel.show_results(results)
|
||||||
|
|
@ -961,10 +992,7 @@ class ActionHandlers:
|
||||||
def _on_about(self) -> None:
|
def _on_about(self) -> None:
|
||||||
box = QMessageBox(self)
|
box = QMessageBox(self)
|
||||||
box.setWindowTitle("About OTKO")
|
box.setWindowTitle("About OTKO")
|
||||||
box.setText(
|
box.setText(f"<h3>OTKO {__version__}</h3><p>A modern desktop GUI for OpenSeesPy.</p>")
|
||||||
f"<h3>OTKO {__version__}</h3>"
|
|
||||||
"<p>A modern desktop GUI for OpenSeesPy.</p>"
|
|
||||||
)
|
|
||||||
app_svg = Path(__file__).resolve().parent.parent / "resources" / "icons" / "app.svg"
|
app_svg = Path(__file__).resolve().parent.parent / "resources" / "icons" / "app.svg"
|
||||||
if app_svg.is_file():
|
if app_svg.is_file():
|
||||||
box.setIconPixmap(QIcon(str(app_svg)).pixmap(64, 64))
|
box.setIconPixmap(QIcon(str(app_svg)).pixmap(64, 64))
|
||||||
|
|
@ -1034,6 +1062,18 @@ class ActionHandlers:
|
||||||
window itself); call them when present and never fail when
|
window itself); call them when present and never fail when
|
||||||
absent.
|
absent.
|
||||||
"""
|
"""
|
||||||
|
try:
|
||||||
|
project = self._vm.project
|
||||||
|
if project is not None and hasattr(self, "_results_panel"):
|
||||||
|
try:
|
||||||
|
self._results_panel.set_units( # type: ignore[attr-defined]
|
||||||
|
project.meta.units,
|
||||||
|
project.ndf,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
hosts = [getattr(self, "_dock_manager", None), self]
|
hosts = [getattr(self, "_dock_manager", None), self]
|
||||||
seen: set[int] = set()
|
seen: set[int] = set()
|
||||||
for host in hosts:
|
for host in hosts:
|
||||||
|
|
|
||||||
|
|
@ -34,14 +34,21 @@ from otko.services.element_forces import DiagramData, ForceComponent
|
||||||
_LOG = logging.getLogger("otko.diagram")
|
_LOG = logging.getLogger("otko.diagram")
|
||||||
|
|
||||||
# Components that draw perpendicular to the element axis vs along it.
|
# Components that draw perpendicular to the element axis vs along it.
|
||||||
_PERPENDICULAR = {ForceComponent.V2, ForceComponent.V3,
|
_PERPENDICULAR = {
|
||||||
ForceComponent.M2, ForceComponent.M3, ForceComponent.T}
|
ForceComponent.V2,
|
||||||
|
ForceComponent.V3,
|
||||||
|
ForceComponent.M2,
|
||||||
|
ForceComponent.M3,
|
||||||
|
ForceComponent.T,
|
||||||
|
}
|
||||||
|
|
||||||
# Which local axis the value is plotted along (2 = local y, 3 = local z).
|
# Which local axis the value is plotted along (2 = local y, 3 = local z).
|
||||||
_LOCAL_AXIS = {
|
_LOCAL_AXIS = {
|
||||||
ForceComponent.V2: 2, ForceComponent.M3: 2,
|
ForceComponent.V2: 2,
|
||||||
ForceComponent.V3: 3, ForceComponent.M2: 3,
|
ForceComponent.M3: 2,
|
||||||
ForceComponent.T: 2,
|
ForceComponent.V3: 3,
|
||||||
|
ForceComponent.M2: 3,
|
||||||
|
ForceComponent.T: 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -58,10 +65,23 @@ class DiagramRenderer:
|
||||||
self._plotter = plotter
|
self._plotter = plotter
|
||||||
self._actor: Any = None
|
self._actor: Any = None
|
||||||
self._label_actor: Any = None
|
self._label_actor: Any = None
|
||||||
|
self._units_label: str = ""
|
||||||
|
|
||||||
# ── public ──────────────────────────────────────────────────────
|
# ── public ──────────────────────────────────────────────────────
|
||||||
def render(self, project: Project, data: DiagramData, scale: float) -> None:
|
def render(
|
||||||
"""Build and display the diagram for ``data`` at ``scale``."""
|
self,
|
||||||
|
project: Project,
|
||||||
|
data: DiagramData,
|
||||||
|
scale: float,
|
||||||
|
*,
|
||||||
|
units_label: str = "",
|
||||||
|
) -> None:
|
||||||
|
"""Build and display the diagram for ``data`` at ``scale``.
|
||||||
|
|
||||||
|
``units_label`` (e.g. ``"kip"``) is appended to the min/max
|
||||||
|
value annotations; values stay native.
|
||||||
|
"""
|
||||||
|
self._units_label = units_label or ""
|
||||||
self.clear()
|
self.clear()
|
||||||
if data.element_ids.size == 0:
|
if data.element_ids.size == 0:
|
||||||
return
|
return
|
||||||
|
|
@ -69,8 +89,10 @@ class DiagramRenderer:
|
||||||
# All values are zero → no diagram to draw. This is normal —
|
# All values are zero → no diagram to draw. This is normal —
|
||||||
# e.g. asking for "torsion" on a planar bending model. Log
|
# e.g. asking for "torsion" on a planar bending model. Log
|
||||||
# a hint so the user understands the empty viewport.
|
# a hint so the user understands the empty viewport.
|
||||||
_LOG.info("All '%s' values are zero for this analysis step "
|
_LOG.info(
|
||||||
"— nothing to draw.", data.component.name)
|
"All '%s' values are zero for this analysis step — nothing to draw.",
|
||||||
|
data.component.name,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
node_pos = {n.id: np.asarray(n.coords, dtype=float) for n in project.nodes}
|
node_pos = {n.id: np.asarray(n.coords, dtype=float) for n in project.nodes}
|
||||||
|
|
@ -79,8 +101,8 @@ class DiagramRenderer:
|
||||||
is_perpendicular = data.component in _PERPENDICULAR
|
is_perpendicular = data.component in _PERPENDICULAR
|
||||||
axis_id = _LOCAL_AXIS.get(data.component, 2)
|
axis_id = _LOCAL_AXIS.get(data.component, 2)
|
||||||
|
|
||||||
polys: list[np.ndarray] = [] # vertex arrays for each polygon
|
polys: list[np.ndarray] = [] # vertex arrays for each polygon
|
||||||
scalars: list[float] = [] # one value per polygon (avg of end values)
|
scalars: list[float] = [] # one value per polygon (avg of end values)
|
||||||
for k, eid in enumerate(data.element_ids):
|
for k, eid in enumerate(data.element_ids):
|
||||||
elem = elem_lookup.get(int(eid))
|
elem = elem_lookup.get(int(eid))
|
||||||
if elem is None:
|
if elem is None:
|
||||||
|
|
@ -106,15 +128,19 @@ class DiagramRenderer:
|
||||||
perp = self._local_perp(pi, pj, 2)
|
perp = self._local_perp(pi, pj, 2)
|
||||||
if perp is None:
|
if perp is None:
|
||||||
continue
|
continue
|
||||||
width = abs(v_i) * 0.5 # half-width fall-off
|
width = abs(v_i) * 0.5 # half-width fall-off
|
||||||
if width == 0.0:
|
if width == 0.0:
|
||||||
width = abs(v_j) * 0.5
|
width = abs(v_j) * 0.5
|
||||||
if width == 0.0:
|
if width == 0.0:
|
||||||
continue
|
continue
|
||||||
quad = np.vstack([
|
quad = np.vstack(
|
||||||
pi - perp * width, pj - perp * width,
|
[
|
||||||
pj + perp * width, pi + perp * width,
|
pi - perp * width,
|
||||||
])
|
pj - perp * width,
|
||||||
|
pj + perp * width,
|
||||||
|
pi + perp * width,
|
||||||
|
]
|
||||||
|
)
|
||||||
polys.append(quad)
|
polys.append(quad)
|
||||||
scalars.append(0.5 * (data.values_i[k] + data.values_j[k]))
|
scalars.append(0.5 * (data.values_i[k] + data.values_j[k]))
|
||||||
|
|
||||||
|
|
@ -138,7 +164,9 @@ class DiagramRenderer:
|
||||||
# Symmetric color range so zero stays at the colormap mid-point.
|
# Symmetric color range so zero stays at the colormap mid-point.
|
||||||
vmax = float(np.max(np.abs(scalars))) or 1.0
|
vmax = float(np.max(np.abs(scalars))) or 1.0
|
||||||
# Defensive: accept either ForceComponent enum or its name string.
|
# Defensive: accept either ForceComponent enum or its name string.
|
||||||
comp_label = data.component.value if hasattr(data.component, "value") else str(data.component)
|
comp_label = (
|
||||||
|
data.component.value if hasattr(data.component, "value") else str(data.component)
|
||||||
|
)
|
||||||
self._actor = self._plotter.add_mesh(
|
self._actor = self._plotter.add_mesh(
|
||||||
mesh,
|
mesh,
|
||||||
scalars="value",
|
scalars="value",
|
||||||
|
|
@ -157,8 +185,7 @@ class DiagramRenderer:
|
||||||
# ── Numerical labels at the global min and max element ends. ──
|
# ── Numerical labels at the global min and max element ends. ──
|
||||||
self._label_actor = self._add_value_labels(project, data, scale)
|
self._label_actor = self._add_value_labels(project, data, scale)
|
||||||
|
|
||||||
def _add_value_labels(self, project: Project, data: DiagramData,
|
def _add_value_labels(self, project: Project, data: DiagramData, scale: float) -> Any:
|
||||||
scale: float) -> Any:
|
|
||||||
"""Annotate the diagram's extreme ends with their numerical values.
|
"""Annotate the diagram's extreme ends with their numerical values.
|
||||||
|
|
||||||
Avoids visual clutter by labelling only the two ends carrying the
|
Avoids visual clutter by labelling only the two ends carrying the
|
||||||
|
|
@ -199,13 +226,15 @@ class DiagramRenderer:
|
||||||
# If max == min (all equal) just one label; skip the dup.
|
# If max == min (all equal) just one label; skip the dup.
|
||||||
unique_indices = [idx_max] if idx_max == idx_min else [idx_max, idx_min]
|
unique_indices = [idx_max] if idx_max == idx_min else [idx_max, idx_min]
|
||||||
positions = np.vstack([candidates[i][1] for i in unique_indices])
|
positions = np.vstack([candidates[i][1] for i in unique_indices])
|
||||||
labels = [self._format_value(candidates[i][0]) for i in unique_indices]
|
unit_suffix = f" {self._units_label}" if getattr(self, "_units_label", "") else ""
|
||||||
|
labels = [f"{self._format_value(candidates[i][0])}{unit_suffix}" for i in unique_indices]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return self._plotter.add_point_labels(
|
return self._plotter.add_point_labels(
|
||||||
positions, labels,
|
positions,
|
||||||
|
labels,
|
||||||
font_size=14,
|
font_size=14,
|
||||||
point_size=0, # don't draw the underlying points
|
point_size=0, # don't draw the underlying points
|
||||||
shape=None,
|
shape=None,
|
||||||
always_visible=True,
|
always_visible=True,
|
||||||
pickable=False,
|
pickable=False,
|
||||||
|
|
|
||||||
|
|
@ -1140,9 +1140,7 @@ class ModelRenderer:
|
||||||
if self._node_pd is None or len(project.nodes) == 0:
|
if self._node_pd is None or len(project.nodes) == 0:
|
||||||
return None
|
return None
|
||||||
pts = np.asarray(self._node_pd.points)
|
pts = np.asarray(self._node_pd.points)
|
||||||
labels = [
|
labels = [node.display_name for node in project.nodes]
|
||||||
(node.name.strip() if node.name.strip() else f"N{node.id}") for node in project.nodes
|
|
||||||
]
|
|
||||||
return self._plotter.add_point_labels(
|
return self._plotter.add_point_labels(
|
||||||
pts,
|
pts,
|
||||||
labels,
|
labels,
|
||||||
|
|
@ -1170,7 +1168,7 @@ class ModelRenderer:
|
||||||
if i is None or j is None:
|
if i is None or j is None:
|
||||||
continue
|
continue
|
||||||
centers.append((pts[i] + pts[j]) / 2.0)
|
centers.append((pts[i] + pts[j]) / 2.0)
|
||||||
labels.append(el.name.strip() if el.name.strip() else f"E{el.id}")
|
labels.append(el.display_name)
|
||||||
if not centers:
|
if not centers:
|
||||||
return None
|
return None
|
||||||
return self._plotter.add_point_labels(
|
return self._plotter.add_point_labels(
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ from PySide6.QtWidgets import (
|
||||||
|
|
||||||
from otko.core import Node
|
from otko.core import Node
|
||||||
from otko.core.geometry import GridSystem
|
from otko.core.geometry import GridSystem
|
||||||
|
from otko.core.units import UnitSystem, native_label
|
||||||
|
|
||||||
|
|
||||||
def _snap(value: float, lines: list[float]) -> float:
|
def _snap(value: float, lines: list[float]) -> float:
|
||||||
|
|
@ -40,18 +41,23 @@ class AddNodeDialog(QDialog):
|
||||||
the node lands on the active plane by default.
|
the node lands on the active plane by default.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, next_node_id: int,
|
def __init__(
|
||||||
grid: GridSystem,
|
self,
|
||||||
ndm: int = 3,
|
next_node_id: int,
|
||||||
parent: QWidget | None = None,
|
grid: GridSystem,
|
||||||
*,
|
ndm: int = 3,
|
||||||
plane_elevation: float | None = None) -> None:
|
parent: QWidget | None = None,
|
||||||
|
*,
|
||||||
|
plane_elevation: float | None = None,
|
||||||
|
units: UnitSystem = UnitSystem.METRIC,
|
||||||
|
) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setWindowTitle("Add Node")
|
self.setWindowTitle("Add Node")
|
||||||
self._next_id = next_node_id
|
self._next_id = next_node_id
|
||||||
self._grid = grid
|
self._grid = grid
|
||||||
self._ndm = ndm
|
self._ndm = ndm
|
||||||
self._plane_elevation = plane_elevation
|
self._plane_elevation = plane_elevation
|
||||||
|
self._units = units
|
||||||
self._build_ui()
|
self._build_ui()
|
||||||
|
|
||||||
def _build_ui(self) -> None:
|
def _build_ui(self) -> None:
|
||||||
|
|
@ -62,25 +68,30 @@ class AddNodeDialog(QDialog):
|
||||||
self._x = self._spin()
|
self._x = self._spin()
|
||||||
self._y = self._spin()
|
self._y = self._spin()
|
||||||
self._z = self._spin()
|
self._z = self._spin()
|
||||||
|
try:
|
||||||
|
length_u = native_label(self._units, "length")
|
||||||
|
except KeyError:
|
||||||
|
length_u = ""
|
||||||
if self._plane_elevation is not None and self._ndm == 3:
|
if self._plane_elevation is not None and self._ndm == 3:
|
||||||
self._z.setValue(float(self._plane_elevation))
|
self._z.setValue(float(self._plane_elevation))
|
||||||
form.addRow("X:", self._x)
|
form.addRow(f"X ({length_u}):" if length_u else "X:", self._x)
|
||||||
form.addRow("Y:", self._y)
|
form.addRow(f"Y ({length_u}):" if length_u else "Y:", self._y)
|
||||||
if self._ndm == 3:
|
if self._ndm == 3:
|
||||||
form.addRow("Z:", self._z)
|
form.addRow(f"Z ({length_u}):" if length_u else "Z:", self._z)
|
||||||
else:
|
else:
|
||||||
self._z.setVisible(False)
|
self._z.setVisible(False)
|
||||||
if self._plane_elevation is not None and self._ndm == 3:
|
if self._plane_elevation is not None and self._ndm == 3:
|
||||||
form.addRow(QLabel(
|
form.addRow(
|
||||||
f"<i>Suggested Z = active working-plane level "
|
QLabel(
|
||||||
f"({self._plane_elevation:g}).</i>",
|
f"<i>Suggested Z = active working-plane level ({self._plane_elevation:g}).</i>",
|
||||||
))
|
)
|
||||||
|
)
|
||||||
root.addLayout(form)
|
root.addLayout(form)
|
||||||
|
|
||||||
self._snap_cb = QCheckBox("Snap to nearest grid intersection")
|
self._snap_cb = QCheckBox("Snap to nearest grid intersection")
|
||||||
self._snap_cb.setChecked(bool(
|
self._snap_cb.setChecked(
|
||||||
self._grid.x_lines or self._grid.y_lines or self._grid.z_lines
|
bool(self._grid.x_lines or self._grid.y_lines or self._grid.z_lines)
|
||||||
))
|
)
|
||||||
root.addWidget(self._snap_cb)
|
root.addWidget(self._snap_cb)
|
||||||
|
|
||||||
buttons = QDialogButtonBox(
|
buttons = QDialogButtonBox(
|
||||||
|
|
|
||||||
|
|
@ -24,16 +24,14 @@ from PySide6.QtWidgets import (
|
||||||
)
|
)
|
||||||
|
|
||||||
from otko.core import Project
|
from otko.core import Project
|
||||||
from otko.core.units import UnitSystem
|
from otko.core.units import UnitSystem, native_label
|
||||||
|
|
||||||
# Nominal elastic-interior defaults per unit system (stiffness scale only;
|
# Nominal elastic-interior defaults per unit system (stiffness scale only;
|
||||||
# geometric A/Iz/Iy/J defaults are nominal either way). Stored project
|
# geometric A/Iz/Iy/J defaults are nominal either way). Stored project
|
||||||
# values always win when the selection provides them.
|
# values always win when the selection provides them.
|
||||||
_UNIT_DEFAULTS: dict[str, dict[str, object]] = {
|
_UNIT_DEFAULTS: dict[str, dict[str, object]] = {
|
||||||
UnitSystem.SI_M_N.value: {"E": 200e9, "G": 80e9, "stress": "Pa"},
|
UnitSystem.METRIC.value: {"E": 200e9, "G": 80e9, "stress": "Pa"},
|
||||||
UnitSystem.SI_MM_N.value: {"E": 200e3, "G": 80e3, "stress": "MPa"},
|
UnitSystem.IMPERIAL.value: {"E": 29000.0, "G": 11200.0, "stress": "ksi"},
|
||||||
UnitSystem.US_IN_KIP.value: {"E": 29000.0, "G": 11200.0, "stress": "ksi"},
|
|
||||||
UnitSystem.US_FT_KIP.value: {"E": 29000.0 * 144.0, "G": 11200.0 * 144.0, "stress": "ksf"},
|
|
||||||
}
|
}
|
||||||
_FALLBACK_DEFAULTS: dict[str, object] = {"E": 200e9, "G": 80e9, "stress": "Pa"}
|
_FALLBACK_DEFAULTS: dict[str, object] = {"E": 200e9, "G": 80e9, "stress": "Pa"}
|
||||||
|
|
||||||
|
|
@ -64,9 +62,7 @@ class AssignHingeDialog(QDialog):
|
||||||
|
|
||||||
def _build_ui(self, n_selected: int) -> None:
|
def _build_ui(self, n_selected: int) -> None:
|
||||||
layout = QVBoxLayout(self)
|
layout = QVBoxLayout(self)
|
||||||
header = (
|
header = f"Convert <b>{n_selected}</b> selected element(s) to BeamWithHinges."
|
||||||
f"Convert <b>{n_selected}</b> selected element(s) to BeamWithHinges."
|
|
||||||
)
|
|
||||||
prefill = self._selection_prefill()
|
prefill = self._selection_prefill()
|
||||||
if prefill is None and self._element_ids:
|
if prefill is None and self._element_ids:
|
||||||
header += " <i>Selection is mixed — unit-scaled defaults shown.</i>"
|
header += " <i>Selection is mixed — unit-scaled defaults shown.</i>"
|
||||||
|
|
@ -79,6 +75,24 @@ class AssignHingeDialog(QDialog):
|
||||||
g_default = float(units["G"]) # type: ignore[arg-type]
|
g_default = float(units["G"]) # type: ignore[arg-type]
|
||||||
stress = str(units["stress"])
|
stress = str(units["stress"])
|
||||||
|
|
||||||
|
# Unit hints only — values are entered in native stored units.
|
||||||
|
try:
|
||||||
|
length_u = native_label(self._project.meta.units, "length")
|
||||||
|
except KeyError:
|
||||||
|
length_u = ""
|
||||||
|
try:
|
||||||
|
stress_u = native_label(self._project.meta.units, "stress")
|
||||||
|
except KeyError:
|
||||||
|
stress_u = stress
|
||||||
|
try:
|
||||||
|
area_u = native_label(self._project.meta.units, "area")
|
||||||
|
except KeyError:
|
||||||
|
area_u = ""
|
||||||
|
try:
|
||||||
|
inertia_u = native_label(self._project.meta.units, "inertia")
|
||||||
|
except KeyError:
|
||||||
|
inertia_u = ""
|
||||||
|
|
||||||
form = QFormLayout()
|
form = QFormLayout()
|
||||||
# Hinge section pickers
|
# Hinge section pickers
|
||||||
self._sec_i = QComboBox()
|
self._sec_i = QComboBox()
|
||||||
|
|
@ -99,29 +113,35 @@ class AssignHingeDialog(QDialog):
|
||||||
self._lp_i.setDecimals(4)
|
self._lp_i.setDecimals(4)
|
||||||
self._lp_i.setValue(float(prefill.get("lp_i", 0.1)) if prefill else 0.1)
|
self._lp_i.setValue(float(prefill.get("lp_i", 0.1)) if prefill else 0.1)
|
||||||
self._lp_i.setSingleStep(0.01)
|
self._lp_i.setSingleStep(0.01)
|
||||||
form.addRow("Lp_i (hinge length i):", self._lp_i)
|
form.addRow(
|
||||||
|
f"Lp_i (hinge length i) ({length_u}):" if length_u else "Lp_i (hinge length i):",
|
||||||
|
self._lp_i,
|
||||||
|
)
|
||||||
|
|
||||||
self._lp_j = QDoubleSpinBox()
|
self._lp_j = QDoubleSpinBox()
|
||||||
self._lp_j.setRange(1e-6, 100.0)
|
self._lp_j.setRange(1e-6, 100.0)
|
||||||
self._lp_j.setDecimals(4)
|
self._lp_j.setDecimals(4)
|
||||||
self._lp_j.setValue(float(prefill.get("lp_j", 0.1)) if prefill else 0.1)
|
self._lp_j.setValue(float(prefill.get("lp_j", 0.1)) if prefill else 0.1)
|
||||||
self._lp_j.setSingleStep(0.01)
|
self._lp_j.setSingleStep(0.01)
|
||||||
form.addRow("Lp_j (hinge length j):", self._lp_j)
|
form.addRow(
|
||||||
|
f"Lp_j (hinge length j) ({length_u}):" if length_u else "Lp_j (hinge length j):",
|
||||||
|
self._lp_j,
|
||||||
|
)
|
||||||
|
|
||||||
# Elastic interior properties (defaults follow project.meta.units).
|
# Elastic interior properties (defaults follow project.meta.units).
|
||||||
form.addRow(QLabel(f"<b>Elastic interior ({stress} / project length units):</b>"))
|
form.addRow(QLabel(f"<b>Elastic interior ({stress_u} / {length_u}):</b>"))
|
||||||
self._E = self._dspin(float(prefill.get("E", e_default)) if prefill else e_default, 1e6)
|
self._E = self._dspin(float(prefill.get("E", e_default)) if prefill else e_default, 1e6)
|
||||||
self._A = self._dspin(float(prefill.get("A", 0.01)) if prefill else 0.01, 0.001)
|
self._A = self._dspin(float(prefill.get("A", 0.01)) if prefill else 0.01, 0.001)
|
||||||
self._Iz = self._dspin(float(prefill.get("Iz", 1e-4)) if prefill else 1e-4, 1e-6)
|
self._Iz = self._dspin(float(prefill.get("Iz", 1e-4)) if prefill else 1e-4, 1e-6)
|
||||||
self._Iy = self._dspin(float(prefill.get("Iy", 1e-4)) if prefill else 1e-4, 1e-6)
|
self._Iy = self._dspin(float(prefill.get("Iy", 1e-4)) if prefill else 1e-4, 1e-6)
|
||||||
self._G = self._dspin(float(prefill.get("G", g_default)) if prefill else g_default, 1e6)
|
self._G = self._dspin(float(prefill.get("G", g_default)) if prefill else g_default, 1e6)
|
||||||
self._J = self._dspin(float(prefill.get("J", 1e-5)) if prefill else 1e-5, 1e-7)
|
self._J = self._dspin(float(prefill.get("J", 1e-5)) if prefill else 1e-5, 1e-7)
|
||||||
form.addRow(f"E ({stress}):", self._E)
|
form.addRow(f"E ({stress_u}):", self._E)
|
||||||
form.addRow("A:", self._A)
|
form.addRow(f"A ({area_u}):" if area_u else "A:", self._A)
|
||||||
form.addRow("Iz:", self._Iz)
|
form.addRow(f"Iz ({inertia_u}):" if inertia_u else "Iz:", self._Iz)
|
||||||
form.addRow("Iy:", self._Iy)
|
form.addRow(f"Iy ({inertia_u}):" if inertia_u else "Iy:", self._Iy)
|
||||||
form.addRow(f"G ({stress}):", self._G)
|
form.addRow(f"G ({stress_u}):", self._G)
|
||||||
form.addRow("J:", self._J)
|
form.addRow(f"J ({inertia_u}):" if inertia_u else "J:", self._J)
|
||||||
|
|
||||||
layout.addLayout(form)
|
layout.addLayout(form)
|
||||||
|
|
||||||
|
|
@ -160,18 +180,20 @@ class AssignHingeDialog(QDialog):
|
||||||
if el is None:
|
if el is None:
|
||||||
return None
|
return None
|
||||||
if getattr(el, "type", None) == "BeamWithHinges":
|
if getattr(el, "type", None) == "BeamWithHinges":
|
||||||
collected.append({
|
collected.append(
|
||||||
"section_i_id": getattr(el, "section_i_id", None),
|
{
|
||||||
"section_j_id": getattr(el, "section_j_id", None),
|
"section_i_id": getattr(el, "section_i_id", None),
|
||||||
"lp_i": getattr(el, "lp_i", None),
|
"section_j_id": getattr(el, "section_j_id", None),
|
||||||
"lp_j": getattr(el, "lp_j", None),
|
"lp_i": getattr(el, "lp_i", None),
|
||||||
"E": getattr(el, "E", None),
|
"lp_j": getattr(el, "lp_j", None),
|
||||||
"A": getattr(el, "A", None),
|
"E": getattr(el, "E", None),
|
||||||
"Iz": getattr(el, "Iz", None),
|
"A": getattr(el, "A", None),
|
||||||
"Iy": getattr(el, "Iy", None),
|
"Iz": getattr(el, "Iz", None),
|
||||||
"G": getattr(el, "G", None),
|
"Iy": getattr(el, "Iy", None),
|
||||||
"J": getattr(el, "J", None),
|
"G": getattr(el, "G", None),
|
||||||
})
|
"J": getattr(el, "J", None),
|
||||||
|
}
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
sec_id = getattr(el, "section_id", None)
|
sec_id = getattr(el, "section_id", None)
|
||||||
if sec_id is None:
|
if sec_id is None:
|
||||||
|
|
@ -185,21 +207,22 @@ class AssignHingeDialog(QDialog):
|
||||||
# prefill the hinge pickers with the section itself.
|
# prefill the hinge pickers with the section itself.
|
||||||
collected.append({"section_i_id": sec_id, "section_j_id": sec_id})
|
collected.append({"section_i_id": sec_id, "section_j_id": sec_id})
|
||||||
continue
|
continue
|
||||||
collected.append({
|
collected.append(
|
||||||
"section_i_id": sec_id,
|
{
|
||||||
"section_j_id": sec_id,
|
"section_i_id": sec_id,
|
||||||
"E": getattr(sec, "E", None),
|
"section_j_id": sec_id,
|
||||||
"A": getattr(sec, "A", None),
|
"E": getattr(sec, "E", None),
|
||||||
"Iz": getattr(sec, "Iz", None),
|
"A": getattr(sec, "A", None),
|
||||||
"Iy": getattr(sec, "Iy", None),
|
"Iz": getattr(sec, "Iz", None),
|
||||||
"G": getattr(sec, "G", None),
|
"Iy": getattr(sec, "Iy", None),
|
||||||
"J": getattr(sec, "J", None),
|
"G": getattr(sec, "G", None),
|
||||||
})
|
"J": getattr(sec, "J", None),
|
||||||
|
}
|
||||||
|
)
|
||||||
if not collected:
|
if not collected:
|
||||||
return None
|
return None
|
||||||
merged: dict = {}
|
merged: dict = {}
|
||||||
for key in ("section_i_id", "section_j_id", "lp_i", "lp_j",
|
for key in ("section_i_id", "section_j_id", "lp_i", "lp_j", "E", "A", "Iz", "Iy", "G", "J"):
|
||||||
"E", "A", "Iz", "Iy", "G", "J"):
|
|
||||||
vals = {c.get(key) for c in collected if c.get(key) is not None}
|
vals = {c.get(key) for c in collected if c.get(key) is not None}
|
||||||
if len(vals) == 1:
|
if len(vals) == 1:
|
||||||
merged[key] = next(iter(vals))
|
merged[key] = next(iter(vals))
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ from PySide6.QtWidgets import (
|
||||||
QWidget,
|
QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from otko.core.units import UnitSystem, native_label
|
||||||
|
|
||||||
# Sentinel the pattern combo's userData holds to mean "create a new one".
|
# Sentinel the pattern combo's userData holds to mean "create a new one".
|
||||||
_NEW_PATTERN_SENTINEL = "__new__"
|
_NEW_PATTERN_SENTINEL = "__new__"
|
||||||
|
|
||||||
|
|
@ -38,6 +40,7 @@ class AssignLoadDialog(QDialog):
|
||||||
current_forces: tuple[float, float, float, float, float, float] | None = None,
|
current_forces: tuple[float, float, float, float, float, float] | None = None,
|
||||||
active_pattern_id: int | None = None,
|
active_pattern_id: int | None = None,
|
||||||
mixed: bool = False,
|
mixed: bool = False,
|
||||||
|
units: UnitSystem = UnitSystem.METRIC,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setWindowTitle("Assign Nodal Load")
|
self.setWindowTitle("Assign Nodal Load")
|
||||||
|
|
@ -45,6 +48,7 @@ class AssignLoadDialog(QDialog):
|
||||||
self._current_forces = current_forces
|
self._current_forces = current_forces
|
||||||
self._active_pattern_id = active_pattern_id
|
self._active_pattern_id = active_pattern_id
|
||||||
self._mixed = mixed
|
self._mixed = mixed
|
||||||
|
self._units = units
|
||||||
self._build_ui(n_selected)
|
self._build_ui(n_selected)
|
||||||
|
|
||||||
def _build_ui(self, n_selected: int) -> None:
|
def _build_ui(self, n_selected: int) -> None:
|
||||||
|
|
@ -80,9 +84,7 @@ class AssignLoadDialog(QDialog):
|
||||||
pf.addRow("Active:", self._active_pattern_label)
|
pf.addRow("Active:", self._active_pattern_label)
|
||||||
|
|
||||||
self._new_name_edit = QLineEdit("Pattern")
|
self._new_name_edit = QLineEdit("Pattern")
|
||||||
self._new_name_edit.setPlaceholderText(
|
self._new_name_edit.setPlaceholderText("Name for the new pattern (e.g. RefMoment)")
|
||||||
"Name for the new pattern (e.g. RefMoment)"
|
|
||||||
)
|
|
||||||
pf.addRow("New name:", self._new_name_edit)
|
pf.addRow("New name:", self._new_name_edit)
|
||||||
|
|
||||||
# TimeSeries type selector — only used when creating a new
|
# TimeSeries type selector — only used when creating a new
|
||||||
|
|
@ -98,6 +100,15 @@ class AssignLoadDialog(QDialog):
|
||||||
self._on_pattern_changed(self._pattern_cb.currentIndex())
|
self._on_pattern_changed(self._pattern_cb.currentIndex())
|
||||||
|
|
||||||
# ── Force vector ──
|
# ── Force vector ──
|
||||||
|
# Unit hints only — values are entered in native stored units.
|
||||||
|
try:
|
||||||
|
force_u = native_label(self._units, "force")
|
||||||
|
except KeyError:
|
||||||
|
force_u = ""
|
||||||
|
try:
|
||||||
|
moment_u = native_label(self._units, "moment")
|
||||||
|
except KeyError:
|
||||||
|
moment_u = ""
|
||||||
form = QFormLayout()
|
form = QFormLayout()
|
||||||
self._spinboxes: dict[str, QDoubleSpinBox] = {}
|
self._spinboxes: dict[str, QDoubleSpinBox] = {}
|
||||||
prefill = self._current_forces or (0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
|
prefill = self._current_forces or (0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
|
||||||
|
|
@ -108,7 +119,9 @@ class AssignLoadDialog(QDialog):
|
||||||
sb.setSingleStep(1.0)
|
sb.setSingleStep(1.0)
|
||||||
sb.setValue(float(val))
|
sb.setValue(float(val))
|
||||||
self._spinboxes[label] = sb
|
self._spinboxes[label] = sb
|
||||||
form.addRow(f"{label}:", sb)
|
unit = force_u if label.startswith("F") else moment_u
|
||||||
|
row_label = f"{label} ({unit}):" if unit else f"{label}:"
|
||||||
|
form.addRow(row_label, sb)
|
||||||
layout.addLayout(form)
|
layout.addLayout(form)
|
||||||
self._refresh_active_label()
|
self._refresh_active_label()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ from PySide6.QtWidgets import (
|
||||||
QWidget,
|
QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from otko.core.units import UnitSystem, native_label
|
||||||
|
|
||||||
|
|
||||||
class AssignMassesDialog(QDialog):
|
class AssignMassesDialog(QDialog):
|
||||||
"""Modal dialog: enter translational + rotational mass components.
|
"""Modal dialog: enter translational + rotational mass components.
|
||||||
|
|
@ -27,6 +29,8 @@ class AssignMassesDialog(QDialog):
|
||||||
current: ``(Mx, My, Mz, Mxx, Myy, Mzz)`` values already on the
|
current: ``(Mx, My, Mz, Mxx, Myy, Mzz)`` values already on the
|
||||||
selection, if known — prefilled instead of zeros.
|
selection, if known — prefilled instead of zeros.
|
||||||
mixed: True when the selection carries differing masses.
|
mixed: True when the selection carries differing masses.
|
||||||
|
units: Project's native unit system — label hints only; values
|
||||||
|
are entered in native stored units, never converted.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|
@ -37,12 +41,14 @@ class AssignMassesDialog(QDialog):
|
||||||
*,
|
*,
|
||||||
current: tuple[float, float, float, float, float, float] | None = None,
|
current: tuple[float, float, float, float, float, float] | None = None,
|
||||||
mixed: bool = False,
|
mixed: bool = False,
|
||||||
|
units: UnitSystem = UnitSystem.METRIC,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setWindowTitle("Assign Masses")
|
self.setWindowTitle("Assign Masses")
|
||||||
self._ndf = ndf
|
self._ndf = ndf
|
||||||
self._current = current
|
self._current = current
|
||||||
self._mixed = mixed
|
self._mixed = mixed
|
||||||
|
self._units = units
|
||||||
self._build_ui(n_selected)
|
self._build_ui(n_selected)
|
||||||
|
|
||||||
def _build_ui(self, n_selected: int) -> None:
|
def _build_ui(self, n_selected: int) -> None:
|
||||||
|
|
@ -54,18 +60,43 @@ class AssignMassesDialog(QDialog):
|
||||||
header += " <i>Fields prefilled with the selection's current values.</i>"
|
header += " <i>Fields prefilled with the selection's current values.</i>"
|
||||||
root.addWidget(QLabel(header))
|
root.addWidget(QLabel(header))
|
||||||
|
|
||||||
|
# Unit hints only — values are entered in native stored units.
|
||||||
|
try:
|
||||||
|
mass_u = native_label(self._units, "mass")
|
||||||
|
except KeyError:
|
||||||
|
mass_u = ""
|
||||||
|
try:
|
||||||
|
rot_u = f"{mass_u}·{native_label(self._units, 'length')}²" if mass_u else ""
|
||||||
|
except KeyError:
|
||||||
|
rot_u = ""
|
||||||
|
|
||||||
cur = self._current or (0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
|
cur = self._current or (0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
|
||||||
form = QFormLayout()
|
form = QFormLayout()
|
||||||
self._mx = self._spin(cur[0]); form.addRow("Translation X:", self._mx)
|
self._mx = self._spin(cur[0])
|
||||||
self._my = self._spin(cur[1]); form.addRow("Translation Y:", self._my)
|
form.addRow(f"Translation X ({mass_u}):" if mass_u else "Translation X:", self._mx)
|
||||||
|
self._my = self._spin(cur[1])
|
||||||
|
form.addRow(f"Translation Y ({mass_u}):" if mass_u else "Translation Y:", self._my)
|
||||||
if self._ndf >= 3:
|
if self._ndf >= 3:
|
||||||
self._mz = self._spin(cur[2]); form.addRow("Translation Z:", self._mz)
|
self._mz = self._spin(cur[2])
|
||||||
|
form.addRow(f"Translation Z ({mass_u}):" if mass_u else "Translation Z:", self._mz)
|
||||||
else:
|
else:
|
||||||
self._mz = self._spin(cur[2])
|
self._mz = self._spin(cur[2])
|
||||||
if self._ndf == 6:
|
if self._ndf == 6:
|
||||||
self._mxx = self._spin(cur[3]); form.addRow("Rotation X (Ixx):", self._mxx)
|
self._mxx = self._spin(cur[3])
|
||||||
self._myy = self._spin(cur[4]); form.addRow("Rotation Y (Iyy):", self._myy)
|
form.addRow(
|
||||||
self._mzz = self._spin(cur[5]); form.addRow("Rotation Z (Izz):", self._mzz)
|
f"Rotation X [Ixx] ({rot_u}):" if rot_u else "Rotation X [Ixx]:",
|
||||||
|
self._mxx,
|
||||||
|
)
|
||||||
|
self._myy = self._spin(cur[4])
|
||||||
|
form.addRow(
|
||||||
|
f"Rotation Y [Iyy] ({rot_u}):" if rot_u else "Rotation Y [Iyy]:",
|
||||||
|
self._myy,
|
||||||
|
)
|
||||||
|
self._mzz = self._spin(cur[5])
|
||||||
|
form.addRow(
|
||||||
|
f"Rotation Z [Izz] ({rot_u}):" if rot_u else "Rotation Z [Izz]:",
|
||||||
|
self._mzz,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self._mxx = self._spin(cur[3])
|
self._mxx = self._spin(cur[3])
|
||||||
self._myy = self._spin(cur[4])
|
self._myy = self._spin(cur[4])
|
||||||
|
|
@ -77,8 +108,7 @@ class AssignMassesDialog(QDialog):
|
||||||
root.addWidget(self._xy_link)
|
root.addWidget(self._xy_link)
|
||||||
|
|
||||||
buttons = QDialogButtonBox(
|
buttons = QDialogButtonBox(
|
||||||
QDialogButtonBox.StandardButton.Ok
|
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel,
|
||||||
| QDialogButtonBox.StandardButton.Cancel,
|
|
||||||
)
|
)
|
||||||
buttons.accepted.connect(self.accept)
|
buttons.accepted.connect(self.accept)
|
||||||
buttons.rejected.connect(self.reject)
|
buttons.rejected.connect(self.reject)
|
||||||
|
|
@ -106,6 +136,10 @@ class AssignMassesDialog(QDialog):
|
||||||
def mass_vector(self) -> tuple[float, float, float, float, float, float]:
|
def mass_vector(self) -> tuple[float, float, float, float, float, float]:
|
||||||
"""Return the 6-tuple (Mx, My, Mz, Mxx, Myy, Mzz)."""
|
"""Return the 6-tuple (Mx, My, Mz, Mxx, Myy, Mzz)."""
|
||||||
return (
|
return (
|
||||||
self._mx.value(), self._my.value(), self._mz.value(),
|
self._mx.value(),
|
||||||
self._mxx.value(), self._myy.value(), self._mzz.value(),
|
self._my.value(),
|
||||||
|
self._mz.value(),
|
||||||
|
self._mxx.value(),
|
||||||
|
self._myy.value(),
|
||||||
|
self._mzz.value(),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -34,12 +34,26 @@ from otko.core import (
|
||||||
StaticCase,
|
StaticCase,
|
||||||
TransientCase,
|
TransientCase,
|
||||||
)
|
)
|
||||||
|
from otko.core.units import UnitSystem, native_label
|
||||||
|
|
||||||
|
|
||||||
|
def _u(units: UnitSystem, quantity: str) -> str:
|
||||||
|
"""Display label for ``quantity`` ("" when dimensionless/unknown)."""
|
||||||
|
try:
|
||||||
|
return native_label(units, quantity)
|
||||||
|
except KeyError:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────── helpers ───────────────────────────
|
# ─────────────────────────── helpers ───────────────────────────
|
||||||
def _spin(default: float = 0.0, *, decimals: int = 6,
|
def _spin(
|
||||||
minimum: float = -1e15, maximum: float = 1e15,
|
default: float = 0.0,
|
||||||
step: float = 1.0) -> QDoubleSpinBox:
|
*,
|
||||||
|
decimals: int = 6,
|
||||||
|
minimum: float = -1e15,
|
||||||
|
maximum: float = 1e15,
|
||||||
|
step: float = 1.0,
|
||||||
|
) -> QDoubleSpinBox:
|
||||||
sb = QDoubleSpinBox()
|
sb = QDoubleSpinBox()
|
||||||
sb.setRange(minimum, maximum)
|
sb.setRange(minimum, maximum)
|
||||||
sb.setDecimals(decimals)
|
sb.setDecimals(decimals)
|
||||||
|
|
@ -200,11 +214,14 @@ class CaseFormBase(QWidget):
|
||||||
patterns: list[LoadPattern],
|
patterns: list[LoadPattern],
|
||||||
analyses: list[AnalysisCase],
|
analyses: list[AnalysisCase],
|
||||||
parent: QWidget | None = None,
|
parent: QWidget | None = None,
|
||||||
|
*,
|
||||||
|
units: UnitSystem = UnitSystem.METRIC,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self._case_id: int | None = None
|
self._case_id: int | None = None
|
||||||
self._patterns = patterns
|
self._patterns = patterns
|
||||||
self._analyses = analyses
|
self._analyses = analyses
|
||||||
|
self._units = units
|
||||||
self._layout = QFormLayout(self)
|
self._layout = QFormLayout(self)
|
||||||
self._name_edit = QLineEdit()
|
self._name_edit = QLineEdit()
|
||||||
self._layout.addRow("Name:", self._name_edit)
|
self._layout.addRow("Name:", self._name_edit)
|
||||||
|
|
@ -227,7 +244,14 @@ class CaseFormBase(QWidget):
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────── Static ───────────────────────────
|
# ─────────────────────────── Static ───────────────────────────
|
||||||
_STATIC_SYSTEMS = ["BandGeneral", "BandSPD", "ProfileSPD", "SparseGeneral", "UmfPack", "FullGeneral"]
|
_STATIC_SYSTEMS = [
|
||||||
|
"BandGeneral",
|
||||||
|
"BandSPD",
|
||||||
|
"ProfileSPD",
|
||||||
|
"SparseGeneral",
|
||||||
|
"UmfPack",
|
||||||
|
"FullGeneral",
|
||||||
|
]
|
||||||
_CONSTRAINTS = ["Plain", "Lagrange", "Penalty", "Transformation"]
|
_CONSTRAINTS = ["Plain", "Lagrange", "Penalty", "Transformation"]
|
||||||
_INTEGRATORS_STATIC = ["LoadControl", "DisplacementControl", "ArcLength"]
|
_INTEGRATORS_STATIC = ["LoadControl", "DisplacementControl", "ArcLength"]
|
||||||
_ALGORITHMS = ["Linear", "Newton", "ModifiedNewton", "KrylovNewton", "BFGS", "Broyden"]
|
_ALGORITHMS = ["Linear", "Newton", "ModifiedNewton", "KrylovNewton", "BFGS", "Broyden"]
|
||||||
|
|
@ -242,8 +266,10 @@ class StaticCaseForm(CaseFormBase):
|
||||||
patterns: list[LoadPattern],
|
patterns: list[LoadPattern],
|
||||||
analyses: list[AnalysisCase],
|
analyses: list[AnalysisCase],
|
||||||
parent: QWidget | None = None,
|
parent: QWidget | None = None,
|
||||||
|
*,
|
||||||
|
units: UnitSystem = UnitSystem.METRIC,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(patterns, analyses, parent)
|
super().__init__(patterns, analyses, parent, units=units)
|
||||||
self._patterns_picker, self._pattern_factor_spins = _make_pattern_picker_with_factors(
|
self._patterns_picker, self._pattern_factor_spins = _make_pattern_picker_with_factors(
|
||||||
patterns
|
patterns
|
||||||
)
|
)
|
||||||
|
|
@ -291,7 +317,8 @@ class StaticCaseForm(CaseFormBase):
|
||||||
|
|
||||||
def _read_specific(self, cid: int) -> StaticCase:
|
def _read_specific(self, cid: int) -> StaticCase:
|
||||||
return StaticCase(
|
return StaticCase(
|
||||||
id=cid, name=self._name_edit.text(),
|
id=cid,
|
||||||
|
name=self._name_edit.text(),
|
||||||
pattern_ids=_require_pattern_ids(self._patterns_picker),
|
pattern_ids=_require_pattern_ids(self._patterns_picker),
|
||||||
**_factor_kwargs(StaticCase, self._patterns_picker, self._pattern_factor_spins),
|
**_factor_kwargs(StaticCase, self._patterns_picker, self._pattern_factor_spins),
|
||||||
n_steps=self._n_steps.value(),
|
n_steps=self._n_steps.value(),
|
||||||
|
|
@ -315,16 +342,18 @@ class ModalCaseForm(CaseFormBase):
|
||||||
patterns: list[LoadPattern],
|
patterns: list[LoadPattern],
|
||||||
analyses: list[AnalysisCase],
|
analyses: list[AnalysisCase],
|
||||||
parent: QWidget | None = None,
|
parent: QWidget | None = None,
|
||||||
|
*,
|
||||||
|
units: UnitSystem = UnitSystem.METRIC,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(patterns, analyses, parent)
|
super().__init__(patterns, analyses, parent, units=units)
|
||||||
self._n_modes = _int_spin(3)
|
self._n_modes = _int_spin(3)
|
||||||
self._solver = QComboBox()
|
self._solver = QComboBox()
|
||||||
self._solver.addItems(["genBandArpack", "fullGenLapack", "symmBandLapack"])
|
self._solver.addItems(["genBandArpack", "fullGenLapack", "symmBandLapack"])
|
||||||
self._layout.addRow("Number of modes:", self._n_modes)
|
self._layout.addRow("Number of modes:", self._n_modes)
|
||||||
self._layout.addRow("Solver:", self._solver)
|
self._layout.addRow("Solver:", self._solver)
|
||||||
self._layout.addRow(QLabel(
|
self._layout.addRow(
|
||||||
"<i>The runner auto-falls back to fullGenLapack for very small models.</i>"
|
QLabel("<i>The runner auto-falls back to fullGenLapack for very small models.</i>")
|
||||||
))
|
)
|
||||||
|
|
||||||
def _populate_specific(self, c: ModalCase) -> None:
|
def _populate_specific(self, c: ModalCase) -> None:
|
||||||
self._n_modes.setValue(c.n_modes)
|
self._n_modes.setValue(c.n_modes)
|
||||||
|
|
@ -332,7 +361,8 @@ class ModalCaseForm(CaseFormBase):
|
||||||
|
|
||||||
def _read_specific(self, cid: int) -> ModalCase:
|
def _read_specific(self, cid: int) -> ModalCase:
|
||||||
return ModalCase(
|
return ModalCase(
|
||||||
id=cid, name=self._name_edit.text(),
|
id=cid,
|
||||||
|
name=self._name_edit.text(),
|
||||||
n_modes=self._n_modes.value(),
|
n_modes=self._n_modes.value(),
|
||||||
solver=self._solver.currentText(),
|
solver=self._solver.currentText(),
|
||||||
)
|
)
|
||||||
|
|
@ -350,8 +380,10 @@ class TransientCaseForm(CaseFormBase):
|
||||||
patterns: list[LoadPattern],
|
patterns: list[LoadPattern],
|
||||||
analyses: list[AnalysisCase],
|
analyses: list[AnalysisCase],
|
||||||
parent: QWidget | None = None,
|
parent: QWidget | None = None,
|
||||||
|
*,
|
||||||
|
units: UnitSystem = UnitSystem.METRIC,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(patterns, analyses, parent)
|
super().__init__(patterns, analyses, parent, units=units)
|
||||||
self._patterns_picker, self._pattern_factor_spins = _make_pattern_picker_with_factors(
|
self._patterns_picker, self._pattern_factor_spins = _make_pattern_picker_with_factors(
|
||||||
patterns
|
patterns
|
||||||
)
|
)
|
||||||
|
|
@ -359,13 +391,19 @@ class TransientCaseForm(CaseFormBase):
|
||||||
self._remove_patterns_picker = _make_pattern_picker(patterns)
|
self._remove_patterns_picker = _make_pattern_picker(patterns)
|
||||||
self._dt = _spin(0.01, decimals=8, minimum=1e-12, step=1e-3)
|
self._dt = _spin(0.01, decimals=8, minimum=1e-12, step=1e-3)
|
||||||
self._n_steps = _int_spin(1000, minimum=1, maximum=10_000_000)
|
self._n_steps = _int_spin(1000, minimum=1, maximum=10_000_000)
|
||||||
self._system = QComboBox(); self._system.addItems(_STATIC_SYSTEMS)
|
self._system = QComboBox()
|
||||||
self._constraints = QComboBox(); self._constraints.addItems(_CONSTRAINTS)
|
self._system.addItems(_STATIC_SYSTEMS)
|
||||||
self._integrator = QComboBox(); self._integrator.addItems(_INTEGRATORS_TRANSIENT)
|
self._constraints = QComboBox()
|
||||||
|
self._constraints.addItems(_CONSTRAINTS)
|
||||||
|
self._integrator = QComboBox()
|
||||||
|
self._integrator.addItems(_INTEGRATORS_TRANSIENT)
|
||||||
self._gamma = _spin(0.5, decimals=4, minimum=0.0, maximum=1.0, step=0.01)
|
self._gamma = _spin(0.5, decimals=4, minimum=0.0, maximum=1.0, step=0.01)
|
||||||
self._beta = _spin(0.25, decimals=4, minimum=0.0, maximum=1.0, step=0.01)
|
self._beta = _spin(0.25, decimals=4, minimum=0.0, maximum=1.0, step=0.01)
|
||||||
self._algorithm = QComboBox(); self._algorithm.addItems(_ALGORITHMS); self._algorithm.setCurrentText("Newton")
|
self._algorithm = QComboBox()
|
||||||
self._test = QComboBox(); self._test.addItems(_TESTS)
|
self._algorithm.addItems(_ALGORITHMS)
|
||||||
|
self._algorithm.setCurrentText("Newton")
|
||||||
|
self._test = QComboBox()
|
||||||
|
self._test.addItems(_TESTS)
|
||||||
self._tol = _spin(1e-6, decimals=12, minimum=1e-15, step=1e-7)
|
self._tol = _spin(1e-6, decimals=12, minimum=1e-15, step=1e-7)
|
||||||
self._max_iter = _int_spin(25)
|
self._max_iter = _int_spin(25)
|
||||||
self._alpha_m = _spin(0.0, decimals=8, minimum=0.0, maximum=1e12, step=1e-4)
|
self._alpha_m = _spin(0.0, decimals=8, minimum=0.0, maximum=1e12, step=1e-4)
|
||||||
|
|
@ -376,13 +414,16 @@ class TransientCaseForm(CaseFormBase):
|
||||||
self._layout.addRow(self._patterns_picker)
|
self._layout.addRow(self._patterns_picker)
|
||||||
self._layout.addRow(QLabel("<b>Preload static cases (optional):</b>"))
|
self._layout.addRow(QLabel("<b>Preload static cases (optional):</b>"))
|
||||||
self._layout.addRow(self._preload_picker)
|
self._layout.addRow(self._preload_picker)
|
||||||
self._layout.addRow(QLabel(
|
self._layout.addRow(
|
||||||
"<i>Run these Static cases first, then hold them constant via "
|
QLabel(
|
||||||
"loadConst -time 0.0 before the transient starts.</i>"
|
"<i>Run these Static cases first, then hold them constant via "
|
||||||
))
|
"loadConst -time 0.0 before the transient starts.</i>"
|
||||||
|
)
|
||||||
|
)
|
||||||
self._layout.addRow(QLabel("<b>Patterns to remove after preload (optional):</b>"))
|
self._layout.addRow(QLabel("<b>Patterns to remove after preload (optional):</b>"))
|
||||||
self._layout.addRow(self._remove_patterns_picker)
|
self._layout.addRow(self._remove_patterns_picker)
|
||||||
self._layout.addRow("dt:", self._dt)
|
time_u = _u(self._units, "time")
|
||||||
|
self._layout.addRow(f"dt ({time_u}):" if time_u else "dt:", self._dt)
|
||||||
self._layout.addRow("Number of steps:", self._n_steps)
|
self._layout.addRow("Number of steps:", self._n_steps)
|
||||||
self._layout.addRow("System:", self._system)
|
self._layout.addRow("System:", self._system)
|
||||||
self._layout.addRow("Constraints:", self._constraints)
|
self._layout.addRow("Constraints:", self._constraints)
|
||||||
|
|
@ -396,17 +437,20 @@ class TransientCaseForm(CaseFormBase):
|
||||||
self._layout.addRow("Rayleigh αM:", self._alpha_m)
|
self._layout.addRow("Rayleigh αM:", self._alpha_m)
|
||||||
self._layout.addRow("Rayleigh βK:", self._beta_k)
|
self._layout.addRow("Rayleigh βK:", self._beta_k)
|
||||||
self._layout.addRow("Mode-1 damping ratio:", self._mode1_damping)
|
self._layout.addRow("Mode-1 damping ratio:", self._mode1_damping)
|
||||||
self._layout.addRow(QLabel(
|
self._layout.addRow(
|
||||||
"<i>If mode-1 damping is > 0, the runner computes βK = 2ζ/√λ1 "
|
QLabel(
|
||||||
"after preload and uses it instead of the manual βK value.</i>"
|
"<i>If mode-1 damping is > 0, the runner computes βK = 2ζ/√λ1 "
|
||||||
))
|
"after preload and uses it instead of the manual βK value.</i>"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def _populate_specific(self, c: TransientCase) -> None:
|
def _populate_specific(self, c: TransientCase) -> None:
|
||||||
_select_pattern_ids(self._patterns_picker, c.pattern_ids)
|
_select_pattern_ids(self._patterns_picker, c.pattern_ids)
|
||||||
_set_factor_spins(self._pattern_factor_spins, getattr(c, "pattern_factors", {}) or {})
|
_set_factor_spins(self._pattern_factor_spins, getattr(c, "pattern_factors", {}) or {})
|
||||||
_select_case_ids(self._preload_picker, c.preload_case_ids)
|
_select_case_ids(self._preload_picker, c.preload_case_ids)
|
||||||
_select_pattern_ids(self._remove_patterns_picker, c.remove_patterns)
|
_select_pattern_ids(self._remove_patterns_picker, c.remove_patterns)
|
||||||
self._dt.setValue(c.dt); self._n_steps.setValue(c.n_steps)
|
self._dt.setValue(c.dt)
|
||||||
|
self._n_steps.setValue(c.n_steps)
|
||||||
self._system.setCurrentText(c.system)
|
self._system.setCurrentText(c.system)
|
||||||
self._constraints.setCurrentText(c.constraints)
|
self._constraints.setCurrentText(c.constraints)
|
||||||
self._integrator.setCurrentText(c.integrator)
|
self._integrator.setCurrentText(c.integrator)
|
||||||
|
|
@ -423,12 +467,14 @@ class TransientCaseForm(CaseFormBase):
|
||||||
def _read_specific(self, cid: int) -> TransientCase:
|
def _read_specific(self, cid: int) -> TransientCase:
|
||||||
mode1_damping = self._mode1_damping.value()
|
mode1_damping = self._mode1_damping.value()
|
||||||
return TransientCase(
|
return TransientCase(
|
||||||
id=cid, name=self._name_edit.text(),
|
id=cid,
|
||||||
|
name=self._name_edit.text(),
|
||||||
pattern_ids=_require_pattern_ids(self._patterns_picker),
|
pattern_ids=_require_pattern_ids(self._patterns_picker),
|
||||||
**_factor_kwargs(TransientCase, self._patterns_picker, self._pattern_factor_spins),
|
**_factor_kwargs(TransientCase, self._patterns_picker, self._pattern_factor_spins),
|
||||||
preload_case_ids=_selected_case_ids(self._preload_picker),
|
preload_case_ids=_selected_case_ids(self._preload_picker),
|
||||||
remove_patterns=_selected_pattern_ids(self._remove_patterns_picker),
|
remove_patterns=_selected_pattern_ids(self._remove_patterns_picker),
|
||||||
dt=self._dt.value(), n_steps=self._n_steps.value(),
|
dt=self._dt.value(),
|
||||||
|
n_steps=self._n_steps.value(),
|
||||||
system=self._system.currentText(),
|
system=self._system.currentText(),
|
||||||
constraints=self._constraints.currentText(),
|
constraints=self._constraints.currentText(),
|
||||||
integrator=self._integrator.currentText(),
|
integrator=self._integrator.currentText(),
|
||||||
|
|
@ -451,8 +497,10 @@ class PushoverCaseForm(CaseFormBase):
|
||||||
patterns: list[LoadPattern],
|
patterns: list[LoadPattern],
|
||||||
analyses: list[AnalysisCase],
|
analyses: list[AnalysisCase],
|
||||||
parent: QWidget | None = None,
|
parent: QWidget | None = None,
|
||||||
|
*,
|
||||||
|
units: UnitSystem = UnitSystem.METRIC,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(patterns, analyses, parent)
|
super().__init__(patterns, analyses, parent, units=units)
|
||||||
self._patterns_picker, self._pattern_factor_spins = _make_pattern_picker_with_factors(
|
self._patterns_picker, self._pattern_factor_spins = _make_pattern_picker_with_factors(
|
||||||
patterns
|
patterns
|
||||||
)
|
)
|
||||||
|
|
@ -462,12 +510,18 @@ class PushoverCaseForm(CaseFormBase):
|
||||||
self._target = _spin(0.1, decimals=6, minimum=-1e6, maximum=1e6, step=0.001)
|
self._target = _spin(0.1, decimals=6, minimum=-1e6, maximum=1e6, step=0.001)
|
||||||
self._step = _spin(0.001, decimals=8, minimum=1e-12, step=1e-4)
|
self._step = _spin(0.001, decimals=8, minimum=1e-12, step=1e-4)
|
||||||
self._base_nodes = QLineEdit()
|
self._base_nodes = QLineEdit()
|
||||||
self._base_nodes.setPlaceholderText("comma-separated node ids (leave blank for all supports)")
|
self._base_nodes.setPlaceholderText(
|
||||||
self._system = QComboBox(); self._system.addItems(_STATIC_SYSTEMS)
|
"comma-separated node ids (leave blank for all supports)"
|
||||||
self._constraints = QComboBox(); self._constraints.addItems(_CONSTRAINTS)
|
)
|
||||||
self._algorithm = QComboBox(); self._algorithm.addItems(_ALGORITHMS)
|
self._system = QComboBox()
|
||||||
|
self._system.addItems(_STATIC_SYSTEMS)
|
||||||
|
self._constraints = QComboBox()
|
||||||
|
self._constraints.addItems(_CONSTRAINTS)
|
||||||
|
self._algorithm = QComboBox()
|
||||||
|
self._algorithm.addItems(_ALGORITHMS)
|
||||||
self._algorithm.setCurrentText("Newton")
|
self._algorithm.setCurrentText("Newton")
|
||||||
self._test = QComboBox(); self._test.addItems(_TESTS)
|
self._test = QComboBox()
|
||||||
|
self._test.addItems(_TESTS)
|
||||||
self._tol = _spin(1e-6, decimals=12, minimum=1e-15, step=1e-7)
|
self._tol = _spin(1e-6, decimals=12, minimum=1e-15, step=1e-7)
|
||||||
self._max_iter = _int_spin(25)
|
self._max_iter = _int_spin(25)
|
||||||
|
|
||||||
|
|
@ -475,14 +529,20 @@ class PushoverCaseForm(CaseFormBase):
|
||||||
self._layout.addRow(self._patterns_picker)
|
self._layout.addRow(self._patterns_picker)
|
||||||
self._layout.addRow(QLabel("<b>Preload static cases (optional):</b>"))
|
self._layout.addRow(QLabel("<b>Preload static cases (optional):</b>"))
|
||||||
self._layout.addRow(self._preload_picker)
|
self._layout.addRow(self._preload_picker)
|
||||||
self._layout.addRow(QLabel(
|
self._layout.addRow(
|
||||||
"<i>Run these Static cases first, then hold them constant via "
|
QLabel(
|
||||||
"loadConst -time 0.0 before the pushover starts.</i>"
|
"<i>Run these Static cases first, then hold them constant via "
|
||||||
))
|
"loadConst -time 0.0 before the pushover starts.</i>"
|
||||||
|
)
|
||||||
|
)
|
||||||
self._layout.addRow("Control node:", self._control_node)
|
self._layout.addRow("Control node:", self._control_node)
|
||||||
self._layout.addRow("Control DOF:", self._control_dof)
|
self._layout.addRow("Control DOF:", self._control_dof)
|
||||||
self._layout.addRow("Target displacement:", self._target)
|
disp_u = _u(self._units, "displacement")
|
||||||
self._layout.addRow("Step size:", self._step)
|
self._layout.addRow(
|
||||||
|
f"Target displacement ({disp_u}):" if disp_u else "Target displacement:",
|
||||||
|
self._target,
|
||||||
|
)
|
||||||
|
self._layout.addRow(f"Step size ({disp_u}):" if disp_u else "Step size:", self._step)
|
||||||
self._layout.addRow("Base nodes:", self._base_nodes)
|
self._layout.addRow("Base nodes:", self._base_nodes)
|
||||||
self._layout.addRow("System:", self._system)
|
self._layout.addRow("System:", self._system)
|
||||||
self._layout.addRow("Constraints:", self._constraints)
|
self._layout.addRow("Constraints:", self._constraints)
|
||||||
|
|
@ -511,7 +571,8 @@ class PushoverCaseForm(CaseFormBase):
|
||||||
txt = self._base_nodes.text().strip()
|
txt = self._base_nodes.text().strip()
|
||||||
base_ids = [int(x) for x in txt.replace(",", " ").split() if x] if txt else []
|
base_ids = [int(x) for x in txt.replace(",", " ").split() if x] if txt else []
|
||||||
return PushoverCase(
|
return PushoverCase(
|
||||||
id=cid, name=self._name_edit.text(),
|
id=cid,
|
||||||
|
name=self._name_edit.text(),
|
||||||
pattern_ids=_require_pattern_ids(self._patterns_picker),
|
pattern_ids=_require_pattern_ids(self._patterns_picker),
|
||||||
**_factor_kwargs(PushoverCase, self._patterns_picker, self._pattern_factor_spins),
|
**_factor_kwargs(PushoverCase, self._patterns_picker, self._pattern_factor_spins),
|
||||||
preload_case_ids=_selected_case_ids(self._preload_picker),
|
preload_case_ids=_selected_case_ids(self._preload_picker),
|
||||||
|
|
@ -537,8 +598,10 @@ class ResponseSpectrumCaseForm(CaseFormBase):
|
||||||
patterns: list[LoadPattern],
|
patterns: list[LoadPattern],
|
||||||
analyses: list[AnalysisCase],
|
analyses: list[AnalysisCase],
|
||||||
parent: QWidget | None = None,
|
parent: QWidget | None = None,
|
||||||
|
*,
|
||||||
|
units: UnitSystem = UnitSystem.METRIC,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(patterns, analyses, parent)
|
super().__init__(patterns, analyses, parent, units=units)
|
||||||
# Patterns aren't used by RS case but base class wants the param.
|
# Patterns aren't used by RS case but base class wants the param.
|
||||||
self._modal_case = _int_spin(1, minimum=1)
|
self._modal_case = _int_spin(1, minimum=1)
|
||||||
self._spectrum_id = _int_spin(1, minimum=1)
|
self._spectrum_id = _int_spin(1, minimum=1)
|
||||||
|
|
@ -552,10 +615,12 @@ class ResponseSpectrumCaseForm(CaseFormBase):
|
||||||
self._layout.addRow("Direction (DOF):", self._direction)
|
self._layout.addRow("Direction (DOF):", self._direction)
|
||||||
self._layout.addRow("Combination:", self._combination)
|
self._layout.addRow("Combination:", self._combination)
|
||||||
self._layout.addRow("Damping (CQC override):", self._damping)
|
self._layout.addRow("Damping (CQC override):", self._damping)
|
||||||
self._layout.addRow(QLabel(
|
self._layout.addRow(
|
||||||
"<i>Damping is used by CQC modal correlation only; "
|
QLabel(
|
||||||
"leave at 0 to use the spectrum's own damping ratio.</i>",
|
"<i>Damping is used by CQC modal correlation only; "
|
||||||
))
|
"leave at 0 to use the spectrum's own damping ratio.</i>",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def _populate_specific(self, c: ResponseSpectrumCase) -> None:
|
def _populate_specific(self, c: ResponseSpectrumCase) -> None:
|
||||||
self._modal_case.setValue(c.modal_case_id)
|
self._modal_case.setValue(c.modal_case_id)
|
||||||
|
|
@ -568,7 +633,8 @@ class ResponseSpectrumCaseForm(CaseFormBase):
|
||||||
def _read_specific(self, cid: int) -> ResponseSpectrumCase:
|
def _read_specific(self, cid: int) -> ResponseSpectrumCase:
|
||||||
damp_val = self._damping.value()
|
damp_val = self._damping.value()
|
||||||
return ResponseSpectrumCase(
|
return ResponseSpectrumCase(
|
||||||
id=cid, name=self._name_edit.text(),
|
id=cid,
|
||||||
|
name=self._name_edit.text(),
|
||||||
modal_case_id=self._modal_case.value(),
|
modal_case_id=self._modal_case.value(),
|
||||||
spectrum_id=self._spectrum_id.value(),
|
spectrum_id=self._spectrum_id.value(),
|
||||||
direction=self._direction.value(),
|
direction=self._direction.value(),
|
||||||
|
|
@ -591,8 +657,10 @@ def form_for(
|
||||||
case: AnalysisCase,
|
case: AnalysisCase,
|
||||||
patterns: list[LoadPattern],
|
patterns: list[LoadPattern],
|
||||||
analyses: list[AnalysisCase],
|
analyses: list[AnalysisCase],
|
||||||
|
*,
|
||||||
|
units: UnitSystem = UnitSystem.METRIC,
|
||||||
) -> CaseFormBase:
|
) -> CaseFormBase:
|
||||||
cls = FORM_REGISTRY[case.type]
|
cls = FORM_REGISTRY[case.type]
|
||||||
form = cls(patterns, analyses)
|
form = cls(patterns, analyses, units=units)
|
||||||
form.populate(case)
|
form.populate(case)
|
||||||
return form
|
return form
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,13 @@ from otko.commands import (
|
||||||
UpdateAnalysisCaseCommand,
|
UpdateAnalysisCaseCommand,
|
||||||
)
|
)
|
||||||
from otko.core import (
|
from otko.core import (
|
||||||
ModalCase, PushoverCase, ResponseSpectrumCase, StaticCase, TransientCase,
|
ModalCase,
|
||||||
|
PushoverCase,
|
||||||
|
ResponseSpectrumCase,
|
||||||
|
StaticCase,
|
||||||
|
TransientCase,
|
||||||
)
|
)
|
||||||
|
from otko.core.units import UnitSystem
|
||||||
from otko.viewmodels import ProjectViewModel
|
from otko.viewmodels import ProjectViewModel
|
||||||
from otko.views.dialogs.case_forms import FORM_REGISTRY, form_for
|
from otko.views.dialogs.case_forms import FORM_REGISTRY, form_for
|
||||||
|
|
||||||
|
|
@ -56,16 +61,23 @@ def _default_case(kind: str, cid: int, project: Project): # type: ignore[no-unt
|
||||||
return ModalCase(id=cid, name="Modal", n_modes=3)
|
return ModalCase(id=cid, name="Modal", n_modes=3)
|
||||||
if kind == "Transient":
|
if kind == "Transient":
|
||||||
return TransientCase(
|
return TransientCase(
|
||||||
id=cid, name="Transient", pattern_ids=[_first_pattern_id(project)],
|
id=cid,
|
||||||
dt=0.01, n_steps=1000,
|
name="Transient",
|
||||||
|
pattern_ids=[_first_pattern_id(project)],
|
||||||
|
dt=0.01,
|
||||||
|
n_steps=1000,
|
||||||
)
|
)
|
||||||
if kind == "Pushover":
|
if kind == "Pushover":
|
||||||
if not project.nodes:
|
if not project.nodes:
|
||||||
raise ValueError("Define at least one node before adding a Pushover case.")
|
raise ValueError("Define at least one node before adding a Pushover case.")
|
||||||
return PushoverCase(
|
return PushoverCase(
|
||||||
id=cid, name="Pushover", pattern_ids=[_first_pattern_id(project)],
|
id=cid,
|
||||||
control_node=project.nodes[0].id, control_dof=1,
|
name="Pushover",
|
||||||
target_disp=0.1, step_size=0.001,
|
pattern_ids=[_first_pattern_id(project)],
|
||||||
|
control_node=project.nodes[0].id,
|
||||||
|
control_dof=1,
|
||||||
|
target_disp=0.1,
|
||||||
|
step_size=0.001,
|
||||||
)
|
)
|
||||||
if kind == "ResponseSpectrum":
|
if kind == "ResponseSpectrum":
|
||||||
modal_ids = [c.id for c in project.analyses if isinstance(c, ModalCase)]
|
modal_ids = [c.id for c in project.analyses if isinstance(c, ModalCase)]
|
||||||
|
|
@ -73,13 +85,15 @@ def _default_case(kind: str, cid: int, project: Project): # type: ignore[no-unt
|
||||||
raise ValueError("Add a Modal case before adding a ResponseSpectrum case.")
|
raise ValueError("Add a Modal case before adding a ResponseSpectrum case.")
|
||||||
if not project.spectra:
|
if not project.spectra:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Define at least one response spectrum before adding "
|
"Define at least one response spectrum before adding a ResponseSpectrum case."
|
||||||
"a ResponseSpectrum case."
|
|
||||||
)
|
)
|
||||||
return ResponseSpectrumCase(
|
return ResponseSpectrumCase(
|
||||||
id=cid, name="ResponseSpectrum",
|
id=cid,
|
||||||
modal_case_id=modal_ids[0], spectrum_id=project.spectra[0].id,
|
name="ResponseSpectrum",
|
||||||
direction=1, combination="SRSS",
|
modal_case_id=modal_ids[0],
|
||||||
|
spectrum_id=project.spectra[0].id,
|
||||||
|
direction=1,
|
||||||
|
combination="SRSS",
|
||||||
)
|
)
|
||||||
raise ValueError(f"Unknown analysis type: {kind}.")
|
raise ValueError(f"Unknown analysis type: {kind}.")
|
||||||
|
|
||||||
|
|
@ -150,7 +164,7 @@ class AnalysisCaseManagerDialog(QDialog):
|
||||||
if self._list.currentItem() is not None:
|
if self._list.currentItem() is not None:
|
||||||
selected_id = self._list.currentItem().data(Qt.ItemDataRole.UserRole)
|
selected_id = self._list.currentItem().data(Qt.ItemDataRole.UserRole)
|
||||||
self._list.clear()
|
self._list.clear()
|
||||||
for c in (self._vm.project.analyses if self._vm.project else []):
|
for c in self._vm.project.analyses if self._vm.project else []:
|
||||||
label = f"#{c.id} {c.name or '(unnamed)'} [{c.type}]"
|
label = f"#{c.id} {c.name or '(unnamed)'} [{c.type}]"
|
||||||
item = QListWidgetItem(label)
|
item = QListWidgetItem(label)
|
||||||
item.setData(Qt.ItemDataRole.UserRole, c.id)
|
item.setData(Qt.ItemDataRole.UserRole, c.id)
|
||||||
|
|
@ -170,6 +184,11 @@ class AnalysisCaseManagerDialog(QDialog):
|
||||||
cid = item.data(Qt.ItemDataRole.UserRole)
|
cid = item.data(Qt.ItemDataRole.UserRole)
|
||||||
return next((c for c in self._vm.project.analyses if c.id == cid), None)
|
return next((c for c in self._vm.project.analyses if c.id == cid), None)
|
||||||
|
|
||||||
|
def _project_units(self) -> UnitSystem:
|
||||||
|
"""Project display units (Metric default when no project is open)."""
|
||||||
|
proj = self._vm.project
|
||||||
|
return proj.meta.units if proj is not None else UnitSystem.METRIC
|
||||||
|
|
||||||
def _on_row_changed(self, _row: int) -> None:
|
def _on_row_changed(self, _row: int) -> None:
|
||||||
case = self._selected_case()
|
case = self._selected_case()
|
||||||
if case is None or self._vm.project is None:
|
if case is None or self._vm.project is None:
|
||||||
|
|
@ -178,7 +197,12 @@ class AnalysisCaseManagerDialog(QDialog):
|
||||||
old = self._stack.widget(0)
|
old = self._stack.widget(0)
|
||||||
self._stack.removeWidget(old)
|
self._stack.removeWidget(old)
|
||||||
old.deleteLater()
|
old.deleteLater()
|
||||||
form = form_for(case, self._vm.project.load_patterns, self._vm.project.analyses)
|
form = form_for(
|
||||||
|
case,
|
||||||
|
self._vm.project.load_patterns,
|
||||||
|
self._vm.project.analyses,
|
||||||
|
units=self._project_units(),
|
||||||
|
)
|
||||||
self._stack.addWidget(form)
|
self._stack.addWidget(form)
|
||||||
self._stack.setCurrentWidget(form)
|
self._stack.setCurrentWidget(form)
|
||||||
self._type_label.setText(form.type_label)
|
self._type_label.setText(form.type_label)
|
||||||
|
|
@ -201,22 +225,31 @@ class AnalysisCaseManagerDialog(QDialog):
|
||||||
# patterns). Do NOT fall through to the general type picker —
|
# patterns). Do NOT fall through to the general type picker —
|
||||||
# Static/Transient/Pushover would get dangling pattern refs.
|
# Static/Transient/Pushover would get dangling pattern refs.
|
||||||
QMessageBox.information(
|
QMessageBox.information(
|
||||||
self, "No patterns",
|
self,
|
||||||
|
"No patterns",
|
||||||
"No load patterns defined yet — only a Modal case can be "
|
"No load patterns defined yet — only a Modal case can be "
|
||||||
"added (Modal works without patterns). Define a pattern "
|
"added (Modal works without patterns). Define a pattern "
|
||||||
"first for Static, Transient, or Pushover cases.",
|
"first for Static, Transient, or Pushover cases.",
|
||||||
)
|
)
|
||||||
kind, ok = QInputDialog.getItem(
|
kind, ok = QInputDialog.getItem(
|
||||||
self, "Add analysis case", "Type:",
|
self,
|
||||||
["Modal"], current=0, editable=False,
|
"Add analysis case",
|
||||||
|
"Type:",
|
||||||
|
["Modal"],
|
||||||
|
current=0,
|
||||||
|
editable=False,
|
||||||
)
|
)
|
||||||
if not ok:
|
if not ok:
|
||||||
return
|
return
|
||||||
self._create_case_of_kind("Modal")
|
self._create_case_of_kind("Modal")
|
||||||
return
|
return
|
||||||
kind, ok = QInputDialog.getItem(
|
kind, ok = QInputDialog.getItem(
|
||||||
self, "Add analysis case", "Type:",
|
self,
|
||||||
list(_DEFAULTS.keys()), current=0, editable=False,
|
"Add analysis case",
|
||||||
|
"Type:",
|
||||||
|
list(_DEFAULTS.keys()),
|
||||||
|
current=0,
|
||||||
|
editable=False,
|
||||||
)
|
)
|
||||||
if not ok:
|
if not ok:
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ from otko.core.geometry import (
|
||||||
CoordinateSystem,
|
CoordinateSystem,
|
||||||
GridSystem,
|
GridSystem,
|
||||||
)
|
)
|
||||||
|
from otko.core.units import UnitSystem, native_label
|
||||||
from otko.views.dialogs.grid_system import (
|
from otko.views.dialogs.grid_system import (
|
||||||
_coords_from_spacings,
|
_coords_from_spacings,
|
||||||
_parse_spacings,
|
_parse_spacings,
|
||||||
|
|
@ -58,14 +59,16 @@ class CoordSystemDataDialog(QDialog):
|
||||||
existing: CoordinateGridSystem | None = None,
|
existing: CoordinateGridSystem | None = None,
|
||||||
is_global: bool = False,
|
is_global: bool = False,
|
||||||
parent: QWidget | None = None,
|
parent: QWidget | None = None,
|
||||||
|
*,
|
||||||
|
units: UnitSystem = UnitSystem.METRIC,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setWindowTitle(
|
self.setWindowTitle(
|
||||||
"Edit Coordinate/Grid System"
|
"Edit Coordinate/Grid System" if existing is not None else "Add Coordinate/Grid System"
|
||||||
if existing is not None else "Add Coordinate/Grid System"
|
|
||||||
)
|
)
|
||||||
self._is_global = is_global
|
self._is_global = is_global
|
||||||
self._existing = existing
|
self._existing = existing
|
||||||
|
self._units = units
|
||||||
self._build_ui()
|
self._build_ui()
|
||||||
if existing is not None:
|
if existing is not None:
|
||||||
self._load_existing(existing)
|
self._load_existing(existing)
|
||||||
|
|
@ -88,28 +91,37 @@ class CoordSystemDataDialog(QDialog):
|
||||||
loc_box = QGroupBox("Location and Orientation (relative to Global)")
|
loc_box = QGroupBox("Location and Orientation (relative to Global)")
|
||||||
loc_form = QFormLayout(loc_box)
|
loc_form = QFormLayout(loc_box)
|
||||||
|
|
||||||
self._ox = self._spin(); self._oy = self._spin(); self._oz = self._spin()
|
self._ox = self._spin()
|
||||||
|
self._oy = self._spin()
|
||||||
|
self._oz = self._spin()
|
||||||
|
try:
|
||||||
|
length_u = native_label(self._units, "length")
|
||||||
|
except KeyError:
|
||||||
|
length_u = ""
|
||||||
origin_row = QHBoxLayout()
|
origin_row = QHBoxLayout()
|
||||||
for label, w in (("X", self._ox), ("Y", self._oy), ("Z", self._oz)):
|
for label, w in (("X", self._ox), ("Y", self._oy), ("Z", self._oz)):
|
||||||
origin_row.addWidget(QLabel(f"{label}:"))
|
origin_row.addWidget(QLabel(f"{label} ({length_u}):" if length_u else f"{label}:"))
|
||||||
origin_row.addWidget(w)
|
origin_row.addWidget(w)
|
||||||
origin_wrap = QWidget(); origin_wrap.setLayout(origin_row)
|
origin_wrap = QWidget()
|
||||||
|
origin_wrap.setLayout(origin_row)
|
||||||
loc_form.addRow("Origin:", origin_wrap)
|
loc_form.addRow("Origin:", origin_wrap)
|
||||||
|
|
||||||
self._rx = self._rot_spin(); self._ry = self._rot_spin(); self._rz = self._rot_spin()
|
self._rx = self._rot_spin()
|
||||||
|
self._ry = self._rot_spin()
|
||||||
|
self._rz = self._rot_spin()
|
||||||
rot_row = QHBoxLayout()
|
rot_row = QHBoxLayout()
|
||||||
for label, w in (("about X", self._rx), ("about Y", self._ry), ("about Z", self._rz)):
|
for label, w in (("about X", self._rx), ("about Y", self._ry), ("about Z", self._rz)):
|
||||||
rot_row.addWidget(QLabel(f"{label}:"))
|
rot_row.addWidget(QLabel(f"{label}:"))
|
||||||
rot_row.addWidget(w)
|
rot_row.addWidget(w)
|
||||||
rot_wrap = QWidget(); rot_wrap.setLayout(rot_row)
|
rot_wrap = QWidget()
|
||||||
|
rot_wrap.setLayout(rot_row)
|
||||||
loc_form.addRow("Rotation (deg):", rot_wrap)
|
loc_form.addRow("Rotation (deg):", rot_wrap)
|
||||||
|
|
||||||
if self._is_global:
|
if self._is_global:
|
||||||
for w in (self._ox, self._oy, self._oz, self._rx, self._ry, self._rz):
|
for w in (self._ox, self._oy, self._oz, self._rx, self._ry, self._rz):
|
||||||
w.setEnabled(False)
|
w.setEnabled(False)
|
||||||
loc_box.setToolTip(
|
loc_box.setToolTip(
|
||||||
"Global system is anchored at the world origin "
|
"Global system is anchored at the world origin with identity orientation.",
|
||||||
"with identity orientation.",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
layout.addWidget(loc_box)
|
layout.addWidget(loc_box)
|
||||||
|
|
@ -131,9 +143,9 @@ class CoordSystemDataDialog(QDialog):
|
||||||
self._x_edit = QLineEdit("5")
|
self._x_edit = QLineEdit("5")
|
||||||
self._y_edit = QLineEdit("5")
|
self._y_edit = QLineEdit("5")
|
||||||
self._z_edit = QLineEdit("")
|
self._z_edit = QLineEdit("")
|
||||||
grid_form.addRow("X spacings:", self._x_edit)
|
grid_form.addRow(f"X spacings ({length_u}):" if length_u else "X spacings:", self._x_edit)
|
||||||
grid_form.addRow("Y spacings:", self._y_edit)
|
grid_form.addRow(f"Y spacings ({length_u}):" if length_u else "Y spacings:", self._y_edit)
|
||||||
grid_form.addRow("Z spacings:", self._z_edit)
|
grid_form.addRow(f"Z spacings ({length_u}):" if length_u else "Z spacings:", self._z_edit)
|
||||||
|
|
||||||
self._visible_cb = QCheckBox("Visible in 3D view")
|
self._visible_cb = QCheckBox("Visible in 3D view")
|
||||||
self._visible_cb.setChecked(True)
|
self._visible_cb.setChecked(True)
|
||||||
|
|
@ -160,8 +172,7 @@ class CoordSystemDataDialog(QDialog):
|
||||||
|
|
||||||
# ── Buttons ──────────────────────────────────────────────
|
# ── Buttons ──────────────────────────────────────────────
|
||||||
buttons = QDialogButtonBox(
|
buttons = QDialogButtonBox(
|
||||||
QDialogButtonBox.StandardButton.Ok
|
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel,
|
||||||
| QDialogButtonBox.StandardButton.Cancel,
|
|
||||||
)
|
)
|
||||||
buttons.accepted.connect(self._on_accept)
|
buttons.accepted.connect(self._on_accept)
|
||||||
buttons.rejected.connect(self.reject)
|
buttons.rejected.connect(self.reject)
|
||||||
|
|
@ -189,9 +200,13 @@ class CoordSystemDataDialog(QDialog):
|
||||||
def _load_existing(self, cs: CoordinateGridSystem) -> None:
|
def _load_existing(self, cs: CoordinateGridSystem) -> None:
|
||||||
self._name_edit.setText(cs.name)
|
self._name_edit.setText(cs.name)
|
||||||
ox, oy, oz = cs.coord.origin
|
ox, oy, oz = cs.coord.origin
|
||||||
self._ox.setValue(ox); self._oy.setValue(oy); self._oz.setValue(oz)
|
self._ox.setValue(ox)
|
||||||
|
self._oy.setValue(oy)
|
||||||
|
self._oz.setValue(oz)
|
||||||
rx, ry, rz = cs.coord.rotation_deg
|
rx, ry, rz = cs.coord.rotation_deg
|
||||||
self._rx.setValue(rx); self._ry.setValue(ry); self._rz.setValue(rz)
|
self._rx.setValue(rx)
|
||||||
|
self._ry.setValue(ry)
|
||||||
|
self._rz.setValue(rz)
|
||||||
self._x_edit.setText(_spacings_text(cs.grid.x_lines))
|
self._x_edit.setText(_spacings_text(cs.grid.x_lines))
|
||||||
self._y_edit.setText(_spacings_text(cs.grid.y_lines))
|
self._y_edit.setText(_spacings_text(cs.grid.y_lines))
|
||||||
self._z_edit.setText(_spacings_text(cs.grid.z_lines))
|
self._z_edit.setText(_spacings_text(cs.grid.z_lines))
|
||||||
|
|
@ -245,9 +260,7 @@ class CoordinateGridSystemsDialog(QDialog):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setWindowTitle("Coordinate/Grid Systems")
|
self.setWindowTitle("Coordinate/Grid Systems")
|
||||||
self.resize(600, 420)
|
self.resize(600, 420)
|
||||||
self._systems: list[CoordinateGridSystem] = [
|
self._systems: list[CoordinateGridSystem] = [s.model_copy(deep=True) for s in systems]
|
||||||
s.model_copy(deep=True) for s in systems
|
|
||||||
]
|
|
||||||
self._build_ui()
|
self._build_ui()
|
||||||
self._refresh_list()
|
self._refresh_list()
|
||||||
|
|
||||||
|
|
@ -290,8 +303,7 @@ class CoordinateGridSystemsDialog(QDialog):
|
||||||
right.addStretch(1)
|
right.addStretch(1)
|
||||||
|
|
||||||
buttons = QDialogButtonBox(
|
buttons = QDialogButtonBox(
|
||||||
QDialogButtonBox.StandardButton.Ok
|
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel,
|
||||||
| QDialogButtonBox.StandardButton.Cancel,
|
|
||||||
)
|
)
|
||||||
buttons.accepted.connect(self.accept)
|
buttons.accepted.connect(self.accept)
|
||||||
buttons.rejected.connect(self.reject)
|
buttons.rejected.connect(self.reject)
|
||||||
|
|
@ -332,9 +344,7 @@ class CoordinateGridSystemsDialog(QDialog):
|
||||||
self._btn_modify.setEnabled(cs is not None)
|
self._btn_modify.setEnabled(cs is not None)
|
||||||
self._cb_general.blockSignals(True)
|
self._cb_general.blockSignals(True)
|
||||||
self._cb_general.setChecked(cs.grid.is_general if cs is not None else False)
|
self._cb_general.setChecked(cs.grid.is_general if cs is not None else False)
|
||||||
self._cb_general.setEnabled(
|
self._cb_general.setEnabled(cs is not None and not cs.grid.is_general)
|
||||||
cs is not None and not cs.grid.is_general
|
|
||||||
)
|
|
||||||
self._cb_general.blockSignals(False)
|
self._cb_general.blockSignals(False)
|
||||||
|
|
||||||
# ── actions ──────────────────────────────────────────────────
|
# ── actions ──────────────────────────────────────────────────
|
||||||
|
|
@ -351,14 +361,17 @@ class CoordinateGridSystemsDialog(QDialog):
|
||||||
from otko.views.dialogs.define_grid_data import (
|
from otko.views.dialogs.define_grid_data import (
|
||||||
DefineGridSystemDataDialog,
|
DefineGridSystemDataDialog,
|
||||||
)
|
)
|
||||||
|
|
||||||
dlg = DefineGridSystemDataDialog(parent=self)
|
dlg = DefineGridSystemDataDialog(parent=self)
|
||||||
if dlg.exec() != QDialog.DialogCode.Accepted:
|
if dlg.exec() != QDialog.DialogCode.Accepted:
|
||||||
return
|
return
|
||||||
new_sys = dlg.system()
|
new_sys = dlg.system()
|
||||||
if any(s.name == new_sys.name for s in self._systems):
|
if any(s.name == new_sys.name for s in self._systems):
|
||||||
new_sys = new_sys.model_copy(update={
|
new_sys = new_sys.model_copy(
|
||||||
"name": self._unique_name(new_sys.name),
|
update={
|
||||||
})
|
"name": self._unique_name(new_sys.name),
|
||||||
|
}
|
||||||
|
)
|
||||||
self._systems.append(new_sys)
|
self._systems.append(new_sys)
|
||||||
self._refresh_list(select_name=new_sys.name)
|
self._refresh_list(select_name=new_sys.name)
|
||||||
|
|
||||||
|
|
@ -380,8 +393,11 @@ class CoordinateGridSystemsDialog(QDialog):
|
||||||
from otko.views.dialogs.define_grid_data import (
|
from otko.views.dialogs.define_grid_data import (
|
||||||
DefineGridSystemDataDialog,
|
DefineGridSystemDataDialog,
|
||||||
)
|
)
|
||||||
|
|
||||||
dlg = DefineGridSystemDataDialog(
|
dlg = DefineGridSystemDataDialog(
|
||||||
existing=cs, is_global=cs.is_global(), parent=self,
|
existing=cs,
|
||||||
|
is_global=cs.is_global(),
|
||||||
|
parent=self,
|
||||||
)
|
)
|
||||||
if dlg.exec() != QDialog.DialogCode.Accepted:
|
if dlg.exec() != QDialog.DialogCode.Accepted:
|
||||||
return
|
return
|
||||||
|
|
@ -392,9 +408,11 @@ class CoordinateGridSystemsDialog(QDialog):
|
||||||
# Enforce unique names against siblings.
|
# Enforce unique names against siblings.
|
||||||
others = [s for s in self._systems if s is not cs]
|
others = [s for s in self._systems if s is not cs]
|
||||||
if any(s.name == updated.name for s in others):
|
if any(s.name == updated.name for s in others):
|
||||||
updated = updated.model_copy(update={
|
updated = updated.model_copy(
|
||||||
"name": self._unique_name(updated.name),
|
update={
|
||||||
})
|
"name": self._unique_name(updated.name),
|
||||||
|
}
|
||||||
|
)
|
||||||
row = self._list.currentRow()
|
row = self._list.currentRow()
|
||||||
self._systems[row] = updated
|
self._systems[row] = updated
|
||||||
self._refresh_list(select_name=updated.name)
|
self._refresh_list(select_name=updated.name)
|
||||||
|
|
@ -414,7 +432,8 @@ class CoordinateGridSystemsDialog(QDialog):
|
||||||
if cs.grid.is_general and not checked:
|
if cs.grid.is_general and not checked:
|
||||||
# SAP2000: once converted to General, can't convert back.
|
# SAP2000: once converted to General, can't convert back.
|
||||||
QMessageBox.information(
|
QMessageBox.information(
|
||||||
self, "Convert to General",
|
self,
|
||||||
|
"Convert to General",
|
||||||
"Once a system is converted to General, it cannot be "
|
"Once a system is converted to General, it cannot be "
|
||||||
"converted back to a regular Cartesian system.",
|
"converted back to a regular Cartesian system.",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,15 @@ from otko.core.geometry import (
|
||||||
GridLine,
|
GridLine,
|
||||||
GridSystem,
|
GridSystem,
|
||||||
)
|
)
|
||||||
|
from otko.core.units import UnitSystem, native_label
|
||||||
|
|
||||||
|
|
||||||
|
def _length_unit(units: UnitSystem) -> str:
|
||||||
|
"""Display label for ordinate/spacing values ("" when unknown)."""
|
||||||
|
try:
|
||||||
|
return native_label(units, "length")
|
||||||
|
except KeyError:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
_LINE_TYPE_CHOICES = ["Primary", "Secondary"]
|
_LINE_TYPE_CHOICES = ["Primary", "Secondary"]
|
||||||
|
|
@ -62,18 +71,33 @@ class _AxisGridTable(QWidget):
|
||||||
|
|
||||||
COLUMNS = ["Grid ID", "Ordinate", "Line Type", "Visibility", "Bubble Loc", "Color"]
|
COLUMNS = ["Grid ID", "Ordinate", "Line Type", "Visibility", "Bubble Loc", "Color"]
|
||||||
|
|
||||||
def __init__(self, axis: str, parent: QWidget | None = None) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
axis: str,
|
||||||
|
parent: QWidget | None = None,
|
||||||
|
*,
|
||||||
|
units: UnitSystem = UnitSystem.METRIC,
|
||||||
|
) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.axis = axis
|
self.axis = axis
|
||||||
self._show_spacing = False
|
self._show_spacing = False
|
||||||
|
self._units = units
|
||||||
self._build_ui()
|
self._build_ui()
|
||||||
|
|
||||||
|
def _columns(self) -> list[str]:
|
||||||
|
"""Column headers with the length unit on Ordinate/Spacing."""
|
||||||
|
cols = self.COLUMNS.copy()
|
||||||
|
length_u = _length_unit(self._units)
|
||||||
|
base = "Spacing" if self._show_spacing else "Ordinate"
|
||||||
|
cols[1] = f"{base} ({length_u})" if length_u else base
|
||||||
|
return cols
|
||||||
|
|
||||||
def _build_ui(self) -> None:
|
def _build_ui(self) -> None:
|
||||||
root = QVBoxLayout(self)
|
root = QVBoxLayout(self)
|
||||||
root.setContentsMargins(0, 0, 0, 0)
|
root.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
self._table = QTableWidget(0, len(self.COLUMNS))
|
self._table = QTableWidget(0, len(self.COLUMNS))
|
||||||
self._table.setHorizontalHeaderLabels(self.COLUMNS)
|
self._table.setHorizontalHeaderLabels(self._columns())
|
||||||
self._table.horizontalHeader().setSectionResizeMode(
|
self._table.horizontalHeader().setSectionResizeMode(
|
||||||
QHeaderView.ResizeMode.Interactive,
|
QHeaderView.ResizeMode.Interactive,
|
||||||
)
|
)
|
||||||
|
|
@ -112,9 +136,7 @@ class _AxisGridTable(QWidget):
|
||||||
# Read the current ordinates, then rewrite as spacings (or back).
|
# Read the current ordinates, then rewrite as spacings (or back).
|
||||||
ords = self._collect_ordinates()
|
ords = self._collect_ordinates()
|
||||||
self._show_spacing = as_spacing
|
self._show_spacing = as_spacing
|
||||||
header = self.COLUMNS.copy()
|
self._table.setHorizontalHeaderLabels(self._columns())
|
||||||
header[1] = "Spacing" if as_spacing else "Ordinate"
|
|
||||||
self._table.setHorizontalHeaderLabels(header)
|
|
||||||
for row, v in enumerate(self._iter_display_values(ords)):
|
for row, v in enumerate(self._iter_display_values(ords)):
|
||||||
item = self._table.item(row, 1)
|
item = self._table.item(row, 1)
|
||||||
if item is not None:
|
if item is not None:
|
||||||
|
|
@ -127,9 +149,9 @@ class _AxisGridTable(QWidget):
|
||||||
prev = 0.0
|
prev = 0.0
|
||||||
for i, v in enumerate(ords):
|
for i, v in enumerate(ords):
|
||||||
if i == 0:
|
if i == 0:
|
||||||
out.append(v) # first row: absolute position
|
out.append(v) # first row: absolute position
|
||||||
else:
|
else:
|
||||||
out.append(v - prev) # subsequent rows: spacing from previous
|
out.append(v - prev) # subsequent rows: spacing from previous
|
||||||
prev = v
|
prev = v
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
@ -169,23 +191,28 @@ class _AxisGridTable(QWidget):
|
||||||
visible_widget = self._table.cellWidget(row, 3)
|
visible_widget = self._table.cellWidget(row, 3)
|
||||||
bubble_widget = self._table.cellWidget(row, 4)
|
bubble_widget = self._table.cellWidget(row, 4)
|
||||||
color_item = self._table.item(row, 5)
|
color_item = self._table.item(row, 5)
|
||||||
out.append(GridLine(
|
out.append(
|
||||||
id=(id_item.text().strip() if id_item else f"{self.axis}{row + 1}"),
|
GridLine(
|
||||||
ordinate=ords[row] if row < len(ords) else 0.0,
|
id=(id_item.text().strip() if id_item else f"{self.axis}{row + 1}"),
|
||||||
line_type=(
|
ordinate=ords[row] if row < len(ords) else 0.0,
|
||||||
line_type_widget.currentText() # type: ignore[union-attr]
|
line_type=(
|
||||||
if isinstance(line_type_widget, QComboBox) else "Primary"
|
line_type_widget.currentText() # type: ignore[union-attr]
|
||||||
),
|
if isinstance(line_type_widget, QComboBox)
|
||||||
visible=(
|
else "Primary"
|
||||||
visible_widget.isChecked() # type: ignore[union-attr]
|
),
|
||||||
if isinstance(visible_widget, QCheckBox) else True
|
visible=(
|
||||||
),
|
visible_widget.isChecked() # type: ignore[union-attr]
|
||||||
bubble_loc=(
|
if isinstance(visible_widget, QCheckBox)
|
||||||
bubble_widget.currentText() # type: ignore[union-attr]
|
else True
|
||||||
if isinstance(bubble_widget, QComboBox) else "End"
|
),
|
||||||
),
|
bubble_loc=(
|
||||||
color=(color_item.text() if color_item else "#808080"),
|
bubble_widget.currentText() # type: ignore[union-attr]
|
||||||
))
|
if isinstance(bubble_widget, QComboBox)
|
||||||
|
else "End"
|
||||||
|
),
|
||||||
|
color=(color_item.text() if color_item else "#808080"),
|
||||||
|
)
|
||||||
|
)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
# ── row ops ───────────────────────────────────────────────────
|
# ── row ops ───────────────────────────────────────────────────
|
||||||
|
|
@ -218,7 +245,8 @@ class _AxisGridTable(QWidget):
|
||||||
vis = QCheckBox()
|
vis = QCheckBox()
|
||||||
vis.setChecked(ln.visible)
|
vis.setChecked(ln.visible)
|
||||||
vis_wrap = QWidget()
|
vis_wrap = QWidget()
|
||||||
lay = QHBoxLayout(vis_wrap); lay.setContentsMargins(0, 0, 0, 0)
|
lay = QHBoxLayout(vis_wrap)
|
||||||
|
lay.setContentsMargins(0, 0, 0, 0)
|
||||||
lay.addWidget(vis)
|
lay.addWidget(vis)
|
||||||
lay.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
lay.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
# We keep the QCheckBox directly; the wrap is optional visual
|
# We keep the QCheckBox directly; the wrap is optional visual
|
||||||
|
|
@ -250,13 +278,13 @@ class _AxisGridTable(QWidget):
|
||||||
lines = self.collect_lines()
|
lines = self.collect_lines()
|
||||||
lines.sort(key=lambda ln: ln.ordinate)
|
lines.sort(key=lambda ln: ln.ordinate)
|
||||||
self._table.setRowCount(0)
|
self._table.setRowCount(0)
|
||||||
self._show_spacing = False # reset to ordinate view after sort
|
self._show_spacing = False # reset to ordinate view after sort
|
||||||
self._table.setHorizontalHeaderLabels(self.COLUMNS)
|
self._table.setHorizontalHeaderLabels(self._columns())
|
||||||
for ln in lines:
|
for ln in lines:
|
||||||
self._append_row(ln)
|
self._append_row(ln)
|
||||||
|
|
||||||
def _on_cell_double_clicked(self, row: int, col: int) -> None:
|
def _on_cell_double_clicked(self, row: int, col: int) -> None:
|
||||||
if col == 5: # Color picker
|
if col == 5: # Color picker
|
||||||
item = self._table.item(row, col)
|
item = self._table.item(row, col)
|
||||||
current = QColor(item.text() if item else "#808080")
|
current = QColor(item.text() if item else "#808080")
|
||||||
new = QColorDialog.getColor(current, self, "Grid Line Color")
|
new = QColorDialog.getColor(current, self, "Grid Line Color")
|
||||||
|
|
@ -275,14 +303,16 @@ class DefineGridSystemDataDialog(QDialog):
|
||||||
existing: CoordinateGridSystem | None = None,
|
existing: CoordinateGridSystem | None = None,
|
||||||
is_global: bool = False,
|
is_global: bool = False,
|
||||||
parent: QWidget | None = None,
|
parent: QWidget | None = None,
|
||||||
|
*,
|
||||||
|
units: UnitSystem = UnitSystem.METRIC,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setWindowTitle(
|
self.setWindowTitle(
|
||||||
"Define Grid System Data" if existing is not None
|
"Define Grid System Data" if existing is not None else "Add Grid System"
|
||||||
else "Add Grid System"
|
|
||||||
)
|
)
|
||||||
self.resize(780, 620)
|
self.resize(780, 620)
|
||||||
self._is_global = is_global
|
self._is_global = is_global
|
||||||
|
self._units = units
|
||||||
# Working copy: keep origin/rotation for round-tripping without
|
# Working copy: keep origin/rotation for round-tripping without
|
||||||
# re-opening the Coord System Location form.
|
# re-opening the Coord System Location form.
|
||||||
self._origin: tuple[float, float, float] = (
|
self._origin: tuple[float, float, float] = (
|
||||||
|
|
@ -328,9 +358,7 @@ class DefineGridSystemDataDialog(QDialog):
|
||||||
self._btn_locate.clicked.connect(self._on_locate_origin)
|
self._btn_locate.clicked.connect(self._on_locate_origin)
|
||||||
if self._is_global:
|
if self._is_global:
|
||||||
self._btn_locate.setEnabled(False)
|
self._btn_locate.setEnabled(False)
|
||||||
self._btn_locate.setToolTip(
|
self._btn_locate.setToolTip("Global system is anchored at the world origin.")
|
||||||
"Global system is anchored at the world origin."
|
|
||||||
)
|
|
||||||
head.addWidget(self._btn_locate)
|
head.addWidget(self._btn_locate)
|
||||||
root.addLayout(head)
|
root.addLayout(head)
|
||||||
|
|
||||||
|
|
@ -342,9 +370,9 @@ class DefineGridSystemDataDialog(QDialog):
|
||||||
|
|
||||||
# ── Tabs: X / Y / Z grid data spreadsheets ──
|
# ── Tabs: X / Y / Z grid data spreadsheets ──
|
||||||
self._tabs = QTabWidget()
|
self._tabs = QTabWidget()
|
||||||
self._tab_x = _AxisGridTable("X")
|
self._tab_x = _AxisGridTable("X", units=self._units)
|
||||||
self._tab_y = _AxisGridTable("Y")
|
self._tab_y = _AxisGridTable("Y", units=self._units)
|
||||||
self._tab_z = _AxisGridTable("Z")
|
self._tab_z = _AxisGridTable("Z", units=self._units)
|
||||||
self._tabs.addTab(self._tab_x, "X Grid Data")
|
self._tabs.addTab(self._tab_x, "X Grid Data")
|
||||||
self._tabs.addTab(self._tab_y, "Y Grid Data")
|
self._tabs.addTab(self._tab_y, "Y Grid Data")
|
||||||
self._tabs.addTab(self._tab_z, "Z Grid Data")
|
self._tabs.addTab(self._tab_z, "Z Grid Data")
|
||||||
|
|
@ -375,8 +403,7 @@ class DefineGridSystemDataDialog(QDialog):
|
||||||
bottom.addWidget(self._btn_quick)
|
bottom.addWidget(self._btn_quick)
|
||||||
bottom.addStretch(1)
|
bottom.addStretch(1)
|
||||||
buttons = QDialogButtonBox(
|
buttons = QDialogButtonBox(
|
||||||
QDialogButtonBox.StandardButton.Ok
|
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel,
|
||||||
| QDialogButtonBox.StandardButton.Cancel,
|
|
||||||
)
|
)
|
||||||
buttons.accepted.connect(self._on_accept)
|
buttons.accepted.connect(self._on_accept)
|
||||||
buttons.rejected.connect(self.reject)
|
buttons.rejected.connect(self.reject)
|
||||||
|
|
@ -387,9 +414,10 @@ class DefineGridSystemDataDialog(QDialog):
|
||||||
def _update_origin_label(self) -> None:
|
def _update_origin_label(self) -> None:
|
||||||
ox, oy, oz = self._origin
|
ox, oy, oz = self._origin
|
||||||
rx, ry, rz = self._rotation_deg
|
rx, ry, rz = self._rotation_deg
|
||||||
|
length_u = _length_unit(self._units)
|
||||||
|
origin_txt = f"Origin ({length_u}): " if length_u else "Origin: "
|
||||||
self._origin_label.setText(
|
self._origin_label.setText(
|
||||||
f"Origin: ({ox:g}, {oy:g}, {oz:g}) "
|
f"{origin_txt}({ox:g}, {oy:g}, {oz:g}) Rotation: ({rx:g}°, {ry:g}°, {rz:g}°)"
|
||||||
f"Rotation: ({rx:g}°, {ry:g}°, {rz:g}°)"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def _load_existing(self, cs: CoordinateGridSystem) -> None:
|
def _load_existing(self, cs: CoordinateGridSystem) -> None:
|
||||||
|
|
@ -415,10 +443,12 @@ class DefineGridSystemDataDialog(QDialog):
|
||||||
from otko.views.dialogs.locate_origin import (
|
from otko.views.dialogs.locate_origin import (
|
||||||
CoordSystemLocationOrientationDialog,
|
CoordSystemLocationOrientationDialog,
|
||||||
)
|
)
|
||||||
|
|
||||||
dlg = CoordSystemLocationOrientationDialog(
|
dlg = CoordSystemLocationOrientationDialog(
|
||||||
origin=self._origin,
|
origin=self._origin,
|
||||||
rotation_deg=self._rotation_deg,
|
rotation_deg=self._rotation_deg,
|
||||||
parent=self,
|
parent=self,
|
||||||
|
units=self._units,
|
||||||
)
|
)
|
||||||
if dlg.exec() != QDialog.DialogCode.Accepted:
|
if dlg.exec() != QDialog.DialogCode.Accepted:
|
||||||
return
|
return
|
||||||
|
|
@ -429,11 +459,13 @@ class DefineGridSystemDataDialog(QDialog):
|
||||||
from otko.views.dialogs.quick_grid_lines import (
|
from otko.views.dialogs.quick_grid_lines import (
|
||||||
QuickGridLinesDialog,
|
QuickGridLinesDialog,
|
||||||
)
|
)
|
||||||
dlg = QuickGridLinesDialog(parent=self)
|
|
||||||
|
dlg = QuickGridLinesDialog(parent=self, units=self._units)
|
||||||
if dlg.exec() != QDialog.DialogCode.Accepted:
|
if dlg.exec() != QDialog.DialogCode.Accepted:
|
||||||
return
|
return
|
||||||
xs, ys, zs = dlg.ordinates()
|
xs, ys, zs = dlg.ordinates()
|
||||||
from otko.core.geometry import make_grid_lines
|
from otko.core.geometry import make_grid_lines
|
||||||
|
|
||||||
# Revert to ordinate display mode before replacing rows.
|
# Revert to ordinate display mode before replacing rows.
|
||||||
self._rb_ordinates.setChecked(True)
|
self._rb_ordinates.setChecked(True)
|
||||||
self._tab_x.load_lines(make_grid_lines("X", xs))
|
self._tab_x.load_lines(make_grid_lines("X", xs))
|
||||||
|
|
@ -465,7 +497,8 @@ class DefineGridSystemDataDialog(QDialog):
|
||||||
return CoordinateGridSystem(
|
return CoordinateGridSystem(
|
||||||
name=self._name_edit.text().strip(),
|
name=self._name_edit.text().strip(),
|
||||||
coord=CoordinateSystem(
|
coord=CoordinateSystem(
|
||||||
origin=self._origin, rotation_deg=self._rotation_deg,
|
origin=self._origin,
|
||||||
|
rotation_deg=self._rotation_deg,
|
||||||
),
|
),
|
||||||
grid=grid,
|
grid=grid,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ from PySide6.QtWidgets import (
|
||||||
QWidget,
|
QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from otko.core.units import UnitSystem, native_label
|
||||||
|
|
||||||
# Sentinel the pattern combo's userData holds to mean "create a new one".
|
# Sentinel the pattern combo's userData holds to mean "create a new one".
|
||||||
_NEW_PATTERN_SENTINEL = "__new__"
|
_NEW_PATTERN_SENTINEL = "__new__"
|
||||||
|
|
||||||
|
|
@ -49,6 +51,7 @@ class AssignDistributedLoadDialog(QDialog):
|
||||||
current: tuple[float, float, float] | None = None,
|
current: tuple[float, float, float] | None = None,
|
||||||
active_pattern: str | None = None,
|
active_pattern: str | None = None,
|
||||||
mixed: bool = False,
|
mixed: bool = False,
|
||||||
|
units: UnitSystem = UnitSystem.METRIC,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setWindowTitle("Assign Distributed Load")
|
self.setWindowTitle("Assign Distributed Load")
|
||||||
|
|
@ -56,6 +59,7 @@ class AssignDistributedLoadDialog(QDialog):
|
||||||
self._current = current
|
self._current = current
|
||||||
self._active_pattern = active_pattern
|
self._active_pattern = active_pattern
|
||||||
self._mixed = mixed
|
self._mixed = mixed
|
||||||
|
self._units = units
|
||||||
self._build_ui(n_selected)
|
self._build_ui(n_selected)
|
||||||
|
|
||||||
def _build_ui(self, n_selected: int) -> None:
|
def _build_ui(self, n_selected: int) -> None:
|
||||||
|
|
@ -96,6 +100,10 @@ class AssignDistributedLoadDialog(QDialog):
|
||||||
# Show / hide the new-name field in line with the combo.
|
# Show / hide the new-name field in line with the combo.
|
||||||
self._on_pattern_changed(self._pattern_cb.currentIndex())
|
self._on_pattern_changed(self._pattern_cb.currentIndex())
|
||||||
|
|
||||||
|
try:
|
||||||
|
dist_u = native_label(self._units, "distributed_load")
|
||||||
|
except KeyError:
|
||||||
|
dist_u = ""
|
||||||
form = QFormLayout()
|
form = QFormLayout()
|
||||||
self._spinboxes: dict[str, QDoubleSpinBox] = {}
|
self._spinboxes: dict[str, QDoubleSpinBox] = {}
|
||||||
prefill = {"wx": 0.0, "wy": 0.0, "wz": 0.0}
|
prefill = {"wx": 0.0, "wy": 0.0, "wz": 0.0}
|
||||||
|
|
@ -111,9 +119,10 @@ class AssignDistributedLoadDialog(QDialog):
|
||||||
sb.setDecimals(4)
|
sb.setDecimals(4)
|
||||||
sb.setSingleStep(1.0)
|
sb.setSingleStep(1.0)
|
||||||
sb.setValue(float(prefill[label]))
|
sb.setValue(float(prefill[label]))
|
||||||
sb.setToolTip(tip)
|
sb.setToolTip(f"{tip} [{dist_u}]" if dist_u else tip)
|
||||||
self._spinboxes[label] = sb
|
self._spinboxes[label] = sb
|
||||||
form.addRow(f"{label}:", sb)
|
row_label = f"{label} ({dist_u}):" if dist_u else f"{label}:"
|
||||||
|
form.addRow(row_label, sb)
|
||||||
layout.addLayout(form)
|
layout.addLayout(form)
|
||||||
|
|
||||||
layout.addWidget(
|
layout.addWidget(
|
||||||
|
|
@ -141,9 +150,11 @@ class AssignDistributedLoadDialog(QDialog):
|
||||||
|
|
||||||
def values(self) -> tuple[float, float, float]:
|
def values(self) -> tuple[float, float, float]:
|
||||||
"""Return (wy, wz, wx)."""
|
"""Return (wy, wz, wx)."""
|
||||||
return (self._spinboxes["wy"].value(),
|
return (
|
||||||
self._spinboxes["wz"].value(),
|
self._spinboxes["wy"].value(),
|
||||||
self._spinboxes["wx"].value())
|
self._spinboxes["wz"].value(),
|
||||||
|
self._spinboxes["wx"].value(),
|
||||||
|
)
|
||||||
|
|
||||||
def selected_pattern_id(self) -> int | None:
|
def selected_pattern_id(self) -> int | None:
|
||||||
"""Return the chosen pattern id, or ``None`` if the user picked
|
"""Return the chosen pattern id, or ``None`` if the user picked
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ from PySide6.QtWidgets import (
|
||||||
|
|
||||||
from otko.core import Node
|
from otko.core import Node
|
||||||
from otko.core.geometry import GridSystem
|
from otko.core.geometry import GridSystem
|
||||||
|
from otko.core.units import UnitSystem, native_label
|
||||||
|
|
||||||
|
|
||||||
def _parse_spacings(text: str) -> list[float]:
|
def _parse_spacings(text: str) -> list[float]:
|
||||||
|
|
@ -63,13 +64,19 @@ def _coords_from_spacings(spacings: list[float], origin: float = 0.0) -> list[fl
|
||||||
class GridSystemDialog(QDialog):
|
class GridSystemDialog(QDialog):
|
||||||
"""Dialog for entering X/Y/Z grid-line coordinates."""
|
"""Dialog for entering X/Y/Z grid-line coordinates."""
|
||||||
|
|
||||||
def __init__(self, next_node_id: int,
|
def __init__(
|
||||||
existing: GridSystem | None = None,
|
self,
|
||||||
parent: QWidget | None = None) -> None:
|
next_node_id: int,
|
||||||
|
existing: GridSystem | None = None,
|
||||||
|
parent: QWidget | None = None,
|
||||||
|
*,
|
||||||
|
units: UnitSystem = UnitSystem.METRIC,
|
||||||
|
) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setWindowTitle("Grid System")
|
self.setWindowTitle("Grid System")
|
||||||
self._next_id = next_node_id
|
self._next_id = next_node_id
|
||||||
self._existing = existing
|
self._existing = existing
|
||||||
|
self._units = units
|
||||||
self._build_ui()
|
self._build_ui()
|
||||||
if existing is not None:
|
if existing is not None:
|
||||||
self._load_existing(existing)
|
self._load_existing(existing)
|
||||||
|
|
@ -95,18 +102,23 @@ class GridSystemDialog(QDialog):
|
||||||
self._x_edit = QLineEdit("5")
|
self._x_edit = QLineEdit("5")
|
||||||
self._y_edit = QLineEdit("5")
|
self._y_edit = QLineEdit("5")
|
||||||
self._z_edit = QLineEdit("5")
|
self._z_edit = QLineEdit("5")
|
||||||
form.addRow("X spacings:", self._x_edit)
|
try:
|
||||||
form.addRow("Y spacings:", self._y_edit)
|
length_u = native_label(self._units, "length")
|
||||||
form.addRow("Z spacings:", self._z_edit)
|
except KeyError:
|
||||||
|
length_u = ""
|
||||||
|
form.addRow(f"X spacings ({length_u}):" if length_u else "X spacings:", self._x_edit)
|
||||||
|
form.addRow(f"Y spacings ({length_u}):" if length_u else "Y spacings:", self._y_edit)
|
||||||
|
form.addRow(f"Z spacings ({length_u}):" if length_u else "Z spacings:", self._z_edit)
|
||||||
|
for edit in (self._x_edit, self._y_edit, self._z_edit):
|
||||||
|
if length_u:
|
||||||
|
edit.setPlaceholderText(f"spacings in {length_u}")
|
||||||
layout.addLayout(form)
|
layout.addLayout(form)
|
||||||
|
|
||||||
self._visible_cb = QCheckBox("Show grid in 3D view")
|
self._visible_cb = QCheckBox("Show grid in 3D view")
|
||||||
self._visible_cb.setChecked(True)
|
self._visible_cb.setChecked(True)
|
||||||
layout.addWidget(self._visible_cb)
|
layout.addWidget(self._visible_cb)
|
||||||
|
|
||||||
self._generate_nodes_cb = QCheckBox(
|
self._generate_nodes_cb = QCheckBox("Also create nodes at every intersection")
|
||||||
"Also create nodes at every intersection"
|
|
||||||
)
|
|
||||||
self._generate_nodes_cb.setChecked(False)
|
self._generate_nodes_cb.setChecked(False)
|
||||||
layout.addWidget(self._generate_nodes_cb)
|
layout.addWidget(self._generate_nodes_cb)
|
||||||
|
|
||||||
|
|
@ -128,6 +140,7 @@ class GridSystemDialog(QDialog):
|
||||||
|
|
||||||
def _load_existing(self, grid: GridSystem) -> None:
|
def _load_existing(self, grid: GridSystem) -> None:
|
||||||
"""Pre-fill editors from an existing GridSystem (as spacings)."""
|
"""Pre-fill editors from an existing GridSystem (as spacings)."""
|
||||||
|
|
||||||
def to_spacings_text(coords: list[float]) -> str:
|
def to_spacings_text(coords: list[float]) -> str:
|
||||||
if not coords:
|
if not coords:
|
||||||
return ""
|
return ""
|
||||||
|
|
@ -136,6 +149,7 @@ class GridSystemDialog(QDialog):
|
||||||
# Single line at nonzero origin — degenerate, show empty.
|
# Single line at nonzero origin — degenerate, show empty.
|
||||||
return ""
|
return ""
|
||||||
return ", ".join(f"{s:g}" for s in spacings)
|
return ", ".join(f"{s:g}" for s in spacings)
|
||||||
|
|
||||||
self._x_edit.setText(to_spacings_text(grid.x_lines))
|
self._x_edit.setText(to_spacings_text(grid.x_lines))
|
||||||
self._y_edit.setText(to_spacings_text(grid.y_lines))
|
self._y_edit.setText(to_spacings_text(grid.y_lines))
|
||||||
self._z_edit.setText(to_spacings_text(grid.z_lines))
|
self._z_edit.setText(to_spacings_text(grid.z_lines))
|
||||||
|
|
@ -152,8 +166,7 @@ class GridSystemDialog(QDialog):
|
||||||
else:
|
else:
|
||||||
extra = "<br>Nodes will NOT be created automatically."
|
extra = "<br>Nodes will NOT be created automatically."
|
||||||
self._preview.setText(
|
self._preview.setText(
|
||||||
f"Grid: <b>{len(xs)}</b> × <b>{len(ys)}</b> × <b>{len(zs)}</b> lines."
|
f"Grid: <b>{len(xs)}</b> × <b>{len(ys)}</b> × <b>{len(zs)}</b> lines.{extra}"
|
||||||
f"{extra}"
|
|
||||||
)
|
)
|
||||||
except (ValueError, IndexError) as exc:
|
except (ValueError, IndexError) as exc:
|
||||||
self._preview.setText(f"<span style='color:red'>Parse error: {exc}</span>")
|
self._preview.setText(f"<span style='color:red'>Parse error: {exc}</span>")
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ from PySide6.QtWidgets import (
|
||||||
QWidget,
|
QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from otko.core.units import UnitSystem, native_label
|
||||||
|
|
||||||
|
|
||||||
class CoordSystemLocationOrientationDialog(QDialog):
|
class CoordSystemLocationOrientationDialog(QDialog):
|
||||||
"""Just origin + XYZ Euler rotation in degrees."""
|
"""Just origin + XYZ Euler rotation in degrees."""
|
||||||
|
|
@ -28,36 +30,50 @@ class CoordSystemLocationOrientationDialog(QDialog):
|
||||||
origin: tuple[float, float, float] = (0.0, 0.0, 0.0),
|
origin: tuple[float, float, float] = (0.0, 0.0, 0.0),
|
||||||
rotation_deg: tuple[float, float, float] = (0.0, 0.0, 0.0),
|
rotation_deg: tuple[float, float, float] = (0.0, 0.0, 0.0),
|
||||||
parent: QWidget | None = None,
|
parent: QWidget | None = None,
|
||||||
|
*,
|
||||||
|
units: UnitSystem = UnitSystem.METRIC,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setWindowTitle("Coord System Location and Orientation")
|
self.setWindowTitle("Coord System Location and Orientation")
|
||||||
|
self._units = units
|
||||||
self._build_ui()
|
self._build_ui()
|
||||||
self._set_values(origin, rotation_deg)
|
self._set_values(origin, rotation_deg)
|
||||||
|
|
||||||
def _build_ui(self) -> None:
|
def _build_ui(self) -> None:
|
||||||
root = QVBoxLayout(self)
|
root = QVBoxLayout(self)
|
||||||
root.addWidget(QLabel(
|
root.addWidget(
|
||||||
"Enter the system's origin and orientation relative to <b>Global</b>. "
|
QLabel(
|
||||||
"Rotations are XYZ Euler angles in degrees."
|
"Enter the system's origin and orientation relative to <b>Global</b>. "
|
||||||
))
|
"Rotations are XYZ Euler angles in degrees."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
origin_box = QGroupBox("Origin (world units)")
|
try:
|
||||||
|
length_u = native_label(self._units, "length")
|
||||||
|
except KeyError:
|
||||||
|
length_u = ""
|
||||||
|
origin_box = QGroupBox(f"Origin ({length_u})" if length_u else "Origin (world units)")
|
||||||
of = QFormLayout(origin_box)
|
of = QFormLayout(origin_box)
|
||||||
self._ox = self._spin(); of.addRow("X:", self._ox)
|
self._ox = self._spin()
|
||||||
self._oy = self._spin(); of.addRow("Y:", self._oy)
|
of.addRow(f"X ({length_u}):" if length_u else "X:", self._ox)
|
||||||
self._oz = self._spin(); of.addRow("Z:", self._oz)
|
self._oy = self._spin()
|
||||||
|
of.addRow(f"Y ({length_u}):" if length_u else "Y:", self._oy)
|
||||||
|
self._oz = self._spin()
|
||||||
|
of.addRow(f"Z ({length_u}):" if length_u else "Z:", self._oz)
|
||||||
root.addWidget(origin_box)
|
root.addWidget(origin_box)
|
||||||
|
|
||||||
rot_box = QGroupBox("Rotation about axes (degrees)")
|
rot_box = QGroupBox("Rotation about axes (degrees)")
|
||||||
rf = QFormLayout(rot_box)
|
rf = QFormLayout(rot_box)
|
||||||
self._rx = self._rot_spin(); rf.addRow("about X:", self._rx)
|
self._rx = self._rot_spin()
|
||||||
self._ry = self._rot_spin(); rf.addRow("about Y:", self._ry)
|
rf.addRow("about X:", self._rx)
|
||||||
self._rz = self._rot_spin(); rf.addRow("about Z:", self._rz)
|
self._ry = self._rot_spin()
|
||||||
|
rf.addRow("about Y:", self._ry)
|
||||||
|
self._rz = self._rot_spin()
|
||||||
|
rf.addRow("about Z:", self._rz)
|
||||||
root.addWidget(rot_box)
|
root.addWidget(rot_box)
|
||||||
|
|
||||||
buttons = QDialogButtonBox(
|
buttons = QDialogButtonBox(
|
||||||
QDialogButtonBox.StandardButton.Ok
|
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel,
|
||||||
| QDialogButtonBox.StandardButton.Cancel,
|
|
||||||
)
|
)
|
||||||
buttons.accepted.connect(self.accept)
|
buttons.accepted.connect(self.accept)
|
||||||
buttons.rejected.connect(self.reject)
|
buttons.rejected.connect(self.reject)
|
||||||
|
|
|
||||||
|
|
@ -30,13 +30,28 @@ from otko.core import (
|
||||||
Steel01,
|
Steel01,
|
||||||
Steel02,
|
Steel02,
|
||||||
)
|
)
|
||||||
|
from otko.core.units import UnitSystem, native_label
|
||||||
|
|
||||||
|
|
||||||
|
def _u(units: UnitSystem, quantity: str) -> str:
|
||||||
|
"""Display label for ``quantity`` ("" when dimensionless/unknown)."""
|
||||||
|
try:
|
||||||
|
return native_label(units, quantity)
|
||||||
|
except KeyError:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────── helpers ───────────────────────────
|
# ─────────────────────────── helpers ───────────────────────────
|
||||||
def _spin(default: float = 0.0, *, decimals: int = 6,
|
def _spin(
|
||||||
minimum: float = -1e15, maximum: float = 1e15,
|
default: float = 0.0,
|
||||||
step: float = 1.0) -> QDoubleSpinBox:
|
*,
|
||||||
|
decimals: int = 6,
|
||||||
|
minimum: float = -1e15,
|
||||||
|
maximum: float = 1e15,
|
||||||
|
step: float = 1.0,
|
||||||
|
) -> QDoubleSpinBox:
|
||||||
from PySide6.QtCore import QLocale
|
from PySide6.QtCore import QLocale
|
||||||
|
|
||||||
sb = QDoubleSpinBox()
|
sb = QDoubleSpinBox()
|
||||||
# Force C locale so "." is always the decimal separator.
|
# Force C locale so "." is always the decimal separator.
|
||||||
sb.setLocale(QLocale(QLocale.Language.C))
|
sb.setLocale(QLocale(QLocale.Language.C))
|
||||||
|
|
@ -61,8 +76,11 @@ class MaterialFormBase(QWidget):
|
||||||
|
|
||||||
type_label: str = "Material"
|
type_label: str = "Material"
|
||||||
|
|
||||||
def __init__(self, parent: QWidget | None = None) -> None:
|
def __init__(
|
||||||
|
self, parent: QWidget | None = None, *, units: UnitSystem = UnitSystem.METRIC
|
||||||
|
) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
self._units = units
|
||||||
self._material_id: int | None = None
|
self._material_id: int | None = None
|
||||||
self._layout = QFormLayout(self)
|
self._layout = QFormLayout(self)
|
||||||
self._name_edit = QLineEdit()
|
self._name_edit = QLineEdit()
|
||||||
|
|
@ -91,16 +109,21 @@ class MaterialFormBase(QWidget):
|
||||||
class Steel01Form(MaterialFormBase):
|
class Steel01Form(MaterialFormBase):
|
||||||
type_label = "Steel01 — bilinear with kinematic hardening"
|
type_label = "Steel01 — bilinear with kinematic hardening"
|
||||||
|
|
||||||
def __init__(self, parent: QWidget | None = None) -> None:
|
def __init__(
|
||||||
super().__init__(parent)
|
self, parent: QWidget | None = None, *, units: UnitSystem = UnitSystem.METRIC
|
||||||
|
) -> None:
|
||||||
|
super().__init__(parent, units=units)
|
||||||
# Unit-agnostic: type any positive value directly. Arrow keys
|
# Unit-agnostic: type any positive value directly. Arrow keys
|
||||||
# step by 1.0 so kip-in users (Fy ≈ 60) get useful increments;
|
# step by 1.0 so kip-in users (Fy ≈ 60) get useful increments;
|
||||||
# SI-Pa users (Fy ≈ 4e8) still type directly.
|
# SI-Pa users (Fy ≈ 4e8) still type directly.
|
||||||
self._fy = _spin(420e6, step=1.0, minimum=1e-9)
|
self._fy = _spin(420e6, step=1.0, minimum=1e-9)
|
||||||
self._e0 = _spin(200e9, step=1.0, minimum=1e-9)
|
self._e0 = _spin(200e9, step=1.0, minimum=1e-9)
|
||||||
self._b = _spin(0.01, decimals=4, minimum=0.0, maximum=1.0, step=0.01)
|
self._b = _spin(0.01, decimals=4, minimum=0.0, maximum=1.0, step=0.01)
|
||||||
self._layout.addRow("Fy:", self._fy)
|
stress_u = _u(self._units, "stress")
|
||||||
self._layout.addRow("E0:", self._e0)
|
fy_label = f"Fy ({stress_u}):" if stress_u else "Fy:"
|
||||||
|
e0_label = f"E0 ({stress_u}):" if stress_u else "E0:"
|
||||||
|
self._layout.addRow(fy_label, self._fy)
|
||||||
|
self._layout.addRow(e0_label, self._e0)
|
||||||
self._layout.addRow("b (hardening ratio):", self._b)
|
self._layout.addRow("b (hardening ratio):", self._b)
|
||||||
|
|
||||||
def _populate_specific(self, m: Steel01) -> None:
|
def _populate_specific(self, m: Steel01) -> None:
|
||||||
|
|
@ -110,8 +133,11 @@ class Steel01Form(MaterialFormBase):
|
||||||
|
|
||||||
def _read_specific(self, mid: int) -> Steel01:
|
def _read_specific(self, mid: int) -> Steel01:
|
||||||
return Steel01(
|
return Steel01(
|
||||||
id=mid, name=self._name_edit.text(),
|
id=mid,
|
||||||
Fy=self._fy.value(), E0=self._e0.value(), b=self._b.value(),
|
name=self._name_edit.text(),
|
||||||
|
Fy=self._fy.value(),
|
||||||
|
E0=self._e0.value(),
|
||||||
|
b=self._b.value(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -119,27 +145,47 @@ class Steel01Form(MaterialFormBase):
|
||||||
class Steel02Form(MaterialFormBase):
|
class Steel02Form(MaterialFormBase):
|
||||||
type_label = "Steel02 — Giuffré-Menegotto-Pinto"
|
type_label = "Steel02 — Giuffré-Menegotto-Pinto"
|
||||||
|
|
||||||
def __init__(self, parent: QWidget | None = None) -> None:
|
def __init__(
|
||||||
super().__init__(parent)
|
self, parent: QWidget | None = None, *, units: UnitSystem = UnitSystem.METRIC
|
||||||
|
) -> None:
|
||||||
|
super().__init__(parent, units=units)
|
||||||
self._fy = _spin(355e6, step=1.0, minimum=1e-9)
|
self._fy = _spin(355e6, step=1.0, minimum=1e-9)
|
||||||
self._e0 = _spin(200e9, step=1.0, minimum=1e-9)
|
self._e0 = _spin(200e9, step=1.0, minimum=1e-9)
|
||||||
self._b = _spin(0.005, decimals=4, minimum=0.0, maximum=1.0, step=0.001)
|
self._b = _spin(0.005, decimals=4, minimum=0.0, maximum=1.0, step=0.001)
|
||||||
self._r0 = _spin(18.0, decimals=2, minimum=10.0, maximum=20.0, step=0.5)
|
self._r0 = _spin(18.0, decimals=2, minimum=10.0, maximum=20.0, step=0.5)
|
||||||
self._cR1 = _spin(0.925, decimals=4, step=0.01)
|
self._cR1 = _spin(0.925, decimals=4, step=0.01)
|
||||||
self._cR2 = _spin(0.15, decimals=4, step=0.01)
|
self._cR2 = _spin(0.15, decimals=4, step=0.01)
|
||||||
for label, w in (("Fy:", self._fy), ("E0:", self._e0), ("b:", self._b),
|
stress_u = _u(self._units, "stress")
|
||||||
("R0:", self._r0), ("cR1:", self._cR1), ("cR2:", self._cR2)):
|
fy_label = f"Fy ({stress_u}):" if stress_u else "Fy:"
|
||||||
|
e0_label = f"E0 ({stress_u}):" if stress_u else "E0:"
|
||||||
|
for label, w in (
|
||||||
|
(fy_label, self._fy),
|
||||||
|
(e0_label, self._e0),
|
||||||
|
("b:", self._b),
|
||||||
|
("R0:", self._r0),
|
||||||
|
("cR1:", self._cR1),
|
||||||
|
("cR2:", self._cR2),
|
||||||
|
):
|
||||||
self._layout.addRow(label, w)
|
self._layout.addRow(label, w)
|
||||||
|
|
||||||
def _populate_specific(self, m: Steel02) -> None:
|
def _populate_specific(self, m: Steel02) -> None:
|
||||||
self._fy.setValue(m.Fy); self._e0.setValue(m.E0); self._b.setValue(m.b)
|
self._fy.setValue(m.Fy)
|
||||||
self._r0.setValue(m.R0); self._cR1.setValue(m.cR1); self._cR2.setValue(m.cR2)
|
self._e0.setValue(m.E0)
|
||||||
|
self._b.setValue(m.b)
|
||||||
|
self._r0.setValue(m.R0)
|
||||||
|
self._cR1.setValue(m.cR1)
|
||||||
|
self._cR2.setValue(m.cR2)
|
||||||
|
|
||||||
def _read_specific(self, mid: int) -> Steel02:
|
def _read_specific(self, mid: int) -> Steel02:
|
||||||
return Steel02(
|
return Steel02(
|
||||||
id=mid, name=self._name_edit.text(),
|
id=mid,
|
||||||
Fy=self._fy.value(), E0=self._e0.value(), b=self._b.value(),
|
name=self._name_edit.text(),
|
||||||
R0=self._r0.value(), cR1=self._cR1.value(), cR2=self._cR2.value(),
|
Fy=self._fy.value(),
|
||||||
|
E0=self._e0.value(),
|
||||||
|
b=self._b.value(),
|
||||||
|
R0=self._r0.value(),
|
||||||
|
cR1=self._cR1.value(),
|
||||||
|
cR2=self._cR2.value(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -147,8 +193,10 @@ class Steel02Form(MaterialFormBase):
|
||||||
class Concrete01Form(MaterialFormBase):
|
class Concrete01Form(MaterialFormBase):
|
||||||
type_label = "Concrete01 — Kent-Scott-Park, no tension"
|
type_label = "Concrete01 — Kent-Scott-Park, no tension"
|
||||||
|
|
||||||
def __init__(self, parent: QWidget | None = None) -> None:
|
def __init__(
|
||||||
super().__init__(parent)
|
self, parent: QWidget | None = None, *, units: UnitSystem = UnitSystem.METRIC
|
||||||
|
) -> None:
|
||||||
|
super().__init__(parent, units=units)
|
||||||
# f'c, f'cu: any non-zero negative value. The old -1e3 max
|
# f'c, f'cu: any non-zero negative value. The old -1e3 max
|
||||||
# assumed SI-Pa and locked out kip-in users who wanted
|
# assumed SI-Pa and locked out kip-in users who wanted
|
||||||
# values like -6 ksi; -1e-6 leaves the full unit range open.
|
# values like -6 ksi; -1e-6 leaves the full unit range open.
|
||||||
|
|
@ -156,19 +204,31 @@ class Concrete01Form(MaterialFormBase):
|
||||||
self._epsc0 = _spin(-0.002, decimals=6, minimum=-1.0, maximum=0.0, step=1e-4)
|
self._epsc0 = _spin(-0.002, decimals=6, minimum=-1.0, maximum=0.0, step=1e-4)
|
||||||
self._fpcu = _spin(-15e6, step=1.0, maximum=0.0)
|
self._fpcu = _spin(-15e6, step=1.0, maximum=0.0)
|
||||||
self._epsU = _spin(-0.005, decimals=6, minimum=-1.0, maximum=0.0, step=1e-4)
|
self._epsU = _spin(-0.005, decimals=6, minimum=-1.0, maximum=0.0, step=1e-4)
|
||||||
for label, w in (("f'c (-):", self._fpc), ("ε_c0 (-):", self._epsc0),
|
stress_u = _u(self._units, "stress")
|
||||||
("f'cu (-):", self._fpcu), ("ε_U (-):", self._epsU)):
|
fpc_label = f"f'c ({stress_u}):" if stress_u else "f'c (-):"
|
||||||
|
fpcu_label = f"f'cu ({stress_u}):" if stress_u else "f'cu (-):"
|
||||||
|
for label, w in (
|
||||||
|
(fpc_label, self._fpc),
|
||||||
|
("ε_c0:", self._epsc0),
|
||||||
|
(fpcu_label, self._fpcu),
|
||||||
|
("ε_U:", self._epsU),
|
||||||
|
):
|
||||||
self._layout.addRow(label, w)
|
self._layout.addRow(label, w)
|
||||||
|
|
||||||
def _populate_specific(self, m: Concrete01) -> None:
|
def _populate_specific(self, m: Concrete01) -> None:
|
||||||
self._fpc.setValue(m.fpc); self._epsc0.setValue(m.epsc0)
|
self._fpc.setValue(m.fpc)
|
||||||
self._fpcu.setValue(m.fpcu); self._epsU.setValue(m.epsU)
|
self._epsc0.setValue(m.epsc0)
|
||||||
|
self._fpcu.setValue(m.fpcu)
|
||||||
|
self._epsU.setValue(m.epsU)
|
||||||
|
|
||||||
def _read_specific(self, mid: int) -> Concrete01:
|
def _read_specific(self, mid: int) -> Concrete01:
|
||||||
return Concrete01(
|
return Concrete01(
|
||||||
id=mid, name=self._name_edit.text(),
|
id=mid,
|
||||||
fpc=self._fpc.value(), epsc0=self._epsc0.value(),
|
name=self._name_edit.text(),
|
||||||
fpcu=self._fpcu.value(), epsU=self._epsU.value(),
|
fpc=self._fpc.value(),
|
||||||
|
epsc0=self._epsc0.value(),
|
||||||
|
fpcu=self._fpcu.value(),
|
||||||
|
epsU=self._epsU.value(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -176,8 +236,10 @@ class Concrete01Form(MaterialFormBase):
|
||||||
class Concrete02Form(MaterialFormBase):
|
class Concrete02Form(MaterialFormBase):
|
||||||
type_label = "Concrete02 — Kent-Scott-Park with linear tension"
|
type_label = "Concrete02 — Kent-Scott-Park with linear tension"
|
||||||
|
|
||||||
def __init__(self, parent: QWidget | None = None) -> None:
|
def __init__(
|
||||||
super().__init__(parent)
|
self, parent: QWidget | None = None, *, units: UnitSystem = UnitSystem.METRIC
|
||||||
|
) -> None:
|
||||||
|
super().__init__(parent, units=units)
|
||||||
# Same range fix as Concrete01 — permissive so kip-in values
|
# Same range fix as Concrete01 — permissive so kip-in values
|
||||||
# like -6 aren't clamped by a hard-coded SI-Pa maximum.
|
# like -6 aren't clamped by a hard-coded SI-Pa maximum.
|
||||||
self._fpc = _spin(-30e6, step=1.0, maximum=0.0)
|
self._fpc = _spin(-30e6, step=1.0, maximum=0.0)
|
||||||
|
|
@ -187,23 +249,41 @@ class Concrete02Form(MaterialFormBase):
|
||||||
self._lambda = _spin(0.1, decimals=4, minimum=0.0, maximum=1.0, step=0.01)
|
self._lambda = _spin(0.1, decimals=4, minimum=0.0, maximum=1.0, step=0.01)
|
||||||
self._ft = _spin(3e6, step=1.0, minimum=1e-9)
|
self._ft = _spin(3e6, step=1.0, minimum=1e-9)
|
||||||
self._ets = _spin(2e9, step=1.0, minimum=1e-9)
|
self._ets = _spin(2e9, step=1.0, minimum=1e-9)
|
||||||
for label, w in (("f'c (-):", self._fpc), ("ε_c0 (-):", self._epsc0),
|
stress_u = _u(self._units, "stress")
|
||||||
("f'cu (-):", self._fpcu), ("ε_U (-):", self._epsU),
|
fpc_label = f"f'c ({stress_u}):" if stress_u else "f'c (-):"
|
||||||
("λ (unload ratio):", self._lambda),
|
fpcu_label = f"f'cu ({stress_u}):" if stress_u else "f'cu (-):"
|
||||||
("ft (tensile):", self._ft), ("Ets (soften):", self._ets)):
|
ft_label = f"ft (tensile, {stress_u}):" if stress_u else "ft (tensile):"
|
||||||
|
ets_label = f"Ets (soften, {stress_u}):" if stress_u else "Ets (soften):"
|
||||||
|
for label, w in (
|
||||||
|
(fpc_label, self._fpc),
|
||||||
|
("ε_c0:", self._epsc0),
|
||||||
|
(fpcu_label, self._fpcu),
|
||||||
|
("ε_U:", self._epsU),
|
||||||
|
("λ (unload ratio):", self._lambda),
|
||||||
|
(ft_label, self._ft),
|
||||||
|
(ets_label, self._ets),
|
||||||
|
):
|
||||||
self._layout.addRow(label, w)
|
self._layout.addRow(label, w)
|
||||||
|
|
||||||
def _populate_specific(self, m: Concrete02) -> None:
|
def _populate_specific(self, m: Concrete02) -> None:
|
||||||
self._fpc.setValue(m.fpc); self._epsc0.setValue(m.epsc0)
|
self._fpc.setValue(m.fpc)
|
||||||
self._fpcu.setValue(m.fpcu); self._epsU.setValue(m.epsU)
|
self._epsc0.setValue(m.epsc0)
|
||||||
self._lambda.setValue(m.lambda_); self._ft.setValue(m.ft); self._ets.setValue(m.Ets)
|
self._fpcu.setValue(m.fpcu)
|
||||||
|
self._epsU.setValue(m.epsU)
|
||||||
|
self._lambda.setValue(m.lambda_)
|
||||||
|
self._ft.setValue(m.ft)
|
||||||
|
self._ets.setValue(m.Ets)
|
||||||
|
|
||||||
def _read_specific(self, mid: int) -> Concrete02:
|
def _read_specific(self, mid: int) -> Concrete02:
|
||||||
return Concrete02(
|
return Concrete02(
|
||||||
id=mid, name=self._name_edit.text(),
|
id=mid,
|
||||||
fpc=self._fpc.value(), epsc0=self._epsc0.value(),
|
name=self._name_edit.text(),
|
||||||
fpcu=self._fpcu.value(), epsU=self._epsU.value(),
|
fpc=self._fpc.value(),
|
||||||
ft=self._ft.value(), Ets=self._ets.value(),
|
epsc0=self._epsc0.value(),
|
||||||
|
fpcu=self._fpcu.value(),
|
||||||
|
epsU=self._epsU.value(),
|
||||||
|
ft=self._ft.value(),
|
||||||
|
Ets=self._ets.value(),
|
||||||
**{"lambda": self._lambda.value()},
|
**{"lambda": self._lambda.value()},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -212,8 +292,10 @@ class Concrete02Form(MaterialFormBase):
|
||||||
class Concrete04Form(MaterialFormBase):
|
class Concrete04Form(MaterialFormBase):
|
||||||
type_label = "Concrete04 — Popovics concrete (optional tension)"
|
type_label = "Concrete04 — Popovics concrete (optional tension)"
|
||||||
|
|
||||||
def __init__(self, parent: QWidget | None = None) -> None:
|
def __init__(
|
||||||
super().__init__(parent)
|
self, parent: QWidget | None = None, *, units: UnitSystem = UnitSystem.METRIC
|
||||||
|
) -> None:
|
||||||
|
super().__init__(parent, units=units)
|
||||||
self._fpc = _spin(-30e6, step=1.0, maximum=0.0)
|
self._fpc = _spin(-30e6, step=1.0, maximum=0.0)
|
||||||
self._epsc0 = _spin(-0.002, decimals=6, minimum=-1.0, maximum=0.0, step=1e-4)
|
self._epsc0 = _spin(-0.002, decimals=6, minimum=-1.0, maximum=0.0, step=1e-4)
|
||||||
self._epscu = _spin(-0.005, decimals=6, minimum=-1.0, maximum=0.0, step=1e-4)
|
self._epscu = _spin(-0.005, decimals=6, minimum=-1.0, maximum=0.0, step=1e-4)
|
||||||
|
|
@ -221,12 +303,16 @@ class Concrete04Form(MaterialFormBase):
|
||||||
# fct=0 means no-tension model; positive value activates tensile branch.
|
# fct=0 means no-tension model; positive value activates tensile branch.
|
||||||
self._fct = _spin(0.0, step=1.0, minimum=0.0)
|
self._fct = _spin(0.0, step=1.0, minimum=0.0)
|
||||||
self._et = _spin(0.0, decimals=6, minimum=0.0, step=1e-5)
|
self._et = _spin(0.0, decimals=6, minimum=0.0, step=1e-5)
|
||||||
|
stress_u = _u(self._units, "stress")
|
||||||
|
fpc_label = f"f'c ({stress_u}):" if stress_u else "f'c (-):"
|
||||||
|
ec_label = f"Ec ({stress_u}):" if stress_u else "Ec:"
|
||||||
|
fct_label = f"fct (0 = no tension, {stress_u}):" if stress_u else "fct (0 = no tension):"
|
||||||
for label, w in (
|
for label, w in (
|
||||||
("f'c (-):", self._fpc),
|
(fpc_label, self._fpc),
|
||||||
("ε_c0 (-):", self._epsc0),
|
("ε_c0:", self._epsc0),
|
||||||
("ε_cu (-):", self._epscu),
|
("ε_cu:", self._epscu),
|
||||||
("Ec:", self._Ec),
|
(ec_label, self._Ec),
|
||||||
("fct (0 = no tension):", self._fct),
|
(fct_label, self._fct),
|
||||||
("et (if fct > 0):", self._et),
|
("et (if fct > 0):", self._et),
|
||||||
):
|
):
|
||||||
self._layout.addRow(label, w)
|
self._layout.addRow(label, w)
|
||||||
|
|
@ -243,9 +329,12 @@ class Concrete04Form(MaterialFormBase):
|
||||||
fct_val = self._fct.value()
|
fct_val = self._fct.value()
|
||||||
et_val = self._et.value()
|
et_val = self._et.value()
|
||||||
return Concrete04(
|
return Concrete04(
|
||||||
id=mid, name=self._name_edit.text(),
|
id=mid,
|
||||||
fpc=self._fpc.value(), epsc0=self._epsc0.value(),
|
name=self._name_edit.text(),
|
||||||
epscu=self._epscu.value(), Ec=self._Ec.value(),
|
fpc=self._fpc.value(),
|
||||||
|
epsc0=self._epsc0.value(),
|
||||||
|
epscu=self._epscu.value(),
|
||||||
|
Ec=self._Ec.value(),
|
||||||
fct=fct_val if fct_val > 0.0 else None,
|
fct=fct_val if fct_val > 0.0 else None,
|
||||||
et=et_val if et_val > 0.0 else None,
|
et=et_val if et_val > 0.0 else None,
|
||||||
)
|
)
|
||||||
|
|
@ -255,20 +344,27 @@ class Concrete04Form(MaterialFormBase):
|
||||||
class ElasticUniaxialForm(MaterialFormBase):
|
class ElasticUniaxialForm(MaterialFormBase):
|
||||||
type_label = "Elastic (uniaxial)"
|
type_label = "Elastic (uniaxial)"
|
||||||
|
|
||||||
def __init__(self, parent: QWidget | None = None) -> None:
|
def __init__(
|
||||||
super().__init__(parent)
|
self, parent: QWidget | None = None, *, units: UnitSystem = UnitSystem.METRIC
|
||||||
|
) -> None:
|
||||||
|
super().__init__(parent, units=units)
|
||||||
self._e = _spin(200e9, step=1.0, minimum=1e-9)
|
self._e = _spin(200e9, step=1.0, minimum=1e-9)
|
||||||
self._eta = _spin(0.0, decimals=4)
|
self._eta = _spin(0.0, decimals=4)
|
||||||
self._layout.addRow("E:", self._e)
|
stress_u = _u(self._units, "stress")
|
||||||
|
e_label = f"E ({stress_u}):" if stress_u else "E:"
|
||||||
|
self._layout.addRow(e_label, self._e)
|
||||||
self._layout.addRow("η (damping):", self._eta)
|
self._layout.addRow("η (damping):", self._eta)
|
||||||
|
|
||||||
def _populate_specific(self, m: ElasticUniaxial) -> None:
|
def _populate_specific(self, m: ElasticUniaxial) -> None:
|
||||||
self._e.setValue(m.E); self._eta.setValue(m.eta)
|
self._e.setValue(m.E)
|
||||||
|
self._eta.setValue(m.eta)
|
||||||
|
|
||||||
def _read_specific(self, mid: int) -> ElasticUniaxial:
|
def _read_specific(self, mid: int) -> ElasticUniaxial:
|
||||||
return ElasticUniaxial(
|
return ElasticUniaxial(
|
||||||
id=mid, name=self._name_edit.text(),
|
id=mid,
|
||||||
E=self._e.value(), eta=self._eta.value(),
|
name=self._name_edit.text(),
|
||||||
|
E=self._e.value(),
|
||||||
|
eta=self._eta.value(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -276,22 +372,40 @@ class ElasticUniaxialForm(MaterialFormBase):
|
||||||
class ElasticIsotropicForm(MaterialFormBase):
|
class ElasticIsotropicForm(MaterialFormBase):
|
||||||
type_label = "Elastic Isotropic (3D)"
|
type_label = "Elastic Isotropic (3D)"
|
||||||
|
|
||||||
def __init__(self, parent: QWidget | None = None) -> None:
|
def __init__(
|
||||||
super().__init__(parent)
|
self, parent: QWidget | None = None, *, units: UnitSystem = UnitSystem.METRIC
|
||||||
|
) -> None:
|
||||||
|
super().__init__(parent, units=units)
|
||||||
self._e = _spin(200e9, step=1.0, minimum=1e-9)
|
self._e = _spin(200e9, step=1.0, minimum=1e-9)
|
||||||
self._nu = _spin(0.3, decimals=4, minimum=-1.0, maximum=0.5, step=0.01)
|
self._nu = _spin(0.3, decimals=4, minimum=-1.0, maximum=0.5, step=0.01)
|
||||||
self._rho = _spin(7850.0, decimals=2, minimum=0.0, step=100.0)
|
self._rho = _spin(7850.0, decimals=2, minimum=0.0, step=100.0)
|
||||||
self._layout.addRow("E:", self._e)
|
stress_u = _u(self._units, "stress")
|
||||||
|
mass_u = _u(self._units, "mass")
|
||||||
|
length_u = _u(self._units, "length")
|
||||||
|
e_label = f"E ({stress_u}):" if stress_u else "E:"
|
||||||
|
if mass_u and length_u:
|
||||||
|
rho_label = f"ρ (density, {mass_u}/{length_u}³):" # noqa: RUF001
|
||||||
|
rho_tip = f"Mass density in {mass_u}/{length_u}³ (native units, no conversion)."
|
||||||
|
else:
|
||||||
|
rho_label = "ρ (density):"
|
||||||
|
rho_tip = "Mass density in native project units (no conversion)."
|
||||||
|
self._layout.addRow(e_label, self._e)
|
||||||
self._layout.addRow("ν (Poisson):", self._nu)
|
self._layout.addRow("ν (Poisson):", self._nu)
|
||||||
self._layout.addRow("ρ (density):", self._rho)
|
self._layout.addRow(rho_label, self._rho)
|
||||||
|
self._rho.setToolTip(rho_tip)
|
||||||
|
|
||||||
def _populate_specific(self, m: ElasticIsotropic) -> None:
|
def _populate_specific(self, m: ElasticIsotropic) -> None:
|
||||||
self._e.setValue(m.E); self._nu.setValue(m.nu); self._rho.setValue(m.rho)
|
self._e.setValue(m.E)
|
||||||
|
self._nu.setValue(m.nu)
|
||||||
|
self._rho.setValue(m.rho)
|
||||||
|
|
||||||
def _read_specific(self, mid: int) -> ElasticIsotropic:
|
def _read_specific(self, mid: int) -> ElasticIsotropic:
|
||||||
return ElasticIsotropic(
|
return ElasticIsotropic(
|
||||||
id=mid, name=self._name_edit.text(),
|
id=mid,
|
||||||
E=self._e.value(), nu=self._nu.value(), rho=self._rho.value(),
|
name=self._name_edit.text(),
|
||||||
|
E=self._e.value(),
|
||||||
|
nu=self._nu.value(),
|
||||||
|
rho=self._rho.value(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -299,20 +413,27 @@ class ElasticIsotropicForm(MaterialFormBase):
|
||||||
class ElasticPPForm(MaterialFormBase):
|
class ElasticPPForm(MaterialFormBase):
|
||||||
type_label = "Elastic-Perfectly-Plastic"
|
type_label = "Elastic-Perfectly-Plastic"
|
||||||
|
|
||||||
def __init__(self, parent: QWidget | None = None) -> None:
|
def __init__(
|
||||||
super().__init__(parent)
|
self, parent: QWidget | None = None, *, units: UnitSystem = UnitSystem.METRIC
|
||||||
|
) -> None:
|
||||||
|
super().__init__(parent, units=units)
|
||||||
self._e = _spin(200e9, step=1.0, minimum=1e-9)
|
self._e = _spin(200e9, step=1.0, minimum=1e-9)
|
||||||
self._epsy = _spin(0.002, decimals=6, minimum=1e-6, step=1e-4)
|
self._epsy = _spin(0.002, decimals=6, minimum=1e-6, step=1e-4)
|
||||||
self._layout.addRow("E:", self._e)
|
stress_u = _u(self._units, "stress")
|
||||||
|
e_label = f"E ({stress_u}):" if stress_u else "E:"
|
||||||
|
self._layout.addRow(e_label, self._e)
|
||||||
self._layout.addRow("ε_y (yield strain):", self._epsy)
|
self._layout.addRow("ε_y (yield strain):", self._epsy)
|
||||||
|
|
||||||
def _populate_specific(self, m: ElasticPP) -> None:
|
def _populate_specific(self, m: ElasticPP) -> None:
|
||||||
self._e.setValue(m.E); self._epsy.setValue(m.epsy_pos)
|
self._e.setValue(m.E)
|
||||||
|
self._epsy.setValue(m.epsy_pos)
|
||||||
|
|
||||||
def _read_specific(self, mid: int) -> ElasticPP:
|
def _read_specific(self, mid: int) -> ElasticPP:
|
||||||
return ElasticPP(
|
return ElasticPP(
|
||||||
id=mid, name=self._name_edit.text(),
|
id=mid,
|
||||||
E=self._e.value(), epsy_pos=self._epsy.value(),
|
name=self._name_edit.text(),
|
||||||
|
E=self._e.value(),
|
||||||
|
epsy_pos=self._epsy.value(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -330,9 +451,9 @@ FORM_REGISTRY: dict[str, type[MaterialFormBase]] = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def form_for(material: Any) -> MaterialFormBase:
|
def form_for(material: Any, *, units: UnitSystem = UnitSystem.METRIC) -> MaterialFormBase:
|
||||||
"""Return a form instance pre-populated for ``material``."""
|
"""Return a form instance pre-populated for ``material``."""
|
||||||
cls = FORM_REGISTRY[material.type]
|
cls = FORM_REGISTRY[material.type]
|
||||||
form = cls()
|
form = cls(units=units)
|
||||||
form.populate(material)
|
form.populate(material)
|
||||||
return form
|
return form
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ from otko.commands import (
|
||||||
DeleteMaterialsCommand,
|
DeleteMaterialsCommand,
|
||||||
UpdateMaterialCommand,
|
UpdateMaterialCommand,
|
||||||
)
|
)
|
||||||
|
from otko.core.units import UnitSystem
|
||||||
from otko.viewmodels import ProjectViewModel
|
from otko.viewmodels import ProjectViewModel
|
||||||
from otko.views.dialogs.material_forms import FORM_REGISTRY, form_for
|
from otko.views.dialogs.material_forms import FORM_REGISTRY, form_for
|
||||||
|
|
||||||
|
|
@ -121,6 +122,11 @@ class MaterialLibraryDialog(QDialog):
|
||||||
return next((m for m in self._vm.project.materials if m.id == mid), None)
|
return next((m for m in self._vm.project.materials if m.id == mid), None)
|
||||||
|
|
||||||
# ── slots ────────────────────────────────────────────────────────
|
# ── slots ────────────────────────────────────────────────────────
|
||||||
|
def _project_units(self) -> UnitSystem:
|
||||||
|
"""Project display units (Metric default when no project is open)."""
|
||||||
|
proj = self._vm.project
|
||||||
|
return proj.meta.units if proj is not None else UnitSystem.METRIC
|
||||||
|
|
||||||
def _on_row_changed(self, _row: int) -> None:
|
def _on_row_changed(self, _row: int) -> None:
|
||||||
material = self._selected_material()
|
material = self._selected_material()
|
||||||
if material is None:
|
if material is None:
|
||||||
|
|
@ -130,7 +136,7 @@ class MaterialLibraryDialog(QDialog):
|
||||||
old = self._stack.widget(0)
|
old = self._stack.widget(0)
|
||||||
self._stack.removeWidget(old)
|
self._stack.removeWidget(old)
|
||||||
old.deleteLater()
|
old.deleteLater()
|
||||||
form = form_for(material)
|
form = form_for(material, units=self._project_units())
|
||||||
self._stack.addWidget(form)
|
self._stack.addWidget(form)
|
||||||
self._stack.setCurrentWidget(form)
|
self._stack.setCurrentWidget(form)
|
||||||
self._type_label.setText(form.type_label)
|
self._type_label.setText(form.type_label)
|
||||||
|
|
@ -150,7 +156,12 @@ class MaterialLibraryDialog(QDialog):
|
||||||
return
|
return
|
||||||
kinds = list(FORM_REGISTRY.keys())
|
kinds = list(FORM_REGISTRY.keys())
|
||||||
kind, ok = QInputDialog.getItem(
|
kind, ok = QInputDialog.getItem(
|
||||||
self, "Add material", "Type:", kinds, current=0, editable=False,
|
self,
|
||||||
|
"Add material",
|
||||||
|
"Type:",
|
||||||
|
kinds,
|
||||||
|
current=0,
|
||||||
|
editable=False,
|
||||||
)
|
)
|
||||||
if not ok:
|
if not ok:
|
||||||
return
|
return
|
||||||
|
|
@ -158,7 +169,7 @@ class MaterialLibraryDialog(QDialog):
|
||||||
# Build a default-valued instance using the form: instantiate, populate
|
# Build a default-valued instance using the form: instantiate, populate
|
||||||
# nothing (defaults shown in the form), then read with the new id.
|
# nothing (defaults shown in the form), then read with the new id.
|
||||||
form_cls = FORM_REGISTRY[kind]
|
form_cls = FORM_REGISTRY[kind]
|
||||||
form = form_cls()
|
form = form_cls(units=self._project_units())
|
||||||
form._material_id = new_id # type: ignore[attr-defined]
|
form._material_id = new_id # type: ignore[attr-defined]
|
||||||
try:
|
try:
|
||||||
new_material = form.read()
|
new_material = form.read()
|
||||||
|
|
@ -179,9 +190,10 @@ class MaterialLibraryDialog(QDialog):
|
||||||
if material is None:
|
if material is None:
|
||||||
return
|
return
|
||||||
reply = QMessageBox.question(
|
reply = QMessageBox.question(
|
||||||
self, "Delete material",
|
self,
|
||||||
|
"Delete material",
|
||||||
f"Delete material #{material.id} ({material.type})?\n"
|
f"Delete material #{material.id} ({material.type})?\n"
|
||||||
"Elements that reference it will be invalid until reassigned."
|
"Elements that reference it will be invalid until reassigned.",
|
||||||
)
|
)
|
||||||
if reply != QMessageBox.StandardButton.Yes:
|
if reply != QMessageBox.StandardButton.Yes:
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -232,7 +232,7 @@ class MaterialTesterDialog(QDialog):
|
||||||
"""Stress label for the current project units (strain is unitless)."""
|
"""Stress label for the current project units (strain is unitless)."""
|
||||||
if self._vm.project is not None:
|
if self._vm.project is not None:
|
||||||
return labels_for(self._vm.project.meta.units).stress
|
return labels_for(self._vm.project.meta.units).stress
|
||||||
return labels_for(UnitSystem.SI_M_N).stress
|
return labels_for(UnitSystem.METRIC).stress
|
||||||
|
|
||||||
def _refresh_materials(self) -> None:
|
def _refresh_materials(self) -> None:
|
||||||
"""Repopulate the picker from the project, keeping the selection."""
|
"""Repopulate the picker from the project, keeping the selection."""
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,22 @@ from PySide6.QtWidgets import (
|
||||||
QWidget,
|
QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from otko.core.units import UnitSystem, native_label
|
||||||
|
|
||||||
|
|
||||||
class MoveDialog(QDialog):
|
class MoveDialog(QDialog):
|
||||||
"""Dialog for an (dx, dy, dz) translation."""
|
"""Dialog for an (dx, dy, dz) translation."""
|
||||||
|
|
||||||
def __init__(self, n_nodes: int, parent: QWidget | None = None) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
n_nodes: int,
|
||||||
|
parent: QWidget | None = None,
|
||||||
|
*,
|
||||||
|
units: UnitSystem = UnitSystem.METRIC,
|
||||||
|
) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setWindowTitle("Move")
|
self.setWindowTitle("Move")
|
||||||
|
self._units = units
|
||||||
self._build_ui(n_nodes)
|
self._build_ui(n_nodes)
|
||||||
|
|
||||||
def _build_ui(self, n_nodes: int) -> None:
|
def _build_ui(self, n_nodes: int) -> None:
|
||||||
|
|
@ -36,9 +45,13 @@ class MoveDialog(QDialog):
|
||||||
return sb
|
return sb
|
||||||
|
|
||||||
self._dx, self._dy, self._dz = _spin(), _spin(), _spin()
|
self._dx, self._dy, self._dz = _spin(), _spin(), _spin()
|
||||||
form.addRow("dX:", self._dx)
|
try:
|
||||||
form.addRow("dY:", self._dy)
|
length_u = native_label(self._units, "length")
|
||||||
form.addRow("dZ:", self._dz)
|
except KeyError:
|
||||||
|
length_u = ""
|
||||||
|
form.addRow(f"dX ({length_u}):" if length_u else "dX:", self._dx)
|
||||||
|
form.addRow(f"dY ({length_u}):" if length_u else "dY:", self._dy)
|
||||||
|
form.addRow(f"dZ ({length_u}):" if length_u else "dZ:", self._dz)
|
||||||
layout.addLayout(form)
|
layout.addLayout(form)
|
||||||
|
|
||||||
buttons = QDialogButtonBox(
|
buttons = QDialogButtonBox(
|
||||||
|
|
|
||||||
|
|
@ -68,8 +68,8 @@ class PathTimeSeriesDialog(QDialog):
|
||||||
self._factor_spin.setValue(386.4) # default: convert g → in/s²
|
self._factor_spin.setValue(386.4) # default: convert g → in/s²
|
||||||
self._factor_spin.setToolTip(
|
self._factor_spin.setToolTip(
|
||||||
"Multiplier applied to every value at runtime. Typical use: "
|
"Multiplier applied to every value at runtime. Typical use: "
|
||||||
"386.4 for ground motion in g → in/s² (US_IN_KIP), 9.81 for "
|
"386.4 for ground motion in g → in/s² (Imperial), 9.81 for "
|
||||||
"SI, 1.0 for data already in the project's unit system."
|
"Metric, 1.0 for data already in the project's unit system."
|
||||||
)
|
)
|
||||||
form.addRow("Factor:", self._factor_spin)
|
form.addRow("Factor:", self._factor_spin)
|
||||||
root.addLayout(form)
|
root.addLayout(form)
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ from otko.core import (
|
||||||
UniformElementLoad,
|
UniformElementLoad,
|
||||||
UniformExcitationPattern,
|
UniformExcitationPattern,
|
||||||
)
|
)
|
||||||
|
from otko.core.units import UnitSystem, native_label
|
||||||
|
|
||||||
# Column headers for the two tables. Order matches how the Assign
|
# Column headers for the two tables. Order matches how the Assign
|
||||||
# dialogs expose the values, so reading one row is a direct mapping
|
# dialogs expose the values, so reading one row is a direct mapping
|
||||||
|
|
@ -62,11 +63,19 @@ class PatternLoadsDialog(QDialog):
|
||||||
parent: QWidget | None = None,
|
parent: QWidget | None = None,
|
||||||
*,
|
*,
|
||||||
vm: Any | None = None,
|
vm: Any | None = None,
|
||||||
|
units: UnitSystem | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self._pattern = pattern
|
self._pattern = pattern
|
||||||
self._pattern_id = pattern.id
|
self._pattern_id = pattern.id
|
||||||
self._vm = vm
|
self._vm = vm
|
||||||
|
if units is not None:
|
||||||
|
self._units = units
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
self._units = vm.project.meta.units # type: ignore[union-attr]
|
||||||
|
except Exception:
|
||||||
|
self._units = UnitSystem.METRIC
|
||||||
self._rebuilding = False
|
self._rebuilding = False
|
||||||
self._info = QLabel("")
|
self._info = QLabel("")
|
||||||
# Keep references for tests + future programmatic inspection.
|
# Keep references for tests + future programmatic inspection.
|
||||||
|
|
@ -134,6 +143,34 @@ class PatternLoadsDialog(QDialog):
|
||||||
f"is applied directly to the model DOFs.</i>",
|
f"is applied directly to the model DOFs.</i>",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _unit(self, quantity: str) -> str:
|
||||||
|
try:
|
||||||
|
return native_label(self._units, quantity)
|
||||||
|
except KeyError:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def _nodal_headers(self) -> tuple[str, ...]:
|
||||||
|
f = self._unit("force")
|
||||||
|
m = self._unit("moment")
|
||||||
|
return (
|
||||||
|
"Node",
|
||||||
|
f"Fx ({f})" if f else "Fx",
|
||||||
|
f"Fy ({f})" if f else "Fy",
|
||||||
|
f"Fz ({f})" if f else "Fz",
|
||||||
|
f"Mx ({m})" if m else "Mx",
|
||||||
|
f"My ({m})" if m else "My",
|
||||||
|
f"Mz ({m})" if m else "Mz",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _element_headers(self) -> tuple[str, ...]:
|
||||||
|
d = self._unit("distributed_load")
|
||||||
|
return (
|
||||||
|
"Element",
|
||||||
|
f"wx ({d})" if d else "wx",
|
||||||
|
f"wy ({d})" if d else "wy",
|
||||||
|
f"wz ({d})" if d else "wz",
|
||||||
|
)
|
||||||
|
|
||||||
def _make_nodal_section(self) -> QWidget:
|
def _make_nodal_section(self) -> QWidget:
|
||||||
assert isinstance(self._pattern, PlainLoadPattern)
|
assert isinstance(self._pattern, PlainLoadPattern)
|
||||||
container = QWidget(self)
|
container = QWidget(self)
|
||||||
|
|
@ -144,7 +181,7 @@ class PatternLoadsDialog(QDialog):
|
||||||
if not loads:
|
if not loads:
|
||||||
v.addWidget(QLabel("<i>(none)</i>"))
|
v.addWidget(QLabel("<i>(none)</i>"))
|
||||||
return container
|
return container
|
||||||
table = self._new_table(len(loads), _NODAL_HEADERS)
|
table = self._new_table(len(loads), self._nodal_headers())
|
||||||
self._fill_nodal_table(table, loads)
|
self._fill_nodal_table(table, loads)
|
||||||
table.itemChanged.connect(self._on_nodal_edited)
|
table.itemChanged.connect(self._on_nodal_edited)
|
||||||
self._nodal_table = table
|
self._nodal_table = table
|
||||||
|
|
@ -161,7 +198,7 @@ class PatternLoadsDialog(QDialog):
|
||||||
if not loads:
|
if not loads:
|
||||||
v.addWidget(QLabel("<i>(none)</i>"))
|
v.addWidget(QLabel("<i>(none)</i>"))
|
||||||
return container
|
return container
|
||||||
table = self._new_table(len(loads), _ELEMENT_HEADERS)
|
table = self._new_table(len(loads), self._element_headers())
|
||||||
self._fill_element_table(table, loads)
|
self._fill_element_table(table, loads)
|
||||||
table.itemChanged.connect(self._on_element_edited)
|
table.itemChanged.connect(self._on_element_edited)
|
||||||
self._element_table = table
|
self._element_table = table
|
||||||
|
|
|
||||||
|
|
@ -20,64 +20,95 @@ from PySide6.QtWidgets import (
|
||||||
QWidget,
|
QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from otko.core.units import UnitSystem, native_label
|
||||||
|
|
||||||
|
|
||||||
class QuickGridLinesDialog(QDialog):
|
class QuickGridLinesDialog(QDialog):
|
||||||
"""Three parallel (n, spacing, first_coord) inputs — one per axis."""
|
"""Three parallel (n, spacing, first_coord) inputs — one per axis."""
|
||||||
|
|
||||||
def __init__(self, parent: QWidget | None = None) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
parent: QWidget | None = None,
|
||||||
|
*,
|
||||||
|
units: UnitSystem = UnitSystem.METRIC,
|
||||||
|
) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setWindowTitle("Quick Grid Lines")
|
self.setWindowTitle("Quick Grid Lines")
|
||||||
|
self._units = units
|
||||||
self._build_ui()
|
self._build_ui()
|
||||||
|
|
||||||
def _build_ui(self) -> None:
|
def _build_ui(self) -> None:
|
||||||
root = QVBoxLayout(self)
|
root = QVBoxLayout(self)
|
||||||
root.addWidget(QLabel(
|
root.addWidget(
|
||||||
"<b>Quick-define a regular (Cartesian) grid.</b><br>"
|
QLabel(
|
||||||
"Each axis: number of lines, equal spacing, first line coordinate."
|
"<b>Quick-define a regular (Cartesian) grid.</b><br>"
|
||||||
))
|
"Each axis: number of lines, equal spacing, first line coordinate."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
self._x_n, self._x_s, self._x_f = self._axis_group("X Grid Data", root)
|
self._x_n, self._x_s, self._x_f = self._axis_group("X Grid Data", root)
|
||||||
self._y_n, self._y_s, self._y_f = self._axis_group("Y Grid Data", root)
|
self._y_n, self._y_s, self._y_f = self._axis_group("Y Grid Data", root)
|
||||||
self._z_n, self._z_s, self._z_f = self._axis_group("Z Grid Data", root)
|
self._z_n, self._z_s, self._z_f = self._axis_group("Z Grid Data", root)
|
||||||
|
|
||||||
# Sensible defaults.
|
# Sensible defaults.
|
||||||
self._x_n.setValue(3); self._x_s.setValue(6.0); self._x_f.setValue(0.0)
|
self._x_n.setValue(3)
|
||||||
self._y_n.setValue(3); self._y_s.setValue(6.0); self._y_f.setValue(0.0)
|
self._x_s.setValue(6.0)
|
||||||
self._z_n.setValue(2); self._z_s.setValue(3.0); self._z_f.setValue(0.0)
|
self._x_f.setValue(0.0)
|
||||||
|
self._y_n.setValue(3)
|
||||||
|
self._y_s.setValue(6.0)
|
||||||
|
self._y_f.setValue(0.0)
|
||||||
|
self._z_n.setValue(2)
|
||||||
|
self._z_s.setValue(3.0)
|
||||||
|
self._z_f.setValue(0.0)
|
||||||
|
|
||||||
buttons = QDialogButtonBox(
|
buttons = QDialogButtonBox(
|
||||||
QDialogButtonBox.StandardButton.Ok
|
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel,
|
||||||
| QDialogButtonBox.StandardButton.Cancel,
|
|
||||||
)
|
)
|
||||||
buttons.accepted.connect(self.accept)
|
buttons.accepted.connect(self.accept)
|
||||||
buttons.rejected.connect(self.reject)
|
buttons.rejected.connect(self.reject)
|
||||||
root.addWidget(buttons)
|
root.addWidget(buttons)
|
||||||
|
|
||||||
def _axis_group(
|
def _axis_group(
|
||||||
self, title: str, parent_layout: QVBoxLayout,
|
self,
|
||||||
|
title: str,
|
||||||
|
parent_layout: QVBoxLayout,
|
||||||
) -> tuple[QSpinBox, QDoubleSpinBox, QDoubleSpinBox]:
|
) -> tuple[QSpinBox, QDoubleSpinBox, QDoubleSpinBox]:
|
||||||
box = QGroupBox(title)
|
box = QGroupBox(title)
|
||||||
row = QHBoxLayout(box)
|
row = QHBoxLayout(box)
|
||||||
|
|
||||||
n = QSpinBox(); n.setRange(0, 200); n.setValue(3)
|
n = QSpinBox()
|
||||||
s = QDoubleSpinBox(); s.setRange(0.0, 1e6); s.setDecimals(4)
|
n.setRange(0, 200)
|
||||||
s.setSingleStep(0.5); s.setValue(1.0)
|
n.setValue(3)
|
||||||
f = QDoubleSpinBox(); f.setRange(-1e6, 1e6); f.setDecimals(4)
|
s = QDoubleSpinBox()
|
||||||
f.setSingleStep(0.5); f.setValue(0.0)
|
s.setRange(0.0, 1e6)
|
||||||
|
s.setDecimals(4)
|
||||||
|
s.setSingleStep(0.5)
|
||||||
|
s.setValue(1.0)
|
||||||
|
f = QDoubleSpinBox()
|
||||||
|
f.setRange(-1e6, 1e6)
|
||||||
|
f.setDecimals(4)
|
||||||
|
f.setSingleStep(0.5)
|
||||||
|
f.setValue(0.0)
|
||||||
|
|
||||||
row.addWidget(QLabel("Number of lines:"))
|
row.addWidget(QLabel("Number of lines:"))
|
||||||
row.addWidget(n)
|
row.addWidget(n)
|
||||||
row.addWidget(QLabel("Spacing:"))
|
try:
|
||||||
|
length_u = native_label(self._units, "length")
|
||||||
|
except KeyError:
|
||||||
|
length_u = ""
|
||||||
|
row.addWidget(QLabel(f"Spacing ({length_u}):" if length_u else "Spacing:"))
|
||||||
row.addWidget(s)
|
row.addWidget(s)
|
||||||
row.addWidget(QLabel("First coord:"))
|
row.addWidget(QLabel(f"First coord ({length_u}):" if length_u else "First coord:"))
|
||||||
row.addWidget(f)
|
row.addWidget(f)
|
||||||
parent_layout.addWidget(box)
|
parent_layout.addWidget(box)
|
||||||
return n, s, f
|
return n, s, f
|
||||||
|
|
||||||
def ordinates(self) -> tuple[list[float], list[float], list[float]]:
|
def ordinates(self) -> tuple[list[float], list[float], list[float]]:
|
||||||
"""Return (xs, ys, zs) — flat lists of ordinate values."""
|
"""Return (xs, ys, zs) — flat lists of ordinate values."""
|
||||||
|
|
||||||
def axis(n: QSpinBox, s: QDoubleSpinBox, f: QDoubleSpinBox) -> list[float]:
|
def axis(n: QSpinBox, s: QDoubleSpinBox, f: QDoubleSpinBox) -> list[float]:
|
||||||
return [f.value() + i * s.value() for i in range(n.value())]
|
return [f.value() + i * s.value() for i in range(n.value())]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
axis(self._x_n, self._x_s, self._x_f),
|
axis(self._x_n, self._x_s, self._x_f),
|
||||||
axis(self._y_n, self._y_s, self._y_f),
|
axis(self._y_n, self._y_s, self._y_f),
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ from PySide6.QtWidgets import (
|
||||||
QWidget,
|
QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from otko.core.units import UnitSystem, native_label
|
||||||
|
|
||||||
|
|
||||||
class ReplicateDialog(QDialog):
|
class ReplicateDialog(QDialog):
|
||||||
"""Dialog for offset (dx, dy, dz) and number of copies.
|
"""Dialog for offset (dx, dy, dz) and number of copies.
|
||||||
|
|
@ -34,20 +36,22 @@ class ReplicateDialog(QDialog):
|
||||||
parent: QWidget | None = None,
|
parent: QWidget | None = None,
|
||||||
*,
|
*,
|
||||||
ndm: int = 3,
|
ndm: int = 3,
|
||||||
|
units: UnitSystem = UnitSystem.METRIC,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setWindowTitle("Replicate")
|
self.setWindowTitle("Replicate")
|
||||||
self._ndm = ndm
|
self._ndm = ndm
|
||||||
|
self._units = units
|
||||||
self._build_ui(n_nodes, n_elements)
|
self._build_ui(n_nodes, n_elements)
|
||||||
|
|
||||||
def _build_ui(self, n_nodes: int, n_elements: int) -> None:
|
def _build_ui(self, n_nodes: int, n_elements: int) -> None:
|
||||||
layout = QVBoxLayout(self)
|
layout = QVBoxLayout(self)
|
||||||
layout.addWidget(QLabel(
|
layout.addWidget(
|
||||||
f"Replicate <b>{n_nodes}</b> node(s) and <b>{n_elements}</b> element(s)."
|
QLabel(f"Replicate <b>{n_nodes}</b> node(s) and <b>{n_elements}</b> element(s).")
|
||||||
))
|
)
|
||||||
layout.addWidget(QLabel(
|
layout.addWidget(
|
||||||
"<i>Only elements whose endpoints are both in the selection are copied.</i>"
|
QLabel("<i>Only elements whose endpoints are both in the selection are copied.</i>")
|
||||||
))
|
)
|
||||||
|
|
||||||
form = QFormLayout()
|
form = QFormLayout()
|
||||||
|
|
||||||
|
|
@ -66,9 +70,13 @@ class ReplicateDialog(QDialog):
|
||||||
self._n.setRange(1, 200)
|
self._n.setRange(1, 200)
|
||||||
self._n.setValue(1)
|
self._n.setValue(1)
|
||||||
|
|
||||||
form.addRow("dX:", self._dx)
|
try:
|
||||||
form.addRow("dY:", self._dy)
|
length_u = native_label(self._units, "length")
|
||||||
form.addRow("dZ:", self._dz)
|
except KeyError:
|
||||||
|
length_u = ""
|
||||||
|
form.addRow(f"dX ({length_u}):" if length_u else "dX:", self._dx)
|
||||||
|
form.addRow(f"dY ({length_u}):" if length_u else "dY:", self._dy)
|
||||||
|
form.addRow(f"dZ ({length_u}):" if length_u else "dZ:", self._dz)
|
||||||
form.addRow("Number of copies:", self._n)
|
form.addRow("Number of copies:", self._n)
|
||||||
layout.addLayout(form)
|
layout.addLayout(form)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -65,12 +65,14 @@ class RunAnalysisDialog(QDialog):
|
||||||
self._alpha_m.setRange(0.0, 1e6)
|
self._alpha_m.setRange(0.0, 1e6)
|
||||||
self._alpha_m.setDecimals(6)
|
self._alpha_m.setDecimals(6)
|
||||||
self._alpha_m.setSingleStep(0.01)
|
self._alpha_m.setSingleStep(0.01)
|
||||||
|
self._alpha_m.setToolTip("Mass-proportional damping (1/s) — native units, no conversion.")
|
||||||
form.addRow("alphaM (mass-prop.):", self._alpha_m)
|
form.addRow("alphaM (mass-prop.):", self._alpha_m)
|
||||||
|
|
||||||
self._beta_k = QDoubleSpinBox()
|
self._beta_k = QDoubleSpinBox()
|
||||||
self._beta_k.setRange(0.0, 1e6)
|
self._beta_k.setRange(0.0, 1e6)
|
||||||
self._beta_k.setDecimals(6)
|
self._beta_k.setDecimals(6)
|
||||||
self._beta_k.setSingleStep(1e-4)
|
self._beta_k.setSingleStep(1e-4)
|
||||||
|
self._beta_k.setToolTip("Stiffness-proportional damping (s) — native units, no conversion.")
|
||||||
form.addRow("betaK (stiffness-prop.):", self._beta_k)
|
form.addRow("betaK (stiffness-prop.):", self._beta_k)
|
||||||
|
|
||||||
self._mode1_damping = QDoubleSpinBox()
|
self._mode1_damping = QDoubleSpinBox()
|
||||||
|
|
@ -79,12 +81,14 @@ class RunAnalysisDialog(QDialog):
|
||||||
self._mode1_damping.setSingleStep(0.01)
|
self._mode1_damping.setSingleStep(0.01)
|
||||||
form.addRow("Mode-1 damping zeta:", self._mode1_damping)
|
form.addRow("Mode-1 damping zeta:", self._mode1_damping)
|
||||||
|
|
||||||
form.addRow(QLabel(
|
form.addRow(
|
||||||
"<i>Values are applied for this run only via a copy of the case — "
|
QLabel(
|
||||||
"the stored case is not modified. If mode-1 damping is "
|
"<i>Values are applied for this run only via a copy of the case — "
|
||||||
"greater than zero, the runner computes betaK from the first "
|
"the stored case is not modified. If mode-1 damping is "
|
||||||
"mode after preload and overrides the manual betaK value.</i>",
|
"greater than zero, the runner computes betaK from the first "
|
||||||
))
|
"mode after preload and overrides the manual betaK value.</i>",
|
||||||
|
)
|
||||||
|
)
|
||||||
layout.addWidget(self._damping_box)
|
layout.addWidget(self._damping_box)
|
||||||
|
|
||||||
self._progress = QProgressBar()
|
self._progress = QProgressBar()
|
||||||
|
|
@ -98,7 +102,8 @@ class RunAnalysisDialog(QDialog):
|
||||||
layout.addWidget(self._log, stretch=1)
|
layout.addWidget(self._log, stretch=1)
|
||||||
|
|
||||||
self._buttons = QDialogButtonBox(
|
self._buttons = QDialogButtonBox(
|
||||||
QDialogButtonBox.StandardButton.Close, parent=self,
|
QDialogButtonBox.StandardButton.Close,
|
||||||
|
parent=self,
|
||||||
)
|
)
|
||||||
self._run_btn = QPushButton("Run")
|
self._run_btn = QPushButton("Run")
|
||||||
self._buttons.addButton(self._run_btn, QDialogButtonBox.ButtonRole.ActionRole)
|
self._buttons.addButton(self._run_btn, QDialogButtonBox.ButtonRole.ActionRole)
|
||||||
|
|
@ -157,11 +162,13 @@ class RunAnalysisDialog(QDialog):
|
||||||
return
|
return
|
||||||
if case.type == "Transient":
|
if case.type == "Transient":
|
||||||
mode1 = float(self._mode1_damping.value())
|
mode1 = float(self._mode1_damping.value())
|
||||||
case = case.model_copy(update={
|
case = case.model_copy(
|
||||||
"rayleigh_alpha_m": float(self._alpha_m.value()),
|
update={
|
||||||
"rayleigh_beta_k": float(self._beta_k.value()),
|
"rayleigh_alpha_m": float(self._alpha_m.value()),
|
||||||
"rayleigh_mode1_damping": mode1 if mode1 > 0.0 else None,
|
"rayleigh_beta_k": float(self._beta_k.value()),
|
||||||
})
|
"rayleigh_mode1_damping": mode1 if mode1 > 0.0 else None,
|
||||||
|
}
|
||||||
|
)
|
||||||
self._results = None
|
self._results = None
|
||||||
self._log.clear()
|
self._log.clear()
|
||||||
results_dir: Path | None = None
|
results_dir: Path | None = None
|
||||||
|
|
|
||||||
|
|
@ -39,32 +39,48 @@ from otko.core.sections import (
|
||||||
RectangularPatch,
|
RectangularPatch,
|
||||||
StraightLayer,
|
StraightLayer,
|
||||||
)
|
)
|
||||||
|
from otko.core.units import UnitSystem, native_label
|
||||||
from otko.services.section_properties import (
|
from otko.services.section_properties import (
|
||||||
compute_section_props,
|
compute_section_props,
|
||||||
expand_fibres,
|
expand_fibres,
|
||||||
)
|
)
|
||||||
|
|
||||||
_PATCH_COLORS = [
|
_PATCH_COLORS = [
|
||||||
"#4e79a7", "#f28e2b", "#e15759", "#76b7b2",
|
"#4e79a7",
|
||||||
"#59a14f", "#edc948", "#b07aa1", "#ff9da7",
|
"#f28e2b",
|
||||||
|
"#e15759",
|
||||||
|
"#76b7b2",
|
||||||
|
"#59a14f",
|
||||||
|
"#edc948",
|
||||||
|
"#b07aa1",
|
||||||
|
"#ff9da7",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class FiberSectionEditor(QDialog):
|
class FiberSectionEditor(QDialog):
|
||||||
"""Modal dialog: build a FiberSection from patches + layers."""
|
"""Modal dialog: build a FiberSection from patches + layers."""
|
||||||
|
|
||||||
def __init__(self, material_ids: list[int],
|
def __init__(
|
||||||
existing: FiberSection | None = None,
|
self,
|
||||||
parent: QWidget | None = None) -> None:
|
material_ids: list[int],
|
||||||
|
existing: FiberSection | None = None,
|
||||||
|
parent: QWidget | None = None,
|
||||||
|
*,
|
||||||
|
units: UnitSystem = UnitSystem.METRIC,
|
||||||
|
) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setWindowTitle("Fiber Section Editor")
|
self.setWindowTitle("Fiber Section Editor")
|
||||||
self.resize(1000, 650)
|
self.resize(1000, 650)
|
||||||
|
self._units = units
|
||||||
self._material_ids = material_ids
|
self._material_ids = material_ids
|
||||||
self._patches: list[Any] = [] # RectangularPatch | CircularPatch
|
self._patches: list[Any] = [] # RectangularPatch | CircularPatch
|
||||||
self._layers: list[StraightLayer] = []
|
self._layers: list[StraightLayer] = []
|
||||||
self._section_id: int = existing.id if existing else 1
|
self._section_id: int = existing.id if existing else 1
|
||||||
self._section_name: str = existing.name if existing else ""
|
self._section_name: str = existing.name if existing else ""
|
||||||
self._gj: float | None = existing.GJ if existing else None
|
self._gj: float | None = existing.GJ if existing else None
|
||||||
|
# (form, field widget, base name, quantity) for rows whose label
|
||||||
|
# carries a unit hint — refreshed by set_units().
|
||||||
|
self._unit_rows: list[tuple[QFormLayout, QWidget, str, str]] = []
|
||||||
self._build_ui()
|
self._build_ui()
|
||||||
if existing is not None:
|
if existing is not None:
|
||||||
self._load_existing(existing)
|
self._load_existing(existing)
|
||||||
|
|
@ -106,28 +122,49 @@ class FiberSectionEditor(QDialog):
|
||||||
pf.addRow("Material:", self._patch_mat)
|
pf.addRow("Material:", self._patch_mat)
|
||||||
|
|
||||||
# Rect fields
|
# Rect fields
|
||||||
self._rect_yi = self._spin(-0.15); self._rect_zi = self._spin(-0.15)
|
self._rect_yi = self._spin(-0.15)
|
||||||
self._rect_yj = self._spin(0.15); self._rect_zj = self._spin(0.15)
|
self._rect_zi = self._spin(-0.15)
|
||||||
self._rect_ny = self._ispin(8); self._rect_nz = self._ispin(8)
|
self._rect_yj = self._spin(0.15)
|
||||||
|
self._rect_zj = self._spin(0.15)
|
||||||
|
self._rect_ny = self._ispin(8)
|
||||||
|
self._rect_nz = self._ispin(8)
|
||||||
|
# Unit hints only — values are entered in native stored units.
|
||||||
|
self._add_unit_row(pf, "y_i", "length", self._rect_yi)
|
||||||
|
self._add_unit_row(pf, "z_i", "length", self._rect_zi)
|
||||||
|
self._add_unit_row(pf, "y_j", "length", self._rect_yj)
|
||||||
|
self._add_unit_row(pf, "z_j", "length", self._rect_zj)
|
||||||
|
pf.addRow("n_fib_y:", self._rect_ny)
|
||||||
|
pf.addRow("n_fib_z:", self._rect_nz)
|
||||||
self._rect_rows = [
|
self._rect_rows = [
|
||||||
("y_i:", self._rect_yi), ("z_i:", self._rect_zi),
|
("y_i:", self._rect_yi),
|
||||||
("y_j:", self._rect_yj), ("z_j:", self._rect_zj),
|
("z_i:", self._rect_zi),
|
||||||
("n_fib_y:", self._rect_ny), ("n_fib_z:", self._rect_nz),
|
("y_j:", self._rect_yj),
|
||||||
|
("z_j:", self._rect_zj),
|
||||||
|
("n_fib_y:", self._rect_ny),
|
||||||
|
("n_fib_z:", self._rect_nz),
|
||||||
]
|
]
|
||||||
for label, widget in self._rect_rows:
|
|
||||||
pf.addRow(label, widget)
|
|
||||||
|
|
||||||
# Circ fields (initially hidden)
|
# Circ fields (initially hidden)
|
||||||
self._circ_yc = self._spin(0.0); self._circ_zc = self._spin(0.0)
|
self._circ_yc = self._spin(0.0)
|
||||||
self._circ_ri = self._spin(0.0); self._circ_ro = self._spin(0.15)
|
self._circ_zc = self._spin(0.0)
|
||||||
self._circ_nc = self._ispin(16); self._circ_nr = self._ispin(4)
|
self._circ_ri = self._spin(0.0)
|
||||||
|
self._circ_ro = self._spin(0.15)
|
||||||
|
self._circ_nc = self._ispin(16)
|
||||||
|
self._circ_nr = self._ispin(4)
|
||||||
|
self._add_unit_row(pf, "y_center", "length", self._circ_yc)
|
||||||
|
self._add_unit_row(pf, "z_center", "length", self._circ_zc)
|
||||||
|
self._add_unit_row(pf, "r_inner", "length", self._circ_ri)
|
||||||
|
self._add_unit_row(pf, "r_outer", "length", self._circ_ro)
|
||||||
|
pf.addRow("n_circ:", self._circ_nc)
|
||||||
|
pf.addRow("n_rad:", self._circ_nr)
|
||||||
self._circ_rows = [
|
self._circ_rows = [
|
||||||
("y_center:", self._circ_yc), ("z_center:", self._circ_zc),
|
("y_center:", self._circ_yc),
|
||||||
("r_inner:", self._circ_ri), ("r_outer:", self._circ_ro),
|
("z_center:", self._circ_zc),
|
||||||
("n_circ:", self._circ_nc), ("n_rad:", self._circ_nr),
|
("r_inner:", self._circ_ri),
|
||||||
|
("r_outer:", self._circ_ro),
|
||||||
|
("n_circ:", self._circ_nc),
|
||||||
|
("n_rad:", self._circ_nr),
|
||||||
]
|
]
|
||||||
for label, widget in self._circ_rows:
|
|
||||||
pf.addRow(label, widget)
|
|
||||||
|
|
||||||
self._add_patch_btn = QPushButton("Add patch")
|
self._add_patch_btn = QPushButton("Add patch")
|
||||||
self._add_patch_btn.clicked.connect(self._on_add_patch)
|
self._add_patch_btn.clicked.connect(self._on_add_patch)
|
||||||
|
|
@ -145,12 +182,16 @@ class FiberSectionEditor(QDialog):
|
||||||
lf.addRow("Material:", self._layer_mat)
|
lf.addRow("Material:", self._layer_mat)
|
||||||
self._layer_nbars = self._ispin(4)
|
self._layer_nbars = self._ispin(4)
|
||||||
self._layer_area = self._spin(0.0005, step=0.0001, minimum=1e-12)
|
self._layer_area = self._spin(0.0005, step=0.0001, minimum=1e-12)
|
||||||
self._layer_ys = self._spin(-0.12); self._layer_zs = self._spin(-0.12)
|
self._layer_ys = self._spin(-0.12)
|
||||||
self._layer_ye = self._spin(0.12); self._layer_ze = self._spin(-0.12)
|
self._layer_zs = self._spin(-0.12)
|
||||||
|
self._layer_ye = self._spin(0.12)
|
||||||
|
self._layer_ze = self._spin(-0.12)
|
||||||
lf.addRow("n_bars:", self._layer_nbars)
|
lf.addRow("n_bars:", self._layer_nbars)
|
||||||
lf.addRow("bar_area:", self._layer_area)
|
self._add_unit_row(lf, "bar_area", "area", self._layer_area)
|
||||||
lf.addRow("y_start:", self._layer_ys); lf.addRow("z_start:", self._layer_zs)
|
self._add_unit_row(lf, "y_start", "length", self._layer_ys)
|
||||||
lf.addRow("y_end:", self._layer_ye); lf.addRow("z_end:", self._layer_ze)
|
self._add_unit_row(lf, "z_start", "length", self._layer_zs)
|
||||||
|
self._add_unit_row(lf, "y_end", "length", self._layer_ye)
|
||||||
|
self._add_unit_row(lf, "z_end", "length", self._layer_ze)
|
||||||
self._add_layer_btn = QPushButton("Add layer")
|
self._add_layer_btn = QPushButton("Add layer")
|
||||||
self._add_layer_btn.clicked.connect(self._on_add_layer)
|
self._add_layer_btn.clicked.connect(self._on_add_layer)
|
||||||
lf.addRow(self._add_layer_btn)
|
lf.addRow(self._add_layer_btn)
|
||||||
|
|
@ -181,8 +222,8 @@ class FiberSectionEditor(QDialog):
|
||||||
self._preview = pg.PlotWidget()
|
self._preview = pg.PlotWidget()
|
||||||
self._preview.setBackground("#1e1e1e")
|
self._preview.setBackground("#1e1e1e")
|
||||||
self._preview.setAspectLocked(True)
|
self._preview.setAspectLocked(True)
|
||||||
self._preview.setLabel("bottom", "y (m)")
|
self._apply_preview_labels()
|
||||||
self._preview.setLabel("left", "z (m)")
|
self._preview.showGrid(x=True, y=True, alpha=0.3)
|
||||||
self._preview.showGrid(x=True, y=True, alpha=0.3)
|
self._preview.showGrid(x=True, y=True, alpha=0.3)
|
||||||
right_layout.addWidget(self._preview, 1)
|
right_layout.addWidget(self._preview, 1)
|
||||||
|
|
||||||
|
|
@ -195,10 +236,79 @@ class FiberSectionEditor(QDialog):
|
||||||
|
|
||||||
self._on_patch_type_changed(0)
|
self._on_patch_type_changed(0)
|
||||||
|
|
||||||
|
def set_units(self, units: UnitSystem) -> None:
|
||||||
|
"""Re-apply the project's stored unit system to labels.
|
||||||
|
|
||||||
|
Values are always entered in native units; only the row and axis
|
||||||
|
labels change. Never throws.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self._units = units
|
||||||
|
self._apply_preview_labels()
|
||||||
|
self._apply_row_labels()
|
||||||
|
self._refresh_preview()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _apply_preview_labels(self) -> None:
|
||||||
|
"""Set preview axis labels from the stored length unit."""
|
||||||
|
try:
|
||||||
|
length = native_label(self._units, "length")
|
||||||
|
except KeyError:
|
||||||
|
length = "m"
|
||||||
|
self._preview.setLabel("bottom", f"y ({length})")
|
||||||
|
self._preview.setLabel("left", f"z ({length})")
|
||||||
|
|
||||||
|
def _unit_suffix(self, quantity: str) -> str:
|
||||||
|
"""Return ``" (unit)"`` for a quantity, or ``""`` when unknown.
|
||||||
|
|
||||||
|
Unit hints only — values are entered in native stored units,
|
||||||
|
never converted.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return f" ({native_label(self._units, quantity)})"
|
||||||
|
except KeyError:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def _add_unit_row(self, form: QFormLayout, base: str, quantity: str, widget: QWidget) -> None:
|
||||||
|
"""Add a ``"<base> (<unit>):"`` row and register it for set_units."""
|
||||||
|
form.addRow(f"{base}{self._unit_suffix(quantity)}:", widget)
|
||||||
|
self._unit_rows.append((form, widget, base, quantity))
|
||||||
|
|
||||||
|
def _apply_row_labels(self) -> None:
|
||||||
|
"""Refresh registered unit-hint row labels for the stored units."""
|
||||||
|
for form, widget, base, quantity in self._unit_rows:
|
||||||
|
try:
|
||||||
|
pos = form.getWidgetPosition(widget)
|
||||||
|
except RuntimeError:
|
||||||
|
continue
|
||||||
|
if not isinstance(pos, tuple) or not pos:
|
||||||
|
continue
|
||||||
|
row = pos[0]
|
||||||
|
item = form.itemAt(row, QFormLayout.ItemRole.LabelRole)
|
||||||
|
label_widget = item.widget() if item is not None else None
|
||||||
|
if isinstance(label_widget, QLabel):
|
||||||
|
label_widget.setText(f"{base}{self._unit_suffix(quantity)}:")
|
||||||
|
|
||||||
|
def _gj_label(self) -> str:
|
||||||
|
"""Return the GJ unit hint (moment·length² composite)."""
|
||||||
|
try:
|
||||||
|
moment_u = native_label(self._units, "moment")
|
||||||
|
length_u = native_label(self._units, "length")
|
||||||
|
except KeyError:
|
||||||
|
return ""
|
||||||
|
return f"{moment_u}·{length_u}²"
|
||||||
|
|
||||||
|
def _gj_props_suffix(self) -> str:
|
||||||
|
"""Return ``" GJ = … <unit>"`` when GJ is set, else ``""``."""
|
||||||
|
if self._gj is None:
|
||||||
|
return ""
|
||||||
|
gj_u = self._gj_label()
|
||||||
|
return f" GJ = {self._gj:.4e} {gj_u}" if gj_u else f" GJ = {self._gj:.4e}"
|
||||||
|
|
||||||
# ── helpers ─────────────────────────────────────────────────────
|
# ── helpers ─────────────────────────────────────────────────────
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _spin(default: float = 0.0, *, step: float = 0.01,
|
def _spin(default: float = 0.0, *, step: float = 0.01, minimum: float = -1e6) -> QDoubleSpinBox:
|
||||||
minimum: float = -1e6) -> QDoubleSpinBox:
|
|
||||||
sb = QDoubleSpinBox()
|
sb = QDoubleSpinBox()
|
||||||
sb.setRange(minimum, 1e6)
|
sb.setRange(minimum, 1e6)
|
||||||
sb.setDecimals(6)
|
sb.setDecimals(6)
|
||||||
|
|
@ -214,7 +324,7 @@ class FiberSectionEditor(QDialog):
|
||||||
return sb
|
return sb
|
||||||
|
|
||||||
def _on_patch_type_changed(self, idx: int) -> None:
|
def _on_patch_type_changed(self, idx: int) -> None:
|
||||||
is_rect = (idx == 0)
|
is_rect = idx == 0
|
||||||
for _, w in self._rect_rows:
|
for _, w in self._rect_rows:
|
||||||
w.setVisible(is_rect)
|
w.setVisible(is_rect)
|
||||||
for _, w in self._circ_rows:
|
for _, w in self._circ_rows:
|
||||||
|
|
@ -226,28 +336,38 @@ class FiberSectionEditor(QDialog):
|
||||||
if self._patch_type.currentIndex() == 0:
|
if self._patch_type.currentIndex() == 0:
|
||||||
p = RectangularPatch(
|
p = RectangularPatch(
|
||||||
material_id=mid,
|
material_id=mid,
|
||||||
n_fib_y=self._rect_ny.value(), n_fib_z=self._rect_nz.value(),
|
n_fib_y=self._rect_ny.value(),
|
||||||
y_i=self._rect_yi.value(), z_i=self._rect_zi.value(),
|
n_fib_z=self._rect_nz.value(),
|
||||||
y_j=self._rect_yj.value(), z_j=self._rect_zj.value(),
|
y_i=self._rect_yi.value(),
|
||||||
|
z_i=self._rect_zi.value(),
|
||||||
|
y_j=self._rect_yj.value(),
|
||||||
|
z_j=self._rect_zj.value(),
|
||||||
)
|
)
|
||||||
self._patches.append(p)
|
self._patches.append(p)
|
||||||
self._item_list.addItem(QListWidgetItem(
|
self._item_list.addItem(
|
||||||
f"Rect patch mat={mid} "
|
QListWidgetItem(
|
||||||
f"({p.y_i:.3f},{p.z_i:.3f})→({p.y_j:.3f},{p.z_j:.3f}) "
|
f"Rect patch mat={mid} "
|
||||||
f"{p.n_fib_y}×{p.n_fib_z}",
|
f"({p.y_i:.3f},{p.z_i:.3f})→({p.y_j:.3f},{p.z_j:.3f}) "
|
||||||
))
|
f"{p.n_fib_y}×{p.n_fib_z}",
|
||||||
|
)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
p = CircularPatch(
|
p = CircularPatch(
|
||||||
material_id=mid,
|
material_id=mid,
|
||||||
n_fib_circ=self._circ_nc.value(), n_fib_rad=self._circ_nr.value(),
|
n_fib_circ=self._circ_nc.value(),
|
||||||
y_center=self._circ_yc.value(), z_center=self._circ_zc.value(),
|
n_fib_rad=self._circ_nr.value(),
|
||||||
r_inner=self._circ_ri.value(), r_outer=self._circ_ro.value(),
|
y_center=self._circ_yc.value(),
|
||||||
|
z_center=self._circ_zc.value(),
|
||||||
|
r_inner=self._circ_ri.value(),
|
||||||
|
r_outer=self._circ_ro.value(),
|
||||||
)
|
)
|
||||||
self._patches.append(p)
|
self._patches.append(p)
|
||||||
self._item_list.addItem(QListWidgetItem(
|
self._item_list.addItem(
|
||||||
f"Circ patch mat={mid} "
|
QListWidgetItem(
|
||||||
f"r={p.r_inner:.3f}→{p.r_outer:.3f} {p.n_fib_circ}×{p.n_fib_rad}",
|
f"Circ patch mat={mid} "
|
||||||
))
|
f"r={p.r_inner:.3f}→{p.r_outer:.3f} {p.n_fib_circ}×{p.n_fib_rad}",
|
||||||
|
)
|
||||||
|
)
|
||||||
self._refresh_preview()
|
self._refresh_preview()
|
||||||
|
|
||||||
def _on_add_layer(self) -> None:
|
def _on_add_layer(self) -> None:
|
||||||
|
|
@ -256,13 +376,17 @@ class FiberSectionEditor(QDialog):
|
||||||
material_id=mid,
|
material_id=mid,
|
||||||
n_bars=self._layer_nbars.value(),
|
n_bars=self._layer_nbars.value(),
|
||||||
bar_area=self._layer_area.value(),
|
bar_area=self._layer_area.value(),
|
||||||
y_start=self._layer_ys.value(), z_start=self._layer_zs.value(),
|
y_start=self._layer_ys.value(),
|
||||||
y_end=self._layer_ye.value(), z_end=self._layer_ze.value(),
|
z_start=self._layer_zs.value(),
|
||||||
|
y_end=self._layer_ye.value(),
|
||||||
|
z_end=self._layer_ze.value(),
|
||||||
)
|
)
|
||||||
self._layers.append(lay)
|
self._layers.append(lay)
|
||||||
self._item_list.addItem(QListWidgetItem(
|
self._item_list.addItem(
|
||||||
f"Layer mat={mid} {lay.n_bars} bars A={lay.bar_area:.4g}",
|
QListWidgetItem(
|
||||||
))
|
f"Layer mat={mid} {lay.n_bars} bars A={lay.bar_area:.4g}",
|
||||||
|
)
|
||||||
|
)
|
||||||
self._refresh_preview()
|
self._refresh_preview()
|
||||||
|
|
||||||
def _on_remove(self) -> None:
|
def _on_remove(self) -> None:
|
||||||
|
|
@ -280,8 +404,7 @@ class FiberSectionEditor(QDialog):
|
||||||
# ── preview ─────────────────────────────────────────────────────
|
# ── preview ─────────────────────────────────────────────────────
|
||||||
def _refresh_preview(self) -> None:
|
def _refresh_preview(self) -> None:
|
||||||
self._preview.clear()
|
self._preview.clear()
|
||||||
sec = FiberSection(id=999999, patches=list(self._patches),
|
sec = FiberSection(id=999999, patches=list(self._patches), layers=list(self._layers))
|
||||||
layers=list(self._layers))
|
|
||||||
props = compute_section_props(sec)
|
props = compute_section_props(sec)
|
||||||
if props.n_fibres == 0:
|
if props.n_fibres == 0:
|
||||||
self._props_label.setText("Add patches or layers to see the preview.")
|
self._props_label.setText("Add patches or layers to see the preview.")
|
||||||
|
|
@ -295,8 +418,9 @@ class FiberSectionEditor(QDialog):
|
||||||
for lay in self._layers:
|
for lay in self._layers:
|
||||||
mat_ids.append(lay.material_id)
|
mat_ids.append(lay.material_id)
|
||||||
unique_mats = sorted(set(mat_ids)) if mat_ids else [1]
|
unique_mats = sorted(set(mat_ids)) if mat_ids else [1]
|
||||||
mat_to_color = {mid: _PATCH_COLORS[i % len(_PATCH_COLORS)]
|
mat_to_color = {
|
||||||
for i, mid in enumerate(unique_mats)}
|
mid: _PATCH_COLORS[i % len(_PATCH_COLORS)] for i, mid in enumerate(unique_mats)
|
||||||
|
}
|
||||||
|
|
||||||
# Draw patch fibres as squares, layer fibres as circles.
|
# Draw patch fibres as squares, layer fibres as circles.
|
||||||
# Expand per-patch for color assignment.
|
# Expand per-patch for color assignment.
|
||||||
|
|
@ -307,9 +431,13 @@ class FiberSectionEditor(QDialog):
|
||||||
continue
|
continue
|
||||||
color = mat_to_color.get(p.material_id, "#888888")
|
color = mat_to_color.get(p.material_id, "#888888")
|
||||||
self._preview.plot(
|
self._preview.plot(
|
||||||
sub[:, 0], sub[:, 1],
|
sub[:, 0],
|
||||||
pen=None, symbol="s", symbolSize=6,
|
sub[:, 1],
|
||||||
symbolBrush=color, symbolPen=None,
|
pen=None,
|
||||||
|
symbol="s",
|
||||||
|
symbolSize=6,
|
||||||
|
symbolBrush=color,
|
||||||
|
symbolPen=None,
|
||||||
)
|
)
|
||||||
for lay in self._layers:
|
for lay in self._layers:
|
||||||
sub_sec = FiberSection(id=999999, layers=[lay])
|
sub_sec = FiberSection(id=999999, layers=[lay])
|
||||||
|
|
@ -318,39 +446,54 @@ class FiberSectionEditor(QDialog):
|
||||||
continue
|
continue
|
||||||
color = mat_to_color.get(lay.material_id, "#ff0000")
|
color = mat_to_color.get(lay.material_id, "#ff0000")
|
||||||
self._preview.plot(
|
self._preview.plot(
|
||||||
sub[:, 0], sub[:, 1],
|
sub[:, 0],
|
||||||
pen=None, symbol="o", symbolSize=8,
|
sub[:, 1],
|
||||||
symbolBrush=color, symbolPen=pg.mkPen("#ffffff", width=1),
|
pen=None,
|
||||||
|
symbol="o",
|
||||||
|
symbolSize=8,
|
||||||
|
symbolBrush=color,
|
||||||
|
symbolPen=pg.mkPen("#ffffff", width=1),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Centroid marker
|
# Centroid marker
|
||||||
self._preview.plot(
|
self._preview.plot(
|
||||||
[props.centroid_y], [props.centroid_z],
|
[props.centroid_y],
|
||||||
pen=None, symbol="+", symbolSize=16,
|
[props.centroid_z],
|
||||||
symbolBrush=None, symbolPen=pg.mkPen("#ff0000", width=2),
|
pen=None,
|
||||||
|
symbol="+",
|
||||||
|
symbolSize=16,
|
||||||
|
symbolBrush=None,
|
||||||
|
symbolPen=pg.mkPen("#ff0000", width=2),
|
||||||
)
|
)
|
||||||
|
|
||||||
self._props_label.setText(
|
self._props_label.setText(
|
||||||
f"Fibres: {props.n_fibres} "
|
f"Fibres: {props.n_fibres} "
|
||||||
f"A = {props.area:.4e} m² "
|
f"A = {props.area:.4e} m² "
|
||||||
f"Centroid = ({props.centroid_y:.4e}, {props.centroid_z:.4e}) "
|
f"Centroid = ({props.centroid_y:.4e}, {props.centroid_z:.4e}) "
|
||||||
f"Iy = {props.Iy:.4e} m⁴ Iz = {props.Iz:.4e} m⁴",
|
f"Iy = {props.Iy:.4e} m⁴ Iz = {props.Iz:.4e} m⁴"
|
||||||
|
f"{self._gj_props_suffix()}",
|
||||||
)
|
)
|
||||||
|
|
||||||
def _load_existing(self, sec: FiberSection) -> None:
|
def _load_existing(self, sec: FiberSection) -> None:
|
||||||
for p in sec.patches:
|
for p in sec.patches:
|
||||||
self._patches.append(p)
|
self._patches.append(p)
|
||||||
if isinstance(p, RectangularPatch):
|
if isinstance(p, RectangularPatch):
|
||||||
self._item_list.addItem(QListWidgetItem(
|
self._item_list.addItem(
|
||||||
f"Rect patch mat={p.material_id} {p.n_fib_y}×{p.n_fib_z}",
|
QListWidgetItem(
|
||||||
))
|
f"Rect patch mat={p.material_id} {p.n_fib_y}×{p.n_fib_z}",
|
||||||
|
)
|
||||||
|
)
|
||||||
elif isinstance(p, CircularPatch):
|
elif isinstance(p, CircularPatch):
|
||||||
self._item_list.addItem(QListWidgetItem(
|
self._item_list.addItem(
|
||||||
f"Circ patch mat={p.material_id} {p.n_fib_circ}×{p.n_fib_rad}",
|
QListWidgetItem(
|
||||||
))
|
f"Circ patch mat={p.material_id} {p.n_fib_circ}×{p.n_fib_rad}",
|
||||||
|
)
|
||||||
|
)
|
||||||
for lay in sec.layers:
|
for lay in sec.layers:
|
||||||
if isinstance(lay, StraightLayer):
|
if isinstance(lay, StraightLayer):
|
||||||
self._layers.append(lay)
|
self._layers.append(lay)
|
||||||
self._item_list.addItem(QListWidgetItem(
|
self._item_list.addItem(
|
||||||
f"Layer mat={lay.material_id} {lay.n_bars} bars",
|
QListWidgetItem(
|
||||||
))
|
f"Layer mat={lay.material_id} {lay.n_bars} bars",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -23,11 +23,25 @@ from otko.core import (
|
||||||
FiberSection,
|
FiberSection,
|
||||||
SectionAggregator,
|
SectionAggregator,
|
||||||
)
|
)
|
||||||
|
from otko.core.units import UnitSystem, native_label
|
||||||
|
|
||||||
|
|
||||||
def _spin(default: float = 0.0, *, decimals: int = 8,
|
def _u(units: UnitSystem, quantity: str) -> str:
|
||||||
minimum: float = 1e-12, maximum: float = 1e15,
|
"""Display label for ``quantity`` ("" when dimensionless/unknown)."""
|
||||||
step: float = 1.0) -> QDoubleSpinBox:
|
try:
|
||||||
|
return native_label(units, quantity)
|
||||||
|
except KeyError:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _spin(
|
||||||
|
default: float = 0.0,
|
||||||
|
*,
|
||||||
|
decimals: int = 8,
|
||||||
|
minimum: float = 1e-12,
|
||||||
|
maximum: float = 1e15,
|
||||||
|
step: float = 1.0,
|
||||||
|
) -> QDoubleSpinBox:
|
||||||
sb = QDoubleSpinBox()
|
sb = QDoubleSpinBox()
|
||||||
sb.setRange(minimum, maximum)
|
sb.setRange(minimum, maximum)
|
||||||
sb.setDecimals(decimals)
|
sb.setDecimals(decimals)
|
||||||
|
|
@ -62,8 +76,11 @@ def _opt_value(sb: QDoubleSpinBox) -> float | None:
|
||||||
class SectionFormBase(QWidget):
|
class SectionFormBase(QWidget):
|
||||||
type_label: str = "Section"
|
type_label: str = "Section"
|
||||||
|
|
||||||
def __init__(self, parent: QWidget | None = None) -> None:
|
def __init__(
|
||||||
|
self, parent: QWidget | None = None, *, units: UnitSystem = UnitSystem.METRIC
|
||||||
|
) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
self._units = units
|
||||||
self._section_id: int | None = None
|
self._section_id: int | None = None
|
||||||
self._layout = QFormLayout(self)
|
self._layout = QFormLayout(self)
|
||||||
self._name_edit = QLineEdit()
|
self._name_edit = QLineEdit()
|
||||||
|
|
@ -99,17 +116,33 @@ class ElasticSectionForm(SectionFormBase):
|
||||||
"circle": ("diameter",),
|
"circle": ("diameter",),
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, parent: QWidget | None = None) -> None:
|
def __init__(
|
||||||
|
self, parent: QWidget | None = None, *, units: UnitSystem = UnitSystem.METRIC
|
||||||
|
) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
self._units = units
|
||||||
self._e = _spin(200e9, step=1e9)
|
self._e = _spin(200e9, step=1e9)
|
||||||
self._a = _spin(0.01, step=1e-4)
|
self._a = _spin(0.01, step=1e-4)
|
||||||
self._iz = _spin(8.33e-6, step=1e-7)
|
self._iz = _spin(8.33e-6, step=1e-7)
|
||||||
self._iy = _spin(8.33e-6, step=1e-7)
|
self._iy = _spin(8.33e-6, step=1e-7)
|
||||||
self._g = _spin(80e9, step=1e9)
|
self._g = _spin(80e9, step=1e9)
|
||||||
self._j = _spin(1e-6, step=1e-7)
|
self._j = _spin(1e-6, step=1e-7)
|
||||||
for label, w in (("E:", self._e), ("A:", self._a),
|
# Unit hints only — values are entered in native stored units.
|
||||||
("Iz:", self._iz), ("Iy:", self._iy),
|
stress_u = _u(self._units, "stress")
|
||||||
("G:", self._g), ("J:", self._j)):
|
area_u = _u(self._units, "area")
|
||||||
|
inertia_u = _u(self._units, "inertia")
|
||||||
|
|
||||||
|
def _lbl(name: str, unit: str) -> str:
|
||||||
|
return f"{name} ({unit}):" if unit else f"{name}:"
|
||||||
|
|
||||||
|
for label, w in (
|
||||||
|
(_lbl("E", stress_u), self._e),
|
||||||
|
(_lbl("A", area_u), self._a),
|
||||||
|
(_lbl("Iz", inertia_u), self._iz),
|
||||||
|
(_lbl("Iy", inertia_u), self._iy),
|
||||||
|
(_lbl("G", stress_u), self._g),
|
||||||
|
(_lbl("J", inertia_u), self._j),
|
||||||
|
):
|
||||||
self._layout.addRow(label, w)
|
self._layout.addRow(label, w)
|
||||||
self._layout.addRow(QLabel("<i>Iy, G, J required for 3D models.</i>"))
|
self._layout.addRow(QLabel("<i>Iy, G, J required for 3D models.</i>"))
|
||||||
|
|
||||||
|
|
@ -124,9 +157,10 @@ class ElasticSectionForm(SectionFormBase):
|
||||||
|
|
||||||
self._dim_spins: dict[str, QDoubleSpinBox] = {}
|
self._dim_spins: dict[str, QDoubleSpinBox] = {}
|
||||||
self._dim_labels: dict[str, QLabel] = {}
|
self._dim_labels: dict[str, QLabel] = {}
|
||||||
|
sec_u = _u(self._units, "section_dim")
|
||||||
for key in ("b", "h", "d", "tw", "tf", "diameter"):
|
for key in ("b", "h", "d", "tw", "tf", "diameter"):
|
||||||
spin = _opt_spin()
|
spin = _opt_spin()
|
||||||
lbl = QLabel(f"{key}:")
|
lbl = QLabel(f"{key} ({sec_u}):" if sec_u else f"{key}:")
|
||||||
self._dim_spins[key] = spin
|
self._dim_spins[key] = spin
|
||||||
self._dim_labels[key] = lbl
|
self._dim_labels[key] = lbl
|
||||||
self._layout.addRow(lbl, spin)
|
self._layout.addRow(lbl, spin)
|
||||||
|
|
@ -189,17 +223,21 @@ class FiberSectionSummaryForm(SectionFormBase):
|
||||||
|
|
||||||
type_label = "Fiber Section"
|
type_label = "Fiber Section"
|
||||||
|
|
||||||
def __init__(self, parent: QWidget | None = None) -> None:
|
def __init__(
|
||||||
super().__init__(parent)
|
self, parent: QWidget | None = None, *, units: UnitSystem = UnitSystem.METRIC
|
||||||
|
) -> None:
|
||||||
|
super().__init__(parent, units=units)
|
||||||
self._name_edit.setEnabled(False)
|
self._name_edit.setEnabled(False)
|
||||||
self._summary = QLabel("")
|
self._summary = QLabel("")
|
||||||
self._summary.setWordWrap(True)
|
self._summary.setWordWrap(True)
|
||||||
self._summary.setStyleSheet("color: #555;")
|
self._summary.setStyleSheet("color: #555;")
|
||||||
self._layout.addRow(self._summary)
|
self._layout.addRow(self._summary)
|
||||||
self._layout.addRow(QLabel(
|
self._layout.addRow(
|
||||||
"<i>Edit this fiber section from the Section Library list — "
|
QLabel(
|
||||||
"Add / Modify uses the visual Fiber Section Editor.</i>"
|
"<i>Edit this fiber section from the Section Library list — "
|
||||||
))
|
"Add / Modify uses the visual Fiber Section Editor.</i>"
|
||||||
|
)
|
||||||
|
)
|
||||||
self._cached: FiberSection | None = None
|
self._cached: FiberSection | None = None
|
||||||
|
|
||||||
def _populate_specific(self, s: FiberSection) -> None:
|
def _populate_specific(self, s: FiberSection) -> None:
|
||||||
|
|
@ -229,8 +267,10 @@ class SectionAggregatorSummaryForm(SectionFormBase):
|
||||||
|
|
||||||
type_label = "Section Aggregator"
|
type_label = "Section Aggregator"
|
||||||
|
|
||||||
def __init__(self, parent: QWidget | None = None) -> None:
|
def __init__(
|
||||||
super().__init__(parent)
|
self, parent: QWidget | None = None, *, units: UnitSystem = UnitSystem.METRIC
|
||||||
|
) -> None:
|
||||||
|
super().__init__(parent, units=units)
|
||||||
self._name_edit.setEnabled(False)
|
self._name_edit.setEnabled(False)
|
||||||
self._summary = QLabel("")
|
self._summary = QLabel("")
|
||||||
self._summary.setWordWrap(True)
|
self._summary.setWordWrap(True)
|
||||||
|
|
@ -240,9 +280,9 @@ class SectionAggregatorSummaryForm(SectionFormBase):
|
||||||
|
|
||||||
def _populate_specific(self, s: SectionAggregator) -> None:
|
def _populate_specific(self, s: SectionAggregator) -> None:
|
||||||
self._cached = s
|
self._cached = s
|
||||||
pairings = "<br>".join(
|
pairings = (
|
||||||
f" mat #{p.material_id} on DOF {p.dof}" for p in s.pairings
|
"<br>".join(f" mat #{p.material_id} on DOF {p.dof}" for p in s.pairings) or "(none)"
|
||||||
) or "(none)"
|
)
|
||||||
self._summary.setText(
|
self._summary.setText(
|
||||||
f"<b>{s.name or 'Section Aggregator'}</b><br>"
|
f"<b>{s.name or 'Section Aggregator'}</b><br>"
|
||||||
f"Wraps section: {s.section_id}<br>"
|
f"Wraps section: {s.section_id}<br>"
|
||||||
|
|
@ -262,20 +302,19 @@ FORM_REGISTRY: dict[str, type[SectionFormBase]] = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def form_for(section: Any) -> SectionFormBase:
|
def form_for(section: Any, *, units: UnitSystem = UnitSystem.METRIC) -> SectionFormBase:
|
||||||
cls = FORM_REGISTRY.get(section.type)
|
cls = FORM_REGISTRY.get(section.type)
|
||||||
if cls is None:
|
if cls is None:
|
||||||
# Graceful fallback — unknown section types display a minimal
|
# Graceful fallback — unknown section types display a minimal
|
||||||
# placeholder instead of crashing the entire dialog.
|
# placeholder instead of crashing the entire dialog.
|
||||||
form = SectionFormBase()
|
form = SectionFormBase(units=units)
|
||||||
form._layout.addRow(QLabel(
|
form._layout.addRow(
|
||||||
f"<i>No form registered for section type "
|
QLabel(f"<i>No form registered for section type <b>{section.type}</b> yet.</i>")
|
||||||
f"<b>{section.type}</b> yet.</i>"
|
)
|
||||||
))
|
|
||||||
form._section_id = section.id
|
form._section_id = section.id
|
||||||
form._name_edit.setText(getattr(section, "name", "") or "")
|
form._name_edit.setText(getattr(section, "name", "") or "")
|
||||||
form._name_edit.setEnabled(False)
|
form._name_edit.setEnabled(False)
|
||||||
return form
|
return form
|
||||||
form = cls()
|
form = cls(units=units)
|
||||||
form.populate(section)
|
form.populate(section)
|
||||||
return form
|
return form
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ from otko.commands import (
|
||||||
DeleteSectionsCommand,
|
DeleteSectionsCommand,
|
||||||
UpdateSectionCommand,
|
UpdateSectionCommand,
|
||||||
)
|
)
|
||||||
|
from otko.core.units import UnitSystem
|
||||||
from otko.viewmodels import ProjectViewModel
|
from otko.viewmodels import ProjectViewModel
|
||||||
from otko.views.dialogs.section_forms import FORM_REGISTRY, form_for
|
from otko.views.dialogs.section_forms import FORM_REGISTRY, form_for
|
||||||
|
|
||||||
|
|
@ -113,17 +114,28 @@ class SectionLibraryDialog(QDialog):
|
||||||
"""
|
"""
|
||||||
from otko.core import FiberSection
|
from otko.core import FiberSection
|
||||||
from otko.views.dialogs.section_editor import FiberSectionEditor
|
from otko.views.dialogs.section_editor import FiberSectionEditor
|
||||||
|
|
||||||
sec = self._selected_section()
|
sec = self._selected_section()
|
||||||
if not isinstance(sec, FiberSection) or self._vm.project is None:
|
if not isinstance(sec, FiberSection) or self._vm.project is None:
|
||||||
return
|
return
|
||||||
mat_ids = [m.id for m in self._vm.project.materials]
|
mat_ids = [m.id for m in self._vm.project.materials]
|
||||||
dlg = FiberSectionEditor(mat_ids, existing=sec, parent=self)
|
dlg = FiberSectionEditor(
|
||||||
|
mat_ids,
|
||||||
|
existing=sec,
|
||||||
|
parent=self,
|
||||||
|
units=self._vm.project.meta.units,
|
||||||
|
)
|
||||||
if dlg.exec() != QDialog.DialogCode.Accepted:
|
if dlg.exec() != QDialog.DialogCode.Accepted:
|
||||||
return
|
return
|
||||||
updated = dlg.result_section().model_copy(update={"id": sec.id})
|
updated = dlg.result_section().model_copy(update={"id": sec.id})
|
||||||
self._vm.apply_command(UpdateSectionCommand(self._vm, updated))
|
self._vm.apply_command(UpdateSectionCommand(self._vm, updated))
|
||||||
self._select_by_id(sec.id)
|
self._select_by_id(sec.id)
|
||||||
|
|
||||||
|
def _project_units(self) -> UnitSystem:
|
||||||
|
"""Project display units (Metric default when no project is open)."""
|
||||||
|
proj = self._vm.project
|
||||||
|
return proj.meta.units if proj is not None else UnitSystem.METRIC
|
||||||
|
|
||||||
def _on_row_changed(self, _row: int) -> None:
|
def _on_row_changed(self, _row: int) -> None:
|
||||||
section = self._selected_section()
|
section = self._selected_section()
|
||||||
if section is None:
|
if section is None:
|
||||||
|
|
@ -132,7 +144,7 @@ class SectionLibraryDialog(QDialog):
|
||||||
old = self._stack.widget(0)
|
old = self._stack.widget(0)
|
||||||
self._stack.removeWidget(old)
|
self._stack.removeWidget(old)
|
||||||
old.deleteLater()
|
old.deleteLater()
|
||||||
form = form_for(section)
|
form = form_for(section, units=self._project_units())
|
||||||
self._stack.addWidget(form)
|
self._stack.addWidget(form)
|
||||||
self._stack.setCurrentWidget(form)
|
self._stack.setCurrentWidget(form)
|
||||||
self._type_label.setText(form.type_label)
|
self._type_label.setText(form.type_label)
|
||||||
|
|
@ -156,12 +168,17 @@ class SectionLibraryDialog(QDialog):
|
||||||
summary_kinds = {"FiberSection", "SectionAggregator"}
|
summary_kinds = {"FiberSection", "SectionAggregator"}
|
||||||
kinds = [k for k in FORM_REGISTRY if k not in summary_kinds]
|
kinds = [k for k in FORM_REGISTRY if k not in summary_kinds]
|
||||||
kind, ok = QInputDialog.getItem(
|
kind, ok = QInputDialog.getItem(
|
||||||
self, "Add section", "Type:", kinds, current=0, editable=False,
|
self,
|
||||||
|
"Add section",
|
||||||
|
"Type:",
|
||||||
|
kinds,
|
||||||
|
current=0,
|
||||||
|
editable=False,
|
||||||
)
|
)
|
||||||
if not ok:
|
if not ok:
|
||||||
return
|
return
|
||||||
new_id = self._vm.project.next_section_id()
|
new_id = self._vm.project.next_section_id()
|
||||||
form = FORM_REGISTRY[kind]()
|
form = FORM_REGISTRY[kind](units=self._project_units())
|
||||||
form._section_id = new_id # type: ignore[attr-defined]
|
form._section_id = new_id # type: ignore[attr-defined]
|
||||||
try:
|
try:
|
||||||
new_section = form.read()
|
new_section = form.read()
|
||||||
|
|
@ -176,9 +193,14 @@ class SectionLibraryDialog(QDialog):
|
||||||
if self._vm.project is None:
|
if self._vm.project is None:
|
||||||
return
|
return
|
||||||
from otko.views.dialogs.section_editor import FiberSectionEditor
|
from otko.views.dialogs.section_editor import FiberSectionEditor
|
||||||
|
|
||||||
mat_ids = [m.id for m in self._vm.project.materials]
|
mat_ids = [m.id for m in self._vm.project.materials]
|
||||||
new_id = self._vm.project.next_section_id()
|
new_id = self._vm.project.next_section_id()
|
||||||
dlg = FiberSectionEditor(mat_ids, parent=self)
|
dlg = FiberSectionEditor(
|
||||||
|
mat_ids,
|
||||||
|
parent=self,
|
||||||
|
units=self._vm.project.meta.units,
|
||||||
|
)
|
||||||
if dlg.exec() != QDialog.DialogCode.Accepted:
|
if dlg.exec() != QDialog.DialogCode.Accepted:
|
||||||
return
|
return
|
||||||
sec = dlg.result_section().model_copy(update={"id": new_id})
|
sec = dlg.result_section().model_copy(update={"id": new_id})
|
||||||
|
|
@ -196,9 +218,10 @@ class SectionLibraryDialog(QDialog):
|
||||||
if section is None:
|
if section is None:
|
||||||
return
|
return
|
||||||
reply = QMessageBox.question(
|
reply = QMessageBox.question(
|
||||||
self, "Delete section",
|
self,
|
||||||
|
"Delete section",
|
||||||
f"Delete section #{section.id} ({section.type})?\n"
|
f"Delete section #{section.id} ({section.type})?\n"
|
||||||
"Frame elements that reference it will be invalid until reassigned."
|
"Frame elements that reference it will be invalid until reassigned.",
|
||||||
)
|
)
|
||||||
if reply != QMessageBox.StandardButton.Yes:
|
if reply != QMessageBox.StandardButton.Yes:
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,28 @@ from otko.views.docks import (
|
||||||
TimeHistoryView,
|
TimeHistoryView,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
#: Force-diagram component → display quantity (mirrors the dock's map).
|
||||||
|
_DIAGRAM_QUANTITY: dict[str, str] = {
|
||||||
|
"N": "force",
|
||||||
|
"V2": "force",
|
||||||
|
"V3": "force",
|
||||||
|
"M2": "moment",
|
||||||
|
"M3": "moment",
|
||||||
|
"T": "moment",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _diagram_units_label(project: object | None, component: ForceComponent) -> str:
|
||||||
|
"""Display unit for a force-diagram component (``""`` when unknown)."""
|
||||||
|
try:
|
||||||
|
from otko.core.units import native_label
|
||||||
|
|
||||||
|
units = project.meta.units # type: ignore[attr-defined]
|
||||||
|
quantity = _DIAGRAM_QUANTITY.get(component.value, "force")
|
||||||
|
return native_label(units, quantity)
|
||||||
|
except Exception:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
class DockManager:
|
class DockManager:
|
||||||
"""Owns dock construction and post-processing result views."""
|
"""Owns dock construction and post-processing result views."""
|
||||||
|
|
@ -271,7 +293,10 @@ class DockManager:
|
||||||
best_comp, best_data = self._best_initial_component()
|
best_comp, best_data = self._best_initial_component()
|
||||||
suggested = force_diagram_auto_scale(self._vm.project, best_data)
|
suggested = force_diagram_auto_scale(self._vm.project, best_data)
|
||||||
|
|
||||||
view = ForceDiagramView(suggested_scale=suggested)
|
view = ForceDiagramView(
|
||||||
|
suggested_scale=suggested,
|
||||||
|
units=self._vm.project.meta.units,
|
||||||
|
)
|
||||||
# Sync the picker to the auto-chosen best component WITHOUT firing
|
# Sync the picker to the auto-chosen best component WITHOUT firing
|
||||||
# signals (we'll emit a single render below).
|
# signals (we'll emit a single render below).
|
||||||
view._component.blockSignals(True)
|
view._component.blockSignals(True)
|
||||||
|
|
@ -294,7 +319,15 @@ class DockManager:
|
||||||
component,
|
component,
|
||||||
)
|
)
|
||||||
if self._diagram_renderer is not None:
|
if self._diagram_renderer is not None:
|
||||||
self._diagram_renderer.render(self._vm.project, data, scale)
|
self._diagram_renderer.render(
|
||||||
|
self._vm.project,
|
||||||
|
data,
|
||||||
|
scale,
|
||||||
|
units_label=_diagram_units_label(
|
||||||
|
self._vm.project,
|
||||||
|
component,
|
||||||
|
),
|
||||||
|
)
|
||||||
self._canvas.render()
|
self._canvas.render()
|
||||||
|
|
||||||
def _on_component_changed(component: ForceComponent) -> None:
|
def _on_component_changed(component: ForceComponent) -> None:
|
||||||
|
|
@ -347,7 +380,7 @@ class DockManager:
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
self._tear_down_post_dock()
|
self._tear_down_post_dock()
|
||||||
view = TimeHistoryView()
|
view = TimeHistoryView(units=self._vm.project.meta.units)
|
||||||
view.set_results(self._latest_results, ndm=self._vm.project.ndm, ndf=self._vm.project.ndf)
|
view.set_results(self._latest_results, ndm=self._vm.project.ndm, ndf=self._vm.project.ndf)
|
||||||
view.set_available_nodes([n.id for n in self._vm.project.nodes])
|
view.set_available_nodes([n.id for n in self._vm.project.nodes])
|
||||||
dock = QDockWidget("Time-History Plot", self)
|
dock = QDockWidget("Time-History Plot", self)
|
||||||
|
|
@ -471,7 +504,7 @@ class DockManager:
|
||||||
# (or "1/in") rather than hard-coded SI m / N.
|
# (or "1/in") rather than hard-coded SI m / N.
|
||||||
from otko.core import UnitSystem
|
from otko.core import UnitSystem
|
||||||
|
|
||||||
units = self._vm.project.meta.units if self._vm.project is not None else UnitSystem.SI_M_N
|
units = self._vm.project.meta.units if self._vm.project is not None else UnitSystem.METRIC
|
||||||
ndf = self._vm.project.ndf if self._vm.project is not None else 6
|
ndf = self._vm.project.ndf if self._vm.project is not None else 6
|
||||||
view = PushoverCurveView(units=units, ndf=ndf)
|
view = PushoverCurveView(units=units, ndf=ndf)
|
||||||
view.set_results(self._latest_results)
|
view.set_results(self._latest_results)
|
||||||
|
|
@ -501,7 +534,7 @@ class DockManager:
|
||||||
spectrum = next((s for s in self._vm.project.spectra if s.id == case.spectrum_id), None)
|
spectrum = next((s for s in self._vm.project.spectra if s.id == case.spectrum_id), None)
|
||||||
|
|
||||||
self._tear_down_post_dock()
|
self._tear_down_post_dock()
|
||||||
view = ResponseSpectrumView()
|
view = ResponseSpectrumView(units=self._vm.project.meta.units)
|
||||||
view.set_results(self._latest_results, spectrum)
|
view.set_results(self._latest_results, spectrum)
|
||||||
dock = QDockWidget("Response Spectrum", self)
|
dock = QDockWidget("Response Spectrum", self)
|
||||||
dock.setWidget(view)
|
dock.setWidget(view)
|
||||||
|
|
|
||||||
|
|
@ -25,23 +25,51 @@ from PySide6.QtWidgets import (
|
||||||
QWidget,
|
QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from otko.core.units import UnitSystem, native_label
|
||||||
from otko.services.element_forces import ForceComponent
|
from otko.services.element_forces import ForceComponent
|
||||||
|
|
||||||
|
#: ForceDiagram component → display quantity (plan §5 taxonomy keys).
|
||||||
|
_COMPONENT_QUANTITY: dict[str, str] = {
|
||||||
|
"N": "force",
|
||||||
|
"V2": "force",
|
||||||
|
"V3": "force",
|
||||||
|
"M2": "moment",
|
||||||
|
"M3": "moment",
|
||||||
|
"T": "moment",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ForceDiagramView(QWidget):
|
class ForceDiagramView(QWidget):
|
||||||
"""Compact controls for live-updating an element-force diagram."""
|
"""Compact controls for live-updating an element-force diagram."""
|
||||||
|
|
||||||
componentChanged = Signal(object) # ForceComponent
|
componentChanged = Signal(object) # ForceComponent
|
||||||
changed = Signal(object, float) # (ForceComponent, scale)
|
changed = Signal(object, float) # (ForceComponent, scale)
|
||||||
closed = Signal()
|
closed = Signal()
|
||||||
|
|
||||||
def __init__(self, suggested_scale: float = 1.0,
|
def __init__(
|
||||||
parent: QWidget | None = None) -> None:
|
self,
|
||||||
|
suggested_scale: float = 1.0,
|
||||||
|
parent: QWidget | None = None,
|
||||||
|
*,
|
||||||
|
units: UnitSystem = UnitSystem.METRIC,
|
||||||
|
) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
self._units = units
|
||||||
self._scale_base = max(suggested_scale, 1e-12)
|
self._scale_base = max(suggested_scale, 1e-12)
|
||||||
self._pending_component_change = False
|
self._pending_component_change = False
|
||||||
self._build_ui(suggested_scale)
|
self._build_ui(suggested_scale)
|
||||||
|
|
||||||
|
def set_units(self, units: UnitSystem, ndf: int | None = None) -> None:
|
||||||
|
"""Re-apply the project's stored units to component labels.
|
||||||
|
|
||||||
|
Values stay native; only human-facing labels change. Never throws.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self._units = units
|
||||||
|
self._refresh_component_labels()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# ── public API ──────────────────────────────────────────────────
|
# ── public API ──────────────────────────────────────────────────
|
||||||
def set_scale_base(self, suggested: float) -> None:
|
def set_scale_base(self, suggested: float) -> None:
|
||||||
"""Replace the slider's reference scale (called when the host
|
"""Replace the slider's reference scale (called when the host
|
||||||
|
|
@ -64,6 +92,34 @@ class ForceDiagramView(QWidget):
|
||||||
def current_component(self) -> ForceComponent:
|
def current_component(self) -> ForceComponent:
|
||||||
return self._component.currentData()
|
return self._component.currentData()
|
||||||
|
|
||||||
|
def _component_text(self, comp: ForceComponent) -> str:
|
||||||
|
"""Component name with its display unit, e.g. ``"M3 (kN·m)"``."""
|
||||||
|
try:
|
||||||
|
quantity = _COMPONENT_QUANTITY.get(comp.value, "force")
|
||||||
|
unit = native_label(self._units, quantity)
|
||||||
|
return f"{comp.value} ({unit})"
|
||||||
|
except Exception:
|
||||||
|
return comp.value
|
||||||
|
|
||||||
|
def _refresh_component_labels(self) -> None:
|
||||||
|
"""Rewrite combo texts for the current units, keeping selection."""
|
||||||
|
try:
|
||||||
|
current = self._component.currentData()
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
self._component.blockSignals(True)
|
||||||
|
try:
|
||||||
|
for i in range(self._component.count()):
|
||||||
|
comp = self._component.itemData(i)
|
||||||
|
if isinstance(comp, ForceComponent):
|
||||||
|
self._component.setItemText(i, self._component_text(comp))
|
||||||
|
if current is not None:
|
||||||
|
idx = self._component.findData(current)
|
||||||
|
if idx >= 0:
|
||||||
|
self._component.setCurrentIndex(idx)
|
||||||
|
finally:
|
||||||
|
self._component.blockSignals(False)
|
||||||
|
|
||||||
# ── UI build ────────────────────────────────────────────────────
|
# ── UI build ────────────────────────────────────────────────────
|
||||||
def _build_ui(self, suggested_scale: float) -> None:
|
def _build_ui(self, suggested_scale: float) -> None:
|
||||||
root = QVBoxLayout(self)
|
root = QVBoxLayout(self)
|
||||||
|
|
@ -73,7 +129,7 @@ class ForceDiagramView(QWidget):
|
||||||
|
|
||||||
self._component = QComboBox()
|
self._component = QComboBox()
|
||||||
for comp in ForceComponent:
|
for comp in ForceComponent:
|
||||||
self._component.addItem(comp.value, comp)
|
self._component.addItem(self._component_text(comp), comp)
|
||||||
form.addRow("Component:", self._component)
|
form.addRow("Component:", self._component)
|
||||||
|
|
||||||
self._scale_spin = QDoubleSpinBox()
|
self._scale_spin = QDoubleSpinBox()
|
||||||
|
|
@ -87,7 +143,7 @@ class ForceDiagramView(QWidget):
|
||||||
|
|
||||||
self._slider = QSlider(Qt.Orientation.Horizontal)
|
self._slider = QSlider(Qt.Orientation.Horizontal)
|
||||||
self._slider.setRange(1, 1000)
|
self._slider.setRange(1, 1000)
|
||||||
self._slider.setValue(500) # midpoint = suggested scale
|
self._slider.setValue(500) # midpoint = suggested scale
|
||||||
form.addRow("", self._slider)
|
form.addRow("", self._slider)
|
||||||
|
|
||||||
root.addWidget(group)
|
root.addWidget(group)
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ from PySide6.QtWidgets import (
|
||||||
QWidget,
|
QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from otko.core.units import UnitSystem, native_label
|
||||||
from otko.services.results import TransientResults
|
from otko.services.results import TransientResults
|
||||||
from otko.views.docks._labels import dof_symbol
|
from otko.views.docks._labels import dof_symbol
|
||||||
|
|
||||||
|
|
@ -47,14 +48,51 @@ class HysteresisView(QWidget):
|
||||||
|
|
||||||
closed = Signal()
|
closed = Signal()
|
||||||
|
|
||||||
def __init__(self, parent: QWidget | None = None) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
parent: QWidget | None = None,
|
||||||
|
*,
|
||||||
|
units: UnitSystem = UnitSystem.METRIC,
|
||||||
|
) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
self._units = units
|
||||||
self._results: TransientResults | None = None
|
self._results: TransientResults | None = None
|
||||||
self._ndm: int | None = None
|
self._ndm: int | None = None
|
||||||
self._ndf: int | None = None
|
self._ndf: int | None = None
|
||||||
self._curve: Any | None = None
|
self._curve: Any | None = None
|
||||||
self._build_ui()
|
self._build_ui()
|
||||||
|
|
||||||
|
def set_units(self, units: UnitSystem, ndf: int | None = None) -> None:
|
||||||
|
"""Re-apply stored units to axis labels. Values stay native."""
|
||||||
|
try:
|
||||||
|
self._units = units
|
||||||
|
if ndf is not None:
|
||||||
|
self._ndf = int(ndf)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _axis_unit(self, kind: str) -> str:
|
||||||
|
"""Display unit for an axis kind."""
|
||||||
|
key = {
|
||||||
|
"node_disp": "displacement",
|
||||||
|
"node_vel": "velocity",
|
||||||
|
"node_accel": "acceleration",
|
||||||
|
"element_force": "force",
|
||||||
|
"disp": "displacement",
|
||||||
|
}.get(kind, "displacement")
|
||||||
|
# Moment components (M/T) carry moment units.
|
||||||
|
if kind == "element_force":
|
||||||
|
try:
|
||||||
|
comp = self._y_component.currentData() or ""
|
||||||
|
if str(comp).startswith(("M", "T")):
|
||||||
|
return native_label(self._units, "moment")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
return native_label(self._units, key)
|
||||||
|
except KeyError:
|
||||||
|
return ""
|
||||||
|
|
||||||
# ── public API ──────────────────────────────────────────────────
|
# ── public API ──────────────────────────────────────────────────
|
||||||
def set_results(
|
def set_results(
|
||||||
self, results: TransientResults | None, *, ndm: int | None = None, ndf: int | None = None
|
self, results: TransientResults | None, *, ndm: int | None = None, ndf: int | None = None
|
||||||
|
|
@ -251,9 +289,14 @@ class HysteresisView(QWidget):
|
||||||
y_vals[:n],
|
y_vals[:n],
|
||||||
pen=pg.mkPen("#1f77b4", width=2),
|
pen=pg.mkPen("#1f77b4", width=2),
|
||||||
)
|
)
|
||||||
|
x_unit = self._axis_unit("disp")
|
||||||
self._plot.setLabel(
|
self._plot.setLabel(
|
||||||
"bottom",
|
"bottom",
|
||||||
f"N{self._x_node.currentData()} {self._dof_symbol(self._x_dof.value(), x_dofs)} disp",
|
f"N{self._x_node.currentData()} {self._dof_symbol(self._x_dof.value(), x_dofs)} "
|
||||||
|
f"disp ({x_unit})"
|
||||||
|
if x_unit
|
||||||
|
else f"N{self._x_node.currentData()} "
|
||||||
|
f"{self._dof_symbol(self._x_dof.value(), x_dofs)} disp",
|
||||||
)
|
)
|
||||||
self._plot.setLabel("left", self._y_label(y_dofs))
|
self._plot.setLabel("left", self._y_label(y_dofs))
|
||||||
|
|
||||||
|
|
@ -327,13 +370,15 @@ class HysteresisView(QWidget):
|
||||||
|
|
||||||
def _y_label(self, n_dofs: int) -> str:
|
def _y_label(self, n_dofs: int) -> str:
|
||||||
kind = self._y_kind.currentData()
|
kind = self._y_kind.currentData()
|
||||||
|
unit = self._axis_unit(kind or "")
|
||||||
|
suffix = f" ({unit})" if unit else ""
|
||||||
if kind == "element_force":
|
if kind == "element_force":
|
||||||
return f"E{self._y_element.currentData()} {self._y_component.currentData()}"
|
return f"E{self._y_element.currentData()} {self._y_component.currentData()}{suffix}"
|
||||||
kind_label = {"node_disp": "disp", "node_vel": "vel", "node_accel": "accel"}.get(
|
kind_label = {"node_disp": "disp", "node_vel": "vel", "node_accel": "accel"}.get(
|
||||||
kind, "value"
|
kind, "value"
|
||||||
)
|
)
|
||||||
symbol = self._dof_symbol(self._y_dof.value(), n_dofs)
|
symbol = self._dof_symbol(self._y_dof.value(), n_dofs)
|
||||||
return f"N{self._y_node.currentData()} {symbol} {kind_label}"
|
return f"N{self._y_node.currentData()} {symbol} {kind_label}{suffix}"
|
||||||
|
|
||||||
def _clear_curve(self) -> None:
|
def _clear_curve(self) -> None:
|
||||||
if self._curve is not None:
|
if self._curve is not None:
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ from PySide6.QtWidgets import (
|
||||||
)
|
)
|
||||||
|
|
||||||
from otko.core import Project
|
from otko.core import Project
|
||||||
|
from otko.core.units import UnitSystem, native_label
|
||||||
|
|
||||||
|
|
||||||
# Element types the Properties dock lets the user switch between.
|
# Element types the Properties dock lets the user switch between.
|
||||||
|
|
@ -32,8 +33,11 @@ from otko.core import Project
|
||||||
# uniaxial material + area. The Convert command drops/adds fields to
|
# uniaxial material + area. The Convert command drops/adds fields to
|
||||||
# bridge between them.
|
# bridge between them.
|
||||||
_CONVERTIBLE_ELEMENT_TYPES = [
|
_CONVERTIBLE_ELEMENT_TYPES = [
|
||||||
"Truss", "CorotTruss",
|
"Truss",
|
||||||
"ElasticBeamColumn", "ForceBeamColumn", "DispBeamColumn",
|
"CorotTruss",
|
||||||
|
"ElasticBeamColumn",
|
||||||
|
"ForceBeamColumn",
|
||||||
|
"DispBeamColumn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -53,7 +57,9 @@ class PropertyEditorDock(QScrollArea):
|
||||||
self._layout.setContentsMargins(8, 8, 8, 8)
|
self._layout.setContentsMargins(8, 8, 8, 8)
|
||||||
self.setWidget(self._inner)
|
self.setWidget(self._inner)
|
||||||
self._project: Project | None = None
|
self._project: Project | None = None
|
||||||
self.on_apply_mass: Callable[[int, tuple[float, float, float, float, float, float]], None] | None = None
|
self.on_apply_mass: (
|
||||||
|
Callable[[int, tuple[float, float, float, float, float, float]], None] | None
|
||||||
|
) = None
|
||||||
# Element callbacks — all optional; wired by MainWindow.
|
# Element callbacks — all optional; wired by MainWindow.
|
||||||
self.on_change_element_type: Callable[[int, str], None] | None = None
|
self.on_change_element_type: Callable[[int, str], None] | None = None
|
||||||
self.on_change_element_material: Callable[[int, int], None] | None = None
|
self.on_change_element_material: Callable[[int, int], None] | None = None
|
||||||
|
|
@ -112,6 +118,26 @@ class PropertyEditorDock(QScrollArea):
|
||||||
self._layout.addWidget(lbl)
|
self._layout.addWidget(lbl)
|
||||||
self._layout.addStretch(1)
|
self._layout.addStretch(1)
|
||||||
|
|
||||||
|
def _units(self) -> UnitSystem:
|
||||||
|
"""Return the bound project's unit system (live, per refresh)."""
|
||||||
|
try:
|
||||||
|
if self._project is not None:
|
||||||
|
return self._project.meta.units
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
return UnitSystem.METRIC
|
||||||
|
|
||||||
|
def _unit_suffix(self, quantity: str) -> str:
|
||||||
|
"""Return ``" (unit)"`` for a quantity, or ``""`` when unknown.
|
||||||
|
|
||||||
|
Unit hints only — values are shown in native stored units,
|
||||||
|
never converted.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return f" ({native_label(self._units(), quantity)})"
|
||||||
|
except KeyError:
|
||||||
|
return ""
|
||||||
|
|
||||||
def _show_node(self, node_id: int) -> None:
|
def _show_node(self, node_id: int) -> None:
|
||||||
try:
|
try:
|
||||||
node = self._project.node(node_id) # type: ignore[union-attr]
|
node = self._project.node(node_id) # type: ignore[union-attr]
|
||||||
|
|
@ -121,25 +147,42 @@ class PropertyEditorDock(QScrollArea):
|
||||||
self._layout.addWidget(QLabel(f"<h3>Node #{node.id}</h3>"))
|
self._layout.addWidget(QLabel(f"<h3>Node #{node.id}</h3>"))
|
||||||
form = QFormLayout()
|
form = QFormLayout()
|
||||||
form.addRow("Name:", QLabel(node.name or "—"))
|
form.addRow("Name:", QLabel(node.name or "—"))
|
||||||
form.addRow("X, Y, Z:", QLabel(
|
form.addRow(
|
||||||
f"{node.coords[0]:.4f}, {node.coords[1]:.4f}, {node.coords[2]:.4f}",
|
f"X, Y, Z{self._unit_suffix('length')}:",
|
||||||
))
|
QLabel(
|
||||||
|
f"{node.coords[0]:.4f}, {node.coords[1]:.4f}, {node.coords[2]:.4f}",
|
||||||
|
),
|
||||||
|
)
|
||||||
form.addRow("Restraint:", QLabel(self._fmt_restraint(node.restraint)))
|
form.addRow("Restraint:", QLabel(self._fmt_restraint(node.restraint)))
|
||||||
self._layout.addLayout(form)
|
self._layout.addLayout(form)
|
||||||
|
|
||||||
# ── Mass editor ──
|
# ── Mass editor (unit hints only — native stored values) ──
|
||||||
group = QGroupBox("Lumped mass")
|
group = QGroupBox("Lumped mass")
|
||||||
gform = QFormLayout(group)
|
gform = QFormLayout(group)
|
||||||
self._mass_spins: list[QDoubleSpinBox] = []
|
self._mass_spins: list[QDoubleSpinBox] = []
|
||||||
labels = ("mx", "my", "mz", "Ixx", "Iyy", "Izz")
|
mass_suffix = self._unit_suffix("mass")
|
||||||
for i, lbl in enumerate(labels):
|
try:
|
||||||
|
mass_u = native_label(self._units(), "mass")
|
||||||
|
length_u = native_label(self._units(), "length")
|
||||||
|
rot_suffix = f" ({mass_u}·{length_u}²)" if mass_u and length_u else ""
|
||||||
|
except KeyError:
|
||||||
|
rot_suffix = ""
|
||||||
|
labels = (
|
||||||
|
("mx", mass_suffix),
|
||||||
|
("my", mass_suffix),
|
||||||
|
("mz", mass_suffix),
|
||||||
|
("Ixx", rot_suffix),
|
||||||
|
("Iyy", rot_suffix),
|
||||||
|
("Izz", rot_suffix),
|
||||||
|
)
|
||||||
|
for i, (lbl, suffix) in enumerate(labels):
|
||||||
sb = QDoubleSpinBox()
|
sb = QDoubleSpinBox()
|
||||||
sb.setRange(0.0, 1e12)
|
sb.setRange(0.0, 1e12)
|
||||||
sb.setDecimals(4)
|
sb.setDecimals(4)
|
||||||
sb.setSingleStep(100.0)
|
sb.setSingleStep(100.0)
|
||||||
sb.setValue(float(node.mass[i]))
|
sb.setValue(float(node.mass[i]))
|
||||||
self._mass_spins.append(sb)
|
self._mass_spins.append(sb)
|
||||||
gform.addRow(f"{lbl}:", sb)
|
gform.addRow(f"{lbl}{suffix}:", sb)
|
||||||
|
|
||||||
btn_row = QHBoxLayout()
|
btn_row = QHBoxLayout()
|
||||||
btn_row.addStretch(1)
|
btn_row.addStretch(1)
|
||||||
|
|
@ -179,6 +222,7 @@ class PropertyEditorDock(QScrollArea):
|
||||||
def _on_type_changed(new_type: str, _eid: int = el.id) -> None:
|
def _on_type_changed(new_type: str, _eid: int = el.id) -> None:
|
||||||
if new_type != el.type and self.on_change_element_type is not None:
|
if new_type != el.type and self.on_change_element_type is not None:
|
||||||
self.on_change_element_type(_eid, new_type)
|
self.on_change_element_type(_eid, new_type)
|
||||||
|
|
||||||
type_cb.currentTextChanged.connect(_on_type_changed)
|
type_cb.currentTextChanged.connect(_on_type_changed)
|
||||||
form.addRow("Type:", type_cb)
|
form.addRow("Type:", type_cb)
|
||||||
else:
|
else:
|
||||||
|
|
@ -188,16 +232,19 @@ class PropertyEditorDock(QScrollArea):
|
||||||
form.addRow("Nodes:", QLabel(", ".join(str(n) for n in el.nodes)))
|
form.addRow("Nodes:", QLabel(", ".join(str(n) for n in el.nodes)))
|
||||||
|
|
||||||
# ── Section picker for frame elements. ──
|
# ── Section picker for frame elements. ──
|
||||||
if (hasattr(el, "section_id")
|
if (
|
||||||
and self._project is not None
|
hasattr(el, "section_id")
|
||||||
and self._project.sections
|
and self._project is not None
|
||||||
and self.on_change_element_section is not None):
|
and self._project.sections
|
||||||
|
and self.on_change_element_section is not None
|
||||||
|
):
|
||||||
sec_cb = QComboBox()
|
sec_cb = QComboBox()
|
||||||
for s in self._project.sections:
|
for s in self._project.sections:
|
||||||
sec_cb.addItem(
|
sec_cb.addItem(
|
||||||
f"#{s.id} {s.name or s.type}", s.id,
|
f"#{s.id} {s.name or s.type}",
|
||||||
|
s.id,
|
||||||
)
|
)
|
||||||
idx = sec_cb.findData(el.section_id) # type: ignore[attr-defined]
|
idx = sec_cb.findData(el.section_id) # type: ignore[attr-defined]
|
||||||
if idx >= 0:
|
if idx >= 0:
|
||||||
sec_cb.setCurrentIndex(idx)
|
sec_cb.setCurrentIndex(idx)
|
||||||
|
|
||||||
|
|
@ -205,20 +252,23 @@ class PropertyEditorDock(QScrollArea):
|
||||||
sid = sec_cb.currentData()
|
sid = sec_cb.currentData()
|
||||||
if sid is not None and self.on_change_element_section is not None:
|
if sid is not None and self.on_change_element_section is not None:
|
||||||
self.on_change_element_section(_eid, int(sid))
|
self.on_change_element_section(_eid, int(sid))
|
||||||
|
|
||||||
sec_cb.currentIndexChanged.connect(_on_section_changed)
|
sec_cb.currentIndexChanged.connect(_on_section_changed)
|
||||||
form.addRow("Section:", sec_cb)
|
form.addRow("Section:", sec_cb)
|
||||||
elif hasattr(el, "section_id"):
|
elif hasattr(el, "section_id"):
|
||||||
form.addRow("Section id:", QLabel(str(el.section_id)))
|
form.addRow("Section id:", QLabel(str(el.section_id)))
|
||||||
|
|
||||||
# ── Material picker for truss / uniaxial-material elements. ──
|
# ── Material picker for truss / uniaxial-material elements. ──
|
||||||
if (hasattr(el, "material_id")
|
if (
|
||||||
and self._project is not None
|
hasattr(el, "material_id")
|
||||||
and self._project.materials
|
and self._project is not None
|
||||||
and self.on_change_element_material is not None):
|
and self._project.materials
|
||||||
|
and self.on_change_element_material is not None
|
||||||
|
):
|
||||||
mat_cb = QComboBox()
|
mat_cb = QComboBox()
|
||||||
for m in self._project.materials:
|
for m in self._project.materials:
|
||||||
mat_cb.addItem(f"#{m.id} {m.name or m.type}", m.id)
|
mat_cb.addItem(f"#{m.id} {m.name or m.type}", m.id)
|
||||||
idx = mat_cb.findData(el.material_id) # type: ignore[attr-defined]
|
idx = mat_cb.findData(el.material_id) # type: ignore[attr-defined]
|
||||||
if idx >= 0:
|
if idx >= 0:
|
||||||
mat_cb.setCurrentIndex(idx)
|
mat_cb.setCurrentIndex(idx)
|
||||||
|
|
||||||
|
|
@ -226,6 +276,7 @@ class PropertyEditorDock(QScrollArea):
|
||||||
mid = mat_cb.currentData()
|
mid = mat_cb.currentData()
|
||||||
if mid is not None and self.on_change_element_material is not None:
|
if mid is not None and self.on_change_element_material is not None:
|
||||||
self.on_change_element_material(_eid, int(mid))
|
self.on_change_element_material(_eid, int(mid))
|
||||||
|
|
||||||
mat_cb.currentIndexChanged.connect(_on_material_changed)
|
mat_cb.currentIndexChanged.connect(_on_material_changed)
|
||||||
form.addRow("Material:", mat_cb)
|
form.addRow("Material:", mat_cb)
|
||||||
elif hasattr(el, "material_id"):
|
elif hasattr(el, "material_id"):
|
||||||
|
|
@ -236,9 +287,12 @@ class PropertyEditorDock(QScrollArea):
|
||||||
area_spin = QDoubleSpinBox()
|
area_spin = QDoubleSpinBox()
|
||||||
area_spin.setRange(1e-12, 1e6)
|
area_spin.setRange(1e-12, 1e6)
|
||||||
area_spin.setDecimals(8)
|
area_spin.setDecimals(8)
|
||||||
area_spin.setValue(float(el.area)) # type: ignore[attr-defined]
|
area_spin.setValue(float(el.area)) # type: ignore[attr-defined]
|
||||||
area_spin.setSingleStep(float(el.area) * 0.1 # type: ignore[attr-defined]
|
area_spin.setSingleStep(
|
||||||
if el.area else 0.001) # type: ignore[attr-defined]
|
float(el.area) * 0.1 # type: ignore[attr-defined]
|
||||||
|
if el.area
|
||||||
|
else 0.001
|
||||||
|
) # type: ignore[attr-defined]
|
||||||
|
|
||||||
# Commit on editingFinished so we don't dispatch a command on
|
# Commit on editingFinished so we don't dispatch a command on
|
||||||
# every keystroke (which would spam the undo stack).
|
# every keystroke (which would spam the undo stack).
|
||||||
|
|
@ -255,10 +309,14 @@ class PropertyEditorDock(QScrollArea):
|
||||||
return
|
return
|
||||||
if self.on_change_element_fields is not None:
|
if self.on_change_element_fields is not None:
|
||||||
self.on_change_element_fields(_eid, {"area": new_val})
|
self.on_change_element_fields(_eid, {"area": new_val})
|
||||||
|
|
||||||
area_spin.editingFinished.connect(_on_area_edited)
|
area_spin.editingFinished.connect(_on_area_edited)
|
||||||
form.addRow("Area:", area_spin)
|
form.addRow(f"Area{self._unit_suffix('area')}:", area_spin)
|
||||||
elif hasattr(el, "area"):
|
elif hasattr(el, "area"):
|
||||||
form.addRow("Area:", QLabel(f"{el.area:g}"))
|
form.addRow(
|
||||||
|
f"Area{self._unit_suffix('area')}:",
|
||||||
|
QLabel(f"{el.area:g}"),
|
||||||
|
)
|
||||||
|
|
||||||
if hasattr(el, "geom_transf"):
|
if hasattr(el, "geom_transf"):
|
||||||
form.addRow("Geom transf:", QLabel(el.geom_transf))
|
form.addRow("Geom transf:", QLabel(el.geom_transf))
|
||||||
|
|
@ -268,13 +326,17 @@ class PropertyEditorDock(QScrollArea):
|
||||||
def _show_multi(self, node_ids: frozenset[int], element_ids: frozenset[int]) -> None:
|
def _show_multi(self, node_ids: frozenset[int], element_ids: frozenset[int]) -> None:
|
||||||
self._layout.addWidget(QLabel("<h3>Multi-selection</h3>"))
|
self._layout.addWidget(QLabel("<h3>Multi-selection</h3>"))
|
||||||
if node_ids:
|
if node_ids:
|
||||||
self._layout.addWidget(QLabel(
|
self._layout.addWidget(
|
||||||
f"<b>{len(node_ids)}</b> node(s) selected: {self._fmt_id_list(node_ids)}",
|
QLabel(
|
||||||
))
|
f"<b>{len(node_ids)}</b> node(s) selected: {self._fmt_id_list(node_ids)}",
|
||||||
|
)
|
||||||
|
)
|
||||||
if element_ids:
|
if element_ids:
|
||||||
self._layout.addWidget(QLabel(
|
self._layout.addWidget(
|
||||||
f"<b>{len(element_ids)}</b> element(s) selected: {self._fmt_id_list(element_ids)}",
|
QLabel(
|
||||||
))
|
f"<b>{len(element_ids)}</b> element(s) selected: {self._fmt_id_list(element_ids)}",
|
||||||
|
)
|
||||||
|
)
|
||||||
self._layout.addStretch(1)
|
self._layout.addStretch(1)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
@ -288,4 +350,6 @@ class PropertyEditorDock(QScrollArea):
|
||||||
sorted_ids = sorted(ids)
|
sorted_ids = sorted(ids)
|
||||||
if len(sorted_ids) <= limit:
|
if len(sorted_ids) <= limit:
|
||||||
return ", ".join(str(i) for i in sorted_ids)
|
return ", ".join(str(i) for i in sorted_ids)
|
||||||
return ", ".join(str(i) for i in sorted_ids[:limit]) + f", … (+{len(sorted_ids) - limit} more)"
|
return (
|
||||||
|
", ".join(str(i) for i in sorted_ids[:limit]) + f", … (+{len(sorted_ids) - limit} more)"
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ class PushoverCurveView(QWidget):
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
units: UnitSystem = UnitSystem.SI_M_N,
|
units: UnitSystem = UnitSystem.METRIC,
|
||||||
ndf: int = 6,
|
ndf: int = 6,
|
||||||
parent: QWidget | None = None,
|
parent: QWidget | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ from PySide6.QtWidgets import (
|
||||||
from PySide6.QtCore import Qt
|
from PySide6.QtCore import Qt
|
||||||
|
|
||||||
from otko.core import ResponseSpectrum
|
from otko.core import ResponseSpectrum
|
||||||
|
from otko.core.units import UnitSystem, native_label
|
||||||
from otko.services.results import ResponseSpectrumResults
|
from otko.services.results import ResponseSpectrumResults
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -31,14 +32,71 @@ class ResponseSpectrumView(QWidget):
|
||||||
|
|
||||||
closed = Signal()
|
closed = Signal()
|
||||||
|
|
||||||
def __init__(self, parent: QWidget | None = None) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
parent: QWidget | None = None,
|
||||||
|
*,
|
||||||
|
units: UnitSystem = UnitSystem.METRIC,
|
||||||
|
) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
self._units = units
|
||||||
|
self._results: ResponseSpectrumResults | None = None
|
||||||
|
self._spectrum: ResponseSpectrum | None = None
|
||||||
self._build_ui()
|
self._build_ui()
|
||||||
|
|
||||||
|
def set_units(self, units: UnitSystem, ndf: int | None = None) -> None:
|
||||||
|
"""Re-apply stored units to the Sa axis. Values stay native."""
|
||||||
|
try:
|
||||||
|
self._units = units
|
||||||
|
self._apply_axis_labels()
|
||||||
|
self._refresh_table_headers()
|
||||||
|
if self._results is not None:
|
||||||
|
self.set_results(self._results, self._spectrum)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _accel_unit(self) -> str:
|
||||||
|
try:
|
||||||
|
return native_label(self._units, "acceleration")
|
||||||
|
except KeyError:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def _refresh_table_headers(self) -> None:
|
||||||
|
"""Set mass-table headers with the Sa display unit."""
|
||||||
|
try:
|
||||||
|
unit = self._accel_unit()
|
||||||
|
sa_col = f"Sa(T) ({unit})" if unit else "Sa(T)"
|
||||||
|
self._table.setHorizontalHeaderLabels(
|
||||||
|
[
|
||||||
|
"Mode",
|
||||||
|
"T (s)",
|
||||||
|
"f (Hz)",
|
||||||
|
"Γ",
|
||||||
|
"M_eff",
|
||||||
|
"Mass %",
|
||||||
|
sa_col,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _apply_axis_labels(self) -> None:
|
||||||
|
try:
|
||||||
|
unit = self._accel_unit()
|
||||||
|
label = f"Sa ({unit})" if unit else "Sa"
|
||||||
|
self._plot.setLabel("left", label)
|
||||||
|
self._plot.setLabel("bottom", "Period", units="s")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def set_results(
|
def set_results(
|
||||||
self, results: ResponseSpectrumResults | None,
|
self,
|
||||||
|
results: ResponseSpectrumResults | None,
|
||||||
spectrum: ResponseSpectrum | None,
|
spectrum: ResponseSpectrum | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
self._results = results
|
||||||
|
self._spectrum = spectrum
|
||||||
|
self._apply_axis_labels()
|
||||||
self._plot.clear()
|
self._plot.clear()
|
||||||
self._table.setRowCount(0)
|
self._table.setRowCount(0)
|
||||||
if results is None:
|
if results is None:
|
||||||
|
|
@ -59,9 +117,13 @@ class ResponseSpectrumView(QWidget):
|
||||||
self._plot.plot(p_dense, a_dense, pen=pen, name="Sa(T)")
|
self._plot.plot(p_dense, a_dense, pen=pen, name="Sa(T)")
|
||||||
# Original control points.
|
# Original control points.
|
||||||
self._plot.plot(
|
self._plot.plot(
|
||||||
list(spectrum.periods), list(spectrum.accelerations),
|
list(spectrum.periods),
|
||||||
pen=None, symbol="s", symbolSize=7,
|
list(spectrum.accelerations),
|
||||||
symbolBrush="#1f77b4", symbolPen=None,
|
pen=None,
|
||||||
|
symbol="s",
|
||||||
|
symbolSize=7,
|
||||||
|
symbolBrush="#1f77b4",
|
||||||
|
symbolPen=None,
|
||||||
name="Control pts",
|
name="Control pts",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -77,14 +139,19 @@ class ResponseSpectrumView(QWidget):
|
||||||
# Slight vertical jitter for duplicate periods
|
# Slight vertical jitter for duplicate periods
|
||||||
y_offset = count * m.sa_at_period * 0.04
|
y_offset = count * m.sa_at_period * 0.04
|
||||||
self._plot.plot(
|
self._plot.plot(
|
||||||
[m.period], [m.sa_at_period + y_offset],
|
[m.period],
|
||||||
pen=None, symbol="o", symbolSize=12,
|
[m.sa_at_period + y_offset],
|
||||||
symbolBrush="#d62728", symbolPen=pg.mkPen("#ffffff", width=1),
|
pen=None,
|
||||||
|
symbol="o",
|
||||||
|
symbolSize=12,
|
||||||
|
symbolBrush="#d62728",
|
||||||
|
symbolPen=pg.mkPen("#ffffff", width=1),
|
||||||
name=f"Mode {m.mode_number}" if count == 0 else None,
|
name=f"Mode {m.mode_number}" if count == 0 else None,
|
||||||
)
|
)
|
||||||
# Small text label right next to the marker.
|
# Small text label right next to the marker.
|
||||||
txt = pg.TextItem(
|
txt = pg.TextItem(
|
||||||
f" M{m.mode_number}", color="#d62728",
|
f" M{m.mode_number}",
|
||||||
|
color="#d62728",
|
||||||
anchor=(0.0, 0.5),
|
anchor=(0.0, 0.5),
|
||||||
)
|
)
|
||||||
txt.setPos(m.period, m.sa_at_period + y_offset)
|
txt.setPos(m.period, m.sa_at_period + y_offset)
|
||||||
|
|
@ -133,9 +200,7 @@ class ResponseSpectrumView(QWidget):
|
||||||
splitter.addWidget(self._plot)
|
splitter.addWidget(self._plot)
|
||||||
|
|
||||||
self._table = QTableWidget(0, 7)
|
self._table = QTableWidget(0, 7)
|
||||||
self._table.setHorizontalHeaderLabels([
|
self._refresh_table_headers()
|
||||||
"Mode", "T (s)", "f (Hz)", "Γ", "M_eff", "Mass %", "Sa(T)",
|
|
||||||
])
|
|
||||||
self._table.horizontalHeader().setSectionResizeMode(
|
self._table.horizontalHeader().setSectionResizeMode(
|
||||||
QHeaderView.ResizeMode.Stretch,
|
QHeaderView.ResizeMode.Stretch,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ from PySide6.QtWidgets import (
|
||||||
QWidget,
|
QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from otko.core.units import UnitSystem, native_label
|
||||||
from otko.services.results import (
|
from otko.services.results import (
|
||||||
ModalResults,
|
ModalResults,
|
||||||
StaticResults,
|
StaticResults,
|
||||||
|
|
@ -31,12 +32,46 @@ from otko.views.docks._labels import dof_labels
|
||||||
class ResultsPanel(QWidget):
|
class ResultsPanel(QWidget):
|
||||||
"""Tabbed view that swaps based on the type of results received."""
|
"""Tabbed view that swaps based on the type of results received."""
|
||||||
|
|
||||||
def __init__(self, parent: QWidget | None = None) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
parent: QWidget | None = None,
|
||||||
|
*,
|
||||||
|
units: UnitSystem = UnitSystem.METRIC,
|
||||||
|
) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
self._units = units
|
||||||
self._ndm: int | None = None
|
self._ndm: int | None = None
|
||||||
self._ndf: int | None = None
|
self._ndf: int | None = None
|
||||||
|
self._last_results: Any | None = None
|
||||||
self._build_ui()
|
self._build_ui()
|
||||||
|
|
||||||
|
def set_units(self, units: UnitSystem, ndf: int | None = None) -> None:
|
||||||
|
"""Re-apply stored units to table headers. Values stay native."""
|
||||||
|
try:
|
||||||
|
self._units = units
|
||||||
|
if ndf is not None:
|
||||||
|
self._ndf = int(ndf)
|
||||||
|
if self._last_results is not None:
|
||||||
|
self.show_results(
|
||||||
|
self._last_results,
|
||||||
|
ndm=self._ndm,
|
||||||
|
ndf=self._ndf,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _disp_unit(self) -> str:
|
||||||
|
try:
|
||||||
|
return native_label(self._units, "displacement")
|
||||||
|
except KeyError:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def _force_unit(self) -> str:
|
||||||
|
try:
|
||||||
|
return native_label(self._units, "force")
|
||||||
|
except KeyError:
|
||||||
|
return ""
|
||||||
|
|
||||||
def _build_ui(self) -> None:
|
def _build_ui(self) -> None:
|
||||||
layout = QVBoxLayout(self)
|
layout = QVBoxLayout(self)
|
||||||
layout.setContentsMargins(4, 4, 4, 4)
|
layout.setContentsMargins(4, 4, 4, 4)
|
||||||
|
|
@ -58,6 +93,7 @@ class ResultsPanel(QWidget):
|
||||||
(see :mod:`otko.views.docks._labels`).
|
(see :mod:`otko.views.docks._labels`).
|
||||||
"""
|
"""
|
||||||
self._ndm, self._ndf = ndm, ndf
|
self._ndm, self._ndf = ndm, ndf
|
||||||
|
self._last_results = results
|
||||||
self._tabs.clear()
|
self._tabs.clear()
|
||||||
if isinstance(results, StaticResults):
|
if isinstance(results, StaticResults):
|
||||||
self._title.setText(
|
self._title.setText(
|
||||||
|
|
@ -67,9 +103,7 @@ class ResultsPanel(QWidget):
|
||||||
self._tabs.addTab(self._build_static_disp_table(results), "Displacements")
|
self._tabs.addTab(self._build_static_disp_table(results), "Displacements")
|
||||||
self._tabs.addTab(self._build_static_reaction_table(results), "Reactions")
|
self._tabs.addTab(self._build_static_reaction_table(results), "Reactions")
|
||||||
elif isinstance(results, ModalResults):
|
elif isinstance(results, ModalResults):
|
||||||
self._title.setText(
|
self._title.setText(f"<b>Modal — case #{results.case_id} '{results.case_name}'</b>")
|
||||||
f"<b>Modal — case #{results.case_id} '{results.case_name}'</b>"
|
|
||||||
)
|
|
||||||
self._tabs.addTab(self._build_modal_table(results), "Frequencies")
|
self._tabs.addTab(self._build_modal_table(results), "Frequencies")
|
||||||
elif isinstance(results, TransientResults):
|
elif isinstance(results, TransientResults):
|
||||||
self._title.setText(
|
self._title.setText(
|
||||||
|
|
@ -86,28 +120,36 @@ class ResultsPanel(QWidget):
|
||||||
if not rows:
|
if not rows:
|
||||||
return self._empty_table_widget()
|
return self._empty_table_widget()
|
||||||
ndf = r.node_disp[rows[0]].shape[1]
|
ndf = r.node_disp[rows[0]].shape[1]
|
||||||
headers = ["Node", *dof_labels(ndf, self._ndm, self._ndf, kind="disp")]
|
base = dof_labels(ndf, self._ndm, self._ndf, kind="disp")
|
||||||
|
unit = self._disp_unit()
|
||||||
|
headers = ["Node", *[f"{h} ({unit})" if unit else h for h in base]]
|
||||||
table = self._make_table(headers, len(rows))
|
table = self._make_table(headers, len(rows))
|
||||||
for i, nid in enumerate(rows):
|
for i, nid in enumerate(rows):
|
||||||
self._set_cell(table, i, 0, str(nid))
|
self._set_cell(table, i, 0, str(nid))
|
||||||
last_step = r.node_disp[nid][-1]
|
last_step = r.node_disp[nid][-1]
|
||||||
for j, val in enumerate(last_step):
|
for j, val in enumerate(last_step):
|
||||||
self._set_cell(table, i, j + 1, f"{val:.6g}")
|
self._set_cell(table, i, j + 1, f"{val:.6g}")
|
||||||
return self._wrap(table, "Final-step displacements")
|
unit = self._disp_unit()
|
||||||
|
caption = f"Final-step displacements ({unit})" if unit else "Final-step displacements"
|
||||||
|
return self._wrap(table, caption)
|
||||||
|
|
||||||
def _build_static_reaction_table(self, r: StaticResults) -> QWidget:
|
def _build_static_reaction_table(self, r: StaticResults) -> QWidget:
|
||||||
rows = sorted(r.node_reaction.keys())
|
rows = sorted(r.node_reaction.keys())
|
||||||
if not rows:
|
if not rows:
|
||||||
return self._empty_table_widget()
|
return self._empty_table_widget()
|
||||||
ndf = r.node_reaction[rows[0]].shape[1]
|
ndf = r.node_reaction[rows[0]].shape[1]
|
||||||
headers = ["Node", *dof_labels(ndf, self._ndm, self._ndf, kind="force")]
|
base = dof_labels(ndf, self._ndm, self._ndf, kind="force")
|
||||||
|
unit = self._force_unit()
|
||||||
|
headers = ["Node", *[f"{h} ({unit})" if unit else h for h in base]]
|
||||||
table = self._make_table(headers, len(rows))
|
table = self._make_table(headers, len(rows))
|
||||||
for i, nid in enumerate(rows):
|
for i, nid in enumerate(rows):
|
||||||
self._set_cell(table, i, 0, str(nid))
|
self._set_cell(table, i, 0, str(nid))
|
||||||
last_step = r.node_reaction[nid][-1]
|
last_step = r.node_reaction[nid][-1]
|
||||||
for j, val in enumerate(last_step):
|
for j, val in enumerate(last_step):
|
||||||
self._set_cell(table, i, j + 1, f"{val:.6g}")
|
self._set_cell(table, i, j + 1, f"{val:.6g}")
|
||||||
return self._wrap(table, "Final-step reactions")
|
unit = self._force_unit()
|
||||||
|
caption = f"Final-step reactions ({unit})" if unit else "Final-step reactions"
|
||||||
|
return self._wrap(table, caption)
|
||||||
|
|
||||||
def _build_modal_table(self, r: ModalResults) -> QWidget:
|
def _build_modal_table(self, r: ModalResults) -> QWidget:
|
||||||
n = len(r.eigenvalues)
|
n = len(r.eigenvalues)
|
||||||
|
|
@ -124,15 +166,17 @@ class ResultsPanel(QWidget):
|
||||||
def _transient_summary(self, r: TransientResults) -> QWidget:
|
def _transient_summary(self, r: TransientResults) -> QWidget:
|
||||||
w = QWidget()
|
w = QWidget()
|
||||||
layout = QVBoxLayout(w)
|
layout = QVBoxLayout(w)
|
||||||
layout.addWidget(QLabel(
|
layout.addWidget(
|
||||||
f"<b>Steps:</b> {r.n_steps}<br>"
|
QLabel(
|
||||||
f"<b>dt:</b> {r.dt:g}<br>"
|
f"<b>Steps:</b> {r.n_steps}<br>"
|
||||||
f"<b>Total time:</b> {r.n_steps * r.dt:g}<br>"
|
f"<b>dt:</b> {r.dt:g}<br>"
|
||||||
f"<b>HDF5 file:</b> <code>{r.h5_path}</code>"
|
f"<b>Total time:</b> {r.n_steps * r.dt:g}<br>"
|
||||||
))
|
f"<b>HDF5 file:</b> <code>{r.h5_path}</code>"
|
||||||
layout.addWidget(QLabel(
|
)
|
||||||
"<i>Time-history plots and animation will appear here in Phase 7.</i>"
|
)
|
||||||
))
|
layout.addWidget(
|
||||||
|
QLabel("<i>Time-history plots and animation will appear here in Phase 7.</i>")
|
||||||
|
)
|
||||||
layout.addStretch(1)
|
layout.addStretch(1)
|
||||||
return w
|
return w
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,7 @@ from otko.core import (
|
||||||
TrussElement,
|
TrussElement,
|
||||||
UniformElementLoad,
|
UniformElementLoad,
|
||||||
)
|
)
|
||||||
|
from otko.core.units import UnitSystem, native_label
|
||||||
from otko.views.docks._labels import dof_labels
|
from otko.views.docks._labels import dof_labels
|
||||||
|
|
||||||
_MASS_LABELS = ("mx", "my", "mz", "Ixx", "Iyy", "Izz")
|
_MASS_LABELS = ("mx", "my", "mz", "Ixx", "Iyy", "Izz")
|
||||||
|
|
@ -320,6 +321,36 @@ class TableDock(QWidget):
|
||||||
self._project = project
|
self._project = project
|
||||||
self.refresh()
|
self.refresh()
|
||||||
|
|
||||||
|
def set_units(self, units: UnitSystem, ndf: int | None = None) -> None:
|
||||||
|
"""Refresh headers for a units change. Values stay native."""
|
||||||
|
try:
|
||||||
|
self.refresh()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _units(self) -> UnitSystem:
|
||||||
|
"""Project's stored units, defaulting to Metric."""
|
||||||
|
try:
|
||||||
|
if self._project is not None:
|
||||||
|
return self._project.meta.units # type: ignore[no-any-return]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return UnitSystem.METRIC
|
||||||
|
|
||||||
|
def _u(self, quantity: str) -> str:
|
||||||
|
"""Display label for a quantity, or ``""`` when dimensionless."""
|
||||||
|
try:
|
||||||
|
return native_label(self._units(), quantity)
|
||||||
|
except KeyError:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def _suffixed(self, name: str, quantity: str | None) -> str:
|
||||||
|
"""Column name with its display-unit suffix (bare when None)."""
|
||||||
|
if quantity is None:
|
||||||
|
return name
|
||||||
|
unit = self._u(quantity)
|
||||||
|
return f"{name} ({unit})" if unit else name
|
||||||
|
|
||||||
def refresh(self) -> None:
|
def refresh(self) -> None:
|
||||||
"""Repaint every tab from the current project (``modelMutated``).
|
"""Repaint every tab from the current project (``modelMutated``).
|
||||||
|
|
||||||
|
|
@ -487,12 +518,20 @@ class TableDock(QWidget):
|
||||||
# ── Nodes ──────────────────────────────────────────────────────
|
# ── Nodes ──────────────────────────────────────────────────────
|
||||||
def _load_nodes(self, table: QTableWidget) -> None:
|
def _load_nodes(self, table: QTableWidget) -> None:
|
||||||
nodes = list(self._project.nodes) if self._project is not None else []
|
nodes = list(self._project.nodes) if self._project is not None else []
|
||||||
table.setColumnCount(len(_NODE_HEADERS))
|
headers = [
|
||||||
table.setHorizontalHeaderLabels(_NODE_HEADERS)
|
self._suffixed(h, {"x": "length", "y": "length", "z": "length"}.get(h))
|
||||||
|
if h in ("x", "y", "z")
|
||||||
|
else self._suffixed(h, "mass")
|
||||||
|
if h in ("mx", "my", "mz", "Ixx", "Iyy", "Izz")
|
||||||
|
else h
|
||||||
|
for h in _NODE_HEADERS
|
||||||
|
]
|
||||||
|
table.setColumnCount(len(headers))
|
||||||
|
table.setHorizontalHeaderLabels(headers)
|
||||||
table.setRowCount(len(nodes))
|
table.setRowCount(len(nodes))
|
||||||
for r, n in enumerate(nodes):
|
for r, n in enumerate(nodes):
|
||||||
self._put(table, r, 0, str(n.id), editable=False, data=n.id)
|
self._put(table, r, 0, str(n.id), editable=False, data=n.id)
|
||||||
self._put(table, r, 1, n.name, data=n.id)
|
self._put(table, r, 1, n.display_name, data=n.id)
|
||||||
for c, v in enumerate(n.coords, start=2):
|
for c, v in enumerate(n.coords, start=2):
|
||||||
self._put(table, r, c, f"{v:g}", data=n.id)
|
self._put(table, r, c, f"{v:g}", data=n.id)
|
||||||
for c, v in enumerate(n.mass, start=5):
|
for c, v in enumerate(n.mass, start=5):
|
||||||
|
|
@ -506,6 +545,12 @@ class TableDock(QWidget):
|
||||||
nid = self._row_key(table, row)
|
nid = self._row_key(table, row)
|
||||||
node = project.node(nid)
|
node = project.node(nid)
|
||||||
if col == 1:
|
if col == 1:
|
||||||
|
if not node.name.strip() and item.text().strip() == node.display_name:
|
||||||
|
# Confirmed the displayed fallback unchanged — not a real
|
||||||
|
# rename, so leave the stored name blank instead of
|
||||||
|
# persisting "N<id>" (and keep it off the undo stack).
|
||||||
|
self.refresh()
|
||||||
|
return
|
||||||
updated = self._dry_run(node, {"name": item.text()})
|
updated = self._dry_run(node, {"name": item.text()})
|
||||||
self._dispatch(UpdateNodeFieldsCommand(vm, nid, {"name": updated.name}))
|
self._dispatch(UpdateNodeFieldsCommand(vm, nid, {"name": updated.name}))
|
||||||
elif 2 <= col <= 4:
|
elif 2 <= col <= 4:
|
||||||
|
|
@ -537,7 +582,7 @@ class TableDock(QWidget):
|
||||||
table.setRowCount(len(elements))
|
table.setRowCount(len(elements))
|
||||||
for r, el in enumerate(elements):
|
for r, el in enumerate(elements):
|
||||||
self._put(table, r, 0, str(el.id), editable=False, data=el.id)
|
self._put(table, r, 0, str(el.id), editable=False, data=el.id)
|
||||||
self._put(table, r, 1, el.name, data=el.id)
|
self._put(table, r, 1, el.display_name, data=el.id)
|
||||||
self._put(table, r, 2, el.type, editable=False, data=el.id)
|
self._put(table, r, 2, el.type, editable=False, data=el.id)
|
||||||
self._put(table, r, 3, ", ".join(str(n) for n in el.nodes), data=el.id)
|
self._put(table, r, 3, ", ".join(str(n) for n in el.nodes), data=el.id)
|
||||||
self._put(
|
self._put(
|
||||||
|
|
@ -583,6 +628,12 @@ class TableDock(QWidget):
|
||||||
eid = self._row_key(table, row)
|
eid = self._row_key(table, row)
|
||||||
el = project.element(eid)
|
el = project.element(eid)
|
||||||
if col == 1:
|
if col == 1:
|
||||||
|
if not el.name.strip() and item.text().strip() == el.display_name:
|
||||||
|
# Confirmed the displayed fallback unchanged — not a real
|
||||||
|
# rename, so leave the stored name blank instead of
|
||||||
|
# persisting "E<id>" (and keep it off the undo stack).
|
||||||
|
self.refresh()
|
||||||
|
return
|
||||||
updated = self._dry_run(el, {"name": item.text()})
|
updated = self._dry_run(el, {"name": item.text()})
|
||||||
self._dispatch(UpdateElementFieldsCommand(vm, eid, {"name": updated.name}))
|
self._dispatch(UpdateElementFieldsCommand(vm, eid, {"name": updated.name}))
|
||||||
elif col == 3:
|
elif col == 3:
|
||||||
|
|
@ -616,8 +667,12 @@ class TableDock(QWidget):
|
||||||
# ── Materials ──────────────────────────────────────────────────
|
# ── Materials ──────────────────────────────────────────────────
|
||||||
def _load_materials(self, table: QTableWidget) -> None:
|
def _load_materials(self, table: QTableWidget) -> None:
|
||||||
materials = list(self._project.materials) if self._project is not None else []
|
materials = list(self._project.materials) if self._project is not None else []
|
||||||
table.setColumnCount(len(_MATERIAL_HEADERS))
|
headers_m = [
|
||||||
table.setHorizontalHeaderLabels(_MATERIAL_HEADERS)
|
self._suffixed(h, "stress") if h in ("E", "Fy", "E0", "fpc", "fpcu", "ft") else h
|
||||||
|
for h in _MATERIAL_HEADERS
|
||||||
|
]
|
||||||
|
table.setColumnCount(len(headers_m))
|
||||||
|
table.setHorizontalHeaderLabels(headers_m)
|
||||||
table.setRowCount(len(materials))
|
table.setRowCount(len(materials))
|
||||||
for r, m in enumerate(materials):
|
for r, m in enumerate(materials):
|
||||||
self._put(table, r, 0, str(m.id), editable=False, data=m.id)
|
self._put(table, r, 0, str(m.id), editable=False, data=m.id)
|
||||||
|
|
@ -650,8 +705,20 @@ class TableDock(QWidget):
|
||||||
# ── Sections ───────────────────────────────────────────────────
|
# ── Sections ───────────────────────────────────────────────────
|
||||||
def _load_sections(self, table: QTableWidget) -> None:
|
def _load_sections(self, table: QTableWidget) -> None:
|
||||||
sections = list(self._project.sections) if self._project is not None else []
|
sections = list(self._project.sections) if self._project is not None else []
|
||||||
table.setColumnCount(len(_SECTION_HEADERS))
|
headers_s = [
|
||||||
table.setHorizontalHeaderLabels(_SECTION_HEADERS)
|
self._suffixed(h, "stress")
|
||||||
|
if h in ("E", "G")
|
||||||
|
else self._suffixed(h, "area")
|
||||||
|
if h == "A"
|
||||||
|
else self._suffixed(h, "inertia")
|
||||||
|
if h in ("Iz", "Iy", "J")
|
||||||
|
else self._suffixed(h, "section_dim")
|
||||||
|
if h in ("b", "h", "d", "tw", "tf", "diameter")
|
||||||
|
else h
|
||||||
|
for h in _SECTION_HEADERS
|
||||||
|
]
|
||||||
|
table.setColumnCount(len(headers_s))
|
||||||
|
table.setHorizontalHeaderLabels(headers_s)
|
||||||
table.setRowCount(len(sections))
|
table.setRowCount(len(sections))
|
||||||
for r, s in enumerate(sections):
|
for r, s in enumerate(sections):
|
||||||
self._put(table, r, 0, str(s.id), editable=False, data=s.id)
|
self._put(table, r, 0, str(s.id), editable=False, data=s.id)
|
||||||
|
|
@ -854,8 +921,16 @@ class TableDock(QWidget):
|
||||||
continue
|
continue
|
||||||
for i, load in enumerate(p.nodal_loads):
|
for i, load in enumerate(p.nodal_loads):
|
||||||
rows.append((p.id, i, load))
|
rows.append((p.id, i, load))
|
||||||
table.setColumnCount(len(_NODAL_LOAD_HEADERS))
|
headers = [
|
||||||
table.setHorizontalHeaderLabels(_NODAL_LOAD_HEADERS)
|
self._suffixed(h, "force")
|
||||||
|
if h.startswith("F")
|
||||||
|
else self._suffixed(h, "moment")
|
||||||
|
if h.startswith("M")
|
||||||
|
else h
|
||||||
|
for h in _NODAL_LOAD_HEADERS
|
||||||
|
]
|
||||||
|
table.setColumnCount(len(headers))
|
||||||
|
table.setHorizontalHeaderLabels(headers)
|
||||||
table.setRowCount(len(rows))
|
table.setRowCount(len(rows))
|
||||||
for r, (pid, i, load) in enumerate(rows):
|
for r, (pid, i, load) in enumerate(rows):
|
||||||
self._put(table, r, 0, str(pid), editable=False, data=(pid, i))
|
self._put(table, r, 0, str(pid), editable=False, data=(pid, i))
|
||||||
|
|
@ -875,8 +950,12 @@ class TableDock(QWidget):
|
||||||
continue
|
continue
|
||||||
for i, load in enumerate(p.element_loads):
|
for i, load in enumerate(p.element_loads):
|
||||||
rows.append((p.id, i, load))
|
rows.append((p.id, i, load))
|
||||||
table.setColumnCount(len(_ELEMENT_LOAD_HEADERS))
|
headers_el = [
|
||||||
table.setHorizontalHeaderLabels(_ELEMENT_LOAD_HEADERS)
|
self._suffixed(h, "distributed_load") if h in ("wx", "wy", "wz") else h
|
||||||
|
for h in _ELEMENT_LOAD_HEADERS
|
||||||
|
]
|
||||||
|
table.setColumnCount(len(headers_el))
|
||||||
|
table.setHorizontalHeaderLabels(headers_el)
|
||||||
table.setRowCount(len(rows))
|
table.setRowCount(len(rows))
|
||||||
for r, (pid, i, load) in enumerate(rows):
|
for r, (pid, i, load) in enumerate(rows):
|
||||||
self._put(table, r, 0, str(pid), editable=False, data=(pid, i))
|
self._put(table, r, 0, str(pid), editable=False, data=(pid, i))
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,17 @@ from PySide6.QtWidgets import (
|
||||||
QWidget,
|
QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from otko.core.units import UnitSystem, native_label
|
||||||
from otko.services.results import TransientResults
|
from otko.services.results import TransientResults
|
||||||
from otko.views.docks._labels import dof_symbol
|
from otko.views.docks._labels import dof_symbol
|
||||||
|
|
||||||
|
#: Trace quantity → display quantity key (plan §5 taxonomy).
|
||||||
|
_QUANTITY_KEY: dict[str, str] = {
|
||||||
|
"disp": "displacement",
|
||||||
|
"vel": "velocity",
|
||||||
|
"accel": "acceleration",
|
||||||
|
}
|
||||||
|
|
||||||
# A small palette that reads well on dark + light themes.
|
# A small palette that reads well on dark + light themes.
|
||||||
_COLORS = [
|
_COLORS = [
|
||||||
"#1f77b4",
|
"#1f77b4",
|
||||||
|
|
@ -55,14 +63,46 @@ class TimeHistoryView(QWidget):
|
||||||
|
|
||||||
closed = Signal()
|
closed = Signal()
|
||||||
|
|
||||||
def __init__(self, parent: QWidget | None = None) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
parent: QWidget | None = None,
|
||||||
|
*,
|
||||||
|
units: UnitSystem = UnitSystem.METRIC,
|
||||||
|
) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
self._units = units
|
||||||
self._results: TransientResults | None = None
|
self._results: TransientResults | None = None
|
||||||
self._ndm: int | None = None
|
self._ndm: int | None = None
|
||||||
self._ndf: int | None = None
|
self._ndf: int | None = None
|
||||||
self._traces: list[tuple[int, int, Any]] = [] # (node_id, dof, plot_item)
|
self._traces: list[tuple[int, int, Any]] = [] # (node_id, dof, plot_item)
|
||||||
self._build_ui()
|
self._build_ui()
|
||||||
|
|
||||||
|
def set_units(self, units: UnitSystem, ndf: int | None = None) -> None:
|
||||||
|
"""Re-apply stored units to axis labels. Values stay native."""
|
||||||
|
try:
|
||||||
|
self._units = units
|
||||||
|
if ndf is not None:
|
||||||
|
self._ndf = int(ndf)
|
||||||
|
self._refresh_axis_label()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _y_unit(self, quantity: str = "disp") -> str:
|
||||||
|
"""Display unit for a trace quantity (x-axis always ``s``)."""
|
||||||
|
try:
|
||||||
|
return native_label(self._units, _QUANTITY_KEY.get(quantity, "displacement"))
|
||||||
|
except KeyError:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def _refresh_axis_label(self) -> None:
|
||||||
|
"""Refresh the y-axis label for the current quantity selection."""
|
||||||
|
try:
|
||||||
|
quantity = self._quantity.currentData() or "disp"
|
||||||
|
base = {"disp": "Displacement", "vel": "Velocity", "accel": "Acceleration"}[quantity]
|
||||||
|
self._plot.setLabel("left", f"{base} ({self._y_unit(quantity)})")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# ── public ──────────────────────────────────────────────────────
|
# ── public ──────────────────────────────────────────────────────
|
||||||
def set_results(
|
def set_results(
|
||||||
self, results: TransientResults | None, *, ndm: int | None = None, ndf: int | None = None
|
self, results: TransientResults | None, *, ndm: int | None = None, ndf: int | None = None
|
||||||
|
|
@ -155,7 +195,7 @@ class TimeHistoryView(QWidget):
|
||||||
pg.setConfigOptions(antialias=True)
|
pg.setConfigOptions(antialias=True)
|
||||||
self._plot = pg.PlotWidget()
|
self._plot = pg.PlotWidget()
|
||||||
self._plot.setBackground("#1e1e1e")
|
self._plot.setBackground("#1e1e1e")
|
||||||
self._plot.setLabel("left", "Displacement")
|
self._plot.setLabel("left", f"Displacement ({self._y_unit('disp')})")
|
||||||
self._plot.setLabel("bottom", "Time", units="s")
|
self._plot.setLabel("bottom", "Time", units="s")
|
||||||
self._plot.showGrid(x=True, y=True, alpha=0.3)
|
self._plot.showGrid(x=True, y=True, alpha=0.3)
|
||||||
self._plot.addLegend(offset=(8, 8))
|
self._plot.addLegend(offset=(8, 8))
|
||||||
|
|
@ -204,7 +244,7 @@ class TimeHistoryView(QWidget):
|
||||||
self._trace_list.addItem(QListWidgetItem(label))
|
self._trace_list.addItem(QListWidgetItem(label))
|
||||||
# Update y-axis label to reflect what's plotted (last-write-wins).
|
# Update y-axis label to reflect what's plotted (last-write-wins).
|
||||||
y_label = {"disp": "Displacement", "vel": "Velocity", "accel": "Acceleration"}[quantity]
|
y_label = {"disp": "Displacement", "vel": "Velocity", "accel": "Acceleration"}[quantity]
|
||||||
self._plot.setLabel("left", y_label)
|
self._plot.setLabel("left", f"{y_label} ({self._y_unit(quantity)})")
|
||||||
|
|
||||||
def _clear_traces(self) -> None:
|
def _clear_traces(self) -> None:
|
||||||
for _, _, item in self._traces:
|
for _, _, item in self._traces:
|
||||||
|
|
|
||||||
|
|
@ -315,6 +315,11 @@ class MenuBuilder:
|
||||||
"Assign Point Loads (Ctrl+L)",
|
"Assign Point Loads (Ctrl+L)",
|
||||||
"Assign point loads to the selected nodes.",
|
"Assign point loads to the selected nodes.",
|
||||||
),
|
),
|
||||||
|
"_act_assign_distributed_load": (
|
||||||
|
"assign-distributed",
|
||||||
|
"Assign Distributed Loads…",
|
||||||
|
"Assign distributed loads to the selected frame elements.",
|
||||||
|
),
|
||||||
"_act_quick_guide": (
|
"_act_quick_guide": (
|
||||||
"quick-guide",
|
"quick-guide",
|
||||||
"Quick Guide",
|
"Quick Guide",
|
||||||
|
|
@ -541,6 +546,7 @@ class MenuBuilder:
|
||||||
tb.addSeparator()
|
tb.addSeparator()
|
||||||
tb.addAction(self._act_assign_support)
|
tb.addAction(self._act_assign_support)
|
||||||
tb.addAction(self._act_assign_load)
|
tb.addAction(self._act_assign_load)
|
||||||
|
tb.addAction(self._act_assign_distributed_load)
|
||||||
|
|
||||||
def _build_status_bar(self) -> None:
|
def _build_status_bar(self) -> None:
|
||||||
self._status_label = QLabel("No project")
|
self._status_label = QLabel("No project")
|
||||||
|
|
@ -569,12 +575,14 @@ class MenuBuilder:
|
||||||
def _on_units_combo_changed(self, _idx: int) -> None:
|
def _on_units_combo_changed(self, _idx: int) -> None:
|
||||||
"""Status-bar unit picker → project.meta.units.
|
"""Status-bar unit picker → project.meta.units.
|
||||||
|
|
||||||
Qt stores the userData as a bare string (UnitSystem inherits
|
Routed through ``SetUnitsCommand`` (same as Options → Set Display
|
||||||
from str), so we re-cast to the enum before writing through
|
Units) so the change is undoable and the whole UI refreshes via
|
||||||
and logging.
|
``modelMutated``. Qt stores the userData as a bare string
|
||||||
|
(UnitSystem inherits from str), so we re-cast to the enum first.
|
||||||
"""
|
"""
|
||||||
if self._vm.project is None:
|
if self._vm.project is None:
|
||||||
return
|
return
|
||||||
|
from otko.commands import SetUnitsCommand
|
||||||
from otko.core import UnitSystem
|
from otko.core import UnitSystem
|
||||||
|
|
||||||
raw = self._units_combo.currentData()
|
raw = self._units_combo.currentData()
|
||||||
|
|
@ -583,8 +591,9 @@ class MenuBuilder:
|
||||||
chosen = raw if isinstance(raw, UnitSystem) else UnitSystem(str(raw))
|
chosen = raw if isinstance(raw, UnitSystem) else UnitSystem(str(raw))
|
||||||
if chosen == self._vm.project.meta.units:
|
if chosen == self._vm.project.meta.units:
|
||||||
return
|
return
|
||||||
self._vm.project.meta.units = chosen
|
self._vm.apply_command(SetUnitsCommand(self._vm, chosen))
|
||||||
self._vm.mark_dirty()
|
self._sync_units_combo()
|
||||||
|
self._refresh_dock_units()
|
||||||
self._log(f"Display units set to {chosen.value}.")
|
self._log(f"Display units set to {chosen.value}.")
|
||||||
|
|
||||||
def _sync_view_display_state(self, _project: object = None) -> None:
|
def _sync_view_display_state(self, _project: object = None) -> None:
|
||||||
|
|
|
||||||
|
|
@ -259,8 +259,47 @@ class RenderControls:
|
||||||
self._props.update_for_selection(
|
self._props.update_for_selection(
|
||||||
self._canvas.selection.nodes, self._canvas.selection.elements
|
self._canvas.selection.nodes, self._canvas.selection.elements
|
||||||
)
|
)
|
||||||
|
self._sync_units_everywhere()
|
||||||
self._refresh_action_enablement()
|
self._refresh_action_enablement()
|
||||||
|
|
||||||
|
def _sync_units_everywhere(self) -> None:
|
||||||
|
"""Refresh every units-aware label after a units change.
|
||||||
|
|
||||||
|
Runs on every ``modelMutated`` but short-circuits (combo check)
|
||||||
|
when the units did not actually change, so ordinary edits skip
|
||||||
|
the work. Covers the status combo, the Results panel, and any
|
||||||
|
open post dock — whether the change came from the combo, the
|
||||||
|
Options menu, or undo/redo. Table headers and the property
|
||||||
|
editor read live project units on their own refresh, which this
|
||||||
|
same ``modelMutated`` already triggers. Never raises.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
project = self._vm.project
|
||||||
|
if project is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
combo_data = self._units_combo.currentData()
|
||||||
|
except Exception:
|
||||||
|
combo_data = None
|
||||||
|
if combo_data == project.meta.units:
|
||||||
|
return
|
||||||
|
self._sync_units_combo()
|
||||||
|
try:
|
||||||
|
self._results_panel.set_units( # type: ignore[attr-defined]
|
||||||
|
project.meta.units,
|
||||||
|
project.ndf,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
fn = getattr(self, "refresh_open_dock_units", None)
|
||||||
|
if callable(fn):
|
||||||
|
fn()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def _on_dirty_changed(self, _dirty: bool) -> None:
|
def _on_dirty_changed(self, _dirty: bool) -> None:
|
||||||
self._refresh_status()
|
self._refresh_status()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ def test_is_rotation_dof_ndf6() -> None:
|
||||||
|
|
||||||
@pytest.mark.gui
|
@pytest.mark.gui
|
||||||
def test_si_translation_labels(qtbot) -> None: # type: ignore[no-untyped-def]
|
def test_si_translation_labels(qtbot) -> None: # type: ignore[no-untyped-def]
|
||||||
v = PushoverCurveView(units=UnitSystem.SI_M_N, ndf=6)
|
v = PushoverCurveView(units=UnitSystem.METRIC, ndf=6)
|
||||||
qtbot.addWidget(v)
|
qtbot.addWidget(v)
|
||||||
v.set_results(_pushover(control_dof=1))
|
v.set_results(_pushover(control_dof=1))
|
||||||
assert "m" in v._plot.getAxis("bottom").labelText
|
assert "m" in v._plot.getAxis("bottom").labelText
|
||||||
|
|
@ -53,7 +53,7 @@ def test_si_translation_labels(qtbot) -> None: # type: ignore[no-untyped-def]
|
||||||
|
|
||||||
@pytest.mark.gui
|
@pytest.mark.gui
|
||||||
def test_si_rotation_labels_show_curvature_and_moment(qtbot) -> None: # type: ignore[no-untyped-def]
|
def test_si_rotation_labels_show_curvature_and_moment(qtbot) -> None: # type: ignore[no-untyped-def]
|
||||||
v = PushoverCurveView(units=UnitSystem.SI_M_N, ndf=3)
|
v = PushoverCurveView(units=UnitSystem.METRIC, ndf=3)
|
||||||
qtbot.addWidget(v)
|
qtbot.addWidget(v)
|
||||||
v.set_results(_pushover(control_dof=3))
|
v.set_results(_pushover(control_dof=3))
|
||||||
assert "1/m" in v._plot.getAxis("bottom").labelText
|
assert "1/m" in v._plot.getAxis("bottom").labelText
|
||||||
|
|
@ -65,7 +65,7 @@ def test_si_rotation_labels_show_curvature_and_moment(qtbot) -> None: # type: i
|
||||||
@pytest.mark.gui
|
@pytest.mark.gui
|
||||||
def test_us_in_kip_rotation_labels(qtbot) -> None: # type: ignore[no-untyped-def]
|
def test_us_in_kip_rotation_labels(qtbot) -> None: # type: ignore[no-untyped-def]
|
||||||
"""kip-in Moment-Curvature project must NOT show cm / kN anywhere."""
|
"""kip-in Moment-Curvature project must NOT show cm / kN anywhere."""
|
||||||
v = PushoverCurveView(units=UnitSystem.US_IN_KIP, ndf=3)
|
v = PushoverCurveView(units=UnitSystem.IMPERIAL, ndf=3)
|
||||||
qtbot.addWidget(v)
|
qtbot.addWidget(v)
|
||||||
v.set_results(_pushover(control_dof=3))
|
v.set_results(_pushover(control_dof=3))
|
||||||
x_label = v._plot.getAxis("bottom").labelText
|
x_label = v._plot.getAxis("bottom").labelText
|
||||||
|
|
@ -78,7 +78,7 @@ def test_us_in_kip_rotation_labels(qtbot) -> None: # type: ignore[no-untyped-de
|
||||||
|
|
||||||
@pytest.mark.gui
|
@pytest.mark.gui
|
||||||
def test_us_in_kip_translation_labels(qtbot) -> None: # type: ignore[no-untyped-def]
|
def test_us_in_kip_translation_labels(qtbot) -> None: # type: ignore[no-untyped-def]
|
||||||
v = PushoverCurveView(units=UnitSystem.US_IN_KIP, ndf=3)
|
v = PushoverCurveView(units=UnitSystem.IMPERIAL, ndf=3)
|
||||||
qtbot.addWidget(v)
|
qtbot.addWidget(v)
|
||||||
v.set_results(_pushover(control_dof=1))
|
v.set_results(_pushover(control_dof=1))
|
||||||
assert "in" in v._plot.getAxis("bottom").labelText
|
assert "in" in v._plot.getAxis("bottom").labelText
|
||||||
|
|
@ -90,7 +90,7 @@ def test_no_auto_scaling_applied_to_values(qtbot) -> None: # type: ignore[no-un
|
||||||
"""Regression: earlier versions divided base_shear by 1000 to
|
"""Regression: earlier versions divided base_shear by 1000 to
|
||||||
display kN. The values must now be drawn exactly as stored so
|
display kN. The values must now be drawn exactly as stored so
|
||||||
kip-in users don't see nonsense scaling."""
|
kip-in users don't see nonsense scaling."""
|
||||||
v = PushoverCurveView(units=UnitSystem.US_IN_KIP, ndf=3)
|
v = PushoverCurveView(units=UnitSystem.IMPERIAL, ndf=3)
|
||||||
qtbot.addWidget(v)
|
qtbot.addWidget(v)
|
||||||
r = _pushover(control_dof=3)
|
r = _pushover(control_dof=3)
|
||||||
v.set_results(r)
|
v.set_results(r)
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ from otko.core import UnitSystem # noqa: E402
|
||||||
@pytest.mark.gui
|
@pytest.mark.gui
|
||||||
def test_status_bar_combo_lists_all_unit_systems(qtbot) -> None: # type: ignore[no-untyped-def]
|
def test_status_bar_combo_lists_all_unit_systems(qtbot) -> None: # type: ignore[no-untyped-def]
|
||||||
from otko.views.main_window import MainWindow
|
from otko.views.main_window import MainWindow
|
||||||
|
|
||||||
mw = MainWindow()
|
mw = MainWindow()
|
||||||
qtbot.addWidget(mw)
|
qtbot.addWidget(mw)
|
||||||
assert mw._units_combo.count() == len(list(UnitSystem))
|
assert mw._units_combo.count() == len(list(UnitSystem))
|
||||||
|
|
@ -20,25 +21,27 @@ def test_status_bar_combo_lists_all_unit_systems(qtbot) -> None: # type: ignore
|
||||||
@pytest.mark.gui
|
@pytest.mark.gui
|
||||||
def test_status_bar_combo_reflects_project_units(qtbot) -> None: # type: ignore[no-untyped-def]
|
def test_status_bar_combo_reflects_project_units(qtbot) -> None: # type: ignore[no-untyped-def]
|
||||||
from otko.views.main_window import MainWindow
|
from otko.views.main_window import MainWindow
|
||||||
|
|
||||||
mw = MainWindow()
|
mw = MainWindow()
|
||||||
qtbot.addWidget(mw)
|
qtbot.addWidget(mw)
|
||||||
mw._vm.new_project()
|
mw._vm.new_project()
|
||||||
mw._vm.project.meta.units = UnitSystem.US_IN_KIP
|
mw._vm.project.meta.units = UnitSystem.IMPERIAL
|
||||||
mw._sync_units_combo()
|
mw._sync_units_combo()
|
||||||
assert mw._units_combo.currentData() == UnitSystem.US_IN_KIP
|
assert mw._units_combo.currentData() == UnitSystem.IMPERIAL
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gui
|
@pytest.mark.gui
|
||||||
def test_status_bar_combo_write_updates_project(qtbot) -> None: # type: ignore[no-untyped-def]
|
def test_status_bar_combo_write_updates_project(qtbot) -> None: # type: ignore[no-untyped-def]
|
||||||
"""Changing the combo writes through to project.meta.units."""
|
"""Changing the combo writes through to project.meta.units."""
|
||||||
from otko.views.main_window import MainWindow
|
from otko.views.main_window import MainWindow
|
||||||
|
|
||||||
mw = MainWindow()
|
mw = MainWindow()
|
||||||
qtbot.addWidget(mw)
|
qtbot.addWidget(mw)
|
||||||
mw._vm.new_project()
|
mw._vm.new_project()
|
||||||
# Default is SI_M_N — switch to US_IN_KIP via the combo.
|
# Default is Metric — switch to Imperial via the combo.
|
||||||
target_idx = mw._units_combo.findData(UnitSystem.US_IN_KIP)
|
target_idx = mw._units_combo.findData(UnitSystem.IMPERIAL)
|
||||||
mw._units_combo.setCurrentIndex(target_idx)
|
mw._units_combo.setCurrentIndex(target_idx)
|
||||||
assert mw._vm.project.meta.units == UnitSystem.US_IN_KIP
|
assert mw._vm.project.meta.units == UnitSystem.IMPERIAL
|
||||||
assert mw._vm.is_dirty
|
assert mw._vm.is_dirty
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -46,6 +49,7 @@ def test_status_bar_combo_write_updates_project(qtbot) -> None: # type: ignore[
|
||||||
def test_status_bar_combo_no_project_noop(qtbot) -> None: # type: ignore[no-untyped-def]
|
def test_status_bar_combo_no_project_noop(qtbot) -> None: # type: ignore[no-untyped-def]
|
||||||
"""Before a project is loaded, changing the combo is a no-op."""
|
"""Before a project is loaded, changing the combo is a no-op."""
|
||||||
from otko.views.main_window import MainWindow
|
from otko.views.main_window import MainWindow
|
||||||
|
|
||||||
mw = MainWindow()
|
mw = MainWindow()
|
||||||
qtbot.addWidget(mw)
|
qtbot.addWidget(mw)
|
||||||
# No crash even with no project.
|
# No crash even with no project.
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue