feat: named case-result load combinations with full GUI support

Snapshots the current development tree, headlined by proper load
combinations (user request): a reusable LoadCombination entity of
weighted completed static-case results (e.g. 1.2xDead + 1.6xLive).

- core: LoadCombination/LoadCombinationItem entities, Project
  integration (lookup, unique ids, reference validation)
- services: combinations.py (linear superposition + envelope),
  exported via services __init__
- commands: undoable Add/Delete/Update for combinations
- GUI: Load Combinations manager dialog, Run-dialog evaluation,
  envelope display in Results panel, Combinations tab in Table dock
- tests: unit coverage (validation, math, error paths) + integration
  superposition check vs a single factored run
This commit is contained in:
smill 2026-09-11 13:19:59 -04:00
commit f361fee969
560 changed files with 178701 additions and 0 deletions

132
docs/QUICK_GUIDE.md Normal file
View file

@ -0,0 +1,132 @@
# OTKO Quick Guide
A practical, task-first guide to the OTKO desktop GUI. It assumes you
have already installed the desktop extras and can launch the app:
```bash
pip install -e ".[gui,dev]"
python -m otko
```
Project files use the `.osmodel` extension — a single, Pydantic-validated
JSON document that diffs cleanly in Git. Analysis output is written
separately to `*.osresults.h5`.
For the layer map and the OpenSeesPy command order, see
[`architecture.md`](architecture.md). For the feature-by-feature plan, see
[`roadmap.md`](roadmap.md).
## 1. A cantilever walkthrough
This follows the bundled `examples/cantilever.osmodel` model: a 5 m
horizontal beam, fixed at the left end, with a tip load. If you would
rather build it by hand, the steps are below.
1. **Start a project.** **File → New (3D Frame)**. Pick display units in
the bottom-right **Units** combo before typing any values.
2. **Lay out a grid.** **Define → Coordinate System/Grids…** (Ctrl+G).
Define X lines at 0…5 m (say, every 1 m), Y = 0, Z = 0, and set the
grid as the active coordinate system. The 3D canvas will draw it as
reference geometry.
3. **Add nodes.** **Define → Add Node…** (Ctrl+N), or use the **Draw
Node** tool and click on grid intersections at (0,0,0) … (5,0,0).
4. **Define material and section.** **Define → Material Library…**
(Ctrl+Shift+M) then **Define → Section Library…** (Ctrl+Shift+S). The
example uses a steel `ElasticSection` named `W12x40`.
5. **Draw the element.** **Assign/Define → Draw Frame** (F2), then click
from the first node to the last. Assign the section with
**Assign → Frame → Section…**.
6. **Add the support.** Select the node at x = 0 and use
**Assign → Joint → Restraints…** (Ctrl+R); restrain all six DOF. The
support icon confirms the fixed end.
7. **Add the load.** Select the tip node and use **Assign → Joint →
Point Loads…** (Ctrl+L). The example applies -10 kN in Y. Alternatively
build the distributed case with **Assign → Frame → Distributed
Load…**.
8. **Set up and run the case.** **Analyze → Cases…** (Ctrl+Shift+A) to
create or review a Static case, then **Analyze → Run…** (F5). The
bundled file already contains `Tip-Load`, `Uniform-Load`, and a modal
`Modal-3` case.
### Smoke check
Open `examples/cantilever.osmodel`, run the `Tip-Load` static case, then
**Display → Show Force Diagram… → M3**. The moment diagram is linear and
peaks at **50 kN·m at the fixed end**. V2 is a constant -10 kN along the
span. If you see that, the model, runner, and post-processor are wired up
correctly.
## 2. Running a modal analysis
1. Open a model that has mass assigned (the bundled cantilever lumps mass
at every free node so modal works out of the box).
2. **Analyze → Cases…**, add or select a **Modal** case, and set the
number of modes `n_modes` (the example uses 3).
3. **Analyze → Run…** (F5). Results appear in the results/report panel:
periods, frequencies, and participation factors per mode.
4. **Display → Animate Mode Shape** to view each mode. Use the mode
selector and the animation controls, and **Export…** if you want a
video of the mode shape.
5. Modal results also feed the response-spectrum case: define a response
spectrum, then run the SRSS or CQC combination and open
**Display → Show Response Spectrum**.
## 3. Reviewing results and exporting a report or script
After a run, the results/report panel shows a summary for the active case
(static reactions and forces, modal periods, and so on). Use the display
actions to inspect the model visually:
- **Display → Show Deformed Shape** — with a scale slider.
- **Display → Show Force Diagram…** — axial (P), shear (V2/V3), moment
(M2/M3) diagrams.
- **Display → Show Pushover Curve**, **Show Time-History**, **Show
Hysteresis** as applicable.
To hand the analysis to someone else, or to archive exactly what was run,
export a script:
- **File → Export OpenSeesPy (.py)…** — writes the full model, and
optionally a selected analysis case, as a runnable Python script.
- **File → Export Tcl (.tcl)…** — the same model as classic OpenSees Tcl.
The export dialog lets you choose "Model only (no analysis case)" or one
of the configured cases. The generated script follows the runner's fixed
command order (`wipe → model → node → fix → … → analyze`), so it
reproduces the analysis outside the GUI.
## 4. Changing display units
Use either control, they are the same setting:
- The **Units** combo in the bottom-right of the status bar, or
- **Options → Set Display Units…**
Changing units updates how lengths, forces, and moments are formatted in
the UI and plots. It does **not** rescale the underlying model numbers —
pick the right unit system before you type values, and convert
deliberately if you switch later. A set of unit labels is available in the
unit-label tests under `tests/unit/test_unit_labels.py`.
## 5. Undo and redo
Every model mutation goes through the undo stack, so most edits are
reversible:
- **Edit → Undo** (Ctrl+Z)
- **Edit → Redo** (Ctrl+Y / Ctrl+Shift+Z)
Menu text is dynamic — it names the operation, for example "Undo Add 4
Nodes". Compound operations such as drawing a frame (node + element) are
wrapped in a single macro, so one undo removes the whole step. File
loads, analysis runs, and display-only changes are not model mutations and
are not undoable.
## Where to go next
- [`architecture.md`](architecture.md) — MVVM layering and command order.
- [`roadmap.md`](roadmap.md) — what is done and what is planned.
- `examples/` — 20+ verified models, each generated from a checked-in
Python script.
- [`../CONTRIBUTING.md`](../CONTRIBUTING.md) — setup, rules, and verify
commands.

26
docs/README.md Normal file
View file

