feat: initial otko import
Some checks failed
CI / lint (push) Has been cancelled
CI / test (macos-latest, 3.10) (push) Has been cancelled
CI / test (macos-latest, 3.11) (push) Has been cancelled
CI / test (macos-latest, 3.12) (push) Has been cancelled
CI / test (ubuntu-latest, 3.10) (push) Has been cancelled
CI / test (ubuntu-latest, 3.11) (push) Has been cancelled
CI / test (ubuntu-latest, 3.12) (push) Has been cancelled
CI / test (windows-latest, 3.10) (push) Has been cancelled
CI / test (windows-latest, 3.11) (push) Has been cancelled
CI / test (windows-latest, 3.12) (push) Has been cancelled

This commit is contained in:
smillmorel 2026-09-08 02:12:15 -04:00
commit 612936a00b
540 changed files with 174136 additions and 0 deletions

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
- The `core` package is testable without a display server, without OpenSees,
and without Qt. CI runs `pytest tests/unit/` in milliseconds.
- Replacing OpenSeesPy with another solver (e.g. `xara`, a future fork) only
touches `services/opensees_runner.py`.
- A future CLI or Jupyter frontend reuses `core` and `services` unchanged.
## 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 Studio?** | ✅ 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 Studio? | 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 Studio? | 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 Studio? | 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 Studio? | 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 Studio? | 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 Studio? | 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 Studio? | 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 Studio? | In gidopensees? | Priority |
|---|---|---|---|---|---|
| Truss | Truss | `TrussElement` | ✅ | ✅ | P0 |
| Truss | Corotational_Truss | `CorotTrussElement` | ✅ | ✅ | P0 |
## 9. Surface / Plate Elements
| Category | Object (gidopensees) | OTKO name | In Studio? | 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 Studio? | In gidopensees? | Priority |
|---|---|---|---|---|---|
| Solid | Standard_Brick_Element | — | ❌ | ✅ | P2 |
## 11. Zero-Length / Special Elements
| Category | Object (gidopensees) | OTKO name | In Studio? | 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 Studio? | 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 Studio? | 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 Studio? | 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 Studio? | 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 Studio? | 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 Studio? | 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 Studio 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">Open</tspan><tspan fill="#7BB1F0">Sees</tspan><tspan fill="#E8EDF5"> Studio</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.2 KiB

198
docs/roadmap.md Normal file
View file

@ -0,0 +1,198 @@
# Roadmap
OTKO is built in eight phases. Phases 0–7 ship the core GUI
plus all the post-processing tooling we need for verification work.
Phase 8 layers in the earthquake-engineering primitives that turn the
GUI from "OpenSees frontend" into a usable research tool.
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