Snapshots the current development tree, headlined by proper load combinations (user request): a reusable LoadCombination entity of weighted completed static-case results (e.g. 1.2xDead + 1.6xLive). - core: LoadCombination/LoadCombinationItem entities, Project integration (lookup, unique ids, reference validation) - services: combinations.py (linear superposition + envelope), exported via services __init__ - commands: undoable Add/Delete/Update for combinations - GUI: Load Combinations manager dialog, Run-dialog evaluation, envelope display in Results panel, Combinations tab in Table dock - tests: unit coverage (validation, math, error paths) + integration superposition check vs a single factored run
3.7 KiB
3.7 KiB
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
coretests without a display server, without OpenSees, without Qt. CI runspytest tests/unit/in milliseconds.- Swapping solvers (e.g.
xara, a future fork) touchesservices/opensees_runner.pyand nothing else. - A future CLI or notebook front-end reuses
coreandservicesas-is.
Package map
| Package | Responsibility | Allowed imports |
|---|---|---|
core |
Domain entities and invariants | stdlib, numpy, pydantic |
services |
I/O, solver invocation, persistence | core + stdlib + h5py + openseespy |
viewmodels |
Bridge core ↔ Qt; expose Qt signals; manage undo/redo | core, services, PySide6 |
views |
Pure UI; no business logic | PySide6, pyvistaqt, viewmodels |
commands |
QUndoCommand subclasses; mutate model via services |
services, viewmodels |
Threading
The Qt main thread owns all widgets. Heavy computation happens elsewhere:
- OpenSees analysis runs in a
QThreadworker (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:
wipe()andmodel('basic', '-ndm', ndm, '-ndf', ndf)node(...)for every nodefix(...)for every restrained DOFuniaxialMaterial(...)/nDMaterial(...)section(...)(if used)geomTransf(...)for frame elementselement(...)for every elementtimeSeries(...)pattern(...)with nestedload(...)recorder(...)system / numberer / constraints / integrator / algorithm / analysisanalyze(...)
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.