@ -0,0 +1,26 @@
# OTKO Documentation
Index of the project documentation. Start with the quick guide if you
just want to build and run a model; read the architecture page if you are
changing code.
| Document | What it covers |
| --- | --- |
| [QUICK_GUIDE.md](QUICK_GUIDE.md) | Task-first walkthrough: cantilever model, modal analysis, report and script export, display units, undo/redo. |
| [architecture.md](architecture.md) | MVVM layering, package responsibilities, threading, persistence, and the fixed OpenSeesPy command order. |
| [roadmap.md](roadmap.md) | Phase-by-phase feature status, from scaffolding through the earthquake-engineering primitives. |
| [adr/](adr/) | Architecture Decision Records — the "why" behind individual design choices. |
| [screenshots/](screenshots/) | Screenshots referenced by the docs and README. |
## Architecture Decision Records
- [ADR-0001 — GiD/OpenSees schema import](adr/ADR-0001-gidopensees-schema-import.md)
- [ADR-0002 — Headless / GUI dependency split](adr/ADR-0002-headless-gui-dep-split.md)
## Related documentation
- [`../README.md`](../README.md) — project overview, install, and quick start.
- [`../CONTRIBUTING.md`](../CONTRIBUTING.md) — dev setup, layering rules, commit style, and verify commands.
- [`../AGENTS.md`](../AGENTS.md) — condensed context for automated agents.
- [`gap-analysis-gidopensees.md`](gap-analysis-gidopensees.md) — gap analysis against the GiD/OpenSees reference.
- [`../examples/README.md`](../examples/README.md) — the bundled example models.

View file

