otko/docs/architecture.md
smillmorel 3d809ca301
Some checks failed
CI / lint (pull_request) Has been cancelled
CI / test (macos-latest, 3.10) (pull_request) Has been cancelled
CI / test (macos-latest, 3.11) (pull_request) Has been cancelled
CI / test (macos-latest, 3.12) (pull_request) Has been cancelled
CI / test (ubuntu-latest, 3.10) (pull_request) Has been cancelled
CI / test (ubuntu-latest, 3.11) (pull_request) Has been cancelled
CI / test (ubuntu-latest, 3.12) (pull_request) Has been cancelled
CI / test (windows-latest, 3.10) (pull_request) Has been cancelled
CI / test (windows-latest, 3.11) (pull_request) Has been cancelled
CI / test (windows-latest, 3.12) (pull_request) Has been cancelled
docs: rewrite READMEs dry and blunt, rename Studio to OTKO
2026-09-08 02:41:03 -04:00

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

  • core tests without a display server, without OpenSees, without Qt. CI runs pytest tests/unit/ in milliseconds.
  • Swapping solvers (e.g. xara, a future fork) touches services/opensees_runner.py and nothing else.
  • A future CLI or notebook front-end reuses core and services as-is.

Package map

Package Responsibility Allowed imports
core Domain entities and invariants stdlib, numpy, pydantic
services I/O, solver invocation, persistence core + stdlib + h5py + openseespy
viewmodels Bridge core ↔ Qt; expose Qt signals; manage undo/redo core, services, PySide6
views Pure UI; no business logic PySide6, pyvistaqt, viewmodels
commands QUndoCommand subclasses; mutate model via services services, viewmodels

Threading

The Qt main thread owns all widgets. Heavy computation happens elsewhere:

  • OpenSees analysis runs in a QThread worker (services.opensees_runner.AnalysisWorker).
  • The worker emits progress(int), log(str), finished(ResultsHandle) signals.
  • The worker checks QThread.currentThread().isInterruptionRequested() between analysis steps so the user can cancel.
  • Results are written to HDF5; only a lightweight ResultsHandle (file path + metadata) crosses the thread boundary.

Persistence

  • Project files: *.osmodel — a JSON document validated by Pydantic models. Human-readable, diff-able, version-controllable.
  • Results files: *.osresults.h5 — HDF5; one group per analysis case; datasets for displacements, reactions, element forces, stresses.

OpenSeesPy command sequencing

OpenSeesRunner always emits commands in this order; the model layer enforces that all required pieces exist before a run can be requested:

  1. wipe() and model('basic', '-ndm', ndm, '-ndf', ndf)
  2. node(...) for every node
  3. fix(...) for every restrained DOF
  4. uniaxialMaterial(...) / nDMaterial(...)
  5. section(...) (if used)
  6. geomTransf(...) for frame elements
  7. element(...) for every element
  8. timeSeries(...)
  9. pattern(...) with nested load(...)
  10. recorder(...)
  11. system / numberer / constraints / integrator / algorithm / analysis
  12. analyze(...)

Any deviation from this order is a runtime error in OpenSees. The runner asserts the order at the service boundary; the UI never has to think about it.