diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..10a928c --- /dev/null +++ b/NOTICE @@ -0,0 +1,70 @@ +# NOTICE + +OTKO +Copyright © 2026 Ozan and contributors. + +## OTKO license + +OTKO's own source code is licensed under the **GNU Affero General Public +License v3.0** (AGPL-3.0). The full text is in [`LICENSE`](LICENSE). This +`NOTICE` file does not replace or modify that license; where the two +disagree, `LICENSE` governs. + +OTKO is **not** an MIT-licensed project. Portions of the codebase were +ported or adapted from an earlier, MIT-licensed prototype called +`otko-development`, so the original MIT notice is reproduced below as +required by that license. + +## Ported / adapted code: `otko-development` + +Portions of OTKO were ported or adapted from the `otko-development` +project, which is distributed under the MIT License. The MIT +copyright and permission notice is reproduced verbatim below: + +``` +MIT License + +Copyright (c) 2026 OTKO contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + +## Runtime dependencies + +OTKO depends on third-party software that is not covered by OTKO's +AGPL-3.0 license. Each component remains under its own license, and its +license text ships with the corresponding package. The notes below are a +summary for attribution, not a substitute for those license texts. + +- **OpenSees / OpenSeesPy** — the finite-element solver invoked by + `otko.services.opensees_runner`. OpenSees is copyright The Regents of + the University of California and is distributed under a BSD-style + license; the notice is shipped inside the `openseespy` package. +- **PySide6** — the Qt 6 bindings used by the desktop GUI. PySide6 is + available under the GNU Lesser General Public License v3 (LGPLv3) or a + commercial license. OTKO links against it dynamically, which keeps the + LGPL relinking obligation satisfiable for redistributors. +- **numpy**, **pydantic**, **h5py**, **pyvista**, **VTK**, + **pyqtgraph**, and **imageio** — each is distributed under its own + license (for example BSD-3-Clause, MIT, and similar permissive terms). + See the license file bundled with each installed package for the exact + terms. + +When redistributing OTKO, keep `LICENSE`, this `NOTICE`, and the license +notices of the dependencies above. diff --git a/docs/QUICK_GUIDE.md b/docs/QUICK_GUIDE.md new file mode 100644 index 0000000..0bc4b8c --- /dev/null +++ b/docs/QUICK_GUIDE.md @@ -0,0 +1,132 @@ +# OTKO Quick Guide + +A practical, task-first guide to the OTKO desktop GUI. It assumes you +have already installed the desktop extras and can launch the app: + +```bash +pip install -e ".[gui,dev]" +python -m otko +``` + +Project files use the `.osmodel` extension — a single, Pydantic-validated +JSON document that diffs cleanly in Git. Analysis output is written +separately to `*.osresults.h5`. + +For the layer map and the OpenSeesPy command order, see +[`architecture.md`](architecture.md). For the feature-by-feature plan, see +[`roadmap.md`](roadmap.md). + +## 1. A cantilever walkthrough + +This follows the bundled `examples/cantilever.osmodel` model: a 5 m +horizontal beam, fixed at the left end, with a tip load. If you would +rather build it by hand, the steps are below. + +1. **Start a project.** **File → New (3D Frame)**. Pick display units in + the bottom-right **Units** combo before typing any values. +2. **Lay out a grid.** **Define → Coordinate System/Grids…** (Ctrl+G). + Define X lines at 0…5 m (say, every 1 m), Y = 0, Z = 0, and set the + grid as the active coordinate system. The 3D canvas will draw it as + reference geometry. +3. **Add nodes.** **Define → Add Node…** (Ctrl+N), or use the **Draw + Node** tool and click on grid intersections at (0,0,0) … (5,0,0). +4. **Define material and section.** **Define → Material Library…** + (Ctrl+Shift+M) then **Define → Section Library…** (Ctrl+Shift+S). The + example uses a steel `ElasticSection` named `W12x40`. +5. **Draw the element.** **Assign/Define → Draw Frame** (F2), then click + from the first node to the last. Assign the section with + **Assign → Frame → Section…**. +6. **Add the support.** Select the node at x = 0 and use + **Assign → Joint → Restraints…** (Ctrl+R); restrain all six DOF. The + support icon confirms the fixed end. +7. **Add the load.** Select the tip node and use **Assign → Joint → + Point Loads…** (Ctrl+L). The example applies -10 kN in Y. Alternatively + build the distributed case with **Assign → Frame → Distributed + Load…**. +8. **Set up and run the case.** **Analyze → Cases…** (Ctrl+Shift+A) to + create or review a Static case, then **Analyze → Run…** (F5). The + bundled file already contains `Tip-Load`, `Uniform-Load`, and a modal + `Modal-3` case. + +### Smoke check + +Open `examples/cantilever.osmodel`, run the `Tip-Load` static case, then +**Display → Show Force Diagram… → M3**. The moment diagram is linear and +peaks at **50 kN·m at the fixed end**. V2 is a constant -10 kN along the +span. If you see that, the model, runner, and post-processor are wired up +correctly. + +## 2. Running a modal analysis + +1. Open a model that has mass assigned (the bundled cantilever lumps mass + at every free node so modal works out of the box). +2. **Analyze → Cases…**, add or select a **Modal** case, and set the + number of modes `n_modes` (the example uses 3). +3. **Analyze → Run…** (F5). Results appear in the results/report panel: + periods, frequencies, and participation factors per mode. +4. **Display → Animate Mode Shape** to view each mode. Use the mode + selector and the animation controls, and **Export…** if you want a + video of the mode shape. +5. Modal results also feed the response-spectrum case: define a response + spectrum, then run the SRSS or CQC combination and open + **Display → Show Response Spectrum**. + +## 3. Reviewing results and exporting a report or script + +After a run, the results/report panel shows a summary for the active case +(static reactions and forces, modal periods, and so on). Use the display +actions to inspect the model visually: + +- **Display → Show Deformed Shape** — with a scale slider. +- **Display → Show Force Diagram…** — axial (P), shear (V2/V3), moment + (M2/M3) diagrams. +- **Display → Show Pushover Curve**, **Show Time-History**, **Show + Hysteresis** as applicable. + +To hand the analysis to someone else, or to archive exactly what was run, +export a script: + +- **File → Export OpenSeesPy (.py)…** — writes the full model, and + optionally a selected analysis case, as a runnable Python script. +- **File → Export Tcl (.tcl)…** — the same model as classic OpenSees Tcl. + +The export dialog lets you choose "Model only (no analysis case)" or one +of the configured cases. The generated script follows the runner's fixed +command order (`wipe → model → node → fix → … → analyze`), so it +reproduces the analysis outside the GUI. + +## 4. Changing display units + +Use either control, they are the same setting: + +- The **Units** combo in the bottom-right of the status bar, or +- **Options → Set Display Units…** + +Changing units updates how lengths, forces, and moments are formatted in +the UI and plots. It does **not** rescale the underlying model numbers — +pick the right unit system before you type values, and convert +deliberately if you switch later. A set of unit labels is available in the +unit-label tests under `tests/unit/test_unit_labels.py`. + +## 5. Undo and redo + +Every model mutation goes through the undo stack, so most edits are +reversible: + +- **Edit → Undo** (Ctrl+Z) +- **Edit → Redo** (Ctrl+Y / Ctrl+Shift+Z) + +Menu text is dynamic — it names the operation, for example "Undo Add 4 +Nodes". Compound operations such as drawing a frame (node + element) are +wrapped in a single macro, so one undo removes the whole step. File +loads, analysis runs, and display-only changes are not model mutations and +are not undoable. + +## Where to go next + +- [`architecture.md`](architecture.md) — MVVM layering and command order. +- [`roadmap.md`](roadmap.md) — what is done and what is planned. +- `examples/` — 20+ verified models, each generated from a checked-in + Python script. +- [`../CONTRIBUTING.md`](../CONTRIBUTING.md) — setup, rules, and verify + commands. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..9ef3bfb --- /dev/null +++ b/docs/README.md @@ -0,0 +1,26 @@ +# OTKO Documentation + +Index of the project documentation. Start with the quick guide if you +just want to build and run a model; read the architecture page if you are +changing code. + +| Document | What it covers | +| --- | --- | +| [QUICK_GUIDE.md](QUICK_GUIDE.md) | Task-first walkthrough: cantilever model, modal analysis, report and script export, display units, undo/redo. | +| [architecture.md](architecture.md) | MVVM layering, package responsibilities, threading, persistence, and the fixed OpenSeesPy command order. | +| [roadmap.md](roadmap.md) | Phase-by-phase feature status, from scaffolding through the earthquake-engineering primitives. | +| [adr/](adr/) | Architecture Decision Records — the "why" behind individual design choices. | +| [screenshots/](screenshots/) | Screenshots referenced by the docs and README. | + +## Architecture Decision Records + +- [ADR-0001 — GiD/OpenSees schema import](adr/ADR-0001-gidopensees-schema-import.md) +- [ADR-0002 — Headless / GUI dependency split](adr/ADR-0002-headless-gui-dep-split.md) + +## Related documentation + +- [`../README.md`](../README.md) — project overview, install, and quick start. +- [`../CONTRIBUTING.md`](../CONTRIBUTING.md) — dev setup, layering rules, commit style, and verify commands. +- [`../AGENTS.md`](../AGENTS.md) — condensed context for automated agents. +- [`gap-analysis-gidopensees.md`](gap-analysis-gidopensees.md) — gap analysis against the GiD/OpenSees reference. +- [`../examples/README.md`](../examples/README.md) — the bundled example models. diff --git a/specifications/15-rebuild-adoption-plan.md b/specifications/15-rebuild-adoption-plan.md new file mode 100644 index 0000000..f10ddfd --- /dev/null +++ b/specifications/15-rebuild-adoption-plan.md @@ -0,0 +1,383 @@ +# 15 — Rebuild Adoption Plan + +> **Status:** proposed. **Source of truth for intent:** the greenfield +> rebuild specs at `~/Sync/otko-development/specifications` (`00`–`14`). +> This document plans how to bring that rebuild's good ideas into **this** +> repo without breaking the working app. +> +> Requirement IDs are shown without brackets for brevity (e.g. `PKG-032` = +> `[PKG-032]` in the rebuild set) so they stay grep-able. + +--- + +## 1. Context + +| | This repo (`~/Sync/otko`) | Rebuild (`~/Sync/otko-development`) | +|---|---|---| +| Role | **Working app** — the primary deliverable | Greenfield spec-conformant attempt | +| VCS | Not a git repo in this checkout | `rebuild` branch, commit `725040e` | +| License | AGPL-3.0 | MIT (+ `NOTICE`) | +| Python | 3.10–3.12 | 3.12 only | +| Project file | `.osmodel` | `.otko` | +| Scope | Superset: static, modal, **transient, pushover, response-spectrum**, quad/shell-adjacent elements, HDF5 | Narrow v1: static + modal; reports, combinations, self-weight, units display | +| src LOC | ~37k | ~25k | +| Gate | deps not installed here; **57 collection errors** locally | `ruff` passes; `mypy`/`pytest` deps missing locally | + +The rebuild is *ahead* on: **reporting/Typst, load combinations, self-weight, +display-unit conversion, closed-form diagrams, modal mass participation, +progress/cancel, local-axis editing, viewmodel separation, and quality +gates**. It is *behind* on everything this repo already ships. Therefore the +plan is a **selective harvest**, not a merge. + +## 2. Goal and non-goals + +**Goal.** Adopt the rebuild's good capabilities into this repo, incrementally, +each phase independently shippable, with the existing app and tests staying +green. + +**Non-goals.** +- Do **not** rewrite this repo to the rebuild's architecture (`ProjectStore`, + frozen `Project`, `CommandFactory`, `services/scene.py`, + `services/diagram_data.py` substrates). +- Do **not** replace the runner/export/results surface — this repo's is a + strict superset. +- Do **not** change `.osmodel`, the AGPL license, or the Python 3.10–3.12 + support range as part of this plan. + +## 3. Principles + +1. **Additive, adapt, don't copy.** Port algorithms/patterns; re-type to this + repo's APIs (`StaticResults`/`ModalResults`, mutable `Project`, + 4-value `UnitSystem`, `element_forces.DiagramData`). +2. **Protect the working app.** Every phase keeps `pytest -m "not slow"` and + the GUI smoke tests green. New optional deps stay **lazy** so headless + installs are unaffected. +3. **One phase = one branch = one shippable PR**, with tests and a gate. +4. **Trace every item to a rebuild spec ID** so "is it done?" stays mechanical. +5. **License hygiene.** MIT→AGPL-3.0 is one-way compatible: preserve the MIT + copyright/permission notice for ported files (`NOTICE` + per-file header). + Never copy this repo's AGPL code back into the MIT rebuild. +6. **Establish version control first.** This checkout has **no `.git`**. Before + any work, create a repo/branch per `AGENTS.md` (`origin` is the self-hosted + Forgejo; work on `feat/`, PR against `main`). + +## 4. Adoption matrix + +Verdicts: **PORT** (near-verbatim), **ADAPT** (re-type/re-wire), **REIMPL** +(reimplement against this repo's APIs), **SKIP** (intentional divergence). +Size: **S** ≈ hours · **M** ≈ a few days · **L** ≈ 1–2+ weeks. + +### 4.1 Quality / foundations + +| # | Capability | Rebuild source | Verdict | Phase | Size | Spec IDs | +|---|---|---|---|---|---|---| +| Q1 | Single-source version + solver pin via `_const.py` | `src/otko/_const.py`, `pyproject.toml` `[tool.hatch.version]` | ADAPT | 0 | S | PKG-032/033, RUN-085 | +| Q2 | Layering/import-discipline AST tests | `tests/unit/test_architecture.py` (246) | ADAPT (paths) | 0 | M | ARC-001..003/006/011/060..064 | +| Q3 | CI split into 5 jobs incl. missing **`type` (mypy)** job; mark integration `slow` | `.github/workflows/ci.yml`, `tests/unit/test_ci_config.py`, `test_meta_gates.py` | ADAPT | 0 | M | TST-004/031/040/041 | +| Q4 | conftest hardening (offscreen default; `ops.wipe()` in teardown) | `tests/conftest.py` | ADAPT | 0 | S | TST §7 | +| Q5 | `tests/services/` split from `tests/unit/` | tests layout | ADAPT | 0 | M | TST-002 | +| Q6 | Standalone docs + docs test | `docs/QUICK_GUIDE.md`, `docs/README.md`, `tests/unit/test_docs.py` | ADAPT | 0 | M | — | +| Q7 | `NOTICE` + attribution for ported MIT code | `NOTICE` | REIMPL (AGPL wording) | 0 | S | PKG-042 | +| Q8 | Drop phantom `scipy`/`pandas` from `[gui]` | `pyproject.toml`, `tests/unit/test_packaging.py` | ADAPT | 0 | S | PKG-011 | +| Q9 | Scoped mypy overrides + coverage `fail_under` | `pyproject.toml` | ADAPT | 0 | S | TST-031/032 | +| Q10 | Packaging config tests subset (entrypoint, line length, single-source) | `tests/unit/test_packaging.py` (117) | ADAPT | 0 | S | PKG-030..033 | +| Q11 | Spec-ID traceability harness | `tests/unit/test_spec_traceability.py` (343) | REIMPL | 8 | L | TST-013/024/025 | +| Q12 | Performance probes | `tests/performance/*` | ADAPT | 8 | L | OVR-010..014 | + +### 4.2 Services / engine + +| # | Capability | Rebuild source | Verdict | Phase | Size | Spec IDs | +|---|---|---|---|---|---|---| +| S1 | `SolverSession` Protocol + `Real`/`Recording` sessions | `services/solver_session.py` (154) | PORT | 1 | S | RUN-003/010/011/012 | +| S2 | Persistence refuses to save an invalid model + resolved path | `services/persistence.py` (63) | ADAPT | 1 | S | PER-021/022 | +| S3 | Result fields: `StaticResults.time`, modal `ModeParticipation` record | `services/results.py` (138) | ADAPT | 1 | S | RES-001/003/005, ANL-012 | +| S4 | Component-less element forces = absent, not zeros | `runner/run.py` | ADAPT | 1 | S | RUN-053 | +| S5 | `progress(int)` + `cancelled` + `isInterruptionRequested()` + `cancel()`; real Cancel UX | `services/qt_workers.py`, `viewmodels/analysis_vm.py`, `views/dialogs/run.py` | ADAPT | 2 | M | ANL-031..036, ARC-032/033, UX-081 | +| S6 | Modal mass participation unified with response-spectrum path | `runner/modal.py` (466), `services/results.py` | ADAPT | 2 | M | RUN-062/070..072, ANL-013 | +| S7 | Display-unit layer + `GRAVITY` + `ProjectMeta.display` (adapter over 4-system enum) | `core/units.py` (298) | ADAPT | 3 | M | UNT-003/004/010/020..024/030..032 | +| S8 | Closed-form diagram math (`N/V/M/T` shapes + extrema) | `core/diagrams.py` (436) | PORT | 4 | S | RES-010/011/012 | +| S9 | Backend-neutral `diagram_data` service + `render_matplotlib` | `services/diagram_data.py` (400) | REIMPL | 4 | M | RES-020..024/030..032, CAN-084 | +| S10 | **Report pipeline** CSV / PNG(≥300 dpi) / SVG / Typst (+optional PDF) + case-report action | `services/report/*` (1141) | PORT + ADAPT deps | 5 | M | RES-040..047/050/051/061 | +| S11 | Local axis: `LocalAxis`, element field, geomTransf dedup by `(type,vecxz,roll)`, editor + triad | `core/geometry/local_axis.py`, `runner/emit.py` | ADAPT | 6 | M | GEO-060..064, DOM-040 | +| S12 | Load combinations entity + commands + runner materialisation | `core/combinations.py` (44), `commands/combinations.py` (146) | PORT + ADAPT | 7 | M | LOD-050..058, ANL-001, RUN-050 | +| S13 | Self-weight service + regenerate command | `services/self_weight.py` (364) | REIMPL | 7 | M | UNT-040..045, LOD-011/040..043 | +| S14 | Material `rho` coverage for self-weight | `core/materials` | ADAPT | 7 | S | UNT-041 | + +### 4.3 UI / UX + +| # | Capability | Rebuild source | Verdict | Phase | Size | Spec IDs | +|---|---|---|---|---|---|---| +| U1 | Pure formatting helpers (`DOF/MASS/RESTRAINT_LABELS`, number/float parse) | `views/formatting.py` (84) | PORT | 0 | S | UX-033/062 | +| U2 | Timestamped, severity-tagged console | `views/docks/console.py` (103) | PORT | 0 | S | UX-042 | +| U3 | `ValidatedDialog` base (help line, inline errors, OK-gating, unit suffix) | `views/dialogs/base.py` (191) | PORT + staged | 8 | M | UX-060..064 | +| U4 | `ResultsVM` per-case handle cache; `CanvasVM` selection ownership | `viewmodels/results_vm.py`, `canvas_vm.py` | REIMPL | 8 | M | RES-005, CAN-042/041 | +| U5 | Overlay helpers: ghost undeformed, unit-labelled diagram extremes, zero hint, pattern-filtered loads | `views/canvas/overlays.py` (684) | REIMPL | 8 | M | CAN-052/062/082/083, LOD-063 | +| U6 | `QSettings` layout persistence + `closeEvent` save prompt | `views/main_window.py` | PORT | 0 | S | UX-013, PER-032/033, ARC-050 | +| U7 | `ThemeManager` + dark icon set | `views/resources/theme.py`, `icons.py` | ADAPT (verify icon provenance) | 8 | S | UX-070/071/072 | +| U8 | Thicken `ProjectViewModel` (move `action_handlers` logic into VM methods) | `viewmodels/project_vm.py` (713) | REIMPL | 8 | L | UX-001/002/005, ARC-005/020..022 | +| U9 | Analysis VM status + Simple/Advanced control fields | `viewmodels/analysis_vm.py` | ADAPT | 8 | M | ANL-021/050..054, UX-037 | + +### 4.4 Explicitly deferred / skipped + +| Item | Verdict | Why | +|---|---|---| +| Polygon-first sections + `services/sections_mesh.py` (opstool GPLv3) | DEFER (Phase 9, conditional) | Schema-breaking redesign; adds GPLv3/runtime deps; not report-critical | +| Frozen `Project`/entities + `ProjectStore`/`CommandFactory` rewrite | DEFER (not recommended now) | Large coordinated rewrite of 43 commands + all call sites; low user-visible value vs risk | +| Canvas backend Protocol + `plotly_backend.py` + `services/scene.py` | DEFER (Phase 9) | Requires core + views surgery; plotly pulls QtWebEngine (known teardown SIGSEGV) | +| `.otko` file suffix / greenfield `Project` schema (PER-001/006) | SKIP | Breaks `.osmodel` corpus and compatibility; do only as an explicit user-approved migration | +| Rebuild `runner/export.py` | SKIP | This repo's `export.py` is a superset (5 case types, `.py`+`.tcl`) | +| Rebuild static/modal HDF5 | SKIP | Rebuild has none; this repo's transient HDF5 is already better | +| Rebuild slim `materials`/`analysis`/`loads` unions | SKIP | Dropping `HystereticSM`, Transient/Pushover/ResponseSpectrum, Path/Imposed patterns would regress shipped features | +| MIT license / py3.12-only / single-OS CI assertions | SKIP | Intentional differences (AGPL, 3.10–3.12, 3-OS matrix) | + +## 5. Phases + +### Phase 0 — Foundations, safety net, quick wins +**Why first:** makes later phases verifiable and cheap; all items are +additive and low risk. Also unblocks the mpy gate that currently does nothing. + +- **Prep:** initialise git and a `feat/rebuild-adoption` branch (§3.6). +- **Q1** Create `src/otko/_const.py` (`__version__`, `OPENSEESPY_VERSION`), + switch `pyproject.toml` to `dynamic = ["version"]` + `[tool.hatch.version]`, + and import the pin in `services/export.py` (currently duplicated at + `export.py:52`). Spec: PKG-032/033, RUN-085. +- **Q2** Port `tests/unit/test_architecture.py`; adapt `CANVAS = views/canvas3d` + path and `services/{_emit,_run}.py` paths. Expect it to flag three existing + offenders — decide per item: `OSS_PICK_DEBUG` env var in + `views/canvas3d/model_canvas.py:29` (ARC-052), `QUndoStack` outside VMs, and + the canvas path. Either fix or add a documented, narrow allowlist. +- **Q3** Split CI: `lint` (+`ruff format --check`), **`type` (mypy scopes)**, + `test-headless` (`pytest tests/unit tests/services -m "not slow"`), + `test-gui` (`xvfb-run pytest tests/gui`), `test-integration` + (`pytest tests/integration -m slow`). Add `pytestmark = pytest.mark.slow` to + integration modules. Keep the existing 3-OS × 3.10–3.12 matrix and the long + Linux Qt apt list. +- **Q4** `tests/conftest.py`: `QT_QPA_PLATFORM=offscreen` default; move + `ops.wipe()` to teardown (this repo currently wipes before each test). +- **Q5** Move service-level tests (`runner_translation`, `persistence`, + `results`, `export`, …) into `tests/services/`; update CI and `AGENTS.md`. +- **Q6** Add `docs/QUICK_GUIDE.md` + `docs/README.md` (rewrite `.otko` → + `.osmodel`, rebuild-only APIs → this repo's), update `CONTRIBUTING.md` with + the 5-layer table + install/run/test commands; port `test_docs.py`. +- **Q7** Add `NOTICE` crediting MIT `otko-development` for ported files; + add a per-file header to ported files (`# Ported from otko-development + (MIT), (c) 2026 OTKO contributors`). +- **Q8/Q9/Q10** `pyproject.toml`: drop `scipy`/`pandas` from `[gui]`; convert + mypy to scoped `strict` overrides (core/services/viewmodels); add + `[tool.coverage.run] fail_under`; port the `test_packaging.py` subset that + applies (entrypoint, version single-source, line length). +- **U1/U2/U6** Port `views/formatting.py`; timestamp the console + (`views/docks/console.py` into the existing dock); add `save_layout`/ + `restore_layout` (`QSettings`) and a `closeEvent` unsaved-changes prompt. + +**Gate:** `ruff check src tests`, `ruff format --check src tests`, +`mypy src/otko/core src/otko/services src/otko/viewmodels`, +`pytest -m "not slow"`, arch test, and the new packaging/docs tests pass. + +### Phase 1 — Cheap, safe services wins +- **S1** Port `services/solver_session.py` (`SolverSession`, `RealSolverSession`, + `RecordingSolverSession`). Inject into `OpenSeesRunner` while keeping the + existing `ops_module=` shim; consolidate the two wipe paths + (`_emit.py:105` → `session.reset()`). Tests: + `tests/services/test_solver_session.py`. +- **S2** In `services/persistence.py::save_project`, call + `validate_references()` first and refuse to write an invalid model, + reporting the problem list (PER-022); return the resolved path (PER-021). +- **S3** Extend `StaticResults` with `time` and add a `ModeParticipation` + record to `ModalResults` (keep the existing dataclass names — consumers in + `views/docks/results_panel.py` dispatch on them). +- **S4** `RUN-053`: return an absent (not zero-filled) force entry when a + component is missing; guard the static `eleForce` fallback. + +**Gate:** new service tests + existing runner/persistence tests green; runner +integration suite unchanged. + +### Phase 2 — Progress & cancellation +- **S5** Add to `services/qt_workers.py`: `progress(int)`, `cancelled`, + an `_interruption_requested()` check between steps, and a bounded teardown + wait. Add `AnalysisRunner.cancel()` (calls `QThread.requestInterruption()`). + Wire progress callbacks through static (`RUN-054`) and modal (`RUN-063`) + runs. Replace the indeterminate bar in `views/dialogs/run_analysis.py` with + a real progress bar + working Cancel (UX-081). Add per-case status to the + case manager (ANL-050). +- **S6** Unify modal mass participation: put the rebuild's participation + math into one service and have **both** modal results and the existing + response-spectrum path (`services/spectrum.py`) consume it. Add the ANL-013 + no-mass warning. + +**Gate:** cancel leaves no partial handle; progress reaches 100; participation +sums to 1.0 against a hand-checked example. + +### Phase 3 — Units display layer + GRAVITY +- **S7** Port the display machinery from `core/units.py` **as an adapter**: + keep this repo's 4-value `UnitSystem` and map families + (`SI_M_N|SI_MM_N → METRIC`, `US_FT_KIP|US_IN_KIP → IMPERIAL`); add + `GRAVITY`, `display(value, system, quantity, pref)`, `DisplayPrefs`; add + `ProjectMeta.display` (persisted). Retarget the status-bar/menu unit picker + so it writes **display prefs**, not the stored `meta.units` (fixes UNT-031). +- Watch-outs: rebuild uses `StrEnum` (3.11+) — do **not** adopt it (this repo + targets 3.10). `export.py` currently embeds `meta.units.value`; keep that + but source labels via the new API. + +**Gate:** switching display units changes every label and leaves +`project.model_dump()` byte-identical; old 4-system `.osmodel` files still load. + +### Phase 4 — Closed-form diagrams +- **S8** Port `core/diagrams.py` near-verbatim (pure numpy, self-contained). +- **S9** Build a new `services/diagram_data.py` (REIMPL) on top of this repo's + `StaticResults` + load patterns, reusing `core/diagrams` for interior + shapes/extrema. Run it **alongside** `services/element_forces.py`, migrate + `views/canvas3d/diagram_renderer.py` and `views/dock_manager.py` behind a + flag, then retire the old extractor once the on-screen output matches. + +**Gate:** port `test_diagram_data.py` (end-values, Vz/My plane, axial, +all-six components, shared data, auto-scale); on-screen diagrams unchanged. + +### Phase 5 — Reporting pipeline ★ (the headline v1 capability) +- **S10** Port `services/report/{__init__,csv,figures,typst,pdf}.py`. Add a + `[reports]` extra (`matplotlib`, `imageio-ffmpeg`; Typst CLI optional, probed + via `shutil.which`, graceful degradation per RES-046). Expose a "case report" + action that produces the full set: CSV tables, PNG (≥300 dpi), SVG, a Typst + document (PDF optional), conditional static/modal sections. +- Dependencies: **Phase 3** (units) and **Phase 4** (diagrams) must land first; + reuse **Phase 1** result fields. + +**Gate:** the reference test — cantilever moment **50 kN·m at the fixed end** +(RES-061) — plus report generation off the GUI thread and preservation of the +original model file. Port `test_report_{csv,figures,typst,modal}.py` and the +`test_full_report.py` / `test_report_reference.py` integration tests. + +### Phase 6 — Local axis (user-editable) +- **S11** Port `core/geometry/local_axis.py`; add + `local_axis: LocalAxis` to the frame elements (`ElasticBeamColumn`, + `ForceBeamColumn`, `DispBeamColumn`, `BeamWithHinges`). Change + `services/_emit.py` geomTransf to read the explicit axis and dedup by + `(type, vecxz, roll_deg)` instead of auto-deriving. Update the renderer + triad (its current `getattr(el, "vecxz")` at `model_renderer.py:727` is dead + code) and add an editor (property dock / dialog). +- **Critical migration risk:** a blind default `vecxz=(0,0,1)` is parallel to + a vertical member's axis and would break existing vertical models. Use a + **safe default rule** (e.g. derive the default from geometry exactly as the + runner does today, then only override when the user sets it) so existing + `.osmodel` files keep loading and producing identical results. + +**Gate:** existing frame/eigen integration tests unchanged; new +`test_local_axis_parallel_rejected` + `test_geom_transf_dedup_by_combination`. + +### Phase 7 — Load combinations + self-weight +- **S12** Port `core/combinations.py` and `commands/combinations.py`; add + `Project.combinations`; extend `validate_references`; add + `StaticCase.combination_id` with the XOR invariant (combination **or** + patterns+factors, ANL-001); materialise scaled loads in the runner + (LOD-054) without regressing the existing per-pattern factor path. +- **S13** Reimplement `services/self_weight.py` against this repo's types: + `w = ρ·A·g` distributed local for frames, equivalent nodal loads for trusses + (UNT-042/043), a `ConstantTimeSeries` + `PlainLoadPattern`, regeneration that + replaces the prior pattern (LOD-041), no-density/unresolved reporting + (LOD-042), and the gravity constant recorded in the description (UNT-045). + Add a `RegenerateSelfWeightCommand` (undoable). +- **S14** `ElasticIsotropic` currently is the only material with `rho` + (`materials/__init__.py:28`); add `rho` to the other self-weight-capable + materials, or report them as missing density. +- Depends on **Phase 3** (`GRAVITY`) and **Phase 6** (correct local projection). + +**Gate:** port `test_loads_combinations.py` and `test_self_weight.py` (minus +the combination assertion until S12 lands); verify superposition matches a +hand calculation. + +### Phase 8 — UI/UX adoption (larger, optional but valuable) +- **U3** `ValidatedDialog` base + migrate dialogs incrementally (unit suffix + via the Phase 3 API). +- **U4** Extract `ResultsVM` (per-case handle cache) and `CanvasVM` (selection + ownership) as supersets of the current state holders so existing call sites + keep working. +- **U5** Reimplement overlay **pure helpers** against this repo's + `deformation`/`element_forces`: ghost undeformed (CAN-062), unit-labelled + diagram extremes (CAN-082), zero-component hint (CAN-083), and + pattern-filtered load display (CAN-052/LOD-063). Unit-test them display-free. +- **U7** Adopt `ThemeManager` + a dark icon set — **verify SVG provenance and + license first** before copying any icon assets. +- **U8/U9** Thicken `ProjectViewModel` (move `action_handlers.py` logic into VM + methods, one command family at a time) and add analysis-case status / + Simple-vs-Advanced controls. +- **Q11/Q12** Spec-ID traceability harness and performance probes (only if the + team commits to the `[AREA-NNN]` convention and a perf budget). + +**Gate:** GUI tests under `xvfb-run`; VM logic unit-tested without a display. + +### Phase 9 — Deferred, only on explicit decision +- Polygon-first sections + `sections_mesh` (opstool **GPLv3**, lazy import, + optional `[sections]` extra; ensure AGPL compatibility and attribution). +- Frozen `Project`/`ProjectStore`/`CommandFactory` migration. +- Canvas backend Protocol + plotly backend + `services/scene.py`. + +Revisit only after Phases 0–8 are stable and if the value justifies the blast +radius documented in the dossiers. + +## 6. Dependency graph + +``` +Phase 0 (foundations) + │ + ├─► Phase 1 (sessions, persistence, result fields) + │ │ + │ ├─► Phase 2 (progress/cancel, mass participation) + │ │ + │ └─► Phase 3 (units display + GRAVITY) ──┐ + │ │ + │ Phase 4 (diagrams) ────────────────┤ + │ ▼ + │ Phase 5 (REPORTING) ★ + │ + ├─► Phase 6 (local axis) ──┐ + │ ▼ + └─► Phase 7 (combinations + self-weight) + │ + ▼ + Phase 8 (UI/UX) ──► Phase 9 (deferred) +``` + +**Critical path to the v1 promise (a printable case report):** +`Phase 3 → Phase 4 → Phase 5`, with `Phase 1` as a cheap prerequisite and +`Phase 2` interleavable. + +## 7. Licensing & attribution + +- This repo is **AGPL-3.0**; the rebuild is **MIT**. MIT code may be + incorporated into an AGPL work — **one-way**. Preserve the MIT notice: + add a `NOTICE` (Phase 0) and a short provenance header to each ported file. +- **Never** copy this repo's AGPL code back into the MIT rebuild. +- `opstool` is **GPLv3** (only relevant to deferred `sections_mesh`); GPLv3 ↔ + AGPLv3 are compatible. Keep it lazy/optional and do not vendor its source. +- Verify the provenance/license of any copied **icon/SVG assets** before + adopting the dark icon set (U7). + +## 8. Risks and mitigations + +| Risk | Mitigation | +|---|---| +| Big-bang port destabilises the working app | Strict additive phases; feature flags for diagram/report migration; keep old paths until parity is proven | +| Freezing/command rewrite stalls progress | Explicitly deferred (Phase 9); combinations/self-weight do **not** require it | +| Units enum change corrupts stored models | Keep 4-value enum; add a family mapping + `DisplayPrefs`; byte-identical dump test | +| Local-axis default breaks vertical members | Derive default per-element as today; override only on explicit user set; regression tests on vertical frames | +| opstool private-API coupling / GPL | Deferred; pin version + thin adapter; lazy import; attribution | +| Report pipeline pulls GUI deps into headless | Keep `matplotlib`/Typst optional and imported lazily; headless CI must pass without `[reports]` | +| `StrEnum` / 3.11-only syntax | Target py3.10: avoid `StrEnum`, `zip(strict=)`, etc. | +| No VCS in this checkout | Initialise git + branch before any code changes | +| Tests can't run locally (deps missing) | Phase 0 fixes env expectations; run gates in CI | + +## 9. Tracking + +Adopt the rebuild's `.otko-build/tasks.json` **pattern** (schema, gates, +`{id, phase, layer, specs, title, tests, status, deps}`), seeded from this +plan. Each task names the phase, the spec IDs it implements, and its tests +(the rebuild's `tasks.json` is a working reference). Keep build state out of +versioned source (`.gitignore` it) or commit it deliberately — team choice. + +## 10. Recommended first step + +Execute **Phase 0** in one branch. It is almost entirely additive, it turns +the currently-inert mypy/CI gate into a real one, and it establishes the +attribution and docs baseline the later ports depend on. Then proceed +`1 → 2 → 3 → 4 → 5`, pulling Phase 6/7 forward only if self-weight is needed +before reporting. diff --git a/specifications/README.md b/specifications/README.md new file mode 100644 index 0000000..3c8f8d4 --- /dev/null +++ b/specifications/README.md @@ -0,0 +1,37 @@ +# OTKO — specifications (this repo) + +This folder holds **OTKO's own planning documents**. It is deliberately +separate from the greenfield rebuild spec set at +`~/Sync/otko-development/specifications` (docs `00`–`14`), which stays the +reference for *what a conformant rebuild looks like*. Requirement IDs of +the form `[AREA-NNN]` cited in this folder refer to that rebuild set. + +## Why this folder exists + +`~/Sync/otko` (this repo) is the **working, feature-rich app** and is the +primary deliverable. `~/Sync/otko-development` is a **greenfield rebuild** +that conforms closely to the rebuild specs but is less capable in several +areas (transient/pushover/response-spectrum, richer element/material +coverage, `.osmodel` corpus, HDF5). The goal of the plan below is to +**harvest the rebuild's good, spec-conformant capabilities into this app +without breaking what already works** — not to rewrite this app to the +rebuild's architecture. + +## Documents + +| # | Document | Purpose | +|---|---|---| +| 15 | [15-rebuild-adoption-plan.md](15-rebuild-adoption-plan.md) | Dependency-ordered plan for porting rebuild capabilities into this repo, with adoption matrix, phases, gates, risks and explicit skips. | + +## Ground rules (summary — see doc 15 §3) + +- **Additive, never replacement.** This repo is a superset of the rebuild + in the solver/runner/export surface; protect that. +- **Keep the working app working:** `.osmodel` format, AGPL-3.0 license, + Python 3.10–3.12 support, existing tests green. +- **Adapt, don't copy:** re-type ported code to this repo's APIs + (`StaticResults`/`ModalResults`, mutable `Project`, 4-value `UnitSystem`). +- **One phase = one branch = independently shippable**, each with its own + tests and gate. +- **License hygiene:** the rebuild is MIT, this repo is AGPL-3.0; MIT→AGPL + is compatible, but ported files need attribution (see doc 15 §7).