@ -0,0 +1,331 @@
# ADR-0001 — Import gidopensees Schemas into OTKO
| Field | Value |
|---|---|
| **Status** | Proposed |
| **Date** | 2026-05-22 |
| **Author** | ogunc |
| **Deciders** | Core maintainers |
| **Source project** | [gidopensees](https://github.com/rclab-auth/gidopensees) — AUTh Lab of R/C and Masonry Structures |
---
## 1. Context
### Why gidopensees?
gidopensees is the most complete published schema inventory for the OpenSees
material/element/condition set. It covers 60+ material types, 30+ element
types, and 31 boundary-condition and load types — all expressed as GiD
preprocessor `.mat` / `.cnd` BOOK definitions, with DEPENDENCIES (field
visibility rules), `#UNITS#` annotations, and TKWIDGET hooks for
auto-fill presets and Wiki links.
OTKO currently supports ~34 of these objects (see
`docs/gap-analysis-gidopensees.md`). Importing gidopensees schemas would
close 23 P1 gaps (Phase 8 targets) and 21 P2 gaps without requiring us to
reverse-engineer OpenSeesPy docs for each type.
### What we are NOT doing
This ADR covers **schema definitions only** — Pydantic model fields, field
metadata, and default values. It does not cover:
- The `bas/` Python code-generation templates (separate ADR).
- The `tcl/` TKWIDGET Tcl implementations (separate ADR).
- Any runtime OpenSeesPy command emission — that lives in
`services/opensees_runner.py` and follows naturally once a schema
is accepted.
---
## 2. Decision
### 2.1 New package: `core/catalog/`
A new package `src/otko/core/catalog/` will hold
gidopensees-derived schema definitions alongside curated additions.
```
src/otko/core/catalog/
├── __init__.py # re-exports curated + generated public symbols
├── generated/ # output of the codegen tool — DO NOT edit by hand
│ ├── _header.py # shared attribution header (inserted by codegen)
│ ├── steel.py
│ ├── concrete.py
│ ├── other_uniaxial.py
│ ├── nd_materials.py
│ ├── sections.py
│ └── elements.py
└── curated/ # human-reviewed, hand-edited overrides and additions
├── README.md # explains the curated/ contract
└── …
```
**Existing modules are untouched.** The objects in `core/materials/`,
`core/sections/`, `core/geometry/elements.py` remain authoritative for
every type already supported. Catalog objects enter the UI only after
equivalence is established (see §2.7).
### 2.2 Codegen tool: `tools/gidopensees_import/`
A build-time parser + code-generator lives in
`tools/gidopensees_import/`, outside `src/`:
```
tools/gidopensees_import/
├── README.md
├── parse_mat.py # parses OpenSees.mat into an intermediate IR
├── parse_cnd.py # parses OpenSees.cnd into an intermediate IR
├── codegen.py # renders IR → Pydantic v2 model source files
├── ir.py # intermediate representation dataclasses
└── tests/ # unit tests for the parser and codegen
```
The tool runs once per gidopensees update. Its output (`generated/`) is
committed so CI never requires gidopensees to be present. The tool is
invoked manually by a maintainer:
```bash
python tools/gidopensees_import/codegen.py \
--mat path/to/OpenSees.mat \
--cnd path/to/OpenSees.cnd \
--out src/otko/core/catalog/generated/
```
**Rationale:** keeping the parser outside `src/` prevents it from being
imported at runtime, avoids adding GiD file parsing as a dependency of
the installable package, and makes the "run once, commit output" contract
explicit to contributors.
### 2.3 Attribution
Every file under `core/catalog/generated/` carries this header comment
(inserted by `codegen.py`):
```python
# This file is derived from gidopensees.
# Source: https://github.com/rclab-auth/gidopensees
# Authors: AUTh Lab of R/C and Masonry Structures
# (https://rclab.civil.auth.gr/)
# Modifications: generated by tools/gidopensees_import/codegen.py
```
Files under `curated/` carry a similar header when they derive from
gidopensees content.
### 2.4 `.osmodel` format backward-compatibility
The `.osmodel` JSON format uses Pydantic discriminated unions keyed on a
`"type"` field. New material types from `core/catalog/` appear as new
discriminator values. The union in `core/materials/__init__.py` is
extended only when a catalog type is promoted to stable (see §2.7).
Old project files that do not contain the new discriminator values load
cleanly: Pydantic ignores unknown items in lists when
`model_config = ConfigDict(extra="ignore")`, and the `Project` validator
will log (not raise) on unknown type strings if we add a graceful
fallback.
**Migration path:**
1. Catalog type is added with a temporary discriminator value
(e.g. `"catalog.Concrete04"`).
2. After verification (§2.7) it is promoted to a stable value
(e.g. `"Concrete04"`) and the temporary value is kept as an alias for
one minor version.
3. A `migrate_osmodel.py` script in `tools/` handles the rename if needed.
No existing `.osmodel` file ever breaks on open.
### 2.5 Field metadata for TKWIDGET hooks
gidopensees BOOK definitions contain TKWIDGET directives that fire
auto-fill presets (e.g. `SteelUniaxMaterial::GenerateValues`), Wiki links
(`TK_MaterialWikiInfo`), and the Material Tester dialog
(`TK_MaterialTester`). These are deferred from this ADR.
Each field or model where a TKWIDGET hook is relevant carries a
`json_schema_extra` annotation recording the hook name:
```python
class Concrete04(CatalogEntity):
...
class model_config(ConfigDict):
json_schema_extra = {
"tkwidget_hooks": [
"ConcreteUniaxMaterial::GenerateValues",
"TK_MaterialWikiInfo",
"TK_MaterialTester",
]
}
```
This metadata is visible to future UI layers without coupling `core/` to
Qt. Implementation of the actual preset dialogs and Wiki-link buttons
comes in a later UI phase.
### 2.6 DEPENDENCIES → `dependent_schemas` metadata (not validators)
gidopensees BOOK DEPENDENCIES express field-visibility rules:
```
(1, RESTORE, Gap_length, #CURRENT#), (0, HIDE, Gap_length, #CURRENT#)
```
These are **viewmodel / UI concerns**, not data-validity rules, and
therefore must not become Pydantic validators in `core/`.
Each Pydantic model that has visibility dependencies stores them as
`json_schema_extra["dependencies"]` — a list of dicts describing the
trigger field, trigger value, and affected fields. The viewmodel layer
reads these at dialog-construction time to wire up the show/hide logic.
**Example:**
```python
class ViscousDamper(CatalogEntity):
activate_gap: Literal[0, 1] = 0
gap_length: float | None = None
model_config = ConfigDict(
json_schema_extra={
"dependencies": [
{"trigger": "activate_gap", "value": 1, "restore": ["gap_length"]},
{"trigger": "activate_gap", "value": 0, "hide": ["gap_length"]},
]
}
)
```
### 2.7 Unit-annotated fields (`#UNITS#`)
gidopensees marks numeric fields with `#UNITS#` where the value's
interpretation is unit-system–dependent (forces in kN, lengths in m, etc.).
OTKO already has `core/units.py` with `UnitSystem` and
`UnitLabels`. It does **not** currently attach unit metadata to individual
model fields — the unit system is a project-level property and all numeric
values are stored in the project's native unit system, with `UnitLabels`
used only for display.
**Intended design:** Generated catalog models will mark unit-annotated fields
using a `Field` `metadata` entry (Pydantic v2 `Annotated` style):
```python
from typing import Annotated
from pydantic import Field
class UnitTag:
"""Marker for fields whose display label depends on UnitSystem."""
def __init__(self, quantity: str):
self.quantity = quantity # e.g. "force", "length", "stress"
ForceMagnitude = Annotated[float, UnitTag("force")]
LengthValue = Annotated[float, UnitTag("length")]
StressValue = Annotated[float, UnitTag("stress")]
```
These type aliases would live in `core/catalog/_units.py`. No conversion logic
is added to `core/`; the viewmodel layer reads `UnitTag.quantity` to
select the right `UnitLabels` field for axis labels and input hints.
**Implementation status (2026-05-22) — DEFERRED:** The codegen tool does not
yet emit `UnitTag` annotations. Fields corresponding to `#UNITS#` entries in
the gidopensees source are currently emitted as `str` with a
`# TODO: unit-aware type` comment preserving the gidopensees default string
(e.g. `yield_stress_fy: str = '500 MPa' # TODO: unit-aware type`). This is
a conscious deferral: the `str` placeholder keeps the field present and
round-trippable without binding the codebase to a unit-system convention that
is not yet finalised. A dedicated unit-system layer — covering `UnitTag`,
`_units.py`, and viewmodel wiring — is tracked as future work and will be
addressed in a follow-up ADR before any `#UNITS#` field is promoted to stable.
**If this convention is inadequate** (e.g. if we need per-field unit
conversion in the future), a follow-up ADR should address it before
the convention is applied beyond `catalog/`.
### 2.8 Verification plan
A catalog type is promoted from generated → stable only when **all three**
of the following are satisfied:
1. **Schema equivalence test** (`tests/unit/catalog/test_<name>_schema.py`):
Constructs a model instance with the same arguments as the
corresponding Tcl example from the OpenSees Wiki and asserts that
`model.model_dump()` produces the expected dict. No OpenSeesPy import.
2. **Round-trip test** (`tests/unit/catalog/test_<name>_roundtrip.py`):
Serialises the model to JSON (`.osmodel` fragment), deserialises it
back, and asserts equality. Confirms the discriminator and all field
aliases survive the round-trip.
3. **Integration smoke test** (`tests/integration/catalog/test_<name>.py`):
Builds a minimal project using the new type, runs it through
`OpenSeesRunner`, and checks that the runner does not raise and that at
least one result quantity (reaction, displacement, or force) is finite.
Tagged `@pytest.mark.slow` and skipped if `openseespy` is not installed.
Manual review checklist (for the PR that promotes a type):
- [ ] Attribution header present in the generated file.
- [ ] `json_schema_extra["tkwidget_hooks"]` populated where applicable.
- [ ] `json_schema_extra["dependencies"]` populated for every DEPENDENCY
in the source BOOK.
- [ ] `#UNITS#` fields use the correct `UnitTag` quantity string.
- [ ] The type discriminator value does not collide with any existing type
in `core/materials/__init__.py`, `core/sections/__init__.py`, or
`core/geometry/elements.py`.
- [ ] The integration smoke test result has been spot-checked against the
gidopensees wiki reference or an independent OpenSees Tcl run.
---
## 3. Alternatives considered
### 3A: Extend existing `core/materials/__init__.py` directly
Rejected. The existing module is small and well-tested; adding 60+
unverified types creates noise and makes equivalence tracking harder.
A separate `catalog/` namespace keeps the boundary clear.
### 3B: Use gidopensees at runtime (import `.mat` on startup)
Rejected. The GiD BOOK format is proprietary and requires the GiD parser.
Depending on gidopensees at runtime adds a third-party dependency to the
installed package and makes offline / air-gapped installs harder. The
codegen + committed-output approach keeps the package dependency-clean.
### 3C: Hand-write every new type without the codegen tool
Would work but loses the systematic relationship between gidopensees
DEPENDENCIES / TKWIDGET metadata and the Pydantic model. The codegen
pipeline preserves that metadata structurally so UI implementors can
reference it rather than re-reading `.mat` files.
---
## 4. Risks
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Namespace collision between `catalog/` and existing `core/` types | Medium | Medium | Verify discriminator values before promotion; CI check added to the merge checklist |
| gidopensees schema drift (upstream changes `.mat`) | Low | Medium | `generated/` is committed; only re-run codegen intentionally; diff the output and add a CHANGELOG entry |
| Testing surface explosion (60+ new types × 3 test tiers) | High | Low | Only promoted types get full test coverage; generated-but-not-yet-promoted types have schema + round-trip tests only |
| Attribution omission | Low | High | Codegen always inserts the header; a pre-commit hook (`grep -r "rclab-auth/gidopensees" core/catalog/generated/` must pass) enforces it |
| `#UNITS#` convention inadequacy | Medium | Medium | Convention is isolated to `core/catalog/_units.py`; a follow-up ADR can replace it without touching existing `core/` code |
---
## 5. Out of scope for this ADR
- `bas/` Python template parsing and code generation (separate ADR).
- `tcl/` TKWIDGET Tcl implementations (separate ADR).
- Seismic isolator element schemas (`elastomericBearing*`,
`frictionPendulumBearing`, etc.) — those are new elements, not directly
in the gidopensees BOOK format; they get their own ADR.
- IDA batch runner (analysis feature, not schema).
- Fiber-section visual editor UI polish (UI feature, not schema).
- The OpenSeesPy command emission side — `services/opensees_runner.py`
will need updates for each promoted type, but those changes follow
naturally from the schema and are reviewed in the same PR as the
integration smoke test.

View file

@ -0,0 +1,91 @@
# ADR-0002 — Split pyproject dependencies into headless base and `gui` optional extra
| Field | Value |
|---|---|
| **Status** | Accepted |
| **Date** | 2026-06-15 |
| **Author** | ogunc |
---
## 1. Context
`otko.core` is pure Pydantic v2 (no Qt, no OpenSeesPy imports — stated
explicitly in `core/__init__.py`). `otko.services` adds NumPy, h5py, and
OpenSeesPy for headless computation. Together, these two packages can be used from
scripts, Jupyter notebooks, and web backends **without any Qt or 3D-rendering stack**.
Before this ADR, every `pip install otko` pulled in PySide6, pyvista,
pyvistaqt, vtk, pyqtgraph, and imageio — roughly 800 MB of GUI/visualization
packages — even when only the headless computation layer was needed. Web backends and
CI machines without a display had to work around this with `--no-deps`, which is
fragile and skips genuine compute-layer deps (numpy, h5py) too.
## 2. Decision
Split `[project.dependencies]` into two tiers in `pyproject.toml`:
### Headless base (`pip install -e .`)
Packages imported by `core/` and the non-Qt parts of `services/`:
| Package | Where used |
|---------|-----------|
| `pydantic>=2.5` | All `core/` modules, `material_tester.py` |
| `numpy>=1.26` | `services/` computation modules (7 files) |
| `h5py>=3.10` | `opensees_runner._run_transient()`, `TransientResults` accessors |
| `openseespy==3.8.0.0` | Lazy import in `OpenSeesRunner.__init__` |
| `openseespywin==3.8.0.0 ; sys_platform=='win32'` | Windows DLL companion |
### GUI extra (`pip install -e ".[gui]"`)
Packages only needed by `views/`, `viewmodels/`, `commands/`, `qt_workers.py`,
and `animation_export.py` (which drives a live PyVista plotter):
`PySide6`, `pyvista`, `pyvistaqt`, `vtk`, `pyqtgraph`, `imageio[ffmpeg]`,
`scipy` (forward-compat, currently a phantom dep), `pandas` (same).
## 3. Consequences
- **Desktop developers** install with `pip install -e ".[gui,dev]"`. No change to
what gets installed; only the install command changes from `.[dev]` → `.[gui,dev]`.
- **Web backends / scripts / notebooks** install with `pip install -e .` (or
`pip install otko`) and get a lean ~50 MB environment.
- **otko-web** can drop the `--no-deps` workaround and install the
package normally. The web backend's `requirements.txt` no longer needs to list
pydantic/numpy/h5py separately — they come from the base install.
- **scipy and pandas** are listed under `[gui]` as phantom deps (currently never
imported anywhere in the codebase). They are kept to avoid surprise breakage if a
future feature adds them; audited and flagged on 2026-06-15.
## 4. Verification
After installing only the base set:
```python
import sys
from otko.core import Project
from otko.services.opensees_runner import OpenSeesRunner
from otko.services.material_tester import test_uniaxial_material
# Run a modal analysis on the bundled two-storey shear frame example
import json
from pathlib import Path
from otko.core import ModalCase
data = json.loads(Path("examples/eigen_two_storey_shear_frame.osmodel").read_text())
project = Project.model_validate(data)
modal_case = next(c for c in project.analyses if isinstance(c, ModalCase))
runner = OpenSeesRunner(project)
runner.build()
results = runner._run_modal(modal_case)
assert len(results.eigenvalues) == 2
assert all(ev > 0 for ev in results.eigenvalues)
gui_packages = {"PySide6", "pyvista", "pyvistaqt", "vtk", "pyqtgraph"}
assert not gui_packages.intersection(sys.modules), \
f"GUI package imported: {gui_packages & sys.modules.keys()}"
print("PASS — headless modal analysis complete, no GUI packages imported")
```

80
docs/architecture.md Normal file
View file

@ -0,0 +1,80 @@
# Architecture
## Layering
OTKO uses a strict **MVVM + service layer** architecture. Dependencies
flow in **one direction only**: outer layers may depend on inner layers, never
the reverse.
```
┌─────────────────────────────────────────────────────────────────┐
│ views/ Qt widgets, dialogs, 3D canvas — PySide6 only │
│ ▲ │
│ │ signals/slots, viewmodel binding │
│ viewmodels/ Qt-aware adapters, QUndoStack, selection state │
│ ▲ │
│ │ pure Python calls │
│ services/ OpenSeesRunner, PersistenceService, Results │
│ ▲ │
│ │ │
│ core/ Project, Node, Element, Material — pure Python │
│ NO Qt imports. NO openseespy imports. │
└─────────────────────────────────────────────────────────────────┘
```
### Why this matters
- `core` tests without a display server, without OpenSees, without Qt.
CI runs `pytest tests/unit/` in milliseconds.
- Swapping solvers (e.g. `xara`, a future fork) touches
`services/opensees_runner.py` and nothing else.
- A future CLI or notebook front-end reuses `core` and `services` as-is.
## Package map
| Package | Responsibility | Allowed imports |
|---|---|---|
| `core` | Domain entities and invariants | stdlib, numpy, pydantic |
| `services` | I/O, solver invocation, persistence | core + stdlib + h5py + openseespy |
| `viewmodels` | Bridge core ↔ Qt; expose Qt signals; manage undo/redo | core, services, PySide6 |
| `views` | Pure UI; no business logic | PySide6, pyvistaqt, viewmodels |
| `commands` | `QUndoCommand` subclasses; mutate model via services | services, viewmodels |
## Threading
The Qt main thread owns all widgets. Heavy computation happens elsewhere:
- **OpenSees analysis** runs in a `QThread` worker (`services.opensees_runner.AnalysisWorker`).
- The worker emits `progress(int)`, `log(str)`, `finished(ResultsHandle)` signals.
- The worker checks `QThread.currentThread().isInterruptionRequested()` between
analysis steps so the user can cancel.
- Results are written to HDF5; only a lightweight `ResultsHandle` (file path +
metadata) crosses the thread boundary.
## Persistence
- Project files: `*.osmodel` — a JSON document validated by Pydantic models.
Human-readable, diff-able, version-controllable.
- Results files: `*.osresults.h5` — HDF5; one group per analysis case; datasets
for displacements, reactions, element forces, stresses.
## OpenSeesPy command sequencing
`OpenSeesRunner` always emits commands in this order; the model layer enforces
that all required pieces exist before a run can be requested:
1. `wipe()` and `model('basic', '-ndm', ndm, '-ndf', ndf)`
2. `node(...)` for every node
3. `fix(...)` for every restrained DOF
4. `uniaxialMaterial(...)` / `nDMaterial(...)`
5. `section(...)` (if used)
6. `geomTransf(...)` for frame elements
7. `element(...)` for every element
8. `timeSeries(...)`
9. `pattern(...)` with nested `load(...)`
10. `recorder(...)`
11. `system / numberer / constraints / integrator / algorithm / analysis`
12. `analyze(...)`
Any deviation from this order is a runtime error in OpenSees. The runner
asserts the order at the service boundary; the UI never has to think about it.

View file

@ -0,0 +1,221 @@
# Gap Analysis — OTKO vs gidopensees
**Source:** `D:\GitHub\gidopensees` (AUTh Lab of R/C and Masonry Structures)
**Scope:** Material / section / element / constraint / load / damping schema coverage.
**Date:** 2026-05-22
## How to read this table
| Column | Meaning |
|---|---|
| **Category** | Schema group (material family, element type, etc.) |
| **Object** | Name as it appears in gidopensees BOOK/CONDITION |
| **OTKO name** | Corresponding class in `core/` (if any) |
| **In OTKO?** | ✅ fully supported · 🟡 partial · ❌ missing |
| **In gidopensees?** | ✅ · ❌ |
| **Priority** | P0 = already done · P1 = Phase 8 target · P2 = later |
Priority rationale:
- **P0** — already shipped; included for completeness.
- **P1** — high-value for earthquake-engineering practice; aligns with Phase 8
roadmap items (isolators, Rayleigh per-region, confined concrete models,
shell elements, floor diaphragm constraints).
- **P2** — valid but lower-frequency in typical EQ-engineering workflows
(soil p-y/t-z/q-z springs, 3-D solid elements, multi-yield plasticity,
contact elements).
---
## 1. Uniaxial Materials
| Category | Object (gidopensees) | OTKO name | In OTKO? | In gidopensees? | Priority |
|---|---|---|---|---|---|
| Uniaxial / linear | Elastic | `ElasticUniaxial` | ✅ | ✅ | P0 |
| Uniaxial / elastic-plastic | Elastic_Perfectly_Plastic | `ElasticPP` | ✅ | ✅ | P0 |
| Uniaxial / elastic-plastic | Elastic_Perfectly_Plastic_with_Gap | — | ❌ | ✅ | P1 |
| Uniaxial / damper | Viscous | — | ❌ | ✅ | P1 |
| Uniaxial / damper | Viscous_Damper (Maxwell) | — | ❌ | ✅ | P1 |
| Uniaxial / gap | Hyperbolic_Gap | — | ❌ | ✅ | P2 |
| Uniaxial / soil | PySimple1 | — | ❌ | ✅ | P2 |
| Uniaxial / soil | TzSimple1 | — | ❌ | ✅ | P2 |
| Uniaxial / soil | QzSimple1 | — | ❌ | ✅ | P2 |
| Uniaxial / bond-slip | BondSP01 | — | ❌ | ✅ | P2 |
## 2. Steel Uniaxial Materials
| Category | Object (gidopensees) | OTKO name | In OTKO? | In gidopensees? | Priority |
|---|---|---|---|---|---|
| Steel | Steel01 | `Steel01` | ✅ | ✅ | P0 |
| Steel | Steel02 | `Steel02` | ✅ | ✅ | P0 |
| Steel | Hysteretic | `HystereticMaterial` | ✅ | ✅ | P0 |
| Steel | Reinforcing_steel (DoDD-Restrepo) | — | ❌ | ✅ | P1 |
| Steel | Ramberg-Osgood_steel | — | ❌ | ✅ | P2 |
## 3. Concrete Uniaxial Materials
| Category | Object (gidopensees) | OTKO name | In OTKO? | In gidopensees? | Priority |
|---|---|---|---|---|---|
| Concrete | Concrete01_(Zero_tensile_strength) | `Concrete01` | ✅ | ✅ | P0 |
| Concrete | Concrete02_(Linear_tension_softening) | `Concrete02` | ✅ | ✅ | P0 |
| Concrete | Concrete04_(Popovics) | — | ❌ | ✅ | P1 |
| Concrete | Concrete06 | — | ❌ | ✅ | P2 |
| Concrete | ConcreteCM (Chang-Mander) | — | ❌ | ✅ | P1 |
## 4. Combined Materials
| Category | Object (gidopensees) | OTKO name | In OTKO? | In gidopensees? | Priority |
|---|---|---|---|---|---|
| Combination | Series | — | ❌ | ✅ | P1 |
| Combination | Parallel | — | ❌ | ✅ | P1 |
| Combination | Section_Aggregator (in .mat) | `SectionAggregator` | ✅ | ✅ | P0 |
## 5. nD (Multi-dimensional) Materials
| Category | Object (gidopensees) | OTKO name | In OTKO? | In gidopensees? | Priority |
|---|---|---|---|---|---|
| nD | Elastic_Isotropic | `ElasticIsotropic` | ✅ | ✅ | P0 |
| nD | Elastic_Orthotropic | — | ❌ | ✅ | P2 |
| nD | J2Plasticity | — | ❌ | ✅ | P2 |
| nD | Damage2p | — | ❌ | ✅ | P2 |
| nD | PressureIndependMultiYield | — | ❌ | ✅ | P2 |
| nD | PressureDependMultiYield | — | ❌ | ✅ | P2 |
| nD | PressureDependMultiYield02 | — | ❌ | ✅ | P2 |
| nD | Contact | — | ❌ | ✅ | P2 |
## 6. Sections
| Category | Object (gidopensees) | OTKO name | In OTKO? | In gidopensees? | Priority |
|---|---|---|---|---|---|
| Section | Elastic_Section | `ElasticSection` | ✅ | ✅ | P0 |
| Section | Fiber | `FiberSection` | ✅ | ✅ | P0 |
| Section | Fiber_Custom | `FiberSection` (manual fibres) | 🟡 | ✅ | P0 |
| Section | FiberInt (interaction P-M) | — | ❌ | ✅ | P1 |
| Section | Plate_Fiber | — | ❌ | ✅ | P2 |
| Section | Elastic_Membrane_Plate | — | ❌ | ✅ | P2 |
| Section | LayeredShell | — | ❌ | ✅ | P2 |
| Section | Section_Aggregator | `SectionAggregator` | ✅ | ✅ | P0 |
## 7. Beam-Column Elements
| Category | Object (gidopensees) | OTKO name | In OTKO? | In gidopensees? | Priority |
|---|---|---|---|---|---|
| Frame | Elastic_Beam-Column | `ElasticBeamColumn` | ✅ | ✅ | P0 |
| Frame | Elastic_Timoshenko_Beam-Column | — | ❌ | ✅ | P1 |
| Frame | Force-Based_Beam-Column | `ForceBeamColumn` | ✅ | ✅ | P0 |
| Frame | Displacement-Based_Beam-Column | `DispBeamColumn` | ✅ | ✅ | P0 |
| Frame | Flexure-Shear_Interaction_DispBeamColumn | — | ❌ | ✅ | P2 |
| Frame | BeamWithHinges | `BeamWithHingesElement` | ✅ | ❌ | P0 |
## 8. Truss Elements
| Category | Object (gidopensees) | OTKO name | In OTKO? | In gidopensees? | Priority |
|---|---|---|---|---|---|
| Truss | Truss | `TrussElement` | ✅ | ✅ | P0 |
| Truss | Corotational_Truss | `CorotTrussElement` | ✅ | ✅ | P0 |
## 9. Surface / Plate Elements
| Category | Object (gidopensees) | OTKO name | In OTKO? | In gidopensees? | Priority |
|---|---|---|---|---|---|
| Surface | Quad | `QuadElement` | ✅ | ✅ | P0 |
| Surface | Shell (ShellMITC4 / MITC4) | — | ❌ | ✅ | P1 |
| Surface | ShellDKGQ | — | ❌ | ✅ | P1 |
| Surface | Tri31 | — | ❌ | ✅ | P2 |
| Surface | QuadUP (u-p pore pressure) | — | ❌ | ✅ | P2 |
## 10. Solid Elements
| Category | Object (gidopensees) | OTKO name | In OTKO? | In gidopensees? | Priority |
|---|---|---|---|---|---|
| Solid | Standard_Brick_Element | — | ❌ | ✅ | P2 |
## 11. Zero-Length / Special Elements
| Category | Object (gidopensees) | OTKO name | In OTKO? | In gidopensees? | Priority |
|---|---|---|---|---|---|
| Special | Auto_Zero_Length (per-DOF uniaxial) | `ZeroLengthElement` | ✅ | ✅ | P0 |
| Special | Auto_equal_constraint (auto equalDOF) | `EqualDOFConstraint` | ✅ | ✅ | P0 |
| Special | ZeroLengthSection | `ZeroLengthSectionElement` | ✅ | ❌ | P0 |
| Special | BeamContact (master/slave) | — | ❌ | ✅ | P2 |
## 12. Restraints (Boundary Conditions)
| Category | Object (gidopensees) | OTKO name | In OTKO? | In gidopensees? | Priority |
|---|---|---|---|---|---|
| Restraint | Point_Restraints | Node.restraint (6-tuple) | ✅ | ✅ | P0 |
| Restraint | Line_Restraints (auto-apply to nodes on line) | — | ❌ | ✅ | P2 |
| Restraint | Surface_Restraints (auto-apply to nodes on surface) | — | ❌ | ✅ | P2 |
## 13. Nodal Loads & Displacements
| Category | Object (gidopensees) | OTKO name | In OTKO? | In gidopensees? | Priority |
|---|---|---|---|---|---|
| Load | Point_Forces | `NodalLoad` | ✅ | ✅ | P0 |
| Load | Line_Forces (nodal, along a line) | — | ❌ | ✅ | P2 |
| Load | Surface_Forces (nodal, on a surface) | — | ❌ | ✅ | P2 |
| Load | Line_Uniform_Forces | `UniformElementLoad` | ✅ | ✅ | P0 |
| Load | Point_Displacements (imposed) | — | ❌ | ✅ | P1 |
| Load | Line_Displacements (imposed, on nodes along line) | — | ❌ | ✅ | P2 |
| Load | Surface_Displacements (imposed, on nodes on surface) | — | ❌ | ✅ | P2 |
## 14. Ground Motions
| Category | Object (gidopensees) | OTKO name | In OTKO? | In gidopensees? | Priority |
|---|---|---|---|---|---|
| Ground motion | Point_Ground_Motion_from_Record | `PathTimeSeries` + `UniformExcitationPattern` | ✅ | ✅ | P0 |
| Ground motion | Point_Sine_Ground_Motion | — (no `TrigTimeSeries`) | ❌ | ✅ | P1 |
| Ground motion | Records (BOOK 8 — ground motion file library) | `PathTimeSeries.file_path` (single file, no library) | 🟡 | ✅ | P1 |
## 15. Constraints
| Category | Object (gidopensees) | OTKO name | In OTKO? | In gidopensees? | Priority |
|---|---|---|---|---|---|
| Constraint | Point_Equal_constraint (master + slave) | `EqualDOFConstraint` | ✅ | ✅ | P0 |
| Constraint | Line_Equal_constraint (slave nodes on line) | — | ❌ | ✅ | P1 |
| Constraint | Point_Rigid_link (Bar / Beam) | — | ❌ | ✅ | P1 |
| Constraint | Line_Rigid_link (slave nodes on line) | — | ❌ | ✅ | P1 |
| Constraint | Point_Rigid_diaphragm (master + slave, XY/YZ/ZX plane) | — | ❌ | ✅ | P1 |
| Constraint | Line_Rigid_diaphragm (slave nodes on line) | — | ❌ | ✅ | P1 |
## 16. Mass
| Category | Object (gidopensees) | OTKO name | In OTKO? | In gidopensees? | Priority |
|---|---|---|---|---|---|
| Mass | Point_Mass | node mass (Properties dock + SetMassCommand) | ✅ | ✅ | P0 |
| Mass | Line_Mass (auto-lump to nodes) | — | ❌ | ✅ | P1 |
| Mass | Surface_Mass | — | ❌ | ✅ | P2 |
| Mass | Volume_Mass | — | ❌ | ✅ | P2 |
## 17. Rayleigh Damping
| Category | Object (gidopensees) | OTKO name | In OTKO? | In gidopensees? | Priority |
|---|---|---|---|---|---|
| Damping | Global αM + βK (TransientCase fields) | `TransientCase.rayleigh_alpha_m/beta_k` | ✅ | 🟡 | P0 |
| Damping | Mode-1 stiffness-proportional βK auto-compute | `TransientCase.rayleigh_mode1_damping` | ✅ | ❌ | P0 |
| Damping | Per-region Rayleigh (Line/Surface/Volume/Point) | — | ❌ | ✅ | P1 |
---
## Summary
| Status | Count |
|---|---|
| ✅ Fully in OTKO | 34 |
| 🟡 Partial | 3 |
| ❌ P1 targets (Phase 8 additions) | 23 |
| ❌ P2 deferred | 21 |
**Top P1 targets** (highest EQ-engineering impact, not in OTKO yet):
1. `ElasticPP_with_Gap` — bearing pad / isolation gap nonlinearity
2. `Viscous` / `Viscous_Damper` — supplemental damping devices
3. `ReinforcingSteel` — DoDD-Restrepo model for well-detailed rebar
4. `Concrete04` (Popovics) / `ConcreteCM` (Chang-Mander) — better confined concrete
5. `Series` / `Parallel` — material combination building blocks for isolation systems
6. `RigidDiaphragm` — floor slab constraint, essential for 3D building models
7. `RigidLink` — column/beam offset rigid connections
8. `Shell` (MITC4 / ShellDKGQ) — wall / slab elements
9. `FiberInt` — P-M interaction section for axial-flexure coupling
10. `PointDisplacement` imposed load — displacement-based loading at nodes
11. Per-region Rayleigh damping — finer damping control for mixed models
12. Sine ground motion / ground motion record library — GM workflow completion

46
docs/logo.svg Normal file
View file

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 180" width="720" height="180" role="img" aria-label="OTKO">
<title>OTKO</title>
<desc>Wordmark logo for OTKO — a SAP2000-style desktop GUI for OpenSeesPy.</desc>
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#1F2A3D"/>
<stop offset="100%" stop-color="#0F1620"/>
</linearGradient>
<linearGradient id="frameStroke" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#9DC8FF"/>
<stop offset="100%" stop-color="#4F92E8"/>
</linearGradient>
<linearGradient id="deformed" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#4F92E8" stop-opacity="0.0"/>
<stop offset="50%" stop-color="#4F92E8" stop-opacity="0.6"/>
<stop offset="100%" stop-color="#4F92E8" stop-opacity="0.0"/>
</linearGradient>
</defs>
<!-- Background panel — keeps contrast consistent on light and dark GitHub themes -->
<rect x="0" y="0" width="720" height="180" rx="14" fill="url(#bg)"/>
<!-- Mark: portal frame with a deformed-shape ghost -->
<g transform="translate(40, 36)" stroke-linecap="round" stroke-linejoin="round" fill="none">
<path d="M 6 100 C 22 100, 22 12, 60 12 L 60 12 C 98 12, 98 100, 114 100"
stroke="url(#deformed)" stroke-width="5" stroke-dasharray="3 5"/>
<path d="M 6 100 L 6 8 L 114 8 L 114 100"
stroke="url(#frameStroke)" stroke-width="7"/>
<rect x="-3" y="100" width="18" height="10" fill="#F2A93B" rx="1"/>
<rect x="105" y="100" width="18" height="10" fill="#F2A93B" rx="1"/>
<circle cx="6" cy="8" r="5" fill="#E8EDF5"/>
<circle cx="114" cy="8" r="5" fill="#E8EDF5"/>
</g>
<!-- Wordmark -->
<g font-family="Segoe UI, Inter, Helvetica, Arial, sans-serif">
<text x="200" y="92" font-size="56" font-weight="700" letter-spacing="-1">
<tspan fill="#E8EDF5">OTKO</tspan>
</text>
<text x="202" y="124" font-size="16" font-weight="500" letter-spacing="3" fill="#8FA2BF">
A SAP2000-STYLE GUI FOR OPENSEESPY
</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

197
docs/roadmap.md Normal file
View file

@ -0,0 +1,197 @@
# Roadmap
Eight phases. 0–7 are the core GUI plus post-processing. Phase 8 is the
earthquake-engineering primitives — the part that makes it a research
tool instead of a model viewer.
Status legend: ✅ done · 🟡 partial · ⬜ planned · ✂️ deferred / out-of-scope.
## Phase 0 — Scaffolding ✅
- ✅ Repo, `.gitignore`, `pyproject.toml`
- ✅ Pre-commit + ruff + mypy
- ✅ GitHub Actions CI (Linux/Mac/Win × Py 3.10–3.12)
- ✅ `python -m otko` opens a `MainWindow` with PyVista 3D
viewport, model-tree dock, property dock, console dock, working-plane
toolbar, and a full menu bar (File / Edit / Define / Assign /
Analyze / Display / View / Options / Help)
- 🟡 Application icon and About dialog — wordmark logo done; native
OS icon (`.ico` / `.icns`) still pending
## Phase 1 — Core Data Model ✅
- ✅ `core.geometry`: `Node`, `Element`, `TrussElement`, `CorotTrussElement`,
`ElasticBeamColumn`, `ForceBeamColumn`, `DispBeamColumn`,
`ZeroLengthElement`, `ZeroLengthSectionElement`, `BeamWithHingesElement`,
`QuadElement`, plus `GridSystem` / `CoordinateSystem`
- ✅ `core.materials`: `ElasticIsotropic`, `ElasticUniaxial`, `ElasticPP`,
`Steel01`, `Steel02`, `Concrete01`, `Concrete02`, `HystereticMaterial`
- ✅ `core.sections`: `ElasticSection`, `FiberSection` with rectangular
/ circular patches and straight rebar layers, `SectionAggregator`
- ✅ `core.loads`: `NodalLoad`, `UniformElementLoad`, `LinearTimeSeries`,
`ConstantTimeSeries`, `PathTimeSeries`, `PlainLoadPattern`,
`UniformExcitationPattern`, `ResponseSpectrum`
- ✅ `core.analysis`: `StaticCase`, `ModalCase`, `TransientCase`,
`PushoverCase`, `ResponseSpectrumCase` — including chained preload
via `preload_case_ids` and pattern removal for free-vibration runs
- ✅ `core.constraints`: `EqualDOFConstraint` (multi-point constraints)
- ✅ `core.project.Project` aggregator with id allocation, validation,
`validate_references()`
- ✅ `services.persistence`: `.osmodel` (Pydantic JSON) load/save with
round-trip-clean assertion in every example script
## Phase 2 — OpenSees Service ✅
- ✅ `services.opensees_runner.OpenSeesRunner` emits commands in the
canonical order documented in [`architecture.md`](architecture.md)
- ✅ Verified examples (matched analytically or against the OpenSees
Wiki Tcl reference): cantilever (point + UDL), portal frame, basic
truss, SDOF pushover, RC frame gravity / pushover / earthquake,
Examples 1–4 family, two-storey shear / one-bay frames, simply
supported beam with quad elements
- ✅ `AnalysisWorker(QObject)` runnable inside a `QThread` with
`progress(int)` / `log(str)` / `finished(ResultsHandle)` signals
## Phase 3 — 3D Viewport ✅
- ✅ `views.canvas3d.ModelCanvas` (subclass of `QtInteractor` from pyvistaqt)
- ✅ Grid plane, world axes triad, view-cube-style preset buttons
(Isometric / Top XY / Front XZ / Right YZ), parallel projection toggle
- ✅ Node rendering as glyphs; element rendering as tubes (frames /
trusses) and shells (quads); supports rendered as gizmos
- ✅ Mouse picking → `nodePicked` / `elementPicked` signals; pixel-space
grid snap (rejects clicks more than 15 px from an intersection)
- ✅ Selection highlighting with in-place colour updates
## Phase 4 — Modeling Tools ✅
- ✅ Grid system dialog (X / Y / Z spacing, generates nodes); SAP2000-style
table editor; off-grid clicks rejected
- ✅ Working-plane filter — grid + snap restricted to the active level
- ✅ Draw Node / Draw Frame / Draw Truss tools with hover snap highlight
- ✅ Inline element editing from the Properties dock (truss area,
any scalar field)
- ✅ Assign Support tool (Free / Pin / Roller / Fix + custom 6-DOF dialog)
- ✅ Assign Load: nodal loads, distributed beam loads, ground motions
- ✅ Assign EqualDOF (multi-point constraints) from the UI
- ✅ Show Extruded Sections toolbar shortcut
- ✅ Undo / Redo via `QUndoStack` for every model mutation
- 🟡 Replicate / Mirror / Move / Extrude — basic copy works; story-extrude
and mirror still pending
## Phase 5 — Properties ✅
- ✅ Material library dialog (CRUD)
- ✅ Section library dialog (incl. `FiberSection` rows that previously
crashed are now handled)
- ✅ Property editor dock — context-aware, multi-selection assignment,
inline mass editor
- 🟡 Fiber-section visual editor — exists; some UI polish still needed
## Phase 6 — Analysis Pipeline ✅
- ✅ Analysis case manager dialog with case-type factories
- ✅ Run dialog with progress + log + cancel
- ✅ Per-run Rayleigh damping override (no project mutation)
- ✅ Results stored to `<project>.osresults.h5`
- 🟡 Convergence diagnostics view (residuals per step) — partial info
in run log; dedicated diagnostics dock pending
## Phase 7 — Post-processing ✅
- ✅ Deformed shape with scale-factor slider
- ✅ Mode-shape animator (1-indexed; play / scrub / scale)
- ✅ Element force diagrams (axial, shear, moment) with auto-pick of
the largest-magnitude component on dock open, numerical labels at
global min/max ends
- ✅ Time-history plotter (pyqtgraph) with displacement / velocity /
acceleration switching
- ✅ Hysteresis plotter — node DOF orbits and element local-force loops
- ✅ Pushover curve view in display units
- ✅ Response-spectrum view (Sa-T curve with modal-period markers and a
mass-participation table)
- ✅ Snapshot / video export (mode shapes + time histories) via
`imageio[ffmpeg]`
- ⬜ **Render performance pass** — collapse per-entity actors into glyphed
PolyData (single draw call), in-place colour updates for selection,
AA, lower-tessellation spheres. Target: 10k nodes / 20k frames @ 30 fps
## Phase 8 — Earthquake Engineering 🟡
- ✅ `HystereticMaterial`, `BeamWithHinges`, `FiberSection` → all
flowing into the runner, end-to-end pushover example
- ✅ Response spectrum generator + SRSS / CQC modal combination
- ✅ Ground-motion import via `PathTimeSeries` + `UniformExcitationPattern`,
with an example wired up against the OpenSees A10000 record
- ✅ `ZeroLengthSectionElement` for moment-curvature workflows; closed-form
verification example shipped
- ✅ `Concrete04` (Popovics) end-to-end: model → runner → UI form → tests →
fiber-section cantilever example
- ✅ **Material Tester service** (`services/material_tester.py`) — headless,
Qt-free; runs any uniaxial material through a monotonic or cyclic strain
protocol in an isolated single-element model and returns the full
stress–strain history. Verified: Elastic linearity, ElasticPP plateau,
Steel01 hysteresis energy (EPP formula, <1%), Concrete04 Popovics C1
continuity; state-cleanup and interleave proofs.
- ⬜ **Material Tester dialog** — Qt front-end for the service above; live
stress–strain plot with strain-amplitude and step controls
- ⬜ Seismic isolators: `elastomericBearing*`, `frictionPendulumBearing`,
`singleFPBearing`, `TripleFrictionPendulum`
- ⬜ Ground-motion library (PEER-style record set + scaling tools)
- ⬜ IDA (Incremental Dynamic Analysis) batch runner
- 🟡 Fiber-section editor — exists for rectangular / circular sections;
confined / unconfined visual presets pending
## Out-of-scope (for now)
- ✂️ Code-checking (TBDY-2018, ASCE 41, Eurocode 8)
- ✂️ Soil-structure interaction GUI
- ✂️ Cloud / collaborative editing
- ✂️ Native shell-element rendering / pre-processing (quads exist as a
primitive, but a proper shell workflow is its own phase)
## Next sessions — backlog (Sept 2026 cooldown session)
Session handoff first: ~60 files of uncommitted work in the tree
(Table dock, extrusion shapes, exporter, pattern_factors, all audit
fixes). Commit per Conventional Commits on `develop` before new work
(`feat:`/`fix:` split per lane), then `ruff check src tests &&
ruff format src tests`, `mypy`, `pytest -m "not slow"`.
Requested (user-ordered):
1. ⬜ Toolbar button icons — `resources/icons/` exists; wire `QIcon`s
in `menu_builder.py` toolbar builders (`@designer` lane: layout,
hierarchy, affordances). Include OS icon (`.ico`/`.icns`, Phase 0 🟡).
2. ⬜ Shell objects — analysis first: `QuadElement` today is continuum
(plane stress/strain). Scope OpenSees `ShellMITC4`/`ShellDKGQ` +
shell sections/materials, then core element + `_emit` + renderer
quad→shell + section dialog. Keep solver-source untouched
(emit `-factor`-style floats only). (`@oracle` for the scope call.)
3. ⬜ Input-dialog layout/usability pass — continue the Lane C pattern:
`QFormLayout` consistency, prefill from selection (done for assign
dialogs), inline validation messages instead of silent reverts,
units-aware labels. (`@designer` for layout, orchestrator for copy.)
4. ⬜ Hover tooltips — two halves: (a) canvas entity hover (node/element
id + key values via existing picking signals); (b) widget tooltip
audit (every toolbar button/dialog field documents itself).
5. ⬜ Load visibility filter — show only loads of the selected pattern /
case; hide the rest. Renderer load-overlay filter + selector combo
(builds on the Table Loads tabs' pattern filter). Natural home:
View toolbar next to Show Local Axes.
Identified this session (audit + build leftovers):
6. ⬜ Named load combinations (deferred phase 3) — `pattern_factors`
covers per-case factoring; add a reusable named-combo entity only
if one combo must be shared across many cases.
7. ⬜ Per-row Run + status in Table Analyses tab — run control lives
only in the Run dialog today; add per-case Run button + last-run
status (converged/failed/when). No overlap: the tab lists cases,
this operates them.
8. ⬜ Case-manager edit preservation — `modelMutated` while the manager
is open rebuilds the form and discards in-progress edits (audit C2).
9. ⬜ Table dock phase-4 polish — CSV copy/paste, column visibility
(deferred from the Table plan).
10. ⬜ Display-settings persistence — extruded-sections / local-axes /
parallel-projection toggles reset per project; persist viewport
prefs in `QSettings` (no `DisplaySettings` module exists yet).
11. ⬜ GUI test coverage under xvfb — `views/*` omitted from coverage;
lanes verified via offscreen smoke only. Add pytest-qt tests for:
dock toggles, Level refresh, post-state teardown, Table edits,
factor spins, local-axes overlay.
12. ⬜ Pin ruff version — local ruff (0.15.x) flags pre-existing drift
(UP037/RUF001/RUF003/E702) that repo CI doesn't; pin in
`pyproject.toml` or baseline-allowlist so `ruff check` is green.
13. ⬜ Exported-Tcl round trip — `.py` export is solver-verified;
add an equivalent exec-and-compare test for the `.tcl` renderer.

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 KiB