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
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:
commit
612936a00b
540 changed files with 174136 additions and 0 deletions
331
docs/adr/ADR-0001-gidopensees-schema-import.md
Normal file
331
docs/adr/ADR-0001-gidopensees-schema-import.md
Normal 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.
|
||||
91
docs/adr/ADR-0002-headless-gui-dep-split.md
Normal file
91
docs/adr/ADR-0002-headless-gui-dep-split.md
Normal 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
80
docs/architecture.md
Normal 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.
|
||||
221
docs/gap-analysis-gidopensees.md
Normal file
221
docs/gap-analysis-gidopensees.md
Normal 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
46
docs/logo.svg
Normal 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
198
docs/roadmap.md
Normal 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.
|
||||
BIN
docs/screenshots/main_window.png
Normal file
BIN
docs/screenshots/main_window.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 505 KiB |
Loading…
Reference in a new issue